diff --git a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs index 88f23762d6..1311813262 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs @@ -324,6 +324,7 @@ impl ToolHandler for ExecCommandHandler { yield_time_ms, max_output_tokens: Some(max_output_tokens), cwd, + sandbox_cwd: turn_environment.cwd.clone(), environment, network: context.turn.network.clone(), tty, diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 5bc6687c37..2050ec6969 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -59,6 +59,7 @@ pub struct UnifiedExecRequest { pub hook_command: String, pub process_id: i32, pub cwd: AbsolutePathBuf, + pub sandbox_cwd: AbsolutePathBuf, pub environment: Arc, pub env: HashMap, pub exec_server_env_config: Option, @@ -217,7 +218,7 @@ impl Approvable for UnifiedExecRuntime<'_> { impl<'a> ToolRuntime for UnifiedExecRuntime<'a> { fn sandbox_cwd<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b AbsolutePathBuf> { - Some(&req.cwd) + Some(&req.sandbox_cwd) } fn network_approval_spec( @@ -361,7 +362,10 @@ impl<'a> ToolRuntime for UnifiedExecRunt mod tests { use super::*; use crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS; + use crate::tools::sandboxing::ToolRuntime; + use codex_exec_server::Environment; use std::time::Duration; + use tempfile::tempdir; #[test] fn unified_exec_options_combines_default_timeout_with_network_denial_cancellation() { @@ -384,4 +388,40 @@ mod tests { other => panic!("expected timeout-or-cancellation expiration, got {other:?}"), } } + + #[tokio::test] + async fn unified_exec_uses_the_trusted_sandbox_cwd() { + let cwd_dir = tempdir().expect("create process temp dir"); + let sandbox_dir = tempdir().expect("create sandbox temp dir"); + let cwd = + AbsolutePathBuf::try_from(cwd_dir.path().to_path_buf()).expect("absolute temp dir"); + let sandbox_cwd = AbsolutePathBuf::try_from(sandbox_dir.path().to_path_buf()) + .expect("absolute sandbox temp dir"); + let manager = UnifiedExecProcessManager::default(); + let runtime = UnifiedExecRuntime::new(&manager, UnifiedExecShellMode::Direct); + let request = UnifiedExecRequest { + command: vec!["pwd".to_string()], + hook_command: "pwd".to_string(), + process_id: 1000, + cwd, + sandbox_cwd: sandbox_cwd.clone(), + environment: Arc::new(Environment::default_for_tests()), + env: HashMap::new(), + exec_server_env_config: None, + explicit_env_overrides: HashMap::new(), + network: None, + tty: false, + sandbox_permissions: SandboxPermissions::UseDefault, + additional_permissions: None, + #[cfg(unix)] + additional_permissions_preapproved: false, + justification: None, + exec_approval_requirement: ExecApprovalRequirement::Skip { + bypass_sandbox: false, + proposed_execpolicy_amendment: None, + }, + }; + + assert_eq!(runtime.sandbox_cwd(&request), Some(&sandbox_cwd)); + } } diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 3f548150f7..b6788e8e71 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -92,6 +92,7 @@ pub(crate) struct ExecCommandRequest { pub yield_time_ms: u64, pub max_output_tokens: Option, pub cwd: AbsolutePathBuf, + pub sandbox_cwd: AbsolutePathBuf, pub environment: Arc, pub network: Option, pub tty: bool, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index fa1288be46..3032bd63f3 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -1028,7 +1028,9 @@ impl UnifiedExecProcessManager { approval_policy: context.turn.approval_policy.value(), permission_profile: context.turn.permission_profile(), file_system_sandbox_policy: &file_system_sandbox_policy, - sandbox_cwd: cwd.as_path(), + // The process cwd may be model-controlled. Policy resolution + // stays anchored to the selected turn environment cwd instead. + sandbox_cwd: request.sandbox_cwd.as_path(), sandbox_permissions: if request.additional_permissions_preapproved { crate::sandboxing::SandboxPermissions::UseDefault } else { @@ -1042,6 +1044,7 @@ impl UnifiedExecProcessManager { hook_command: request.hook_command.clone(), process_id: request.process_id, cwd, + sandbox_cwd: request.sandbox_cwd.clone(), environment: Arc::clone(&request.environment), env, exec_server_env_config: Some(exec_server_env_config), diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index bde32c9ab4..5ef5994030 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -176,6 +176,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() { yield_time_ms: 1000, max_output_tokens: None, cwd: turn.cwd.clone(), + sandbox_cwd: turn.cwd.clone(), environment: turn .environments .primary_environment()