diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index b0f5062389..423194b50c 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -82,6 +82,14 @@ use tracing_subscriber::util::SubscriberInitExt; const SQLITE_RECOVERY_CONFIG_WARNING_SUMMARY: &str = "Codex rebuilt its local database."; +fn is_unsupported_untrusted_approval_policy_error(err: &std::io::Error) -> bool { + err.get_ref().is_some_and( + ::is::< + UnsupportedUntrustedApprovalPolicyError, + >, + ) +} + mod analytics_utils; mod app_info; mod app_server_tracing; @@ -505,13 +513,7 @@ pub async fn run_main_with_transport_options( config.http_client_factory(), ); } - Err(err) - if err.get_ref().is_some_and( - ::is::< - UnsupportedUntrustedApprovalPolicyError, - >, - ) => - { + Err(err) if is_unsupported_untrusted_approval_policy_error(&err) => { return Err(err); } Err(err) => { @@ -526,6 +528,9 @@ pub async fn run_main_with_transport_options( .await { Ok(config) => config, + Err(err) if is_unsupported_untrusted_approval_policy_error(&err) => { + return Err(err); + } Err(err) => { if strict_config { return Err(err); diff --git a/codex-rs/shell-command/src/parse_command.rs b/codex-rs/shell-command/src/parse_command.rs index 180e1c23c1..01768d0a7a 100644 --- a/codex-rs/shell-command/src/parse_command.rs +++ b/codex-rs/shell-command/src/parse_command.rs @@ -856,6 +856,34 @@ mod tests { ))); } + #[test] + fn keeps_mutating_sed_in_compound_command() { + for sed_command in [ + "sed -n -i.bak 1p secret.txt", + "sed -ni.bak 1p secret.txt", + "sed -Eni.bak 1p secret.txt", + ] { + let inner = format!("cat README.md && {sed_command}"); + assert_parsed( + &vec_str(&["bash", "-lc", &inner]), + vec![ParsedCommand::Unknown { cmd: inner }], + ); + } + } + + #[test] + fn ignores_sed_operands_after_double_dash_when_checking_mutation() { + let inner = "cat README.md && sed 's/a/x/' -- -input.txt"; + assert_parsed( + &vec_str(&["bash", "-lc", inner]), + vec![ParsedCommand::Read { + cmd: "cat README.md".to_string(), + name: "README.md".to_string(), + path: PathBuf::from("README.md"), + }], + ); + } + #[test] fn empty_tokens_is_not_small() { let empty: Vec = Vec::new(); @@ -1560,7 +1588,8 @@ fn is_valid_sed_n_arg(arg: Option<&str>) -> bool { fn sed_read_path(args: &[String]) -> Option { let args_no_connector = trim_at_connector(args); - if has_in_place_flag(&args_no_connector) || !args_no_connector.iter().any(|arg| arg == "-n") { + if sed_has_in_place_flag(&args_no_connector) || !args_no_connector.iter().any(|arg| arg == "-n") + { return None; } let mut has_range_script = false; @@ -2158,8 +2187,10 @@ fn is_small_formatting_command(tokens: &[String]) -> bool { } "sed" => { // Keep `sed -n file` (treated as a file read elsewhere); - // otherwise consider it a formatting helper in a pipeline. - sed_read_path(&tokens[1..]).is_none() + // keep in-place mutations as unknown actions; otherwise consider it + // a formatting helper in a pipeline. + let args = &tokens[1..]; + !sed_has_in_place_flag(args) && sed_read_path(args).is_none() } _ => false, } @@ -2200,7 +2231,8 @@ fn xargs_is_mutating_subcommand(tokens: &[String]) -> bool { return false; }; match head.as_str() { - "perl" | "ruby" | "sed" => has_in_place_flag(tail), + "perl" | "ruby" => has_in_place_flag(tail), + "sed" => sed_has_in_place_flag(tail), "rg" => tail.iter().any(|token| token == "--replace"), _ => false, } @@ -2217,6 +2249,39 @@ fn has_in_place_flag(tokens: &[String]) -> bool { }) } +fn sed_has_in_place_flag(tokens: &[String]) -> bool { + let mut tokens = tokens.iter(); + while let Some(token) = tokens.next() { + match token.as_str() { + "--" => break, + "-e" | "-f" | "--expression" | "--file" => { + let _ = tokens.next(); + } + "--in-place" => return true, + token if token.starts_with("--in-place=") => return true, + token if token.starts_with("--") => {} + token => { + let Some(short_options) = token.strip_prefix('-') else { + continue; + }; + for (index, option) in short_options.char_indices() { + match option { + 'i' => return true, + 'e' | 'f' => { + if index + option.len_utf8() == short_options.len() { + let _ = tokens.next(); + } + break; + } + _ => {} + } + } + } + } + } + false +} + fn drop_small_formatting_commands(mut commands: Vec>) -> Vec> { commands.retain(|tokens| !is_small_formatting_command(tokens)); commands