Compare commits

...

1 Commits

Author SHA1 Message Date
Michael Bolin
f015e0872b protocol: canonicalize file system permissions 2026-04-20 10:07:16 -07:00
19 changed files with 164 additions and 11 deletions

View File

@@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"

View File

@@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"

View File

@@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"

View File

@@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"

View File

@@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 0.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"

View File

@@ -4,4 +4,4 @@
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, entries?: Array<FileSystemSandboxEntry>, };
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, globScanMaxDepth?: number, entries?: Array<FileSystemSandboxEntry>, };

View File

@@ -2055,6 +2055,7 @@ mod tests {
file_system: Some(v2::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/allowed")]),
write: None,
glob_scan_max_depth: None,
entries: None,
}),
}),

View File

@@ -1162,6 +1162,9 @@ pub struct AdditionalFileSystemPermissions {
pub write: Option<Vec<AbsolutePathBuf>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub glob_scan_max_depth: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub entries: Option<Vec<FileSystemSandboxEntry>>,
}
@@ -1171,12 +1174,14 @@ impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
Self {
read,
write,
glob_scan_max_depth: None,
entries: None,
}
} else {
Self {
read: None,
write: None,
glob_scan_max_depth: value.glob_scan_max_depth,
entries: Some(
value
.entries
@@ -1191,16 +1196,19 @@ impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
impl From<AdditionalFileSystemPermissions> for CoreFileSystemPermissions {
fn from(value: AdditionalFileSystemPermissions) -> Self {
if let Some(entries) = value.entries {
let mut permissions = if let Some(entries) = value.entries {
Self {
entries: entries
.into_iter()
.map(CoreFileSystemSandboxEntry::from)
.collect(),
glob_scan_max_depth: None,
}
} else {
CoreFileSystemPermissions::from_read_write_roots(value.read, value.write)
}
};
permissions.glob_scan_max_depth = value.glob_scan_max_depth;
permissions
}
}
@@ -7074,6 +7082,7 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}
@@ -7145,6 +7154,7 @@ mod tests {
access: CoreFileSystemAccessMode::None,
},
],
glob_scan_max_depth: Some(2),
};
let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone());
@@ -7153,6 +7163,7 @@ mod tests {
AdditionalFileSystemPermissions {
read: None,
write: None,
glob_scan_max_depth: Some(2),
entries: Some(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -7215,6 +7226,7 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}

View File

@@ -781,6 +781,7 @@ mod tests {
codex_app_server_protocol::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/allowed")]),
write: None,
glob_scan_max_depth: None,
entries: None,
},
),
@@ -844,6 +845,7 @@ mod tests {
codex_app_server_protocol::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/allowed")]),
write: None,
glob_scan_max_depth: None,
entries: None,
},
),

View File

@@ -93,6 +93,7 @@ async fn request_permissions_round_trip() -> Result<()> {
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
read: None,
write: Some(vec![requested_writes[0].clone()]),
glob_scan_max_depth: None,
entries: None,
}),
},

View File

@@ -243,6 +243,7 @@ fn dummy_chatgpt_auth_does_not_create_cwd_auth_json_when_identity_is_set() {
agent_runtime_id: "agent_123".to_string(),
agent_private_key: "pkcs8-base64".to_string(),
registered_at: "2026-04-13T12:00:00Z".to_string(),
background_task_id: None,
};
auth.set_agent_identity(record.clone())

View File

