diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index e7dfba8506..690f92a536 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -992,6 +992,52 @@ async fn exec_approval_requirement_prefers_execpolicy_match() { .await; } +#[tokio::test] +async fn git_status_obeys_approval_policy_and_explicit_rules() { + let command = vec_str(&["git", "status"]); + let amendment = Some(ExecPolicyAmendment::new(command.clone())); + + for (approval_policy, policy_src, expected_requirement) in [ + ( + AskForApproval::UnlessTrusted, + None, + ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: amendment.clone(), + }, + ), + ( + AskForApproval::OnRequest, + None, + ExecApprovalRequirement::Skip { + bypass_sandbox: false, + proposed_execpolicy_amendment: amendment, + }, + ), + ( + AskForApproval::UnlessTrusted, + Some(r#"prefix_rule(pattern=["git", "status"], decision="allow")"#.to_string()), + ExecApprovalRequirement::Skip { + bypass_sandbox: true, + proposed_execpolicy_amendment: None, + }, + ), + ] { + assert_exec_approval_requirement_for_command( + ExecApprovalRequirementScenario { + policy_src, + command: command.clone(), + approval_policy, + permission_profile: PermissionProfile::workspace_write(), + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }, + expected_requirement, + ) + .await; + } +} + #[tokio::test] async fn absolute_path_exec_approval_requirement_matches_host_executable_rules() { let git_path = host_program_path("git"); @@ -1044,8 +1090,8 @@ prefix_rule(pattern=["git"], decision="prompt") sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }, - ExecApprovalRequirement::Skip { - bypass_sandbox: false, + ExecApprovalRequirement::NeedsApproval { + reason: None, proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![ disallowed_git_path, "status".to_string(), diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index 13786af416..3c8789f6a8 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -31,10 +31,12 @@ use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_target_windows; +use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::local_selections; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; use std::fs; @@ -101,6 +103,91 @@ fn assert_no_matched_rules_invariant(output_item: &Value) { ); } +#[tokio::test] +async fn git_status_requires_approval_under_unless_trusted() -> Result<()> { + skip_if_wine_exec!(Ok(()), "command approval requires host-native paths"); + + let server = start_mock_server().await; + let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| { + config + .features + .enable(Feature::UnifiedExec) + .expect("test config should allow feature update"); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted); + config + .permissions + .set_permission_profile(PermissionProfile::workspace_write()) + .expect("set workspace-write permissions"); + config.approvals_reviewer = ApprovalsReviewer::User; + }); + let test = builder.build_with_auto_env(&server).await?; + let call_id = "git-status-approval"; + let args = json!({"cmd": "git status", "yield_time_ms": 1_000}); + let initial_mock = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-git-status-1"), + ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?), + ev_completed("resp-git-status-1"), + ]), + ) + .await; + let results_mock = mount_sse_once( + &server, + sse(vec![ + ev_assistant_message("msg-git-status-1", "done"), + ev_completed("resp-git-status-2"), + ]), + ) + .await; + + test.codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "check git status".into(), + text_elements: Vec::new(), + }])) + .await?; + let event = wait_for_event(&test.codex, |event| { + matches!( + event, + EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_) + ) + }) + .await; + let EventMsg::ExecApprovalRequest(approval) = event else { + let output = results_mock.function_call_output_text(call_id); + panic!( + "expected git status to request approval before turn completion; output: {output:?}" + ); + }; + assert_eq!(approval.call_id, call_id); + test.codex + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::denied("git status was not approved"), + }) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + assert!( + initial_mock + .single_request() + .message_input_texts("user") + .iter() + .any(|text| text == "check git status") + ); + let output = results_mock + .single_request() + .function_call_output_text(call_id) + .expect("shell command output"); + assert!(output.contains("git status was not approved"), "{output}"); + + Ok(()) +} + #[tokio::test] async fn startup_migrates_default_policy_and_honors_ignore_rules() -> Result<()> { const LEGACY_POLICY: &str = r#"prefix_rule(pattern=["rm"], decision="allow") diff --git a/codex-rs/shell-command/src/command_safety/is_dangerous_command.rs b/codex-rs/shell-command/src/command_safety/is_dangerous_command.rs index 59fce97566..d689afdccb 100644 --- a/codex-rs/shell-command/src/command_safety/is_dangerous_command.rs +++ b/codex-rs/shell-command/src/command_safety/is_dangerous_command.rs @@ -67,31 +67,6 @@ pub fn dangerous_powershell_words_match(command: &[String]) -> Option bool { - matches!( - arg, - "-C" | "-c" - | "--config-env" - | "--exec-path" - | "--git-dir" - | "--namespace" - | "--super-prefix" - | "--work-tree" - ) -} - -fn is_git_global_option_with_inline_value(arg: &str) -> bool { - matches!( - arg, - s if s.starts_with("--config-env=") - || s.starts_with("--exec-path=") - || s.starts_with("--git-dir=") - || s.starts_with("--namespace=") - || s.starts_with("--super-prefix=") - || s.starts_with("--work-tree=") - ) || ((arg.starts_with("-C") || arg.starts_with("-c")) && arg.len() > 2) -} - pub(crate) fn executable_name_lookup_key(raw: &str) -> Option { #[cfg(windows)] { @@ -118,54 +93,6 @@ pub(crate) fn executable_name_lookup_key(raw: &str) -> Option { } } -/// Find the first matching git subcommand, skipping known global options that -/// may appear before it (e.g., `-C`, `-c`, `--git-dir`). -/// -/// Shared with `is_safe_command` to avoid git-global-option bypasses. -pub(crate) fn find_git_subcommand<'a>( - command: &'a [String], - subcommands: &[&str], -) -> Option<(usize, &'a str)> { - let cmd0 = command.first().map(String::as_str)?; - if executable_name_lookup_key(cmd0).as_deref() != Some("git") { - return None; - } - - let mut skip_next = false; - for (idx, arg) in command.iter().enumerate().skip(1) { - if skip_next { - skip_next = false; - continue; - } - - let arg = arg.as_str(); - - if is_git_global_option_with_inline_value(arg) { - continue; - } - - if is_git_global_option_with_value(arg) { - skip_next = true; - continue; - } - - if arg == "--" || arg.starts_with('-') { - continue; - } - - if subcommands.contains(&arg) { - return Some((idx, arg)); - } - - // In git, the first non-option token is the subcommand. If it isn't - // one of the subcommands we're looking for, we must stop scanning to - // avoid misclassifying later positional args (e.g., branch names). - return None; - } - - None -} - fn dangerous_command_match_for_exec( command: &[String], wrapper_depth: usize, diff --git a/codex-rs/shell-command/src/command_safety/is_safe_command.rs b/codex-rs/shell-command/src/command_safety/is_safe_command.rs index bca44139cd..1f24a6d21d 100644 --- a/codex-rs/shell-command/src/command_safety/is_safe_command.rs +++ b/codex-rs/shell-command/src/command_safety/is_safe_command.rs @@ -1,9 +1,5 @@ use crate::bash::parse_shell_lc_plain_commands; use crate::command_safety::is_dangerous_command::executable_name_lookup_key; -// Find the first matching git subcommand, skipping known global options that -// may appear before it (e.g., `-C`, `-c`, `--git-dir`). -// Implemented in `is_dangerous_command` and shared here. -use crate::command_safety::is_dangerous_command::find_git_subcommand; #[cfg(windows)] use crate::command_safety::windows_safe_commands::is_safe_command_windows; #[cfg(windows)] @@ -153,8 +149,8 @@ fn is_safe_to_call_with_exec(command: &[String]) -> bool { }) } - // Git - Some("git") => is_safe_git_command(command), + // Repository configuration can make even read-only Git commands execute helpers. + Some("git") => false, // Special-case `sed -n {N|M,N}p` Some("sed") @@ -172,128 +168,6 @@ fn is_safe_to_call_with_exec(command: &[String]) -> bool { } } -pub(crate) fn is_safe_git_command(command: &[String]) -> bool { - let Some((subcommand_idx, subcommand)) = - find_git_subcommand(command, &["status", "log", "diff", "show", "branch"]) - else { - return false; - }; - - let global_args = &command[1..subcommand_idx]; - if git_has_unsafe_global_option(global_args) { - return false; - } - - let subcommand_args = &command[subcommand_idx + 1..]; - - match subcommand { - "status" | "log" | "diff" | "show" => git_subcommand_args_are_read_only(subcommand_args), - "branch" => { - git_subcommand_args_are_read_only(subcommand_args) - && git_branch_is_read_only(subcommand_args) - } - other => { - debug_assert!(false, "unexpected git subcommand from matcher: {other}"); - false - } - } -} - -// Treat `git branch` as safe only when the arguments clearly indicate -// a read-only query, not a branch mutation (create/rename/delete). -fn git_branch_is_read_only(branch_args: &[String]) -> bool { - if branch_args.is_empty() { - // `git branch` with no additional args lists branches. - return true; - } - - let mut saw_read_only_flag = false; - for arg in branch_args.iter().map(String::as_str) { - match arg { - "--list" | "-l" | "--show-current" | "-a" | "--all" | "-r" | "--remotes" | "-v" - | "-vv" | "--verbose" => { - saw_read_only_flag = true; - } - _ if arg.starts_with("--format=") => { - saw_read_only_flag = true; - } - _ => { - // Any other flag or positional argument may create, rename, or delete branches. - return false; - } - } - } - - saw_read_only_flag -} - -#[derive(Clone, Copy)] -enum GitOptionPattern { - Exact(&'static str), - ShortWithInlineValue(&'static str), - Prefix(&'static str), -} - -const UNSAFE_GIT_GLOBAL_OPTIONS: &[GitOptionPattern] = &[ - GitOptionPattern::Exact("-C"), - GitOptionPattern::ShortWithInlineValue("-C"), - GitOptionPattern::Exact("-c"), - GitOptionPattern::ShortWithInlineValue("-c"), - GitOptionPattern::Exact("-p"), - GitOptionPattern::Exact("--config-env"), - GitOptionPattern::Prefix("--config-env="), - GitOptionPattern::Exact("--exec-path"), - GitOptionPattern::Prefix("--exec-path="), - GitOptionPattern::Exact("--git-dir"), - GitOptionPattern::Prefix("--git-dir="), - GitOptionPattern::Exact("--namespace"), - GitOptionPattern::Prefix("--namespace="), - GitOptionPattern::Exact("--paginate"), - GitOptionPattern::Exact("--super-prefix"), - GitOptionPattern::Prefix("--super-prefix="), - GitOptionPattern::Exact("--work-tree"), - GitOptionPattern::Prefix("--work-tree="), -]; - -const UNSAFE_GIT_SUBCOMMAND_OPTIONS: &[GitOptionPattern] = &[ - GitOptionPattern::Exact("--output"), - GitOptionPattern::Prefix("--output="), - GitOptionPattern::Exact("--ext-diff"), - GitOptionPattern::Exact("--textconv"), - GitOptionPattern::Exact("--exec"), - GitOptionPattern::Prefix("--exec="), -]; - -impl GitOptionPattern { - fn matches(self, arg: &str) -> bool { - match self { - GitOptionPattern::Exact(option) => arg == option, - GitOptionPattern::ShortWithInlineValue(option) => { - arg.starts_with(option) && arg.len() > option.len() - } - GitOptionPattern::Prefix(prefix) => arg.starts_with(prefix), - } - } -} - -fn git_matches_option_pattern(arg: &str, patterns: &[GitOptionPattern]) -> bool { - patterns.iter().any(|pattern| pattern.matches(arg)) -} - -fn git_has_unsafe_global_option(global_args: &[String]) -> bool { - global_args - .iter() - .map(String::as_str) - .any(|arg| git_matches_option_pattern(arg, UNSAFE_GIT_GLOBAL_OPTIONS)) -} - -fn git_subcommand_args_are_read_only(args: &[String]) -> bool { - !args - .iter() - .map(String::as_str) - .any(|arg| git_matches_option_pattern(arg, UNSAFE_GIT_SUBCOMMAND_OPTIONS)) -} - // (bash parsing helpers implemented in crate::bash) /* ---------------------------------------------------------- @@ -345,13 +219,6 @@ mod tests { #[test] fn known_safe_examples() { assert!(is_safe_to_call_with_exec(&vec_str(&["ls"]))); - assert!(is_safe_to_call_with_exec(&vec_str(&["git", "status"]))); - assert!(is_safe_to_call_with_exec(&vec_str(&["git", "branch"]))); - assert!(is_safe_to_call_with_exec(&vec_str(&[ - "git", - "branch", - "--show-current" - ]))); assert!(is_safe_to_call_with_exec(&vec_str(&["base64"]))); assert!(is_safe_to_call_with_exec(&vec_str(&[ "sed", "-n", "1,5p", "file.txt" @@ -377,153 +244,25 @@ mod tests { } #[test] - fn git_branch_mutating_flags_are_not_safe() { - assert!(!is_known_safe_command(&vec_str(&[ - "git", "branch", "-d", "feature" - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "git", - "branch", - "new-branch" - ]))); - } - - #[test] - fn git_branch_global_options_respect_safety_rules() { - assert!(is_known_safe_command(&vec_str(&[ - "git", - "branch", - "--show-current", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "git", "branch", "-d", "feature", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "bash", - "-lc", - "git branch -d feature", - ]))); - } - - #[test] - fn git_first_positional_is_the_subcommand() { - // In git, the first non-option token is the subcommand. Later positional - // args (like branch names) must not be treated as subcommands. - assert!(!is_known_safe_command(&vec_str(&[ - "git", "checkout", "status", - ]))); - } - - #[test] - fn git_output_flags_are_not_safe() { - assert!(!is_known_safe_command(&vec_str(&[ - "git", - "log", - "--output=/tmp/git-log-out-test", - "-n", - "1", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "git", - "diff", - "--output", - "/tmp/git-diff-out-test", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "git", - "show", - "--output=/tmp/git-show-out-test", - "HEAD", - ]))); - } - - #[test] - fn git_global_pagination_flags_are_not_safe() { - assert!(!is_known_safe_command(&vec_str(&[ - "git", - "--paginate", - "log", - "-1", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "git", "-p", "log", "-1", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "bash", - "-lc", - "git --paginate log -1", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "bash", - "-lc", - "git -p log -1", - ]))); - } - - #[test] - fn git_subcommand_patch_flags_remain_safe() { - assert!(is_known_safe_command(&vec_str(&["git", "log", "-p", "-1"]))); - assert!(is_known_safe_command(&vec_str(&["git", "diff", "-p"]))); - assert!(is_known_safe_command(&vec_str(&[ - "git", "show", "-p", "HEAD", - ]))); - assert!(is_known_safe_command(&vec_str(&[ - "bash", - "-lc", - "git log -p -1", - ]))); - } - - #[test] - fn git_global_override_flags_are_not_safe() { - assert!(!is_known_safe_command(&vec_str(&[ - "git", "-C", ".", "status", - ]))); - assert!(!is_known_safe_command(&vec_str(&["git", "-C.", "status",]))); - assert!(!is_known_safe_command(&vec_str(&[ - "git", - "-c", - "core.pager=cat", - "log", - "-n", - "1", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "git", - "-ccore.pager=cat", - "status", - ]))); - + fn git_commands_are_not_known_safe() { for args in [ - vec_str(&["git", "--config-env", "core.pager=PAGER", "show", "HEAD"]), - vec_str(&["git", "--config-env=core.pager=PAGER", "show", "HEAD"]), - vec_str(&["git", "--git-dir", ".evil-git", "diff", "HEAD~1..HEAD"]), - vec_str(&["git", "--git-dir=.evil-git", "diff", "HEAD~1..HEAD"]), - vec_str(&["git", "--work-tree", ".", "status"]), - vec_str(&["git", "--work-tree=.", "status"]), - vec_str(&["git", "--exec-path", ".git/helpers", "show", "HEAD"]), - vec_str(&["git", "--exec-path=.git/helpers", "show", "HEAD"]), - vec_str(&["git", "--namespace", "attacker", "show", "HEAD"]), - vec_str(&["git", "--namespace=attacker", "show", "HEAD"]), - vec_str(&["git", "--super-prefix", "attacker/", "show", "HEAD"]), - vec_str(&["git", "--super-prefix=attacker/", "show", "HEAD"]), + vec_str(&["git", "status", "--short"]), + vec_str(&["git", "log", "-p", "-1"]), + vec_str(&["git", "diff"]), + vec_str(&["git", "show", "HEAD"]), + vec_str(&["git", "branch"]), + vec_str(&["git", "branch", "--show-current"]), + vec_str(&["git", "--version"]), + vec_str(&["/usr/bin/git", "status"]), + vec_str(&["bash", "-lc", "git status"]), + vec_str(&["zsh", "-lc", "cd nested && git status"]), + vec_str(&["bash", "-lc", "git diff | head -20"]), ] { assert!( !is_known_safe_command(&args), - "expected {args:?} to require approval due to unsafe git global option", + "Git must not be trusted from its arguments alone: {args:?}", ); } - - assert!(!is_known_safe_command(&vec_str(&[ - "bash", - "-lc", - "git -C .project-deps/test-fixtures status", - ]))); - assert!(!is_known_safe_command(&vec_str(&[ - "bash", - "-lc", - "git --git-dir=.evil-git diff HEAD~1..HEAD", - ]))); } #[test] @@ -636,12 +375,12 @@ mod tests { } #[test] - fn windows_git_full_path_is_safe() { + fn windows_git_full_path_is_not_safe() { if !cfg!(windows) { return; } - assert!(is_known_safe_command(&vec_str(&[ + assert!(!is_known_safe_command(&vec_str(&[ r"C:\Program Files\Git\cmd\git.exe", "status", ]))); @@ -651,11 +390,6 @@ mod tests { fn bash_lc_safe_examples() { assert!(is_known_safe_command(&vec_str(&["bash", "-lc", "ls"]))); assert!(is_known_safe_command(&vec_str(&["bash", "-lc", "ls -1"]))); - assert!(is_known_safe_command(&vec_str(&[ - "bash", - "-lc", - "git status" - ]))); assert!(is_known_safe_command(&vec_str(&[ "bash", "-lc", 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 df2025253b..f7630ee6e3 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 @@ -1,4 +1,3 @@ -use crate::command_safety::is_safe_command::is_safe_git_command; use crate::command_safety::powershell_parser::PowershellParseOutcome; use crate::command_safety::powershell_parser::parse_with_powershell_ast; use std::path::Path; @@ -188,7 +187,8 @@ pub(crate) fn is_safe_powershell_words(words: &[String]) -> bool { "select-object" | "select" => true, "get-item" => true, - "git" => is_safe_git_command(words), + // Repository configuration can make even read-only Git commands execute helpers. + "git" => false, "rg" => is_safe_ripgrep(words), @@ -225,7 +225,6 @@ fn is_safe_ripgrep(words: &[String]) -> bool { mod tests { use super::*; use crate::powershell::try_find_pwsh_executable_blocking; - use pretty_assertions::assert_eq; use std::string::ToString; /// Converts a slice of string literals into owned `String`s for the tests. @@ -242,13 +241,6 @@ mod tests { "Get-ChildItem -Path .", ]))); - assert!(is_safe_command_windows(&vec_str(&[ - "powershell.exe", - "-NoProfile", - "-Command", - "git status", - ]))); - assert!(is_safe_command_windows(&vec_str(&[ "powershell.exe", "Get-Content", @@ -290,7 +282,7 @@ mod tests { } #[test] - fn allows_read_only_pipelines_and_git_usage() { + fn allows_read_only_pipelines() { let Some(pwsh) = try_find_pwsh_executable_blocking() else { return; }; @@ -313,12 +305,6 @@ mod tests { "Get-Content foo.rs | Select-Object -Skip 200".to_string() ])); - assert!(is_safe_command_windows(&[ - pwsh.clone(), - "-Command".to_string(), - "git show HEAD:foo.rs".to_string() - ])); - assert!(is_safe_command_windows(&[ pwsh.clone(), "-Command".to_string(), @@ -333,82 +319,36 @@ mod tests { } #[test] - fn rejects_git_global_override_options() { - let Some(pwsh) = try_find_pwsh_executable_blocking() else { - return; - }; - - let pwsh: String = pwsh.as_path().to_str().unwrap().into(); - for script in [ - "git -c core.pager=cat show HEAD:foo.rs", - "git --config-env core.pager=PAGER show HEAD:foo.rs", - "git --config-env=core.pager=PAGER show HEAD:foo.rs", - "git --git-dir .evil-git diff HEAD~1..HEAD", - "git --git-dir=.evil-git diff HEAD~1..HEAD", - "git --work-tree . status", - "git --work-tree=. status", - "git --exec-path .git/helpers show HEAD:foo.rs", - "git --exec-path=.git/helpers show HEAD:foo.rs", - "git --namespace attacker show HEAD:foo.rs", - "git --namespace=attacker show HEAD:foo.rs", - "git --super-prefix attacker/ show HEAD:foo.rs", - "git --super-prefix=attacker/ show HEAD:foo.rs", + fn rejects_git_commands() { + for args in [ + vec_str(&["git", "status", "--short"]), + vec_str(&["git", "log", "-p", "-1"]), + vec_str(&["git", "diff"]), + vec_str(&["git", "show", "HEAD:foo.rs"]), + vec_str(&["git", "branch", "--show-current"]), + vec_str(&["git", "--version"]), ] { + assert!(!is_safe_powershell_words(&args)); + let script = args.join(" "); assert!( !is_safe_command_windows(&[ - pwsh.clone(), - "-NoLogo".to_string(), + "powershell.exe".to_string(), "-NoProfile".to_string(), "-Command".to_string(), - script.to_string(), + script.clone(), ]), - "expected {script:?} to require approval due to unsafe git global option", + "Git must not be trusted from its arguments alone: {script:?}", ); } } #[test] - fn rejects_git_subcommand_options_with_side_effects() { - let results: Vec<(&str, bool)> = [ - "git diff --output codex_poc.txt", - "git diff --ext-diff HEAD", - "git log --textconv -1", - "git show --output=codex_poc.txt HEAD", - "git cat-file --filters HEAD:a.txt", - ] - .into_iter() - .map(|script| { - ( - script, - is_safe_command_windows(&[ - "powershell.exe".to_string(), - "-NoProfile".to_string(), - "-Command".to_string(), - script.to_string(), - ]), - ) - }) - .collect(); - - assert_eq!( - vec![ - ("git diff --output codex_poc.txt", false), - ("git diff --ext-diff HEAD", false), - ("git log --textconv -1", false), - ("git show --output=codex_poc.txt HEAD", false), - ("git cat-file --filters HEAD:a.txt", false), - ], - results - ); - } - - #[test] - fn rejects_stop_parsing_git_forms() { + fn rejects_stop_parsing_forms() { assert!(!is_safe_command_windows(&vec_str(&[ "powershell.exe", "-NoProfile", "-Command", - "git log --% HEAD --output=codex_poc.txt", + "rg --% pattern Cargo.toml", ]))); }