Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/app-server-protocol/schema/json/ServerRequest.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>, };
1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
}),
Expand Down
29 changes: 27 additions & 2 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::path::PathBuf;

use crate::RequestId;
Expand Down Expand Up @@ -1162,6 +1163,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<NonZeroUsize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub entries: Option<Vec<FileSystemSandboxEntry>>,
}

Expand All @@ -1171,12 +1175,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
Expand All @@ -1191,16 +1197,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
}
}

Expand Down Expand Up @@ -6950,6 +6959,7 @@ mod tests {
use codex_utils_absolute_path::test_support::test_path_buf;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::num::NonZeroUsize;
use std::path::PathBuf;

fn absolute_path_string(path: &str) -> String {
Expand Down Expand Up @@ -7084,6 +7094,7 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}
Expand Down Expand Up @@ -7155,6 +7166,7 @@ mod tests {
access: CoreFileSystemAccessMode::None,
},
],
glob_scan_max_depth: NonZeroUsize::new(2),
};

let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone());
Expand All @@ -7163,6 +7175,7 @@ mod tests {
AdditionalFileSystemPermissions {
read: None,
write: None,
glob_scan_max_depth: NonZeroUsize::new(2),
entries: Some(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
Expand All @@ -7185,6 +7198,17 @@ mod tests {
);
}

#[test]
fn additional_file_system_permissions_rejects_zero_glob_scan_depth() {
serde_json::from_value::<AdditionalFileSystemPermissions>(json!({
"read": null,
"write": null,
"globScanMaxDepth": 0,
"entries": [],
}))
.expect_err("zero glob scan depth should fail deserialization");
}

#[test]
fn permissions_request_approval_response_uses_granted_permission_profile_without_macos() {
let read_only_path = if cfg!(windows) {
Expand Down Expand Up @@ -7225,6 +7249,7 @@ mod tests {
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
.expect("path must be absolute"),
]),
glob_scan_max_depth: None,
entries: None,
}),
}
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
),
Expand Down Expand Up @@ -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,
},
),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/request_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
},
Expand Down
96 changes: 90 additions & 6 deletions codex-rs/protocol/src/models.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::fmt;
use std::io;
use std::num::NonZeroUsize;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
Expand Down Expand Up @@ -146,6 +147,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<NonZeroUsize>,
}

pub type LegacyReadWriteRoots = (Option<Vec<AbsolutePathBuf>>, Option<Vec<AbsolutePathBuf>>);
Expand All @@ -172,7 +174,10 @@ impl FileSystemPermissions {
access: FileSystemAccessMode::Write,
}));
}
Self { entries }
Self {
entries,
glob_scan_max_depth: None,
}
}

pub fn explicit_path_entries(
Expand All @@ -190,6 +195,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();

Expand Down Expand Up @@ -225,6 +234,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<NonZeroUsize>,
}

#[derive(Debug, Clone, Deserialize)]
Expand All @@ -244,6 +255,7 @@ impl Serialize for FileSystemPermissions {
} else {
CanonicalFileSystemPermissions {
entries: self.entries.clone(),
glob_scan_max_depth: self.glob_scan_max_depth,
}
.serialize(serializer)
}
Expand All @@ -256,9 +268,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))
}
Expand Down Expand Up @@ -352,13 +368,18 @@ impl From<&FileSystemSandboxPolicy> for FileSystemPermissions {
}]
}
};
Self { entries }
Self {
entries,
glob_scan_max_depth: value.glob_scan_max_depth.and_then(NonZeroUsize::new),
}
}
}

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.map(usize::from);
policy
}
}

Expand Down Expand Up @@ -1828,6 +1849,69 @@ 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: NonZeroUsize::new(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 file_system_permissions_rejects_zero_glob_scan_depth() {
serde_json::from_value::<FileSystemPermissions>(serde_json::json!({
"entries": [],
"glob_scan_max_depth": 0,
}))
.expect_err("zero glob scan depth should fail deserialization");
}

#[test]
fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() {
let contents = vec![serde_json::json!({
Expand Down
Loading
Loading