From 5620995b4e072ec51e85b6a6e7fb7f1b753a14df Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 20 Apr 2026 18:54:00 -0700 Subject: [PATCH] protocol: preserve glob scan depth in permission profiles --- ...CommandExecutionRequestApprovalParams.json | 8 ++ .../PermissionsRequestApprovalParams.json | 8 ++ .../PermissionsRequestApprovalResponse.json | 8 ++ .../schema/json/ServerRequest.json | 8 ++ .../codex_app_server_protocol.schemas.json | 8 ++ .../v2/AdditionalFileSystemPermissions.ts | 2 +- .../src/protocol/common.rs | 1 + .../app-server-protocol/src/protocol/v2.rs | 29 ++++- codex-rs/app-server/src/transport/mod.rs | 2 + .../tests/suite/v2/request_permissions.rs | 1 + codex-rs/protocol/src/models.rs | 96 ++++++++++++++- codex-rs/sandboxing/src/policy_transforms.rs | 75 +++++++++++- .../sandboxing/src/policy_transforms_tests.rs | 109 ++++++++++++++++++ codex-rs/tui/src/app/app_server_requests.rs | 1 + codex-rs/tui/src/app/tests.rs | 2 + .../src/app_server_approval_conversions.rs | 3 + .../tui/src/bottom_pane/approval_overlay.rs | 1 + .../src/chatwidget/tests/approval_requests.rs | 2 + 18 files changed, 352 insertions(+), 12 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index b55e14f4ad8c..3968da2a39ff 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json index 2c8bfba83cde..e389983bfa7c 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalParams.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json index 4564138527d1..11cce8c88cb9 100644 --- a/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json +++ b/codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index b778ae020521..298b97b4bfb0 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 7f645dfd3de3..42ca071963a2 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -16,6 +16,14 @@ "null" ] }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, "read": { "items": { "$ref": "#/definitions/v2/AbsolutePathBuf" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts index 21af5dda71ea..2be8f7f0e7ca 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalFileSystemPermissions.ts @@ -4,4 +4,4 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; -export type AdditionalFileSystemPermissions = { read: Array | null, write: Array | null, entries?: Array, }; +export type AdditionalFileSystemPermissions = { read: Array | null, write: Array | null, globScanMaxDepth?: number, entries?: Array, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 62533156107e..aa3ff8331ca8 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -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, }), }), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 77f339db533f..a029860f2a47 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::collections::HashMap; +use std::num::NonZeroUsize; use std::path::PathBuf; use crate::RequestId; @@ -1162,6 +1163,9 @@ pub struct AdditionalFileSystemPermissions { pub write: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] + pub glob_scan_max_depth: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] pub entries: Option>, } @@ -1171,12 +1175,14 @@ impl From 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 +1197,19 @@ impl From for AdditionalFileSystemPermissions { impl From 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 } } @@ -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 { @@ -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, }), } @@ -7155,6 +7166,7 @@ mod tests { access: CoreFileSystemAccessMode::None, }, ], + glob_scan_max_depth: NonZeroUsize::new(2), }; let permissions = AdditionalFileSystemPermissions::from(core_permissions.clone()); @@ -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 { @@ -7185,6 +7198,17 @@ mod tests { ); } + #[test] + fn additional_file_system_permissions_rejects_zero_glob_scan_depth() { + serde_json::from_value::(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) { @@ -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, }), } diff --git a/codex-rs/app-server/src/transport/mod.rs b/codex-rs/app-server/src/transport/mod.rs index d12107b1d8e0..a07bd44188ac 100644 --- a/codex-rs/app-server/src/transport/mod.rs +++ b/codex-rs/app-server/src/transport/mod.rs @@ -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, }, ), diff --git a/codex-rs/app-server/tests/suite/v2/request_permissions.rs b/codex-rs/app-server/tests/suite/v2/request_permissions.rs index 05a45011dea8..295ac96a3bd1 100644 --- a/codex-rs/app-server/tests/suite/v2/request_permissions.rs +++ b/codex-rs/app-server/tests/suite/v2/request_permissions.rs @@ -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, }), }, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 25c44fae09c2..b0134630f388 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -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; @@ -146,6 +147,7 @@ impl SandboxPermissions { #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, JsonSchema, TS)] pub struct FileSystemPermissions { pub entries: Vec, + pub glob_scan_max_depth: Option, } pub type LegacyReadWriteRoots = (Option>, Option>); @@ -172,7 +174,10 @@ impl FileSystemPermissions { access: FileSystemAccessMode::Write, })); } - Self { entries } + Self { + entries, + glob_scan_max_depth: None, + } } pub fn explicit_path_entries( @@ -190,6 +195,10 @@ impl FileSystemPermissions { } fn as_legacy_permissions(&self) -> Option { + if self.glob_scan_max_depth.is_some() { + return None; + } + let mut read = Vec::new(); let mut write = Vec::new(); @@ -225,6 +234,8 @@ struct LegacyFileSystemPermissions { struct CanonicalFileSystemPermissions { #[serde(default, skip_serializing_if = "Vec::is_empty")] entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + glob_scan_max_depth: Option, } #[derive(Debug, Clone, Deserialize)] @@ -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) } @@ -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)) } @@ -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 } } @@ -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::(serialized)?, + file_system_permissions + ); + Ok(()) + } + + #[test] + fn file_system_permissions_rejects_zero_glob_scan_depth() { + serde_json::from_value::(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!({ diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index aa0ccc267b0e..2b8c01eaae4d 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -13,6 +13,7 @@ use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; use std::collections::HashSet; +use std::num::NonZeroUsize; #[derive(Debug, Clone, PartialEq, Eq)] pub struct EffectiveSandboxPermissions { @@ -45,6 +46,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 +75,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 +119,13 @@ 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: merge_glob_scan_max_depth( + &base.entries, + base.glob_scan_max_depth.map(usize::from), + &permissions.entries, + permissions.glob_scan_max_depth.map(usize::from), + ) + .and_then(NonZeroUsize::new), }) .filter(|file_system| !file_system.is_empty()), (Some(base), None) => Some(base.clone()), @@ -139,12 +151,21 @@ pub fn intersect_permission_profiles( .file_system .map(|requested_file_system| { let granted_file_system = granted.file_system.unwrap_or_default(); - let entries = requested_file_system + let entries: Vec<_> = requested_file_system .entries .into_iter() .filter(|entry| granted_file_system.entries.contains(entry)) .collect(); - FileSystemPermissions { entries } + FileSystemPermissions { + glob_scan_max_depth: merge_glob_scan_max_depth( + &entries, + requested_file_system.glob_scan_max_depth.map(usize::from), + &entries, + granted_file_system.glob_scan_max_depth.map(usize::from), + ) + .and_then(NonZeroUsize::new), + entries, + } }) .filter(|file_system| !file_system.is_empty()); let network = match (requested.network, granted.network) { @@ -167,6 +188,48 @@ pub fn intersect_permission_profiles( } } +fn merge_glob_scan_max_depth( + left_entries: &[FileSystemSandboxEntry], + left_depth: Option, + right_entries: &[FileSystemSandboxEntry], + right_depth: Option, +) -> Option { + let left_depth = effective_glob_scan_depth(left_entries, left_depth); + let right_depth = effective_glob_scan_depth(right_entries, right_depth); + + match (left_depth, right_depth) { + (Some(GlobScanDepth::Unbounded), _) | (_, Some(GlobScanDepth::Unbounded)) => None, + (Some(GlobScanDepth::Bounded(left)), Some(GlobScanDepth::Bounded(right))) => { + Some(left.max(right)) + } + (Some(GlobScanDepth::Bounded(depth)), None) + | (None, Some(GlobScanDepth::Bounded(depth))) => Some(depth), + (None, None) => None, + } +} + +fn effective_glob_scan_depth( + entries: &[FileSystemSandboxEntry], + depth: Option, +) -> Option { + entries + .iter() + .any(|entry| { + entry.access == FileSystemAccessMode::None + && matches!(&entry.path, FileSystemPath::GlobPattern { .. }) + }) + .then_some(match depth { + Some(depth) => GlobScanDepth::Bounded(depth), + None => GlobScanDepth::Unbounded, + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GlobScanDepth { + Bounded(usize), + Unbounded, +} + fn merge_permission_entries( base: &[FileSystemSandboxEntry], permissions: &[FileSystemSandboxEntry], @@ -234,6 +297,12 @@ fn merge_file_system_policy_with_additional_permissions( merged_policy.entries.push(entry.clone()); } } + merged_policy.glob_scan_max_depth = merge_glob_scan_max_depth( + &file_system_policy.entries, + file_system_policy.glob_scan_max_depth, + &additional_permissions.entries, + additional_permissions.glob_scan_max_depth.map(usize::from), + ); merged_policy } FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 5de8a5cd5aa0..8fcff965fe94 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -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: std::num::NonZeroUsize::new(2), }), ..Default::default() }) @@ -207,6 +209,7 @@ fn normalize_additional_permissions_preserves_deny_globs() { }, access: FileSystemAccessMode::None, }], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), }), ..Default::default() } @@ -288,6 +291,76 @@ fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() { ); } +#[test] +fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { + let deny_env_files = FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }; + let requested = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), + }), + ..Default::default() + }; + let granted = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(4), + }), + ..Default::default() + }; + + assert_eq!( + intersect_permission_profiles(requested, granted), + PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files], + glob_scan_max_depth: std::num::NonZeroUsize::new(4), + }), + ..Default::default() + } + ); +} + +#[test] +fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { + let deny_env_files = FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }; + let requested = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), + }), + ..Default::default() + }; + let granted = PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: None, + }), + ..Default::default() + }; + + assert_eq!( + intersect_permission_profiles(requested, granted), + PermissionProfile { + file_system: Some(FileSystemPermissions { + entries: vec![deny_env_files], + glob_scan_max_depth: None, + }), + ..Default::default() + } + ); +} + #[test] fn read_only_additional_permissions_can_enable_network_without_writes() { let temp_dir = TempDir::new().expect("create temp dir"); @@ -402,6 +475,42 @@ fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roo ); } +#[test] +fn merge_file_system_policy_with_additional_permissions_carries_bounded_glob_scan_depth() { + let deny_env_files = FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }; + let merged_policy = merge_file_system_policy_with_additional_permissions( + &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }]), + &FileSystemPermissions { + entries: vec![deny_env_files.clone()], + glob_scan_max_depth: std::num::NonZeroUsize::new(2), + }, + ); + + assert_eq!(merged_policy, { + let mut policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }, + deny_env_files, + ]); + policy.glob_scan_max_depth = Some(2); + policy + }); +} + #[test] fn effective_file_system_sandbox_policy_returns_base_policy_without_additional_permissions() { let temp_dir = TempDir::new().expect("create temp dir"); diff --git a/codex-rs/tui/src/app/app_server_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index c54aae826f5a..58ee962fdb4e 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -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, }), }, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 2fad117cc0eb..5ed786ef050d 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2601,6 +2601,7 @@ async fn inactive_thread_exec_approval_preserves_context() { 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, }), }); @@ -2708,6 +2709,7 @@ async fn inactive_thread_permissions_approval_preserves_file_system_permissions( 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, }), }, diff --git a/codex-rs/tui/src/app_server_approval_conversions.rs b/codex-rs/tui/src/app_server_approval_conversions.rs index 7b8e777d5b46..a26f9b2a902f 100644 --- a/codex-rs/tui/src/app_server_approval_conversions.rs +++ b/codex-rs/tui/src/app_server_approval_conversions.rs @@ -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, diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 1c148cd02aa2..5212d81953d3 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -1372,6 +1372,7 @@ mod tests { access: FileSystemAccessMode::None, }, ], + glob_scan_max_depth: None, }), ..Default::default() }; diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index 472a13c118a0..4e5bc7556ebd 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -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, }), },