diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index d092666369..d805cb0990 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -37,6 +37,7 @@ use crate::sandboxing::SandboxPermissions; use crate::tools::sandboxing::ExecApprovalRequirement; use codex_shell_command::bash::parse_shell_lc_plain_commands; use codex_shell_command::bash::parse_shell_lc_single_command_prefix; +use codex_shell_command::split_powershell_command_sequence; use codex_utils_absolute_path::AbsolutePathBuf; use shlex::try_join as shlex_try_join; @@ -705,6 +706,12 @@ fn commands_for_exec_policy(command: &[String]) -> (Vec>, bool) { return (commands, false); } + if let Some(commands) = split_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 2db07a0fce..a87f1f10a4 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -674,6 +674,23 @@ fn commands_for_exec_policy_falls_back_for_whitespace_shell_script() { assert_eq!(commands_for_exec_policy(&command), (vec![command], false)); } +#[test] +fn commands_for_exec_policy_splits_powershell_command() { + let command = vec![ + "pwsh".to_string(), + "-Command".to_string(), + "git add -A".to_string(), + ]; + + assert_eq!( + commands_for_exec_policy(&command), + ( + vec![vec!["git".to_string(), "add".to_string(), "-A".to_string(),]], + false, + ) + ); +} + #[tokio::test] async fn ignore_user_config_keeps_user_policy_files() -> std::io::Result<()> { let temp = tempdir()?; @@ -1637,6 +1654,32 @@ prefix_rule(pattern=["bash"], decision="allow") .await; } +#[tokio::test] +async fn powershell_command_bypasses_sandbox_when_inner_command_matches_policy_allow() { + let policy_src = r#"prefix_rule(pattern=["git", "add"], decision="allow")"#; + + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src: Some(policy_src.to_string()), + command: vec![ + "pwsh".to_string(), + "-Command".to_string(), + "git add -A".to_string(), + ], + approval_policy: AskForApproval::OnRequest, + sandbox_policy: SandboxPolicy::new_workspace_write_policy(), + file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(), + sandbox_permissions: SandboxPermissions::RequireEscalated, + prefix_rule: None, + }, + ExecApprovalRequirement::Skip { + bypass_sandbox: true, + proposed_execpolicy_amendment: None, + }, + ) + .await; +} + fn derive_requested_execpolicy_amendment_for_test( prefix_rule: Option<&Vec>, matched_rules: &[RuleMatch], 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 b6c5d863bd..112f52d8d9 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 @@ -6,7 +6,7 @@ use std::path::Path; /// On Windows, we conservatively allow only clearly read-only PowerShell invocations /// that match a small safelist. Anything else (including direct CMD commands) is unsafe. pub fn is_safe_command_windows(command: &[String]) -> bool { - if let Some(commands) = try_parse_powershell_command_sequence(command) { + if let Some(commands) = split_powershell_command_sequence(command) { commands .iter() .all(|cmd| is_safe_powershell_command(cmd.as_slice())) @@ -18,7 +18,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 split_powershell_command_sequence(command: &[String]) -> Option>> { let (exe, rest) = command.split_first()?; if is_powershell_executable(exe) { parse_powershell_invocation(exe, rest) diff --git a/codex-rs/shell-command/src/lib.rs b/codex-rs/shell-command/src/lib.rs index 1d9e302a4e..6250217b48 100644 --- a/codex-rs/shell-command/src/lib.rs +++ b/codex-rs/shell-command/src/lib.rs @@ -9,3 +9,4 @@ pub mod powershell; pub use command_safety::is_dangerous_command; pub use command_safety::is_safe_command; +pub use command_safety::windows_safe_commands::split_powershell_command_sequence;