@@ -146,6 +146,7 @@ impl SandboxPermissions {
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, JsonSchema, TS)]
pub struct FileSystemPermissions {
pub entries: Vec<FileSystemSandboxEntry>,
pub glob_scan_max_depth: Option<usize>,
}
pub type LegacyReadWriteRoots = (Option<Vec<AbsolutePathBuf>>, Option<Vec<AbsolutePathBuf>>);
@@ -172,7 +173,10 @@ impl FileSystemPermissions {
access: FileSystemAccessMode::Write,
}));
}
Self { entries }
Self {
entries,
glob_scan_max_depth: None,
}
}
pub fn explicit_path_entries(
@@ -190,6 +194,10 @@ impl FileSystemPermissions {
}
fn as_legacy_permissions(&self) -> Option<LegacyFileSystemPermissions> {
if self.glob_scan_max_depth.is_some() {
return None;
}
let mut read = Vec::new();
let mut write = Vec::new();
@@ -225,6 +233,8 @@ struct LegacyFileSystemPermissions {
struct CanonicalFileSystemPermissions {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
entries: Vec<FileSystemSandboxEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
glob_scan_max_depth: Option<usize>,
}
#[derive(Debug, Clone, Deserialize)]
@@ -244,6 +254,7 @@ impl Serialize for FileSystemPermissions {
} else {
CanonicalFileSystemPermissions {
entries: self.entries.clone(),
glob_scan_max_depth: self.glob_scan_max_depth,
}
.serialize(serializer)
}
@@ -256,9 +267,13 @@ impl<'de> Deserialize<'de> for FileSystemPermissions {
D: Deserializer<'de>,
{
match FileSystemPermissionsDe::deserialize(deserializer)? {
FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions { entries }) => {
Ok(Self { entries })
}
FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions {
entries,
glob_scan_max_depth,
}) => Ok(Self {
entries,
glob_scan_max_depth,
}),
FileSystemPermissionsDe::Legacy(LegacyFileSystemPermissions { read, write }) => {
Ok(Self::from_read_write_roots(read, write))
}
@@ -352,13 +367,18 @@ impl From<&FileSystemSandboxPolicy> for FileSystemPermissions {
}]
}
};
Self { entries }
Self {
entries,
glob_scan_max_depth: value.glob_scan_max_depth,
}
}
}
impl From<&FileSystemPermissions> for FileSystemSandboxPolicy {
fn from(value: &FileSystemPermissions) -> Self {
FileSystemSandboxPolicy::restricted(value.entries.clone())
let mut policy = FileSystemSandboxPolicy::restricted(value.entries.clone());
policy.glob_scan_max_depth = value.glob_scan_max_depth;
policy
}
}
@@ -1828,6 +1848,60 @@ mod tests {
assert_eq!(permission_profile.is_empty(), false);
}
#[test]
fn permission_profile_round_trip_preserves_glob_scan_max_depth() {
let mut file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: FileSystemAccessMode::None,
}]);
file_system_sandbox_policy.glob_scan_max_depth = Some(2);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
assert_eq!(
permission_profile.file_system_sandbox_policy(),
file_system_sandbox_policy
);
}
#[test]
fn file_system_permissions_with_glob_scan_depth_uses_canonical_json() -> Result<()> {
let path = AbsolutePathBuf::try_from(PathBuf::from(if cfg!(windows) {
r"C:\tmp\allowed"
} else {
"/tmp/allowed"
}))
.expect("absolute path");
let file_system_permissions = FileSystemPermissions {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Path { path },
access: FileSystemAccessMode::Read,
}],
glob_scan_max_depth: Some(2),
};
let serialized = serde_json::to_value(&file_system_permissions)?;
assert_eq!(serialized.get("read"), None);
assert_eq!(serialized.get("write"), None);
assert_eq!(
serialized.get("glob_scan_max_depth"),
Some(&serde_json::json!(2))
);
assert!(serialized.get("entries").is_some());
assert_eq!(
serde_json::from_value::<FileSystemPermissions>(serialized)?,
file_system_permissions
);
Ok(())
}
#[test]
fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() {
let contents = vec![serde_json::json!({

View File

@@ -45,6 +45,7 @@ pub fn normalize_additional_permissions(
let file_system = match additional_permissions.file_system {
Some(file_system) => {
let mut entries = Vec::with_capacity(file_system.entries.len());
let glob_scan_max_depth = file_system.glob_scan_max_depth;
for entry in file_system.entries {
if matches!(&entry.path, FileSystemPath::GlobPattern { .. })
&& entry.access != FileSystemAccessMode::None
@@ -73,7 +74,10 @@ pub fn normalize_additional_permissions(
entries.push(normalized_entry);
}
}
let file_system = FileSystemPermissions { entries };
let file_system = FileSystemPermissions {
entries,
glob_scan_max_depth,
};
(!file_system.is_empty()).then_some(file_system)
}
None => None,
@@ -114,6 +118,9 @@ pub fn merge_permission_profiles(
let file_system = match (base.file_system.as_ref(), permissions.file_system.as_ref()) {
(Some(base), Some(permissions)) => Some(FileSystemPermissions {
entries: merge_permission_entries(&base.entries, &permissions.entries),
glob_scan_max_depth: base
.glob_scan_max_depth
.max(permissions.glob_scan_max_depth),
})
.filter(|file_system| !file_system.is_empty()),
(Some(base), None) => Some(base.clone()),
@@ -144,7 +151,10 @@ pub fn intersect_permission_profiles(
.into_iter()
.filter(|entry| granted_file_system.entries.contains(entry))
.collect();
FileSystemPermissions { entries }
FileSystemPermissions {
entries,
glob_scan_max_depth: requested_file_system.glob_scan_max_depth,
}
})
.filter(|file_system| !file_system.is_empty());
let network = match (requested.network, granted.network) {

View File

@@ -171,6 +171,7 @@ fn normalize_additional_permissions_rejects_glob_read_grants() {
},
access: FileSystemAccessMode::Read,
}],
glob_scan_max_depth: None,
}),
..Default::default()
})
@@ -192,6 +193,7 @@ fn normalize_additional_permissions_preserves_deny_globs() {
},
access: FileSystemAccessMode::None,
}],
glob_scan_max_depth: Some(2),
}),
..Default::default()
})
@@ -207,6 +209,7 @@ fn normalize_additional_permissions_preserves_deny_globs() {
},
access: FileSystemAccessMode::None,
}],
glob_scan_max_depth: Some(2),
}),
..Default::default()
}

