mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
protocol: canonicalize file system permissions
This commit is contained in:
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"globScanMaxDepth": {
|
||||
"format": "uint",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
|
||||
+1
-1
@@ -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>, };
|
||||
|
||||
@@ -2056,6 +2056,7 @@ mod tests {
|
||||
file_system: Some(v2::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
write: None,
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7084,6 +7092,7 @@ mod tests {
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
}
|
||||
@@ -7155,6 +7164,7 @@ mod tests {
|
||||
access: CoreFileSystemAccessMode::None,
|
||||
},
|
||||
],
|
||||
glob_scan_max_depth: Some(2),
|
||||
};
|
||||
|
||||
let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone());
|
||||
@@ -7163,6 +7173,7 @@ mod tests {
|
||||
AdditionalFileSystemPermissions {
|
||||
read: None,
|
||||
write: None,
|
||||
glob_scan_max_depth: Some(2),
|
||||
entries: Some(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -7225,6 +7236,7 @@ mod tests {
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1372,6 +1372,7 @@ mod tests {
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user