From 87f71e35b86cc4d2da4d81728004adac45a9dd3a Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Tue, 21 Jul 2026 19:11:29 +0000 Subject: [PATCH] Skip missing paths in filesystem sandbox entries (#34598) ## Why Default read-only protections for project metadata should apply when paths such as `.git`, `.agents`, and `.codex` exist, without causing sandbox setup to materialize missing paths as ACL targets. ## What changed - Add an optional `missing_path_behavior` to filesystem sandbox entries and mark default project-metadata protections with `skip`. - Preserve the behavior through permission transforms and exec/MCP protocol serialization while keeping existing path wire variants stable. - Ignore skip-missing entries when projecting configuration or Windows sandbox overrides, while retaining explicit metadata carveouts. ## Testing - Cover protocol round trips for path and special-path entries. - Verify default metadata protections and Windows explicit carveout handling. GitOrigin-RevId: 6df13dadacdd131c44aab9f15a967c81051355c1 --- .../src/protocol/v2/permissions.rs | 1 + .../src/protocol/v2/tests.rs | 2 + .../app-server/src/bespoke_event_handling.rs | 2 + .../thread_processor_tests.rs | 2 + codex-rs/codex-mcp/src/runtime.rs | 57 ++++ codex-rs/core/src/config/config_tests.rs | 18 + codex-rs/core/src/config/mod.rs | 2 + codex-rs/core/src/config/permissions.rs | 7 + codex-rs/core/src/config/permissions_tests.rs | 1 + .../world_state/environment_render_tests.rs | 3 + codex-rs/core/src/exec_policy_tests.rs | 5 + .../core/src/exec_policy_windows_tests.rs | 2 + codex-rs/core/src/exec_tests.rs | 72 ++++ codex-rs/core/src/guardian/tests.rs | 3 + codex-rs/core/src/safety_tests.rs | 5 + codex-rs/core/src/sandbox_tags_tests.rs | 2 + codex-rs/core/src/session/tests.rs | 9 + codex-rs/core/src/tools/handlers/mod.rs | 2 + .../src/tools/handlers/multi_agents_tests.rs | 1 + .../runtimes/shell/unix_escalation_tests.rs | 5 + codex-rs/core/src/tools/sandboxing_tests.rs | 1 + codex-rs/core/tests/suite/apply_patch_cli.rs | 4 + codex-rs/core/tests/suite/approvals.rs | 1 + .../core/tests/suite/extension_sandbox.rs | 1 + codex-rs/core/tests/suite/remote_env.rs | 4 + codex-rs/core/tests/suite/tools.rs | 1 + codex-rs/core/tests/suite/unified_exec.rs | 1 + .../suite/unified_exec_zsh_fork_approvals.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 1 + codex-rs/core/tests/suite/windows_sandbox.rs | 61 ++++ codex-rs/exec-server-protocol/src/protocol.rs | 73 ++++- codex-rs/exec-server/src/fs_sandbox.rs | 19 +- .../exec-server/src/remote_file_system.rs | 2 + .../src/remote_file_system_path_uri_tests.rs | 1 + codex-rs/exec-server/tests/exec_process.rs | 5 + codex-rs/exec-server/tests/file_stream.rs | 1 + .../exec-server/tests/file_system/support.rs | 2 + .../exec-server/tests/file_system_unix.rs | 2 + ...event_processor_with_human_output_tests.rs | 2 + codex-rs/file-system/src/lib.rs | 5 + codex-rs/linux-sandbox/src/bwrap.rs | 60 +++- codex-rs/linux-sandbox/src/linux_run_main.rs | 1 + .../linux-sandbox/src/linux_run_main_tests.rs | 8 + .../linux-sandbox/tests/suite/landlock.rs | 11 + .../src/permissions_instructions_tests.rs | 4 + codex-rs/protocol/src/models.rs | 27 +- codex-rs/protocol/src/permissions.rs | 308 ++++++++++++------ codex-rs/protocol/src/protocol.rs | 12 + codex-rs/sandboxing/src/manager_tests.rs | 14 + codex-rs/sandboxing/src/policy_transforms.rs | 3 + .../sandboxing/src/policy_transforms_tests.rs | 43 +++ codex-rs/sandboxing/src/seatbelt_tests.rs | 5 + codex-rs/sandboxing/src/windows.rs | 31 +- codex-rs/tui/src/additional_dirs.rs | 2 + codex-rs/tui/src/app/thread_session_state.rs | 2 + codex-rs/tui/src/app_server_session.rs | 4 + .../chatwidget/tests/composer_submission.rs | 2 + .../src/chatwidget/tests/history_replay.rs | 2 + .../tui/src/chatwidget/tests/permissions.rs | 7 + codex-rs/tui/src/permission_compat.rs | 2 + codex-rs/tui/src/status/tests.rs | 4 + .../src/deny_read_resolver.rs | 3 + .../src/resolved_permissions.rs | 13 +- 63 files changed, 825 insertions(+), 132 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs index a6976c9c07..58c3735ba1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -368,6 +368,7 @@ impl TryFrom for CoreFileSystemSandboxEntry { Ok(Self { path: value.path.try_into()?, access: value.access.to_core(), + missing_path_behavior: None, }) } } diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 5c9445307f..23c0c2a941 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -591,12 +591,14 @@ fn additional_file_system_permissions_preserves_canonical_entries() { value: CoreFileSystemSpecialPath::Root, }, access: CoreFileSystemAccessMode::Write, + missing_path_behavior: None, }, CoreFileSystemSandboxEntry { path: CoreFileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: CoreFileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: NonZeroUsize::new(2), diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index c51fd7ffd8..d9de3e5a40 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -3071,6 +3071,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), @@ -3118,6 +3119,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index 38a94ab812..7c2c286f77 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -520,12 +520,14 @@ mod thread_processor_behavior_tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd.clone() }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "/tmp/project/**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/codex-mcp/src/runtime.rs b/codex-rs/codex-mcp/src/runtime.rs index 93caad8b25..d47a5f2238 100644 --- a/codex-rs/codex-mcp/src/runtime.rs +++ b/codex-rs/codex-mcp/src/runtime.rs @@ -148,6 +148,7 @@ mod tests { use codex_exec_server::EnvironmentManager; use codex_utils_path_uri::LegacyAppPathString; use pretty_assertions::assert_eq; + use serde_json::Value; use super::*; @@ -192,6 +193,62 @@ mod tests { } } + #[test] + fn sandbox_state_serializes_skip_missing_entries_as_missing_path_behavior() { + let sandbox_cwd = PathUri::from_host_native_path( + std::env::current_dir().expect("current directory should be available"), + ) + .expect("current directory should convert to a URI"); + let sandbox_state = SandboxState { + permission_profile: PermissionProfile::workspace_write(), + codex_linux_sandbox_exe: None, + sandbox_cwd, + use_legacy_landlock: false, + }; + + let serialized = serde_json::to_value(&sandbox_state).expect("serialize sandbox state"); + let serialized_text = serde_json::to_string(&serialized).expect("serialize JSON text"); + assert!( + !serialized_text.contains("generated_default_path"), + "MCP sandbox metadata must preserve FileSystemPath's stable wire variants" + ); + assert!( + !serialized_text.contains("generated_default_special"), + "MCP sandbox metadata must preserve FileSystemPath's stable wire variants" + ); + + let entries = serialized + .pointer("/permissionProfile/file_system/entries") + .and_then(Value::as_array) + .expect("workspace-write profile should contain filesystem entries"); + let skip_missing_entries = entries + .iter() + .filter(|entry| { + entry.get("missing_path_behavior").and_then(Value::as_str) == Some("skip") + }) + .collect::>(); + assert!( + !skip_missing_entries.is_empty(), + "skip-missing entries should be represented as optional missing_path_behavior" + ); + assert!( + skip_missing_entries.iter().all(|entry| { + matches!( + entry.pointer("/path/type").and_then(Value::as_str), + Some("path" | "special") + ) + }), + "skip-missing entries should use the stable path/special variants" + ); + + let deserialized: SandboxState = + serde_json::from_value(serialized).expect("deserialize sandbox state"); + assert_eq!( + deserialized.permission_profile, + sandbox_state.permission_profile + ); + } + #[test] fn local_stdio_requires_local_stdio_availability() { let runtime_context = McpRuntimeContext::new( diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index fbba8c3332..b4a2d1b30f 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2037,18 +2037,21 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd_root.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd_root.join("docs"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]), ); @@ -2295,12 +2298,14 @@ async fn permission_profile_override_keeps_memories_root_out_of_legacy_projectio value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, @@ -2461,6 +2466,7 @@ async fn workspace_root_glob_none_compiles_to_filesystem_pattern_entry() -> std: pattern: expected_pattern, }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }) ); } @@ -2800,6 +2806,7 @@ async fn default_permissions_profile_can_extend_builtin_workspace() -> std::io:: value: FileSystemSpecialPath::SlashTmp, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, } )), "expected profile extending :workspace to keep inherited :slash_tmp writes, policy: {policy:?}" @@ -2812,6 +2819,7 @@ async fn default_permissions_profile_can_extend_builtin_workspace() -> std::io:: value: FileSystemSpecialPath::Tmpdir, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, } )), "expected child :tmpdir read entry to replace the inherited write entry, policy: {policy:?}" @@ -2824,6 +2832,7 @@ async fn default_permissions_profile_can_extend_builtin_workspace() -> std::io:: value: FileSystemSpecialPath::Tmpdir, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, } )), "expected inherited :tmpdir write entry to be removed, policy: {policy:?}" @@ -3446,6 +3455,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<( ), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]), ); assert_eq!( @@ -3492,6 +3502,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries() value: FileSystemSpecialPath::unknown(":future_special_path", Some("docs".into())), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]), ); assert!( @@ -4247,6 +4258,7 @@ exclude_slash_tmp = true .contains(&FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd.abs() }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }) ); assert!( @@ -4257,6 +4269,7 @@ exclude_slash_tmp = true path: extra_root.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }) ); for subpath in [".git", ".agents", ".codex"] { @@ -4271,6 +4284,9 @@ exclude_slash_tmp = true ), }, access: FileSystemAccessMode::Read, + missing_path_behavior: Some( + codex_protocol::permissions::FileSystemSandboxEntryMissingPathBehavior::Skip, + ), }), "case `{name}` should materialize `{subpath}` for the runtime workspace \ root" @@ -9993,12 +10009,14 @@ async fn permission_profile_override_preserves_split_write_roots() -> std::io::R value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: outside_root.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 0730fd0217..715a52fb11 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2410,6 +2410,7 @@ fn apply_managed_filesystem_constraints( pattern: deny_read.as_str().to_string(), }, access: codex_protocol::permissions::FileSystemAccessMode::Deny, + missing_path_behavior: None, } } else { let Ok(path) = AbsolutePathBuf::try_from(deny_read.as_str()) else { @@ -2418,6 +2419,7 @@ fn apply_managed_filesystem_constraints( codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path }, access: codex_protocol::permissions::FileSystemAccessMode::Deny, + missing_path_behavior: None, } }; if !file_system_sandbox_policy diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 47fa863619..6b75ebef92 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -236,6 +236,10 @@ fn insert_filesystem_permission_toml( entries: &mut BTreeMap, entry: FileSystemSandboxEntry, ) { + if entry.skips_missing_path() { + return; + } + match entry.path { FileSystemPath::Path { path } => { entries.insert( @@ -526,6 +530,7 @@ fn compile_filesystem_permission( entries.push(FileSystemSandboxEntry { path: compile_filesystem_access_path(path, *access, startup_warnings)?, access: *access, + missing_path_behavior: None, }); } FilesystemPermissionToml::Scoped(scoped_entries) => { @@ -544,6 +549,7 @@ fn compile_filesystem_permission( pattern: compile_scoped_filesystem_pattern(path, subpath, *access)?, }, access: *access, + missing_path_behavior: None, }; entries.push(entry); } else { @@ -551,6 +557,7 @@ fn compile_filesystem_permission( entries.push(FileSystemSandboxEntry { path: compile_scoped_filesystem_path(path, subpath, startup_warnings)?, access: *access, + missing_path_behavior: None, }); } } diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index 9c78a1d5e2..130e9d7f06 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -577,6 +577,7 @@ fn read_write_trailing_glob_suffix_compiles_as_subpath() -> std::io::Result<()> value: FileSystemSpecialPath::project_roots(Some("docs".into())), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]), "trailing /** should compile as a subtree path instead of a glob pattern" ); diff --git a/codex-rs/core/src/context/world_state/environment_render_tests.rs b/codex-rs/core/src/context/world_state/environment_render_tests.rs index b943ef997a..9ddbe9fbf2 100644 --- a/codex-rs/core/src/context/world_state/environment_render_tests.rs +++ b/codex-rs/core/src/context/world_state/environment_render_tests.rs @@ -152,18 +152,21 @@ fn workspace_write_permission_profile_with_private_denials() -> PermissionProfil value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some("private".to_string())), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: project_roots_glob_pattern(Path::new("private/**")), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index 5b3e33d3a3..dbec259d93 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -1182,12 +1182,14 @@ fn managed_cwd_write_profile_has_filesystem_restrictions() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -1208,6 +1210,7 @@ fn managed_unresolvable_write_profile_has_filesystem_restrictions() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -1217,6 +1220,7 @@ fn managed_unresolvable_write_profile_has_filesystem_restrictions() { ), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -1237,6 +1241,7 @@ fn managed_full_disk_write_profile_has_no_filesystem_restrictions() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let permission_profile = PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, diff --git a/codex-rs/core/src/exec_policy_windows_tests.rs b/codex-rs/core/src/exec_policy_windows_tests.rs index 735cdd4ddc..bdf1b5f5e3 100644 --- a/codex-rs/core/src/exec_policy_windows_tests.rs +++ b/codex-rs/core/src/exec_policy_windows_tests.rs @@ -142,12 +142,14 @@ fn writable_windows_policy_without_sandbox_backend_still_requires_approval() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 1a4477599d..7a0af61fe8 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -441,6 +441,7 @@ fn windows_restricted_token_rejects_managed_root_write_profiles() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -512,6 +513,7 @@ fn windows_elevated_allows_split_restricted_read_policies() { codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path: docs }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -543,6 +545,7 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { @@ -550,6 +553,7 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { .expect("absolute docs"), }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -582,6 +586,7 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { @@ -589,6 +594,7 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { .expect("absolute docs"), }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -624,6 +630,7 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -632,10 +639,12 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path: docs.clone() }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -679,6 +688,7 @@ fn windows_restricted_token_rejects_unreadable_split_carveouts() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -687,10 +697,12 @@ fn windows_restricted_token_rejects_unreadable_split_carveouts() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path: blocked }, access: codex_protocol::permissions::FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -725,6 +737,7 @@ fn windows_elevated_supports_split_restricted_read_roots() { .expect("absolute docs"), }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -761,6 +774,7 @@ fn windows_elevated_supports_split_write_read_carveouts() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -769,6 +783,7 @@ fn windows_elevated_supports_split_write_read_carveouts() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { @@ -776,6 +791,7 @@ fn windows_elevated_supports_split_write_read_carveouts() { .expect("absolute docs"), }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -803,6 +819,52 @@ fn windows_elevated_supports_split_write_read_carveouts() { ); } +#[cfg(target_os = "windows")] +#[test] +fn windows_workspace_defaults_do_not_hide_explicit_metadata_carveouts() { + let temp_dir = tempfile::TempDir::new().expect("tempdir"); + let cwd = temp_dir.path().canonicalize().expect("canonical cwd").abs(); + + let default_profile = PermissionProfile::workspace_write(); + let default_overrides = resolve_windows_elevated_filesystem_overrides( + SandboxType::WindowsRestrictedToken, + &default_profile, + &cwd, + /*use_windows_elevated_backend*/ true, + ) + .expect("resolve workspace defaults"); + assert!( + default_overrides.is_none_or(|overrides| overrides.additional_deny_write_paths.is_empty()) + ); + + for name in codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES { + let (mut explicit_policy, network_policy) = default_profile.to_runtime_permissions(); + explicit_policy + .entries + .push(codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Special { + value: codex_protocol::permissions::FileSystemSpecialPath::project_roots(Some( + (*name).into(), + )), + }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, + }); + let explicit_profile = + PermissionProfile::from_runtime_permissions(&explicit_policy, network_policy); + + let overrides = resolve_windows_elevated_filesystem_overrides( + SandboxType::WindowsRestrictedToken, + &explicit_profile, + &cwd, + /*use_windows_elevated_backend*/ true, + ) + .expect("resolve explicit metadata carveout") + .expect("explicit metadata carveout needs an override"); + assert_eq!(overrides.additional_deny_write_paths, vec![cwd.join(name)]); + } +} + #[test] fn windows_elevated_supports_unreadable_split_carveouts() { let temp_dir = tempfile::TempDir::new().expect("tempdir"); @@ -815,6 +877,7 @@ fn windows_elevated_supports_unreadable_split_carveouts() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -823,6 +886,7 @@ fn windows_elevated_supports_unreadable_split_carveouts() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { @@ -830,6 +894,7 @@ fn windows_elevated_supports_unreadable_split_carveouts() { .expect("absolute blocked"), }, access: codex_protocol::permissions::FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -874,6 +939,7 @@ fn windows_elevated_supports_unreadable_globs() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -882,12 +948,14 @@ fn windows_elevated_supports_unreadable_globs() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: codex_protocol::permissions::FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -927,6 +995,7 @@ fn windows_elevated_rejects_reopened_writable_descendants() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -935,6 +1004,7 @@ fn windows_elevated_rejects_reopened_writable_descendants() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { @@ -942,6 +1012,7 @@ fn windows_elevated_rejects_reopened_writable_descendants() { .expect("absolute docs"), }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { @@ -949,6 +1020,7 @@ fn windows_elevated_rejects_reopened_writable_descendants() { .expect("absolute nested"), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 117dd512c9..6b8ea2524a 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -473,18 +473,21 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_root.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: denied_glob.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/core/src/safety_tests.rs b/codex-rs/core/src/safety_tests.rs index 6f7efe893b..a8d5821890 100644 --- a/codex-rs/core/src/safety_tests.rs +++ b/codex-rs/core/src/safety_tests.rs @@ -230,12 +230,14 @@ fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked_absolute, }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -278,12 +280,14 @@ fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs_absolute, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -327,6 +331,7 @@ fn missing_project_dot_codex_config_requires_approval() { path: cwd.join(".codex"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }); assert!(!is_write_patch_constrained_to_writable_paths( diff --git a/codex-rs/core/src/sandbox_tags_tests.rs b/codex-rs/core/src/sandbox_tags_tests.rs index 64dc50574f..e96f3eccc9 100644 --- a/codex-rs/core/src/sandbox_tags_tests.rs +++ b/codex-rs/core/src/sandbox_tags_tests.rs @@ -98,6 +98,7 @@ fn root_write_managed_profile_with_enabled_network_is_untagged() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }, @@ -148,6 +149,7 @@ fn profile_policy_tag_reports_closest_legacy_mode() { path: writable_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], }, NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 26773f3c93..f32a5e6747 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -4606,10 +4606,12 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs_dir }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); @@ -4653,6 +4655,7 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }; let mut existing_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( @@ -4716,6 +4719,7 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo path: external_write_path.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let permission_profile = PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, @@ -4965,12 +4969,14 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: original_cwd.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); session_configuration @@ -6208,6 +6214,7 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir path: environment_cwd.join("relative.txt"), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), @@ -6294,6 +6301,7 @@ async fn request_permissions_response_materializes_session_cwd_grants_before_rec value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), @@ -8858,6 +8866,7 @@ fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSy pattern: format!("{cwd_display}/**/*.env"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); policy } diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 6ae144663a..3eaf39c3cf 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -444,12 +444,14 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 55a24254da..8ed94d8ec2 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -2283,6 +2283,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { pattern: "**/.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); let expected_network_sandbox_policy = NetworkSandboxPolicy::from(&expected_sandbox); let expected_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index be88bf3947..e50406f15c 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -69,6 +69,7 @@ fn read_only_file_system_sandbox_policy() -> FileSystemSandboxPolicy { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]) } @@ -79,12 +80,14 @@ fn denied_read_file_system_sandbox_policy() -> FileSystemSandboxPolicy { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]) } @@ -299,12 +302,14 @@ fn shell_request_escalation_execution_is_explicit() { path: AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: AbsolutePathBuf::from_absolute_path("/tmp/secret").unwrap(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let network_sandbox_policy = NetworkSandboxPolicy::Restricted; diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index d65994a9dd..e1b261b7d6 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -151,6 +151,7 @@ fn deny_read_blocks_explicit_escalation_and_policy_bypass() { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }]); assert_eq!( diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index 51ea0421ea..7fbe55d8c0 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -140,12 +140,14 @@ fn workspace_write_with_read_only_root(read_only_root: AbsolutePathBuf) -> Permi path: read_only_root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); PermissionProfile::from_runtime_permissions( @@ -162,12 +164,14 @@ fn workspace_write_with_unreadable_path(unreadable_path: AbsolutePathBuf) -> Per path: unreadable_path, }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); PermissionProfile::from_runtime_permissions( diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 7b589bb94c..c2aa4e9060 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -3588,6 +3588,7 @@ allow_local_binding = true pattern: format!("{}/**/*.env", test.config.cwd.as_path().display()), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); assert!( file_system_sandbox_policy.has_denied_read_restrictions(), diff --git a/codex-rs/core/tests/suite/extension_sandbox.rs b/codex-rs/core/tests/suite/extension_sandbox.rs index c8bef5de7c..e58cb2275c 100644 --- a/codex-rs/core/tests/suite/extension_sandbox.rs +++ b/codex-rs/core/tests/suite/extension_sandbox.rs @@ -115,6 +115,7 @@ async fn extension_tool_receives_turn_environment_sandbox() -> Result<()> { path: denied_path.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); let permission_profile = PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 3d62201514..05d4a38cff 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -1129,6 +1129,7 @@ fn read_only_sandbox(readable_root: PathBuf) -> FileSystemSandboxContext { path: readable_root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]), NetworkSandboxPolicy::Restricted, )) @@ -1142,6 +1143,7 @@ fn workspace_write_sandbox(writable_root: PathBuf) -> FileSystemSandboxContext { path: writable_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]), NetworkSandboxPolicy::Restricted, )) @@ -1361,12 +1363,14 @@ async fn remote_exec_materializes_target_roots_before_sandbox_selection() -> Res value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index 5e47eae52e..71d7e6f9b8 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -453,6 +453,7 @@ async fn shell_command_enforces_glob_deny_read_policy() -> Result<()> { pattern: format!("{}/**/*.env", config.cwd.as_path().display()), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); config .permissions diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 7ede6f7216..5fa4f265d2 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -3095,6 +3095,7 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { pattern: format!("{}/**/*.env", config.cwd.as_path().display()), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); config .permissions diff --git a/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs b/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs index 0d913d6541..4c4fc0e895 100644 --- a/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs +++ b/codex-rs/core/tests/suite/unified_exec_zsh_fork_approvals.rs @@ -337,6 +337,7 @@ fn permission_profile_from_toml(profile: &str) -> Result { Ok(FileSystemSandboxEntry { path, access: *access, + missing_path_behavior: None, }) }) .collect::>>()?; diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index ab567f9845..5181847ade 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -532,6 +532,7 @@ async fn view_image_tool_applies_local_sandbox_read_denies() -> anyhow::Result<( path: denied_path.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); let permission_profile = PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, diff --git a/codex-rs/core/tests/suite/windows_sandbox.rs b/codex-rs/core/tests/suite/windows_sandbox.rs index b8895f49e4..e38fa60b84 100644 --- a/codex-rs/core/tests/suite/windows_sandbox.rs +++ b/codex-rs/core/tests/suite/windows_sandbox.rs @@ -134,24 +134,28 @@ async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> a value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: future_secret, }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -197,6 +201,59 @@ async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> a Ok(()) } +#[tokio::test] +#[serial(codex_home)] +async fn windows_elevated_does_not_create_missing_workspace_metadata() -> anyhow::Result<()> { + let codex_home = + codex_home_for_windows_sandbox_test("windows-elevated-missing-metadata-codex-home")?; + let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); + stage_windows_sandbox_helpers()?; + let workspace = TempDir::new()?; + let cwd = dunce::canonicalize(workspace.path())?.abs(); + let permission_profile = PermissionProfile::workspace_write() + .materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd)); + + let output = process_exec_tool_call( + ExecParams { + command: vec![ + "cmd.exe".to_string(), + "/D".to_string(), + "/C".to_string(), + "echo sandbox-ok".to_string(), + ], + cwd: cwd.clone(), + expiration: 10_000.into(), + capture_policy: ExecCapturePolicy::ShellTool, + env: HashMap::new(), + network: None, + network_environment_id: None, + sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Elevated, + windows_sandbox_private_desktop: false, + justification: None, + arg0: None, + }, + &permission_profile, + &cwd, + std::slice::from_ref(&cwd), + &None, + /*use_legacy_landlock*/ false, + /*stdout_stream*/ None, + ) + .await?; + + assert_eq!(output.exit_code, 0, "sandboxed command should complete"); + for name in codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES { + let path = cwd.join(name); + assert!( + !path.exists(), + "elevated setup should not create missing workspace metadata: {}", + path.display() + ); + } + Ok(()) +} + #[tokio::test] #[serial(codex_home)] async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyhow::Result<()> { @@ -219,22 +276,26 @@ async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyh value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: exact_secret }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index 0b2da10722..0468c413d5 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -743,11 +743,13 @@ mod tests { use codex_network_proxy::NetworkProxyConfig; use codex_network_proxy::RemoteNetworkProxyConfig; use codex_network_proxy::RemoteNetworkProxyLaunchConfig; + use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; + use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; @@ -871,7 +873,75 @@ mod tests { } #[test] - fn filesystem_protocol_round_trips_permission_paths_as_uris() { + fn filesystem_protocol_round_trips_permission_entries() { + let native_cwd = std::env::current_dir().expect("current directory"); + let cwd = PathUri::from_host_native_path(&native_cwd).expect("cwd URI"); + let file_system = ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: native_cwd.clone().try_into().expect("absolute cwd"), + }, + access: FileSystemAccessMode::Read, + missing_path_behavior: None, + }, + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Path { + path: native_cwd.join(".git").try_into().expect("absolute path"), + }, + FileSystemAccessMode::Read, + ), + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Special { + value: FileSystemSpecialPath::ProjectRoots { + subpath: Some(".codex".into()), + }, + }, + FileSystemAccessMode::Read, + ), + ], + glob_scan_max_depth: Some(2.try_into().expect("non-zero depth")), + }; + let permissions = PermissionProfile::Managed { + file_system, + network: NetworkSandboxPolicy::Restricted, + }; + let sandbox = + FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, cwd.clone()); + + let serialized = serde_json::to_value(&sandbox).expect("serialize sandbox"); + + assert_eq!( + serialized["permissions"]["file_system"]["entries"][0]["path"]["path"], + serde_json::json!(cwd.to_string()) + ); + assert_eq!( + serialized["permissions"]["file_system"]["entries"][1]["path"]["type"], + serde_json::json!("path") + ); + assert_eq!( + serialized["permissions"]["file_system"]["entries"][1]["missing_path_behavior"], + serde_json::json!("skip") + ); + assert_eq!( + serialized["permissions"]["file_system"]["entries"][2]["path"]["type"], + serde_json::json!("special") + ); + assert_eq!( + serialized["permissions"]["file_system"]["entries"][2]["missing_path_behavior"], + serde_json::json!("skip") + ); + assert!(!serialized.to_string().contains("generated_default_path")); + assert!(!serialized.to_string().contains("generated_default_special")); + assert_eq!( + serde_json::from_value::(serialized) + .expect("deserialize sandbox"), + sandbox + ); + } + + #[test] + fn filesystem_protocol_round_trips_legacy_policy_paths_as_uris() { let native_cwd = std::env::current_dir().expect("current directory"); let cwd = PathUri::from_host_native_path(&native_cwd).expect("cwd URI"); let mut file_system_policy = @@ -880,6 +950,7 @@ mod tests { path: native_cwd.try_into().expect("absolute cwd"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); file_system_policy.glob_scan_max_depth = Some(2); let permissions = PermissionProfile::from_runtime_permissions( diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index ab122f415d..e90205d9cf 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -206,12 +206,12 @@ fn add_helper_runtime_permissions( cwd: &std::path::Path, ) { if !file_system_policy.has_full_disk_read_access() { - let minimal_read_entry = FileSystemSandboxEntry { - path: FileSystemPath::Special { + let minimal_read_entry = FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Minimal, }, - access: FileSystemAccessMode::Read, - }; + FileSystemAccessMode::Read, + ); if !file_system_policy.entries.contains(&minimal_read_entry) { file_system_policy.entries.push(minimal_read_entry); } @@ -222,12 +222,12 @@ fn add_helper_runtime_permissions( continue; } - file_system_policy.entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Path { + file_system_policy.entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Path { path: helper_read_root.clone(), }, - access: FileSystemAccessMode::Read, - }); + FileSystemAccessMode::Read, + )); } } @@ -618,6 +618,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let sandbox_context = codex_file_system::FileSystemSandboxContext::from_permission_profile( PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), @@ -710,6 +711,7 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access, + missing_path_behavior: None, } } @@ -720,6 +722,7 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Special { value }, access, + missing_path_behavior: None, } } } diff --git a/codex-rs/exec-server/src/remote_file_system.rs b/codex-rs/exec-server/src/remote_file_system.rs index 0d10c3aece..18b209152d 100644 --- a/codex-rs/exec-server/src/remote_file_system.rs +++ b/codex-rs/exec-server/src/remote_file_system.rs @@ -406,6 +406,7 @@ mod tests { path: absolute_test_path("remote-root"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let permissions = PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted); @@ -427,6 +428,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let permissions = PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted); diff --git a/codex-rs/exec-server/src/remote_file_system_path_uri_tests.rs b/codex-rs/exec-server/src/remote_file_system_path_uri_tests.rs index 562f5d5643..1c989704a6 100644 --- a/codex-rs/exec-server/src/remote_file_system_path_uri_tests.rs +++ b/codex-rs/exec-server/src/remote_file_system_path_uri_tests.rs @@ -52,6 +52,7 @@ async fn remote_file_system_sends_path_and_sandbox_cwd_uris_without_native_conve value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index fd78ba1ac5..67f9f4e7a8 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -205,12 +205,14 @@ async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() -> value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( @@ -272,12 +274,14 @@ async fn remote_tty_process_uses_configured_sandbox_helper_with_hostile_path() - value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( @@ -332,6 +336,7 @@ async fn remote_process_preserves_empty_workspace_roots() -> Result<()> { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), diff --git a/codex-rs/exec-server/tests/file_stream.rs b/codex-rs/exec-server/tests/file_stream.rs index 89162daca4..9376792b6b 100644 --- a/codex-rs/exec-server/tests/file_stream.rs +++ b/codex-rs/exec-server/tests/file_stream.rs @@ -396,6 +396,7 @@ fn read_only_sandbox(path: std::path::PathBuf) -> FileSystemSandboxContext { &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]), NetworkSandboxPolicy::Restricted, )) diff --git a/codex-rs/exec-server/tests/file_system/support.rs b/codex-rs/exec-server/tests/file_system/support.rs index d7e454d30c..712b50f1f3 100644 --- a/codex-rs/exec-server/tests/file_system/support.rs +++ b/codex-rs/exec-server/tests/file_system/support.rs @@ -86,6 +86,7 @@ pub(crate) fn read_only_sandbox(readable_root: std::path::PathBuf) -> FileSystem path: readable_root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]) } @@ -98,6 +99,7 @@ pub(crate) fn workspace_write_sandbox( path: writable_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]) } diff --git a/codex-rs/exec-server/tests/file_system_unix.rs b/codex-rs/exec-server/tests/file_system_unix.rs index 878749acfa..e89eca7aad 100644 --- a/codex-rs/exec-server/tests/file_system_unix.rs +++ b/codex-rs/exec-server/tests/file_system_unix.rs @@ -235,6 +235,7 @@ async fn remote_read_file_materializes_environment_workspace_roots() -> Result<( value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), @@ -276,6 +277,7 @@ async fn remote_read_file_preserves_empty_workspace_roots() -> Result<()> { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), diff --git a/codex-rs/exec/src/event_processor_with_human_output_tests.rs b/codex-rs/exec/src/event_processor_with_human_output_tests.rs index 5e89d48c7f..bb64a172e8 100644 --- a/codex-rs/exec/src/event_processor_with_human_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_human_output_tests.rs @@ -143,12 +143,14 @@ fn summarizes_managed_workspace_write_permission_profile() { FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd.clone() }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: cache_root.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index c9effdc20c..21f1cc6e37 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -8,6 +8,7 @@ use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxEntryMissingPathBehavior; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; @@ -160,6 +161,8 @@ impl TryFrom for FileSystemPath { pub struct ExecFileSystemSandboxEntry { pub path: ExecFileSystemPath, pub access: FileSystemAccessMode, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub missing_path_behavior: Option, } impl From for ExecFileSystemSandboxEntry { @@ -167,6 +170,7 @@ impl From for ExecFileSystemSandboxEntry { Self { path: value.path.into(), access: value.access, + missing_path_behavior: value.missing_path_behavior, } } } @@ -178,6 +182,7 @@ impl TryFrom for FileSystemSandboxEntry { Ok(Self { path: value.path.try_into()?, access: value.access, + missing_path_behavior: value.missing_path_behavior, }) } } diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 5c72a98d7c..8178a2740b 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -394,14 +394,14 @@ fn create_filesystem_args( .iter() .filter(|entry| entry.access == FileSystemAccessMode::Read) .filter_map(|entry| { - let FileSystemPath::Special { - value: - FileSystemSpecialPath::ProjectRoots { - subpath: Some(subpath), - }, - } = &entry.path - else { - return None; + let subpath = match &entry.path { + FileSystemPath::Special { + value: + FileSystemSpecialPath::ProjectRoots { + subpath: Some(subpath), + }, + } => subpath, + _ => return None, }; // Automatic repo-metadata read masks are skipped here so the // metadata handling below can apply the root-scoped @@ -1348,6 +1348,7 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, } } @@ -1426,6 +1427,7 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, unreadable_glob_entry(format!("{}/**/*.env", temp_dir.path().display())), ]); @@ -1470,12 +1472,14 @@ mod tests { value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -1544,10 +1548,12 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -1594,6 +1600,7 @@ mod tests { path: logical_memories_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let args = @@ -1633,6 +1640,7 @@ mod tests { let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path: root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let err = @@ -1669,10 +1677,12 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_private }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -1704,10 +1714,12 @@ mod tests { path: workspace_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked_root }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -1750,6 +1762,7 @@ mod tests { path: workspace_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let args = @@ -1799,6 +1812,7 @@ mod tests { path: workspace_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let args = create_filesystem_args(&policy, &workspace, NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH) @@ -1845,6 +1859,7 @@ mod tests { path: link_workspace_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let args = @@ -1914,30 +1929,35 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".git".into())), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".agents".into())), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".codex".into())), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -1972,24 +1992,28 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".vscode".into())), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".secrets".into())), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2105,6 +2129,7 @@ mod tests { .expect("absolute readable root"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let args = @@ -2132,6 +2157,7 @@ mod tests { value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let args = @@ -2169,10 +2195,12 @@ mod tests { path: writable_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2242,16 +2270,19 @@ mod tests { path: writable_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs.clone() }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs_public.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -2294,18 +2325,21 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: allowed.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -2363,18 +2397,21 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: allowed_file.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -2444,14 +2481,17 @@ mod tests { path: writable_root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: allowed }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -2493,12 +2533,14 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2537,12 +2579,14 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked_file.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index f0b90c2b15..75a734ee10 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -455,6 +455,7 @@ fn build_preflight_bwrap_argv( value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let preflight_command = vec![resolve_true_command()]; build_bwrap_argv( diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index 4cfde0ce6f..dbf6da6e2b 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -221,10 +221,12 @@ fn split_only_filesystem_policy_requires_direct_runtime_enforcement() { ), }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path: docs }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -245,10 +247,12 @@ fn root_write_read_only_carveout_requires_direct_runtime_enforcement() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path: docs }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -566,10 +570,12 @@ fn resolve_permission_profile_preserves_direct_runtime_profile() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path: docs }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -620,10 +626,12 @@ fn legacy_landlock_rejects_split_only_filesystem_policies() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: codex_protocol::permissions::FileSystemAccessMode::Read, + missing_path_behavior: None, }, codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Path { path: docs }, access: codex_protocol::permissions::FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 5843e03115..cd82781a14 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -787,6 +787,7 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() { value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { @@ -794,18 +795,21 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() { .expect("absolute helper dir"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir"), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: AbsolutePathBuf::try_from(blocked.as_path()).expect("absolute blocked dir"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -855,6 +859,7 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() { value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { @@ -862,24 +867,28 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() { .expect("absolute helper dir"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir"), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: AbsolutePathBuf::try_from(blocked.as_path()).expect("absolute blocked dir"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: AbsolutePathBuf::try_from(allowed.as_path()).expect("absolute allowed dir"), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( @@ -926,12 +935,14 @@ async fn sandbox_blocks_root_read_carveouts_under_bwrap() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: AbsolutePathBuf::try_from(blocked.as_path()).expect("absolute blocked dir"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permission_profile = PermissionProfile::from_runtime_permissions( diff --git a/codex-rs/prompts/src/permissions_instructions_tests.rs b/codex-rs/prompts/src/permissions_instructions_tests.rs index eb1c2bfa3a..b558f2a983 100644 --- a/codex-rs/prompts/src/permissions_instructions_tests.rs +++ b/codex-rs/prompts/src/permissions_instructions_tests.rs @@ -194,6 +194,7 @@ fn builds_permissions_from_profile() { path: writable_root.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]), NetworkSandboxPolicy::Enabled, ); @@ -230,18 +231,21 @@ fn builds_permissions_from_profile_with_denied_reads() { value: codex_protocol::permissions::FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_root.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: denied_glob.to_string_lossy().into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index dbf1c6a87e..51e22d10fd 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -89,15 +89,19 @@ impl FileSystemPermissions { ) -> Self { let mut entries = Vec::new(); if let Some(read) = read { - entries.extend(read.into_iter().map(|path| FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access: FileSystemAccessMode::Read, + entries.extend(read.into_iter().map(|path| { + FileSystemSandboxEntry::new( + FileSystemPath::Path { path }, + FileSystemAccessMode::Read, + ) })); } if let Some(write) = write { - entries.extend(write.into_iter().map(|path| FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access: FileSystemAccessMode::Write, + entries.extend(write.into_iter().map(|path| { + FileSystemSandboxEntry::new( + FileSystemPath::Path { path }, + FileSystemAccessMode::Write, + ) })); } Self { @@ -636,12 +640,12 @@ impl From<&FileSystemSandboxPolicy> for FileSystemPermissions { let entries = match value.kind { FileSystemSandboxKind::Restricted => value.entries.clone(), FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { - vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { + vec![FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, - access: FileSystemAccessMode::Write, - }] + FileSystemAccessMode::Write, + )] } }; Self { @@ -2497,6 +2501,7 @@ mod tests { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }]); file_system_sandbox_policy.glob_scan_max_depth = Some(2); @@ -2542,6 +2547,7 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: NonZeroUsize::new(2), }, @@ -2685,6 +2691,7 @@ mod tests { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }], glob_scan_max_depth: NonZeroUsize::new(2), }; diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index 6f648df272..977fb80f40 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -174,6 +174,37 @@ impl FileSystemSpecialPath { pub struct FileSystemSandboxEntry { pub path: FileSystemPath, pub access: FileSystemAccessMode, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub missing_path_behavior: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum FileSystemSandboxEntryMissingPathBehavior { + Skip, +} + +impl FileSystemSandboxEntry { + pub fn new(path: FileSystemPath, access: FileSystemAccessMode) -> Self { + Self { + path, + access, + missing_path_behavior: None, + } + } + + pub fn skip_missing_path(path: FileSystemPath, access: FileSystemAccessMode) -> Self { + Self { + path, + access, + missing_path_behavior: Some(FileSystemSandboxEntryMissingPathBehavior::Skip), + } + } + + pub fn skips_missing_path(&self) -> bool { + self.missing_path_behavior == Some(FileSystemSandboxEntryMissingPathBehavior::Skip) + } } #[derive( @@ -355,12 +386,12 @@ pub fn project_roots_glob_pattern(subpath: &Path) -> String { } fn read_only_file_system_entries() -> Vec { - vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { + vec![FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, - access: FileSystemAccessMode::Read, - }] + FileSystemAccessMode::Read, + )] } impl Default for FileSystemSandboxPolicy { @@ -398,6 +429,26 @@ impl FileSystemSandboxPolicy { } } + /// Removes entries that should be skipped when their paths are missing. + /// + /// Callers that materialize filesystem ACL targets should not turn these + /// entries into newly-created sentinel paths. + pub fn remove_skip_missing_path_entries(&mut self) { + self.entries.retain(|entry| !entry.skips_missing_path()); + } + + pub fn has_explicit_non_write_entry_for_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool { + let Some(path) = resolve_candidate_path(path, cwd) else { + return false; + }; + let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok(); + self.entries.iter().any(|entry| { + !entry.skips_missing_path() + && !entry.access.can_write() + && resolve_entry_path(&entry.path, cwd.as_ref()).as_ref() == Some(&path) + }) + } + fn has_root_access(&self, predicate: impl Fn(FileSystemAccessMode) -> bool) -> bool { matches!(self.kind, FileSystemSandboxKind::Restricted) && self.entries.iter().any(|entry| { @@ -449,12 +500,12 @@ impl FileSystemSandboxPolicy { .iter() .any(|entry| entry.access == FileSystemAccessMode::Deny); if matches!(self.kind, FileSystemSandboxKind::Unrestricted) && has_deny_read_entries { - *self = Self::restricted(vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { + *self = Self::restricted(vec![FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, - access: FileSystemAccessMode::Write, - }]); + FileSystemAccessMode::Write, + )]); } if !matches!(self.kind, FileSystemSandboxKind::Restricted) { @@ -521,44 +572,38 @@ impl FileSystemSandboxPolicy { exclude_tmpdir_env_var: bool, exclude_slash_tmp: bool, ) -> Self { - let mut entries = vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { + let mut entries = vec![FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, - access: FileSystemAccessMode::Read, - }]; + FileSystemAccessMode::Read, + )]; - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { + entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, - access: FileSystemAccessMode::Write, - }); + FileSystemAccessMode::Write, + )); if !exclude_slash_tmp { - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { + entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::SlashTmp, }, - access: FileSystemAccessMode::Write, - }); + FileSystemAccessMode::Write, + )); } if !exclude_tmpdir_env_var { - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { + entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Tmpdir, }, - access: FileSystemAccessMode::Write, - }); + FileSystemAccessMode::Write, + )); } - entries.extend( - writable_roots - .iter() - .cloned() - .map(|path| FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access: FileSystemAccessMode::Write, - }), - ); + entries.extend(writable_roots.iter().cloned().map(|path| { + FileSystemSandboxEntry::new(FileSystemPath::Path { path }, FileSystemAccessMode::Write) + })); append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".git"); append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".agents"); @@ -758,6 +803,7 @@ impl FileSystemSandboxPolicy { }, }, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, })); } FileSystemPath::GlobPattern { pattern } => { @@ -767,11 +813,13 @@ impl FileSystemSandboxPolicy { pattern: resolve_project_roots_glob_pattern(subpath, root), }, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, })); } else { entries.push(FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern }, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, }); } } @@ -779,12 +827,14 @@ impl FileSystemSandboxPolicy { entries.push(FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, }); } FileSystemPath::Special { value } => { entries.push(FileSystemSandboxEntry { path: FileSystemPath::Special { value }, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, }); } } @@ -824,10 +874,10 @@ impl FileSystemSandboxPolicy { continue; } - self.entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Path { path: path.clone() }, - access: FileSystemAccessMode::Read, - }); + self.entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Path { path: path.clone() }, + FileSystemAccessMode::Read, + )); } self @@ -843,10 +893,10 @@ impl FileSystemSandboxPolicy { continue; } - self.entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Path { path: path.clone() }, - access: FileSystemAccessMode::Write, - }); + self.entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Path { path: path.clone() }, + FileSystemAccessMode::Write, + )); } self @@ -871,10 +921,10 @@ impl FileSystemSandboxPolicy { entry.access.can_write() && matches!(&entry.path, FileSystemPath::Path { path: existing } if existing == path) }) { - self.entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Path { path: path.clone() }, - access: FileSystemAccessMode::Write, - }); + self.entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Path { path: path.clone() }, + FileSystemAccessMode::Write, + )); } for protected_path in default_read_only_subpaths_for_writable_root( @@ -1258,12 +1308,12 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy { SandboxPolicy::DangerFullAccess => FileSystemSandboxPolicy::unrestricted(), SandboxPolicy::ExternalSandbox { .. } => FileSystemSandboxPolicy::external_sandbox(), SandboxPolicy::ReadOnly { .. } => { - FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { + FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, - access: FileSystemAccessMode::Read, - }]) + FileSystemAccessMode::Read, + )]) } SandboxPolicy::WorkspaceWrite { writable_roots, @@ -1600,45 +1650,39 @@ fn legacy_runtime_file_system_policy_for_cwd( }; let mut entries = vec![ - FileSystemSandboxEntry { - path: FileSystemPath::Special { + FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { + FileSystemAccessMode::Read, + ), + FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, - access: FileSystemAccessMode::Write, - }, + FileSystemAccessMode::Write, + ), ]; if !*exclude_slash_tmp { - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { + entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::SlashTmp, }, - access: FileSystemAccessMode::Write, - }); + FileSystemAccessMode::Write, + )); } if !*exclude_tmpdir_env_var { - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { + entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Special { value: FileSystemSpecialPath::Tmpdir, }, - access: FileSystemAccessMode::Write, - }); + FileSystemAccessMode::Write, + )); } - entries.extend( - writable_roots - .iter() - .cloned() - .map(|path| FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access: FileSystemAccessMode::Write, - }), - ); + entries.extend(writable_roots.iter().cloned().map(|path| { + FileSystemSandboxEntry::new(FileSystemPath::Path { path }, FileSystemAccessMode::Write) + })); if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) { for protected_path in default_read_only_subpaths_for_writable_root( @@ -1689,10 +1733,10 @@ fn append_default_read_only_entry_if_no_explicit_rule( return; } - entries.push(FileSystemSandboxEntry { + entries.push(FileSystemSandboxEntry::skip_missing_path( path, - access: FileSystemAccessMode::Read, - }); + FileSystemAccessMode::Read, + )); } fn has_explicit_resolved_path_entry( @@ -1887,6 +1931,7 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -1896,6 +1941,7 @@ mod tests { ), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -1928,6 +1974,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let writable_roots = policy.get_writable_roots_with_cwd(cwd.path()); @@ -1957,31 +2004,33 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".git".into())), }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { + FileSystemAccessMode::Read, + ), + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".agents".into())), }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { + FileSystemAccessMode::Read, + ), + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".codex".into())), }, - access: FileSystemAccessMode::Read, - }, + FileSystemAccessMode::Read, + ), ]) ); } @@ -2023,12 +2072,14 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: explicit_dot_codex.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -2068,6 +2119,7 @@ mod tests { FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path: root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); assert!(!file_system_policy.can_write_path_with_cwd(&dot_git_config, cwd.path())); @@ -2114,21 +2166,23 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]; expected_entries.extend(PROTECTED_METADATA_PATH_NAMES.iter().map(|name| { - FileSystemSandboxEntry { - path: FileSystemPath::Special { + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some((*name).into())), }, - access: FileSystemAccessMode::Read, - } + FileSystemAccessMode::Read, + ) })); expected_entries.extend( default_read_only_subpaths_for_writable_root( @@ -2136,9 +2190,11 @@ mod tests { /*protect_missing_dot_codex*/ true, ) .into_iter() - .map(|path| FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access: FileSystemAccessMode::Read, + .map(|path| { + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Path { path }, + FileSystemAccessMode::Read, + ) }), ); @@ -2190,10 +2246,12 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2246,16 +2304,19 @@ mod tests { value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2313,6 +2374,7 @@ mod tests { let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path: root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let writable_roots = policy.get_writable_roots_with_cwd(cwd.path()); @@ -2353,10 +2415,12 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_private }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2400,10 +2464,12 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_private }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2442,10 +2508,12 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::Path { path: root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: alias }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2502,10 +2570,12 @@ mod tests { value: FileSystemSpecialPath::Tmpdir, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: link_blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2542,22 +2612,26 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs.clone() }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs_private.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs_private_public.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -2589,10 +2663,12 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -2675,10 +2751,12 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs.clone() }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -2713,10 +2791,12 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs.clone() }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]); @@ -2743,12 +2823,14 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2769,14 +2851,17 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs.clone() }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs.clone() }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -2796,6 +2881,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); let actual = policy @@ -2814,6 +2900,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let actual = policy @@ -2834,6 +2921,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let actual = policy.with_additional_writable_roots(&cwd, std::slice::from_ref(&extra)); @@ -2846,10 +2934,12 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: extra }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]) ); @@ -2868,18 +2958,21 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".git".into())), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: project_roots_glob_pattern(Path::new("**/*.env")), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -2894,24 +2987,28 @@ mod tests { path: first.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: second.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: first.join(".git"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: second.join(".git"), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { @@ -2923,6 +3020,7 @@ mod tests { .into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { @@ -2934,6 +3032,7 @@ mod tests { .into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]) ); @@ -2947,6 +3046,7 @@ mod tests { pattern: project_roots_glob_pattern(Path::new("**/*.env")), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }]); let actual = policy.materialize_project_roots_with_cwd(cwd.path()); @@ -2960,6 +3060,7 @@ mod tests { .into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }]) ); } @@ -2975,6 +3076,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let actual = @@ -2988,19 +3090,21 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: extra.clone() }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, - FileSystemSandboxEntry { - path: FileSystemPath::Path { + FileSystemSandboxEntry::skip_missing_path( + FileSystemPath::Path { path: extra.join(".git") }, - access: FileSystemAccessMode::Read, - }, + FileSystemAccessMode::Read, + ), ]) ); } @@ -3019,6 +3123,7 @@ mod tests { path: denied.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }]); let rebuilt = FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( @@ -3054,6 +3159,7 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, deny_entry, ]); @@ -3067,6 +3173,7 @@ mod tests { path: AbsolutePathBuf::try_from(path).expect("absolute deny path"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }]) } @@ -3074,6 +3181,7 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, } } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 602517b201..434fea10bc 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -4930,6 +4930,7 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); assert!(read_only.has_full_disk_read_access()); assert!(!read_only.has_full_disk_write_access()); @@ -4940,6 +4941,7 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); assert!(writable.has_full_disk_read_access()); assert!(writable.has_full_disk_write_access()); @@ -4970,10 +4972,12 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -5021,16 +5025,19 @@ mod tests { value: FileSystemSpecialPath::Minimal, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: secret }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -5089,14 +5096,17 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: docs_public }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]); @@ -5133,6 +5143,7 @@ mod tests { path: external_write_path, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); let err = policy @@ -6115,6 +6126,7 @@ mod tests { pattern: "/tmp/private/**/*.txt".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ])), model: "gpt-5".to_string(), diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index c1a52182d7..27afea1083 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -64,6 +64,7 @@ fn restricted_file_system_uses_platform_sandbox_without_managed_network() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]), NetworkSandboxPolicy::Enabled, SandboxablePreference::Auto, @@ -194,12 +195,14 @@ fn transform_additional_permissions_preserves_denied_entries() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_path.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let permissions = PermissionProfile::from_runtime_permissions( @@ -243,14 +246,17 @@ fn transform_additional_permissions_preserves_denied_entries() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_path }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: allowed_path }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ]) ); @@ -274,6 +280,7 @@ fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox() { &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd.clone() }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]), NetworkSandboxPolicy::Restricted, ); @@ -291,12 +298,14 @@ fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox() { FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: managed_bundle_path, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, ]) ); @@ -342,6 +351,7 @@ fn wsl1_rejects_linux_bubblewrap_path() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); assert!(matches!( @@ -391,6 +401,7 @@ fn wsl1_allows_non_bubblewrap_linux_paths() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }]); assert!( super::ensure_linux_bubblewrap_is_supported( @@ -476,16 +487,19 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]), NetworkSandboxPolicy::Restricted, diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 958a259182..8f5648cd4e 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -49,6 +49,7 @@ pub fn normalize_additional_permissions( let normalized_entry = FileSystemSandboxEntry { path, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, }; if !entries.contains(&normalized_entry) { entries.push(normalized_entry); @@ -355,6 +356,7 @@ fn materialize_cwd_dependent_entry( .map(|path| FileSystemSandboxEntry { path: FileSystemPath::Path { path }, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, }) .unwrap_or_else(|| entry.clone()), FileSystemPath::GlobPattern { pattern } => FileSystemSandboxEntry { @@ -364,6 +366,7 @@ fn materialize_cwd_dependent_entry( .into_owned(), }, access: entry.access, + missing_path_behavior: entry.missing_path_behavior, }, FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => entry.clone(), } diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index ffe504c02b..2689c41961 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -31,6 +31,7 @@ fn full_access_restricted_policy_skips_platform_sandbox_when_network_is_enabled( value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); assert_eq!( @@ -55,10 +56,12 @@ fn root_write_policy_with_carveouts_still_uses_platform_sandbox() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: blocked }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -79,6 +82,7 @@ fn full_access_restricted_policy_still_uses_platform_sandbox_for_restricted_netw value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]); assert_eq!( @@ -166,6 +170,7 @@ fn normalize_additional_permissions_rejects_glob_read_grants() { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), @@ -188,6 +193,7 @@ fn normalize_additional_permissions_preserves_deny_globs() { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }], glob_scan_max_depth: std::num::NonZeroUsize::new(2), }), @@ -204,6 +210,7 @@ fn normalize_additional_permissions_preserves_deny_globs() { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }], glob_scan_max_depth: std::num::NonZeroUsize::new(2), }), @@ -302,6 +309,7 @@ fn intersect_permission_profiles_accepts_child_path_granted_for_requested_cwd() value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), @@ -335,6 +343,7 @@ fn intersect_permission_profiles_materializes_cwd_grant_for_reuse() { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), @@ -386,10 +395,12 @@ fn intersect_permission_profiles_deduplicates_materialized_grants() { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: cwd.clone() }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -422,12 +433,14 @@ fn intersect_permission_profiles_materializes_cwd_deny_entries() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -445,10 +458,12 @@ fn intersect_permission_profiles_materializes_cwd_deny_entries() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: request_cwd }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -477,10 +492,12 @@ fn intersect_permission_profiles_drops_deny_entries_without_filesystem_grants() value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: secret }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -515,12 +532,14 @@ fn intersect_permission_profiles_rejects_concrete_grants_matched_by_requested_de value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: std::num::NonZeroUsize::new(2), @@ -553,12 +572,14 @@ fn intersect_permission_profiles_materializes_relative_deny_globs_for_reuse() { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }; let deny_env_files = FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }; let permissions = PermissionProfile { file_system: Some(FileSystemPermissions { @@ -581,12 +602,14 @@ fn intersect_permission_profiles_materializes_relative_deny_globs_for_reuse() { path: request_cwd.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: request_cwd.join("**/*.env").to_string_lossy().into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: std::num::NonZeroUsize::new(2), @@ -632,6 +655,7 @@ fn intersect_permission_profiles_drops_broader_cwd_grant_for_requested_child_pat value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }), @@ -652,12 +676,14 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }; let deny_env_files = FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }; let requested = PermissionProfile { file_system: Some(FileSystemPermissions { @@ -690,6 +716,7 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() { .into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: std::num::NonZeroUsize::new(4), @@ -707,12 +734,14 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }; let deny_env_files = FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }; let requested = PermissionProfile { file_system: Some(FileSystemPermissions { @@ -745,6 +774,7 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { .into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -770,12 +800,14 @@ fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roo value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_path.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]), &FileSystemPermissions::from_read_write_roots( @@ -788,6 +820,7 @@ fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roo merged_policy.entries.contains(&FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_path }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }), true ); @@ -795,6 +828,7 @@ fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roo merged_policy.entries.contains(&FileSystemSandboxEntry { path: FileSystemPath::Path { path: allowed_path }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }), true ); @@ -807,6 +841,7 @@ fn merge_file_system_policy_with_additional_permissions_carries_bounded_glob_sca pattern: "**/*.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }; let merged_policy = merge_file_system_policy_with_additional_permissions( &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { @@ -814,6 +849,7 @@ fn merge_file_system_policy_with_additional_permissions_carries_bounded_glob_sca value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }]), &FileSystemPermissions { entries: vec![deny_env_files.clone()], @@ -828,6 +864,7 @@ fn merge_file_system_policy_with_additional_permissions_carries_bounded_glob_sca value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, deny_env_files, ]); @@ -850,10 +887,12 @@ fn effective_file_system_sandbox_policy_returns_base_policy_without_additional_p value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_path }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -878,12 +917,14 @@ fn effective_file_system_sandbox_policy_merges_additional_write_roots() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_path.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); let additional_permissions = PermissionProfile { @@ -901,6 +942,7 @@ fn effective_file_system_sandbox_policy_merges_additional_write_roots() { effective_policy.entries.contains(&FileSystemSandboxEntry { path: FileSystemPath::Path { path: denied_path }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }), true ); @@ -908,6 +950,7 @@ fn effective_file_system_sandbox_policy_merges_additional_write_roots() { effective_policy.entries.contains(&FileSystemSandboxEntry { path: FileSystemPath::Path { path: allowed_path }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }), true ); diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index 189294345a..656f3f05b3 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -193,10 +193,12 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access() value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: unreadable }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -299,10 +301,12 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() { FileSystemSandboxEntry { path: FileSystemPath::Path { path: root }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: unreadable }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]); @@ -411,6 +415,7 @@ fn unreadable_glob_policy_includes_canonicalized_static_prefix() { policy.entries.push(FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }); let seatbelt_policy = build_seatbelt_unreadable_glob_policy(&policy, temp_dir.path()); diff --git a/codex-rs/sandboxing/src/windows.rs b/codex-rs/sandboxing/src/windows.rs index a57281145c..098455e2c6 100644 --- a/codex-rs/sandboxing/src/windows.rs +++ b/codex-rs/sandboxing/src/windows.rs @@ -87,7 +87,7 @@ pub fn resolve_windows_restricted_token_filesystem_overrides( return Ok(None); } - let (file_system_sandbox_policy, network_sandbox_policy) = + let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); let needs_direct_runtime_enforcement = file_system_sandbox_policy @@ -107,6 +107,11 @@ pub fn resolve_windows_restricted_token_filesystem_overrides( )); } + // Windows protects existing metadata paths through the legacy writable root + // projection. Do not turn skip-missing entries into newly-created + // deny-write sentinels. + file_system_sandbox_policy.remove_skip_missing_path_entries(); + // The restricted-token backend can still enforce split write restrictions, // but its WRITE_RESTRICTED token does not make capability SID deny-read ACEs // participate in read access checks. Read restrictions therefore require the @@ -221,7 +226,7 @@ pub fn resolve_windows_elevated_filesystem_overrides( return Ok(None); } - let (file_system_sandbox_policy, network_sandbox_policy) = + let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) { @@ -232,6 +237,11 @@ pub fn resolve_windows_elevated_filesystem_overrides( )); } + // Windows protects existing metadata paths through the legacy writable root + // projection. Do not turn skip-missing entries into newly-created + // deny-write sentinels. + file_system_sandbox_policy.remove_skip_missing_path_entries(); + let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths( &file_system_sandbox_policy, sandbox_policy_cwd, @@ -248,6 +258,12 @@ pub fn resolve_windows_elevated_filesystem_overrides( let needs_direct_runtime_enforcement = file_system_sandbox_policy .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd); + let has_explicit_write_carveouts = split_writable_roots.iter().any(|writable_root| { + writable_root.read_only_subpaths.iter().any(|path| { + file_system_sandbox_policy + .has_explicit_non_write_entry_for_path_with_cwd(path.as_path(), sandbox_policy_cwd) + }) + }); let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path); let legacy_projection = compatibility_sandbox_policy_for_permission_profile( permission_profile, @@ -288,7 +304,9 @@ pub fn resolve_windows_elevated_filesystem_overrides( Some(split_root_paths) }; - let additional_deny_write_paths = if needs_direct_runtime_enforcement { + let additional_deny_write_paths = if needs_direct_runtime_enforcement + || has_explicit_write_carveouts + { let mut deny_paths = BTreeSet::new(); for writable_root in &split_writable_roots { let writable_root_path = normalize_path(writable_root.root.to_path_buf()); @@ -309,7 +327,12 @@ pub fn resolve_windows_elevated_filesystem_overrides( == read_only_subpath_suffix }) }); - if !already_denied_by_legacy { + let explicitly_configured = file_system_sandbox_policy + .has_explicit_non_write_entry_for_path_with_cwd( + read_only_subpath.as_path(), + sandbox_policy_cwd, + ); + if explicitly_configured || !already_denied_by_legacy { deny_paths.insert(normalize_path(read_only_subpath.to_path_buf())); } } diff --git a/codex-rs/tui/src/additional_dirs.rs b/codex-rs/tui/src/additional_dirs.rs index 6d94509a4f..cf7e087e0a 100644 --- a/codex-rs/tui/src/additional_dirs.rs +++ b/codex-rs/tui/src/additional_dirs.rs @@ -112,12 +112,14 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: "/tmp/writable".try_into().expect("absolute path"), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index b867b95354..6758a81e8b 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -300,12 +300,14 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/.env".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index b919434f98..410bf4f636 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -2230,10 +2230,12 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: extra_root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -2258,12 +2260,14 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { subpath: None }, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index c334a7ab35..84125d932c 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -253,12 +253,14 @@ async fn submission_includes_configured_active_permission_profile() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "/home/user/project/secrets/**".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 8d68862eba..365eb5f792 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -481,12 +481,14 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/.secret".to_string(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 00a7b8b2ff..d4349fb68f 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -20,28 +20,33 @@ fn app_server_workspace_write_profile(extra_root: AbsolutePathBuf) -> Permission value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { subpath: None }, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::SlashTmp, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Tmpdir, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: extra_root }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -359,12 +364,14 @@ async fn preset_matching_does_not_treat_non_cwd_writable_profile_as_read_only() value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: test_path_buf("/tmp/writable").abs(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/tui/src/permission_compat.rs b/codex-rs/tui/src/permission_compat.rs index 953793feba..e207db0c4a 100644 --- a/codex-rs/tui/src/permission_compat.rs +++ b/codex-rs/tui/src/permission_compat.rs @@ -65,12 +65,14 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: extra_root.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 6215899d2e..d95bc217f5 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -101,24 +101,28 @@ fn app_server_workspace_write_profile(network_enabled: bool) -> PermissionProfil value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Read, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::ProjectRoots { subpath: None }, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::SlashTmp, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Tmpdir, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, diff --git a/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs b/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs index ce07df1726..e2e04ccd1b 100644 --- a/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs +++ b/codex-rs/windows-sandbox-rs/src/deny_read_resolver.rs @@ -44,6 +44,7 @@ pub fn resolve_windows_deny_read_paths( pattern: pattern.clone(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }) .collect(), ); @@ -206,6 +207,7 @@ mod tests { FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, } } @@ -215,6 +217,7 @@ mod tests { path: AbsolutePathBuf::from_absolute_path(path).expect("absolute path"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, } } diff --git a/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs b/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs index 712a80aade..d924c5c112 100644 --- a/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs +++ b/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs @@ -173,7 +173,7 @@ impl ResolvedWindowsSandboxPermissions { self.file_system .entries .iter() - .any(|FileSystemSandboxEntry { path, access }| { + .any(|FileSystemSandboxEntry { path, access, .. }| { matches!( path, FileSystemPath::Special { @@ -258,6 +258,7 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, }, @@ -298,18 +299,21 @@ mod tests { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(".git".into())), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: project_roots_glob_pattern(Path::new("**/*.env")), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ], glob_scan_max_depth: None, @@ -332,24 +336,28 @@ mod tests { path: first.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: second.clone(), }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: first.join(".git"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::Path { path: second.join(".git"), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { @@ -361,6 +369,7 @@ mod tests { .into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { @@ -372,6 +381,7 @@ mod tests { .into_owned(), }, access: FileSystemAccessMode::Deny, + missing_path_behavior: None, }, ]) ); @@ -455,6 +465,7 @@ mod tests { value: FileSystemSpecialPath::Root, }, access: FileSystemAccessMode::Write, + missing_path_behavior: None, }], glob_scan_max_depth: None, },