diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 0c95af4c02..2a2fbacdbe 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -33,9 +33,11 @@ use tracing::instrument; use crate::bash::parse_shell_lc_plain_commands; use crate::bash::parse_shell_lc_single_command_prefix; use crate::config::Config; +use crate::powershell::extract_powershell_command; use crate::sandboxing::SandboxPermissions; use crate::tools::sandboxing::ExecApprovalRequirement; use codex_utils_absolute_path::AbsolutePathBuf; +use shlex::split as shlex_split; use shlex::try_join as shlex_try_join; const PROMPT_CONFLICT_REASON: &str = @@ -631,9 +633,35 @@ fn commands_for_exec_policy(command: &[String]) -> (Vec>, bool) { return (vec![single_command], true); } + if let Some(single_command) = parse_powershell_plain_command(command) { + return (vec![single_command], false); + } + (vec![command.to_vec()], false) } +fn parse_powershell_plain_command(command: &[String]) -> Option> { + let (_, script) = extract_powershell_command(command)?; + let script = script.trim(); + if script.is_empty() { + return None; + } + + // Only normalize simple wrapper cases where the PowerShell script is + // effectively a plain external command invocation. Anything that looks like + // real PowerShell syntax should continue to use the opaque argv form. + if script.chars().any(|c| { + matches!( + c, + '\n' | '\r' | ';' | '|' | '&' | '(' | ')' | '{' | '}' | '$' | '@' | '`' | '<' | '>' + ) + }) { + return None; + } + + shlex_split(script).filter(|tokens| !tokens.is_empty()) +} + /// Derive a proposed execpolicy amendment when a command requires user approval /// - If any execpolicy rule prompts, return None, because an amendment would not skip that policy requirement. /// - Otherwise return the first heuristics Prompt. diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index fd3fe05e11..c1af0974a4 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -590,6 +590,73 @@ async fn evaluates_heredoc_script_against_prefix_rules() { ); } +#[tokio::test] +async fn powershell_wrapped_plain_command_matches_prefix_rules() { + 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 policy = Arc::new(parser.build()); + let command = vec![ + "pwsh".to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "git add -A".to_string(), + ]; + + let requirement = ExecPolicyManager::new(policy) + .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::UseDefault, + prefix_rule: None, + }) + .await; + + assert_eq!( + requirement, + ExecApprovalRequirement::Skip { + bypass_sandbox: true, + proposed_execpolicy_amendment: None, + } + ); +} + +#[tokio::test] +async fn requested_prefix_rule_can_approve_powershell_wrapped_plain_commands() { + let command = vec![ + "pwsh".to_string(), + "-NoProfile".to_string(), + "-Command".to_string(), + "git add -A".to_string(), + ]; + + let requirement = ExecPolicyManager::default() + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + command: &command, + approval_policy: AskForApproval::UnlessTrusted, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), + file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: Some(vec!["git".to_string(), "add".to_string()]), + }) + .await; + + assert_eq!( + requirement, + ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ + "git".to_string(), + "add".to_string(), + ])), + } + ); +} + #[tokio::test] async fn omits_auto_amendment_for_heredoc_fallback_prompts() { let command = vec![