View File

@@ -9665,6 +9665,7 @@ guardian_approval = true
file_system: Some(AdditionalFileSystemPermissions {
read: Some(vec![test_absolute_path("/tmp/read-only")]),
write: Some(vec![test_absolute_path("/tmp/write")]),
glob_scan_max_depth: None,
entries: None,
}),
});
@@ -9773,6 +9774,7 @@ guardian_approval = true
file_system: Some(AdditionalFileSystemPermissions {
read: Some(vec![test_absolute_path("/tmp/read-only")]),
write: Some(vec![test_absolute_path("/tmp/write")]),
glob_scan_max_depth: None,
entries: None,
}),
},

View File

@@ -558,6 +558,7 @@ mod tests {
file_system: Some(AdditionalFileSystemPermissions {
read: Some(vec![absolute_path(read_path)]),
write: Some(vec![absolute_path(write_path)]),
glob_scan_max_depth: None,
entries: None,
}),
},

View File

@@ -92,6 +92,7 @@ mod tests {
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
read: Some(vec![absolute_path("/tmp/read-only")]),
write: Some(vec![absolute_path("/tmp/write")]),
glob_scan_max_depth: None,
entries: None,
}),
}
@@ -109,6 +110,7 @@ mod tests {
},
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
}),
..Default::default()
}),
@@ -117,6 +119,7 @@ mod tests {
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
read: None,
write: None,
glob_scan_max_depth: None,
entries: Some(vec![codex_app_server_protocol::FileSystemSandboxEntry {
path: codex_app_server_protocol::FileSystemPath::Special {
value: codex_app_server_protocol::FileSystemSpecialPath::Root,

View File

@@ -1372,6 +1372,7 @@ mod tests {
access: FileSystemAccessMode::None,
},
],
glob_scan_max_depth: None,
}),
..Default::default()
};

View File

@@ -113,6 +113,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
file_system: Some(AppServerAdditionalFileSystemPermissions {
read: Some(vec![read_path.clone()]),
write: Some(vec![write_path.clone()]),
glob_scan_max_depth: None,
entries: None,
}),
}),
@@ -163,6 +164,7 @@ fn app_server_request_permissions_preserves_file_system_permissions() {
file_system: Some(AppServerAdditionalFileSystemPermissions {
read: Some(vec![read_path.clone()]),
write: Some(vec![write_path.clone()]),
glob_scan_max_depth: None,
entries: None,
}),
},