diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 0c95af4c02..2208b42d7a 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -25,6 +25,7 @@ use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +use codex_shell_command::command_safety::windows_safe_commands::try_parse_powershell_command_sequence; use thiserror::Error; use tokio::fs; use tokio::task::spawn_blocking; @@ -627,6 +628,12 @@ fn commands_for_exec_policy(command: &[String]) -> (Vec>, bool) { return (commands, false); } + if let Some(commands) = try_parse_powershell_command_sequence(command) + && !commands.is_empty() + { + return (commands, false); + } + if let Some(single_command) = parse_shell_lc_single_command_prefix(command) { return (vec![single_command], true); } diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index fd3fe05e11..69f5f6381d 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -556,6 +556,48 @@ fn commands_for_exec_policy_falls_back_for_whitespace_shell_script() { assert_eq!(commands_for_exec_policy(&command), (vec![command], false)); } +#[tokio::test] +async fn powershell_single_command_prefix_rules_can_bypass_sandbox() { + if which::which("pwsh") + .or_else(|_| which::which("powershell")) + .is_err() + { + return; + } + + let policy_src = r#"prefix_rule(pattern=["git", "add"], decision="allow")"#; + let mut parser = PolicyParser::new(); + parser + .parse("test.rules", policy_src) + .expect("parse policy"); + + let command = vec![ + "pwsh".to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "git add -A".to_string(), + ]; + + let requirement = ExecPolicyManager::new(Arc::new(parser.build())) + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + command: &command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_permissions: SandboxPermissions::RequireEscalated, + prefix_rule: None, + }) + .await; + + assert_eq!( + requirement, + ExecApprovalRequirement::Skip { + bypass_sandbox: true, + proposed_execpolicy_amendment: None, + } + ); +} + #[tokio::test] async fn evaluates_heredoc_script_against_prefix_rules() { let policy_src = r#"prefix_rule(pattern=["python3"], decision="allow")"#; diff --git a/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs b/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs index ac479a4d2c..9e124184ac 100644 --- a/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs +++ b/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs @@ -22,7 +22,7 @@ pub fn is_safe_command_windows(command: &[String]) -> bool { /// Returns each command sequence if the invocation starts with a PowerShell binary. /// For example, the tokens from `pwsh Get-ChildItem | Measure-Object` become two sequences. -fn try_parse_powershell_command_sequence(command: &[String]) -> Option>> { +pub fn try_parse_powershell_command_sequence(command: &[String]) -> Option>> { let (exe, rest) = command.split_first()?; if is_powershell_executable(exe) { parse_powershell_invocation(exe, rest)