From 13e0ec1614518e57a03375d79ad2991d84c862e4 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 24 Apr 2026 13:42:05 -0700 Subject: [PATCH] permissions: make legacy profile conversion cwd-free (#19414) ## Why The profile conversion path still required a `cwd` even when it was only translating a legacy `SandboxPolicy` into a `PermissionProfile`. That made profile producers invent an ambient `cwd`, which is exactly the anchoring we are trying to remove from permission-profile data. A legacy workspace-write policy can be represented symbolically instead: `:cwd = write` plus read-only `:project_roots` metadata subpaths. This PR creates that cwd-free base so the rest of the stack can stop threading cwd through profile construction. Callers that actually need a concrete runtime filesystem policy for a specific cwd still have an explicitly named cwd-bound conversion. ## What Changed - `PermissionProfile::from_legacy_sandbox_policy` now takes only `&SandboxPolicy`. - `FileSystemSandboxPolicy::from_legacy_sandbox_policy` is now the symbolic, cwd-free projection for profiles. - The old concrete projection is retained as `FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd` for runtime/boundary code that must materialize legacy cwd behavior. - Workspace-write profiles preserve `CurrentWorkingDirectory` and `ProjectRoots` special entries instead of materializing cwd into absolute paths. ## Verification - `cargo check -p codex-protocol -p codex-core -p codex-app-server-protocol -p codex-app-server -p codex-exec -p codex-exec-server -p codex-tui -p codex-sandboxing -p codex-linux-sandbox -p codex-analytics --tests` - `just fix -p codex-protocol -p codex-core -p codex-app-server-protocol -p codex-app-server -p codex-exec -p codex-exec-server -p codex-tui -p codex-sandboxing -p codex-linux-sandbox -p codex-analytics` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19414). * #19395 * #19394 * #19393 * #19392 * #19391 * __->__ #19414 --- .../analytics/src/analytics_client_tests.rs | 6 +- .../src/protocol/common.rs | 3 +- .../app-server/src/codex_message_processor.rs | 9 +- codex-rs/core/src/config/config_tests.rs | 2 +- codex-rs/core/src/config/mod.rs | 3 +- codex-rs/core/src/landlock.rs | 6 +- codex-rs/core/src/memories/phase2.rs | 2 +- codex-rs/core/src/memories/tests.rs | 2 +- codex-rs/core/src/safety_tests.rs | 4 +- codex-rs/core/src/session/session.rs | 4 +- codex-rs/core/src/session/tests.rs | 27 ++- codex-rs/core/src/session/turn_context.rs | 9 +- .../src/tools/handlers/multi_agents_tests.rs | 4 +- codex-rs/exec-server/src/file_system.rs | 11 +- codex-rs/exec/src/lib_tests.rs | 1 - codex-rs/exec/tests/suite/sandbox.rs | 2 +- codex-rs/linux-sandbox/src/linux_run_main.rs | 12 +- codex-rs/protocol/src/models.rs | 13 +- codex-rs/protocol/src/permissions.rs | 227 ++++++++++++++---- codex-rs/protocol/src/protocol.rs | 11 +- codex-rs/sandboxing/src/seatbelt.rs | 6 +- codex-rs/sandboxing/src/seatbelt_tests.rs | 6 +- codex-rs/tui/src/app/config_persistence.rs | 2 +- codex-rs/tui/src/app/tests.rs | 7 - codex-rs/tui/src/app/thread_events.rs | 1 - codex-rs/tui/src/app/thread_session_state.rs | 11 +- codex-rs/tui/src/app_server_session.rs | 10 +- codex-rs/tui/src/chatwidget.rs | 4 +- .../src/chatwidget/tests/history_replay.rs | 15 +- 29 files changed, 281 insertions(+), 139 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 9b45a1a82..ed1731463 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -161,11 +161,7 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) - } fn sample_permission_profile() -> AppServerPermissionProfile { - CorePermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::DangerFullAccess, - &test_path_buf("/tmp"), - ) - .into() + CorePermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::DangerFullAccess).into() } fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata { diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 40855a095..1c5be70da 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1471,7 +1471,7 @@ mod tests { model: "gpt-5".to_string(), model_provider: "openai".to_string(), service_tier: None, - cwd: cwd.clone(), + cwd, instruction_sources: vec![absolute_path("/tmp/AGENTS.md")], approval_policy: v2::AskForApproval::OnFailure, approvals_reviewer: v2::ApprovalsReviewer::User, @@ -1479,7 +1479,6 @@ mod tests { permission_profile: Some( codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - cwd.as_path(), ) .into(), ), diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ecadef97f..0e622d7f5 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2291,7 +2291,7 @@ impl CodexMessageProcessor { match self.config.permissions.sandbox_policy.can_set(&policy) { Ok(()) => { let file_system_sandbox_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, &sandbox_cwd); + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd); let network_sandbox_policy = codex_protocol::permissions::NetworkSandboxPolicy::from(&policy); (policy, file_system_sandbox_policy, network_sandbox_policy) @@ -10545,18 +10545,15 @@ mod tests { #[test] fn thread_response_permission_profile_preserves_enforcement() { - let cwd = test_path_buf("/tmp").abs(); let full_access_profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::DangerFullAccess, - cwd.as_path(), ); let external_profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Restricted, }, - cwd.as_path(), ); assert_eq!( @@ -10575,17 +10572,14 @@ mod tests { let full_access_profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::DangerFullAccess, - cwd.as_path(), ); let workspace_write_profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy(), - cwd.as_path(), ); let read_only_profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_read_only_policy(), - cwd.as_path(), ); assert!(requested_permissions_trust_project( @@ -10797,7 +10791,6 @@ mod tests { permission_profile: codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &codex_protocol::protocol::SandboxPolicy::DangerFullAccess, - cwd.as_path(), ), cwd, ephemeral: false, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c5eb1d1fa..7af47fe5e 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1583,7 +1583,7 @@ exclude_slash_tmp = true let sandbox_policy = config.permissions.sandbox_policy.get(); assert_eq!( config.permissions.file_system_sandbox_policy, - FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, cwd.path()), + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.path()), "case `{name}` should preserve filesystem semantics from legacy config" ); assert_eq!( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index cfd316753..9bdbeb9d1 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1866,7 +1866,8 @@ impl Config { } } } - let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &sandbox_policy, resolved_cwd.as_path(), ); diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 088464200..7e2de35e8 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -36,8 +36,10 @@ pub async fn spawn_command_under_linux_sandbox

( where P: AsRef, { - let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd); + let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + sandbox_policy, + sandbox_policy_cwd, + ); let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy); let args = create_linux_sandbox_command_args_for_policies( command, diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index 84404f48f..ac1d0285d 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -329,7 +329,7 @@ mod agent { exclude_slash_tmp: true, }; let consolidation_file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy( + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &consolidation_sandbox_policy, agent_config.cwd.as_path(), ); diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index d4c659bfc..d56ceb1e5 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -742,7 +742,7 @@ mod phase2 { let turn_context = subagent.codex.session.new_default_turn().await; pretty_assertions::assert_eq!( turn_context.file_system_sandbox_policy, - FileSystemSandboxPolicy::from_legacy_sandbox_policy( + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &config_snapshot.sandbox_policy, config_snapshot.cwd.as_path(), ), diff --git a/codex-rs/core/src/safety_tests.rs b/codex-rs/core/src/safety_tests.rs index c0019ce21..a5892b292 100644 --- a/codex-rs/core/src/safety_tests.rs +++ b/codex-rs/core/src/safety_tests.rs @@ -178,7 +178,7 @@ fn read_only_policy_rejects_patch_with_read_only_reason() { let action = ApplyPatchAction::new_add_for_test(&inside_path, "".to_string()); let sandbox_policy = SandboxPolicy::new_read_only_policy(); let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, &cwd); + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &cwd); assert!(!is_write_patch_constrained_to_writable_paths( &action, @@ -300,7 +300,7 @@ fn missing_project_dot_codex_config_requires_approval() { exclude_slash_tmp: true, }; let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, &cwd); + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &cwd); assert!(!is_write_patch_constrained_to_writable_paths( &action, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index af8dec1f8..ccb417a7e 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -121,7 +121,7 @@ impl SessionConfiguration { pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult { let mut next_configuration = self.clone(); let file_system_policy_matches_legacy = self.file_system_sandbox_policy - == FileSystemSandboxPolicy::from_legacy_sandbox_policy( + == FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( self.sandbox_policy.get(), &self.cwd, ); @@ -201,7 +201,7 @@ impl SessionConfiguration { // Preserve richer split policies across cwd-only updates; only // rederive when the session is already using the legacy bridge. next_configuration.file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy( + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( next_configuration.sandbox_policy.get(), &next_configuration.cwd, ); diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 508eadfba..c0afe2442 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1496,7 +1496,6 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> let expected_permission_profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &expected_sandbox_policy, - test.session_configured.cwd.as_path(), ); assert_eq!( test.session_configured.permission_profile, @@ -2886,15 +2885,16 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ }, access: FileSystemAccessMode::None, }; - let mut existing_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( - &workspace_policy, - session_configuration.cwd.as_path(), - ); + let mut existing_file_system_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + &workspace_policy, + session_configuration.cwd.as_path(), + ); existing_file_system_policy.glob_scan_max_depth = Some(2); existing_file_system_policy.entries.push(deny_entry.clone()); session_configuration.file_system_sandbox_policy = existing_file_system_policy; - let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + let requested_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &workspace_policy, session_configuration.cwd.as_path(), ); @@ -3027,7 +3027,7 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_ exclude_slash_tmp: true, }); session_configuration.file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy( + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( session_configuration.sandbox_policy.get(), &session_configuration.cwd, ); @@ -3041,7 +3041,7 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_ assert_eq!( updated.file_system_sandbox_policy, - FileSystemSandboxPolicy::from_legacy_sandbox_policy( + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( updated.sandbox_policy.get(), &project_root, ) @@ -5460,7 +5460,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is } fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSystemSandboxPolicy { - let mut policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + let mut policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( turn_context.sandbox_policy.get(), &turn_context.cwd, ); @@ -5476,10 +5476,11 @@ fn file_system_policy_with_unreadable_glob(turn_context: &TurnContext) -> FileSy #[tokio::test] async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy() { let (_session, mut turn_context) = make_session_and_context().await; - turn_context.file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( - turn_context.sandbox_policy.get(), - &turn_context.cwd, - ); + turn_context.file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + turn_context.sandbox_policy.get(), + &turn_context.cwd, + ); let item = turn_context.to_turn_context_item(); diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 23d6d61fc..f3ca9d37b 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -280,10 +280,11 @@ impl TurnContext { // the legacy sandbox policy. This keeps turn-context payloads stable // while both fields exist; once callers consume only the split policy, // this comparison and the legacy projection should go away. - let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( - self.sandbox_policy.get(), - &self.cwd, - ); + let legacy_file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + self.sandbox_policy.get(), + &self.cwd, + ); (self.file_system_sandbox_policy != legacy_file_system_sandbox_policy) .then(|| self.file_system_sandbox_policy.clone()) } 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 a08f4b1bf..baa88ccaa 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -2101,7 +2101,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { turn.config.permissions.sandbox_policy.get().clone(), ); let expected_file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&expected_sandbox, &turn.cwd); + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, &turn.cwd); let expected_network_sandbox_policy = NetworkSandboxPolicy::from(&expected_sandbox); turn.approval_policy .set(AskForApproval::OnRequest) @@ -3620,7 +3620,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() { turn.config.permissions.sandbox_policy.get().clone(), ); let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, &turn.cwd); + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &turn.cwd); let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); turn.sandbox_policy .set(sandbox_policy) diff --git a/codex-rs/exec-server/src/file_system.rs b/codex-rs/exec-server/src/file_system.rs index 37237f60d..cd31ae63c 100644 --- a/codex-rs/exec-server/src/file_system.rs +++ b/codex-rs/exec-server/src/file_system.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; @@ -57,8 +59,13 @@ pub struct FileSystemSandboxContext { impl FileSystemSandboxContext { pub fn from_legacy_sandbox_policy(sandbox_policy: SandboxPolicy, cwd: AbsolutePathBuf) -> Self { - let permissions = - PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()); + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &cwd); + let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy), + &file_system_sandbox_policy, + NetworkSandboxPolicy::from(&sandbox_policy), + ); Self::from_permission_profile_with_cwd(permissions, cwd) } diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index f24c3fd57..0ec1fbc59 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -430,7 +430,6 @@ fn session_configured_from_thread_response_uses_review_policy_from_response() { permission_profile: Some( codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &codex_protocol::protocol::SandboxPolicy::new_workspace_write_policy(), - &test_path_buf("/tmp"), ) .into(), ), diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index 691b590f4..cd5459d1f 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -44,7 +44,7 @@ async fn spawn_command_under_sandbox( arg0: None, }, sandbox_policy, - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_cwd), + &FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, sandbox_cwd), NetworkSandboxPolicy::from(sandbox_policy), sandbox_cwd, &codex_linux_sandbox_exe, diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 958d8645b..0eede8bb8 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -324,7 +324,7 @@ fn resolve_sandbox_policies( }) } (Some(sandbox_policy), None) => Ok(EffectiveSandboxPolicies { - file_system_sandbox_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy( + file_system_sandbox_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &sandbox_policy, sandbox_policy_cwd, ), @@ -354,8 +354,14 @@ fn legacy_sandbox_policies_match_semantics( ) -> bool { NetworkSandboxPolicy::from(provided) == NetworkSandboxPolicy::from(derived) && file_system_sandbox_policies_match_semantics( - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(provided, sandbox_policy_cwd), - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(derived, sandbox_policy_cwd), + &FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + provided, + sandbox_policy_cwd, + ), + &FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + derived, + sandbox_policy_cwd, + ), sandbox_policy_cwd, ) } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 2b02ee88b..f26a48f7e 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -429,10 +429,10 @@ impl PermissionProfile { } } - pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self { + pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self { Self::from_runtime_permissions_with_enforcement( SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy), - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, cwd), + &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy), NetworkSandboxPolicy::from(sandbox_policy), ) } @@ -1765,10 +1765,8 @@ mod tests { #[test] fn permission_profile_round_trip_preserves_disabled_sandbox() -> Result<()> { let cwd = tempdir()?; - let permission_profile = PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::DangerFullAccess, - cwd.path(), - ); + let permission_profile = + PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::DangerFullAccess); assert_eq!(permission_profile, PermissionProfile::Disabled); assert_eq!( @@ -1839,8 +1837,7 @@ mod tests { let sandbox_policy = SandboxPolicy::ExternalSandbox { network_access: crate::protocol::NetworkAccess::Restricted, }; - let permission_profile = - PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy, cwd.path()); + let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy); assert_eq!( permission_profile, diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index f06fc7798..c1580a90f 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -321,7 +321,7 @@ impl FileSystemSandboxPolicy { cwd: &Path, existing: &Self, ) -> Self { - let mut rebuilt = Self::from_legacy_sandbox_policy(sandbox_policy, cwd); + let mut rebuilt = Self::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd); if !matches!(rebuilt.kind, FileSystemSandboxKind::Restricted) { return rebuilt; } @@ -413,30 +413,74 @@ impl FileSystemSandboxPolicy { }) } + /// Converts a legacy sandbox policy into a cwd-independent filesystem policy. + /// + /// `WorkspaceWrite` uses symbolic entries for cwd-scoped access so callers + /// can preserve the active cwd binding until the policy is actually + /// resolved for a turn or command. + pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self { + let mut file_system_policy = Self::from(sandbox_policy); + let SandboxPolicy::WorkspaceWrite { + writable_roots, + exclude_tmpdir_env_var, + exclude_slash_tmp, + .. + } = sandbox_policy + else { + return file_system_policy; + }; + + prune_read_entries_under_writable_roots( + &mut file_system_policy.entries, + &legacy_non_cwd_writable_roots( + writable_roots, + *exclude_tmpdir_env_var, + *exclude_slash_tmp, + ), + ); + + append_default_read_only_project_root_subpath_if_no_explicit_rule( + &mut file_system_policy.entries, + ".git", + ); + append_default_read_only_project_root_subpath_if_no_explicit_rule( + &mut file_system_policy.entries, + ".agents", + ); + append_default_read_only_project_root_subpath_if_no_explicit_rule( + &mut file_system_policy.entries, + ".codex", + ); + for writable_root in writable_roots { + for protected_path in default_read_only_subpaths_for_writable_root( + writable_root, + /*protect_missing_dot_codex*/ false, + ) { + append_default_read_only_path_if_no_explicit_rule( + &mut file_system_policy.entries, + protected_path, + ); + } + } + + file_system_policy + } + /// Converts a legacy sandbox policy into an equivalent filesystem policy - /// for the provided cwd. + /// after resolving cwd-sensitive legacy defaults for the provided cwd. /// /// Legacy `WorkspaceWrite` policies may list readable roots that live /// under an already-writable root. Those paths were redundant in the /// legacy model and should not become read-only carveouts when projected /// into split filesystem policy. - pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self { + pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self { let mut file_system_policy = Self::from(sandbox_policy); if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = sandbox_policy { let legacy_writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd); - file_system_policy.entries.retain(|entry| { - if entry.access != FileSystemAccessMode::Read { - return true; - } - - match &entry.path { - FileSystemPath::Path { path } => !legacy_writable_roots - .iter() - .any(|root| root.is_path_writable(path.as_path())), - FileSystemPath::GlobPattern { .. } => true, - FileSystemPath::Special { .. } => true, - } - }); + prune_read_entries_under_writable_roots( + &mut file_system_policy.entries, + &legacy_writable_roots, + ); if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) { for protected_path in default_read_only_subpaths_for_writable_root( @@ -584,7 +628,7 @@ impl FileSystemSandboxPolicy { }; self.semantic_signature(cwd) - != FileSystemSandboxPolicy::from_legacy_sandbox_policy(&legacy_policy, cwd) + != FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&legacy_policy, cwd) .semantic_signature(cwd) } @@ -1378,41 +1422,92 @@ fn default_read_only_subpaths_for_writable_root( dedup_absolute_paths(subpaths, /*normalize_effective_paths*/ false) } -fn append_path_entry_if_missing( +fn append_default_read_only_project_root_subpath_if_no_explicit_rule( entries: &mut Vec, - path: AbsolutePathBuf, - access: FileSystemAccessMode, + subpath: impl Into, ) { - if entries.iter().any(|entry| { - entry.access == access - && matches!( - &entry.path, - FileSystemPath::Path { path: existing } if existing == &path - ) - }) { - return; - } - - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access, - }); + append_default_read_only_entry_if_no_explicit_rule( + entries, + FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(subpath.into())), + }, + ); } fn append_default_read_only_path_if_no_explicit_rule( entries: &mut Vec, path: AbsolutePathBuf, ) { - if entries.iter().any(|entry| { - matches!( - &entry.path, - FileSystemPath::Path { path: existing } if existing == &path - ) - }) { + append_default_read_only_entry_if_no_explicit_rule(entries, FileSystemPath::Path { path }); +} + +fn append_default_read_only_entry_if_no_explicit_rule( + entries: &mut Vec, + path: FileSystemPath, +) { + if entries + .iter() + .any(|entry| file_system_paths_share_target(&entry.path, &path)) + { return; } - append_path_entry_if_missing(entries, path, FileSystemAccessMode::Read); + entries.push(FileSystemSandboxEntry { + path, + access: FileSystemAccessMode::Read, + }); +} + +fn prune_read_entries_under_writable_roots( + entries: &mut Vec, + legacy_writable_roots: &[WritableRoot], +) { + entries.retain(|entry| { + if entry.access != FileSystemAccessMode::Read { + return true; + } + + match &entry.path { + FileSystemPath::Path { path } => !legacy_writable_roots + .iter() + .any(|root| root.is_path_writable(path.as_path())), + FileSystemPath::GlobPattern { .. } | FileSystemPath::Special { .. } => true, + } + }); +} + +fn legacy_non_cwd_writable_roots( + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, +) -> Vec { + let mut roots: Vec = writable_roots.to_vec(); + + if cfg!(unix) + && !exclude_slash_tmp + && let Ok(slash_tmp) = AbsolutePathBuf::from_absolute_path("/tmp") + && slash_tmp.as_path().is_dir() + { + roots.push(slash_tmp); + } + + if !exclude_tmpdir_env_var + && let Some(tmpdir) = std::env::var_os("TMPDIR") + && !tmpdir.is_empty() + && let Ok(tmpdir_path) = AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)) + { + roots.push(tmpdir_path); + } + + dedup_absolute_paths(roots, /*normalize_effective_paths*/ true) + .into_iter() + .map(|root| WritableRoot { + read_only_subpaths: default_read_only_subpaths_for_writable_root( + &root, /*protect_missing_dot_codex*/ false, + ), + root, + }) + .collect() } fn has_explicit_resolved_path_entry( @@ -1552,6 +1647,50 @@ mod tests { ); } + #[test] + fn legacy_workspace_write_projection_preserves_symbolic_cwd() { + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + read_only_access: ReadOnlyAccess::Restricted { + include_platform_defaults: false, + readable_roots: Vec::new(), + }, + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + + assert_eq!( + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy), + FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".git".into())), + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".agents".into())), + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".codex".into())), + }, + access: FileSystemAccessMode::Read, + }, + ]) + ); + } + #[cfg(unix)] #[test] fn writable_roots_skip_default_dot_codex_when_explicit_user_rule_exists() { @@ -1612,7 +1751,7 @@ mod tests { }; let file_system_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd.path()); + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, cwd.path()); assert!(!file_system_policy.can_write_path_with_cwd(&dot_codex_config, cwd.path())); } @@ -1639,7 +1778,7 @@ mod tests { }; let file_system_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, relative_cwd); + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, relative_cwd); assert_eq!( file_system_policy, @@ -2098,7 +2237,7 @@ mod tests { policy.needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path(),) ); - let legacy_workspace_write = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + let legacy_workspace_write = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &SandboxPolicy::new_workspace_write_policy(), cwd.path(), ); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 21562f3a9..f2219cdcf 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3058,7 +3058,7 @@ impl TurnContextItem { self.permission_profile.clone().unwrap_or_else(|| { let file_system_sandbox_policy = self.file_system_sandbox_policy.clone().unwrap_or_else(|| { - FileSystemSandboxPolicy::from_legacy_sandbox_policy( + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &self.sandbox_policy, &self.cwd, ) @@ -4644,7 +4644,7 @@ mod tests { assert_eq!( sorted_writable_roots( - FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, cwd.path()) + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, cwd.path()) .get_writable_roots_with_cwd(cwd.path()) ), vec![(canonical_cwd, vec![expected_dot_codex.to_path_buf()])] @@ -4736,9 +4736,10 @@ mod tests { ]; for expected in policies { - let actual = FileSystemSandboxPolicy::from_legacy_sandbox_policy(&expected, cwd.path()) - .to_legacy_sandbox_policy(NetworkSandboxPolicy::from(&expected), cwd.path()) - .expect("legacy bridge should preserve legacy policy semantics"); + let actual = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected, cwd.path()) + .to_legacy_sandbox_policy(NetworkSandboxPolicy::from(&expected), cwd.path()) + .expect("legacy bridge should preserve legacy policy semantics"); assert_same_sandbox_policy_semantics(&expected, &actual, cwd.path()); } diff --git a/codex-rs/sandboxing/src/seatbelt.rs b/codex-rs/sandboxing/src/seatbelt.rs index 57a152e02..c8b9e9f04 100644 --- a/codex-rs/sandboxing/src/seatbelt.rs +++ b/codex-rs/sandboxing/src/seatbelt.rs @@ -532,8 +532,10 @@ fn create_seatbelt_command_args_for_legacy_policy( enforce_managed_network: bool, network: Option<&NetworkProxy>, ) -> Vec { - let file_system_sandbox_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, sandbox_policy_cwd); + let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + sandbox_policy, + sandbox_policy_cwd, + ); create_seatbelt_command_args(CreateSeatbeltCommandArgsParams { command, file_system_sandbox_policy: &file_system_sandbox_policy, diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index 9d958c956..a07e02dfc 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -561,7 +561,7 @@ fn create_seatbelt_args_allowlists_unix_socket_paths() { #[test] fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() { let cwd = TempDir::new().expect("temp cwd"); - let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &SandboxPolicy::new_read_only_policy(), cwd.path(), ); @@ -601,7 +601,7 @@ fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() { #[tokio::test] async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> anyhow::Result<()> { let cwd = TempDir::new().expect("temp cwd"); - let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &SandboxPolicy::new_read_only_policy(), cwd.path(), ); @@ -660,7 +660,7 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a #[test] fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths() { let cwd = TempDir::new().expect("temp cwd"); - let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &SandboxPolicy::new_read_only_policy(), cwd.path(), ); diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 3fc4ed0bd..3515d3756 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -546,7 +546,7 @@ impl App { fn sync_runtime_permissions_from_legacy_sandbox_policy(config: &mut Config) { let sandbox_policy = config.permissions.sandbox_policy.get(); config.permissions.file_system_sandbox_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( sandbox_policy, &config.cwd, ); diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 320e0e1c8..e40f18c65 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2218,7 +2218,6 @@ async fn inactive_thread_approval_bubbles_into_active_view() -> Result<()> { sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy(), - std::path::Path::new("/tmp/agent"), )), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) @@ -2381,7 +2380,6 @@ async fn side_defers_subagent_approval_overlay_until_side_exits() -> Result<()> sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy(), - std::path::Path::new("/tmp/agent"), )), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) @@ -2607,7 +2605,6 @@ async fn inactive_thread_approval_badge_clears_after_turn_completion_notificatio sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy(), - std::path::Path::new("/tmp/agent"), )), rollout_path: Some(test_path_buf("/tmp/agent-rollout.jsonl")), ..test_thread_session(agent_thread_id, test_path_buf("/tmp/agent")) @@ -2664,7 +2661,6 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy(), - std::path::Path::new("/tmp/main"), )), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2780,7 +2776,6 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_ sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy(), - std::path::Path::new("/tmp/main"), )), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -2852,7 +2847,6 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { sandbox_policy: SandboxPolicy::new_workspace_write_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_workspace_write_policy(), - std::path::Path::new("/tmp/main"), )), ..test_thread_session(main_thread_id, test_path_buf("/tmp/main")) }; @@ -3754,7 +3748,6 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState sandbox_policy: SandboxPolicy::new_read_only_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_read_only_policy(), - cwd.as_path(), )), cwd: cwd.abs(), instruction_source_paths: Vec::new(), diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 10415c9f4..4de0b33f1 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -305,7 +305,6 @@ mod tests { sandbox_policy: SandboxPolicy::new_read_only_policy(), permission_profile: Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_read_only_policy(), - cwd.as_path(), )), cwd: cwd.abs(), instruction_source_paths: Vec::new(), diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 269a05037..374307344 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -172,9 +172,14 @@ mod tests { codex_config::Constrained::allow_any(AskForApproval::OnRequest); app.config.approvals_reviewer = ApprovalsReviewer::AutoReview; let expected_sandbox_policy = SandboxPolicy::new_workspace_write_policy(); - let expected_permission_profile = PermissionProfile::from_legacy_sandbox_policy( - &expected_sandbox_policy, - &main_session.cwd, + let expected_file_system_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + &expected_sandbox_policy, + &main_session.cwd, + ); + let expected_permission_profile = PermissionProfile::from_runtime_permissions( + &expected_file_system_policy, + NetworkSandboxPolicy::from(&expected_sandbox_policy), ); app.chat_widget.handle_thread_session(main_session.clone()); app.chat_widget diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 5c768c9e9..e29a0dc18 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1541,10 +1541,9 @@ mod tests { #[test] fn turn_start_permission_overrides_send_profiles_only_for_embedded_runtime_overrides() { - let cwd = test_path_buf("/tmp/project"); let workspace_write = SandboxPolicy::new_workspace_write_policy(); let workspace_write_profile = - PermissionProfile::from_legacy_sandbox_policy(&workspace_write, &cwd); + PermissionProfile::from_legacy_sandbox_policy(&workspace_write); let (sandbox, profile) = turn_start_permission_overrides( ThreadParamsMode::Embedded, @@ -1567,7 +1566,6 @@ mod tests { workspace_write.clone(), Some(PermissionProfile::from_legacy_sandbox_policy( &workspace_write, - &cwd, )), ); assert_eq!(sandbox, Some(workspace_write.into())); @@ -1581,13 +1579,12 @@ mod tests { external_sandbox.clone(), Some(PermissionProfile::from_legacy_sandbox_policy( &external_sandbox, - &cwd, )), ); assert_eq!(sandbox, None); assert_eq!( profile, - Some(PermissionProfile::from_legacy_sandbox_policy(&external_sandbox, &cwd).into()) + Some(PermissionProfile::from_legacy_sandbox_policy(&external_sandbox).into()) ); } @@ -1672,7 +1669,6 @@ mod tests { permission_profile: Some( codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &codex_protocol::protocol::SandboxPolicy::new_read_only_policy(), - &test_path_buf("/tmp/project"), ) .into(), ), @@ -1721,7 +1717,6 @@ mod tests { SandboxPolicy::new_read_only_policy(), Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_read_only_policy(), - std::path::Path::new("/tmp/project"), )), test_path_buf("/tmp/project").abs(), Vec::new(), @@ -1755,7 +1750,6 @@ mod tests { SandboxPolicy::new_read_only_policy(), Some(PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::new_read_only_policy(), - std::path::Path::new("/tmp/project"), )), test_path_buf("/tmp/project").abs(), Vec::new(), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9e851f0fc..b748b11e6 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2125,7 +2125,7 @@ impl ChatWidget { { Some(permission_profile) => permission_profile.to_runtime_permissions(), None => ( - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( &event.sandbox_policy, &event.cwd, ), @@ -9791,7 +9791,7 @@ impl ChatWidget { self.config.permissions.sandbox_policy.set(policy)?; let sandbox_policy = self.config.permissions.sandbox_policy.get(); self.config.permissions.file_system_sandbox_policy = - codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( sandbox_policy, &self.config.cwd, ); diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 5f089ebbf..cc684d0a8 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -321,13 +321,20 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { let updated_sandbox = SandboxPolicy::new_workspace_write_policy(); chat.set_sandbox_policy(updated_sandbox.clone()) .expect("set sandbox policy"); + let updated_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + &updated_sandbox, + &expected_cwd, + ); assert_eq!( chat.config_ref().permissions.permission_profile(), - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &updated_sandbox, - &expected_cwd + codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( + codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy( + &updated_sandbox + ), + &updated_file_system_policy, + NetworkSandboxPolicy::from(&updated_sandbox), ), - "local sandbox changes should replace SessionConfigured profile-derived runtime permissions" + "local sandbox changes should replace SessionConfigured profile-derived runtime permissions using the widget cwd" ); }