From 19d0949aabbf065cf8859fd6229a0fa1b3eaaa5e Mon Sep 17 00:00:00 2001 From: Jack Mousseau Date: Thu, 12 Mar 2026 00:27:11 -0700 Subject: [PATCH] Handle pre-approved permissions in zsh fork (#14431) --- codex-rs/core/src/tools/handlers/shell.rs | 3 + codex-rs/core/src/tools/runtimes/shell.rs | 2 + .../tools/runtimes/shell/unix_escalation.rs | 28 ++++++++- .../runtimes/shell/unix_escalation_tests.rs | 57 +++++++++++++++++++ .../core/src/tools/runtimes/unified_exec.rs | 2 + .../core/src/unified_exec/process_manager.rs | 2 + 6 files changed, 93 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 26b04af9e..a9c3aee34 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -422,6 +422,9 @@ impl ShellHandler { network: exec_params.network.clone(), sandbox_permissions: effective_additional_permissions.sandbox_permissions, additional_permissions: normalized_additional_permissions, + #[cfg(unix)] + additional_permissions_preapproved: effective_additional_permissions + .permissions_preapproved, justification: exec_params.justification.clone(), exec_approval_requirement, }; diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index dea0aa202..daf8e8290 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -51,6 +51,8 @@ pub struct ShellRequest { pub network: Option, pub sandbox_permissions: SandboxPermissions, pub additional_permissions: Option, + #[cfg(unix)] + pub additional_permissions_preapproved: bool, pub justification: Option, pub exec_approval_requirement: ExecApprovalRequirement, } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 36260d59a..4f3240c12 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -71,6 +71,22 @@ const REJECT_RULES_APPROVAL_REASON: &str = const REJECT_SKILL_APPROVAL_REASON: &str = "approval required by skill, but AskForApproval::Reject.skill_approval is set"; +fn approval_sandbox_permissions( + sandbox_permissions: SandboxPermissions, + additional_permissions_preapproved: bool, +) -> SandboxPermissions { + if additional_permissions_preapproved + && matches!( + sandbox_permissions, + SandboxPermissions::WithAdditionalPermissions + ) + { + SandboxPermissions::UseDefault + } else { + sandbox_permissions + } +} + pub(super) async fn try_run_zsh_fork( req: &ShellRequest, attempt: &SandboxAttempt<'_>, @@ -170,6 +186,10 @@ pub(super) async fn try_run_zsh_fork( // escalation server. let stopwatch = Stopwatch::new(effective_timeout); let cancel_token = stopwatch.cancellation_token(); + let approval_sandbox_permissions = approval_sandbox_permissions( + req.sandbox_permissions, + req.additional_permissions_preapproved, + ); let escalation_policy = CoreShellActionProvider { policy: Arc::clone(&exec_policy), session: Arc::clone(&ctx.session), @@ -181,6 +201,7 @@ pub(super) async fn try_run_zsh_fork( file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(), network_sandbox_policy: command_executor.network_sandbox_policy, sandbox_permissions: req.sandbox_permissions, + approval_sandbox_permissions, prompt_permissions: req.additional_permissions.clone(), stopwatch: stopwatch.clone(), }; @@ -281,6 +302,10 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(), network_sandbox_policy: exec_request.network_sandbox_policy, sandbox_permissions: req.sandbox_permissions, + approval_sandbox_permissions: approval_sandbox_permissions( + req.sandbox_permissions, + req.additional_permissions_preapproved, + ), prompt_permissions: req.additional_permissions.clone(), stopwatch: Stopwatch::unlimited(), }; @@ -312,6 +337,7 @@ struct CoreShellActionProvider { file_system_sandbox_policy: FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, sandbox_permissions: SandboxPermissions, + approval_sandbox_permissions: SandboxPermissions, prompt_permissions: Option, stopwatch: Stopwatch, } @@ -696,7 +722,7 @@ impl EscalationPolicy for CoreShellActionProvider { approval_policy: self.approval_policy, sandbox_policy: &self.sandbox_policy, file_system_sandbox_policy: &self.file_system_sandbox_policy, - sandbox_permissions: self.sandbox_permissions, + sandbox_permissions: self.approval_sandbox_permissions, enable_shell_wrapper_parsing: ENABLE_INTERCEPTED_EXEC_POLICY_SHELL_WRAPPER_PARSING, }, 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 a1a34cff4..23f813670 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 @@ -165,6 +165,22 @@ fn execve_prompt_rejection_keeps_unmatched_commands_on_sandbox_flag() { ); } +#[test] +fn approval_sandbox_permissions_only_downgrades_preapproved_additional_permissions() { + assert_eq!( + super::approval_sandbox_permissions(SandboxPermissions::WithAdditionalPermissions, true), + SandboxPermissions::UseDefault, + ); + assert_eq!( + super::approval_sandbox_permissions(SandboxPermissions::WithAdditionalPermissions, false), + SandboxPermissions::WithAdditionalPermissions, + ); + assert_eq!( + super::approval_sandbox_permissions(SandboxPermissions::RequireEscalated, true), + SandboxPermissions::RequireEscalated, + ); +} + #[test] fn extract_shell_script_preserves_login_flag() { assert_eq!( @@ -548,6 +564,47 @@ host_executable(name = "git", paths = ["{git_path_literal}"]) )); } +#[test] +fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default() { + let policy = PolicyParser::new().build(); + let program = AbsolutePathBuf::try_from(host_absolute_path(&["usr", "bin", "printf"])).unwrap(); + let argv = ["printf".to_string(), "hello".to_string()]; + let approval_policy = AskForApproval::OnRequest; + let sandbox_policy = SandboxPolicy::new_workspace_write_policy(); + let file_system_sandbox_policy = read_only_file_system_sandbox_policy(); + + let preapproved = evaluate_intercepted_exec_policy( + &policy, + &program, + &argv, + InterceptedExecPolicyContext { + approval_policy, + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, + sandbox_permissions: super::approval_sandbox_permissions( + SandboxPermissions::WithAdditionalPermissions, + true, + ), + enable_shell_wrapper_parsing: false, + }, + ); + let fresh_request = evaluate_intercepted_exec_policy( + &policy, + &program, + &argv, + InterceptedExecPolicyContext { + approval_policy, + sandbox_policy: &sandbox_policy, + file_system_sandbox_policy: &file_system_sandbox_policy, + sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, + enable_shell_wrapper_parsing: false, + }, + ); + + assert_eq!(preapproved.decision, Decision::Allow); + assert_eq!(fresh_request.decision, Decision::Prompt); +} + #[test] fn intercepted_exec_policy_rejects_disallowed_host_executable_mapping() { let allowed_git = host_absolute_path(&["usr", "bin", "git"]); diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 48052a537..8d15c96c6 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -54,6 +54,8 @@ pub struct UnifiedExecRequest { pub tty: bool, pub sandbox_permissions: SandboxPermissions, pub additional_permissions: Option, + #[cfg(unix)] + pub additional_permissions_preapproved: bool, pub justification: Option, pub exec_approval_requirement: ExecApprovalRequirement, } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index f2c0f7d31..dcf2dd090 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -603,6 +603,8 @@ impl UnifiedExecProcessManager { tty: request.tty, sandbox_permissions: request.sandbox_permissions, additional_permissions: request.additional_permissions.clone(), + #[cfg(unix)] + additional_permissions_preapproved: request.additional_permissions_preapproved, justification: request.justification.clone(), exec_approval_requirement, };