diff --git a/codex-rs/apply-patch/src/invocation.rs b/codex-rs/apply-patch/src/invocation.rs index 203a161611..229cd4bab5 100644 --- a/codex-rs/apply-patch/src/invocation.rs +++ b/codex-rs/apply-patch/src/invocation.rs @@ -107,6 +107,27 @@ fn extract_apply_patch_from_shell( } } +fn is_implicit_patch(argv: &[String], cwd: &PathUri) -> bool { + let body = match argv { + [body] => Some(body.as_str()), + _ => parse_shell_script(argv, cwd).map(|(_, script)| script), + }; + body.is_some_and(|body| parse_patch(body).is_ok()) +} + +impl MaybeApplyPatch { + /// Whether exec intercepts this command, without reading any patch target files. + pub fn intercepts_command(argv: &[String], cwd: &PathUri) -> bool { + if is_implicit_patch(argv, cwd) { + return true; + } + match maybe_parse_apply_patch(argv, cwd) { + Self::Body(_) | Self::PatchParseError(_) => true, + Self::ShellParseError(_) | Self::NotApplyPatch => false, + } + } +} + // TODO: make private once we remove tests in lib.rs /// `cwd` supplies the path convention used to interpret the shell executable in `argv`. pub fn maybe_parse_apply_patch(argv: &[String], cwd: &PathUri) -> MaybeApplyPatch { @@ -144,16 +165,7 @@ pub async fn maybe_parse_apply_patch_verified( fs: &dyn ExecutorFileSystem, sandbox: Option<&codex_exec_server::FileSystemSandboxContext>, ) -> MaybeApplyPatchVerified { - // Detect a raw patch body passed directly as the command or as the body of a shell - // script. In these cases, report an explicit error rather than applying the patch. - if let [body] = argv - && parse_patch(body).is_ok() - { - return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation); - } - if let Some((_, script)) = parse_shell_script(argv, cwd) - && parse_patch(script).is_ok() - { + if is_implicit_patch(argv, cwd) { return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation); } @@ -504,6 +516,26 @@ mod tests { ); } + #[test] + fn classifies_intercepted_commands_without_a_filesystem() { + let cwd = PathUri::parse("file:///workspace").expect("valid POSIX test cwd"); + let patch = wrap_patch("*** Delete File: target"); + let explicit = format!("apply_patch <<'PATCH'\n{patch}\nPATCH"); + let malformed = "apply_patch <<'PATCH'\ninvalid patch\nPATCH"; + for (script, intercepted) in [ + (explicit.as_str(), true), + (patch.as_str(), true), + (malformed, true), + ("echo apply_patch", false), + ] { + assert_eq!( + MaybeApplyPatch::intercepts_command(&args_bash(script), &cwd), + intercepted, + "{script}" + ); + } + } + #[tokio::test] async fn test_implicit_patch_single_arg_is_error() { let patch = "*** Begin Patch\n*** Add File: foo\n+hi\n*** End Patch".to_string();