Skip to content

ApplyPatchInvocation模型

解析 Apply Patch 的直接 argv、shell heredoc、cd 工作目录、跨平台 shell 识别与 implicit invocation 拒绝。

基于rust-v0.150.0
CodexRustExecutionApplyPatch

ApplyPatchInvocation模型 ​

模型提交的 apply_patch 可能有两种形状:apply_patch <body> 的直接 argv,或 bash -lc、PowerShell、Cmd 中的 heredoc/script。Codex 不会把所有包含 apply_patch 字符串的命令都当成 patch,而是先按 environment-native 路径约定识别 shell、解析唯一顶层语句,再提取 patch body 和可选 workdir。verified 阶段还会选择行尾更新模式、读取源文件并拒绝同一路径的重复操作。

本文承接ApplyPatch解析器和Apply Patch语言与语法,面向理解 Tree-sitter AST、PathConvention 和异步 filesystem verify 的读者。范围是 invocation 检测与 verify 入口,不展开 chunk 匹配和文件写入;测试同时覆盖错误连接符、额外命令和 implicit invocation 等失败路径。读完后,你应能解释为什么 cd foo && apply_patch 可以识别,而 cd foo; apply_patch、echo ... && apply_patch 和 raw patch body 会被拒绝。

1. 直接调用 ​

maybe_parse_apply_patch 首先匹配恰好两个 argv:命令名必须是 apply_patch 或 applypatch,第二项必须是完整 patch body。其他 argv 形状转入 shell 识别。

源码位置:codex-rs/apply-patch/src/invocation.rs :: maybe_parse_apply_patch

rust
match argv {
    [cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => {
        match parse_patch(body) {
            Ok(source) => MaybeApplyPatch::Body(source),
            Err(e) => MaybeApplyPatch::PatchParseError(e),
        }
    }
    _ => match parse_shell_script(argv, cwd) {
        Some((shell, script)) => extract_apply_patch_from_shell(shell, script),
        None => MaybeApplyPatch::NotApplyPatch,
    },
}

直接调用识别只证明 argv 形状和 patch 语法;路径存在性、context 匹配和 sandbox 权限仍由 verify 阶段处理。

2. Shell识别 ​

2.1 名称与flag ​

shell 名称先按 cwd 的 PathConvention 取 basename 和 stem,再匹配 Unix、PowerShell 或 Cmd 的启动 flag。这样 Windows 路径分隔符和扩展名不会改变分类。

源码位置:codex-rs/apply-patch/src/invocation.rs :: classify_shell_name、classify_shell、parse_shell_script

rust
fn classify_shell(shell: &str, flag: &str, convention: PathConvention)
    -> Option<ApplyPatchShell>
{
    classify_shell_name(shell, convention).and_then(|name| match name.as_str() {
        "bash" | "zsh" | "sh" if matches!(flag, "-lc" | "-c") => {
            Some(ApplyPatchShell::Unix)
        }
        "pwsh" | "powershell" if flag.eq_ignore_ascii_case("-command") => {
            Some(ApplyPatchShell::PowerShell)
        }
        "cmd" if flag.eq_ignore_ascii_case("/c") => Some(ApplyPatchShell::Cmd),
        _ => None,
    })
}

2.2 noprofile例外 ​

PowerShell 允许 [powershell, -NoProfile, -Command, script],但其他额外 flag 或 argv 长度不会被跳过。识别是保守的,未知 shell 不会猜测。

3. Heredoc AST ​

3.1 唯一顶层语句 ​

Tree-sitter query 要求 program 只有一个 redirected_statement:直接 apply_patch <<EOF,或 cd <path> && apply_patch <<EOF。这避免从包含额外副作用的脚本中截取 patch。

源码位置:codex-rs/apply-patch/src/invocation.rs :: extract_apply_patch_from_bash

text
apply_patch <<'EOF'
*** Begin Patch
*** Add File: file.txt
+content
*** End Patch
EOF

查询还用 any-of 限制命令名只能是 apply_patch 或 applypatch,并捕获 heredoc body 与 cd path。

3.2 cd边界 ​

允许一个 cd 的一个 positional word、双引号 string 或单引号 raw string 参数,并且连接符必须是 &&。;、||、|、第二个 cd、两个 path 或前后额外 command 都不匹配。单引号路径捕获后会去掉外层引号,双引号路径直接捕获 string_content。

源码位置:codex-rs/apply-patch/src/invocation.rs :: extract_apply_patch_from_bash、APPLY_PATCH_QUERY

4. workdir与verify ​

extract 返回可选 workdir;verify 用 cwd.join(workdir) 形成 effective cwd,再让每个 Hunk resolve_path。 同一路径出现第二个操作会立即拒绝。Delete 通过 ExecutorFileSystem::read_file_text 保存原内容;Update 按所选 ApplyPatchFileUpdateMode 读取并计算 diff/new content;Add 不要求目标可读。move destination 相对于 effective cwd 解析,但 change map 的 key 仍是源路径。

源码位置:codex-rs/apply-patch/src/invocation.rs :: try_verify_apply_patch_args

rust
let effective_cwd = workdir
    .as_ref()
    .map(|dir| cwd.join(dir))
    .transpose()?
    .unwrap_or_else(|| cwd.clone());
let mut changes = HashMap::new();
for hunk in hunks {
    let path = hunk.resolve_path(&effective_cwd)?;
    if changes.contains_key(&path) {
        return Err(ParseError::InvalidPatchError(format!(
            "multiple operations target {}",
            path.inferred_native_path_string()
        )).into());
    }
    match hunk {
        Hunk::AddFile { contents, .. } => {
            changes.insert(path, ApplyPatchFileChange::Add { content: contents });
        }
        Hunk::DeleteFile { .. } => {
            let content = fs.read_file_text(&path, Default::default(), sandbox).await.map_err(|source| {
                ApplyPatchError::IoError(IoError {
                    context: format!("Failed to read {}", path.inferred_native_path_string()),
                    source,
                })
            })?;
            changes.insert(path, ApplyPatchFileChange::Delete { content });
        }
        Hunk::UpdateFile { move_path, chunks, .. } => {
            let ApplyPatchFileUpdate { unified_diff, content: contents, .. } =
                unified_diff_from_chunks_with_mode(
                    &path,
                    &chunks,
                    update_file_mode,
                    fs,
                    sandbox,
                ).await?;
            changes.insert(path, ApplyPatchFileChange::Update {
                unified_diff,
                move_path: move_path
                    .map(|path| effective_cwd.join(&path.to_string_lossy()))
                    .transpose()?,
                new_content: contents,
            });
        }
    }
}

ApplyPatchArgs.environment_id 在进入此函数前由 core ApplyPatchHandler 交给 require_environment_id 和 resolve_tool_environment;try_verify_apply_patch_args 只消费 patch、hunks 和 workdir,并对已经选定 的 ExecutorFileSystem 验证。解析出 Environment ID 不等于 invocation crate 自己建立远程连接。

源码位置:codex-rs/core/src/tools/handlers/apply_patch.rs :: ApplyPatchHandler::handle_call、apply_patch_file_update_mode

rust
let selected_environment_id =
    require_environment_id(args.environment_id.as_deref(), self.multi_environment)?;
let Some(turn_environment) = resolve_tool_environment(
    &step_context.environments,
    selected_environment_id.as_deref(),
)? else {
    return Err(FunctionCallError::RespondToModel(
        "apply_patch is unavailable in this session".to_string(),
    ));
};

源码位置:codex-rs/apply-patch/src/lib.rs :: ApplyPatchFileUpdateMode、ApplyPatchArgs、ApplyPatchAction

rust
pub enum ApplyPatchFileUpdateMode {
    NormalizeToLf,
    PreserveLineEndings,
}

pub struct ApplyPatchAction {
    changes: HashMap<PathUri, ApplyPatchFileChange>,
    update_file_mode: ApplyPatchFileUpdateMode,
    pub patch: String,
    pub cwd: PathUri,
}

5. 隐式调用 ​

verified 入口先检查 argv 是否只有 raw patch body,或 shell script 本身能被 parse_patch 解析;这些情况返回 ImplicitInvocation,防止模型把 patch 文本作为普通 shell command 参数时绕过明确的 apply_patch 工具边界。

源码位置:codex-rs/apply-patch/src/invocation.rs :: maybe_parse_apply_patch_verified_with_mode

rust
if let [body] = argv
    && parse_patch(body).is_ok()
{
    return MaybeApplyPatchVerified::CorrectnessError(
        ApplyPatchError::ImplicitInvocation,
    );
}

6. 验证 ​

6.1 跨shell正例 ​

invocation 单测覆盖 bash/zsh/sh、PowerShell、Cmd、NoProfile、applypatch 别名和带 cd 的 quoted path,断言 Body 以及 workdir。

源码位置:codex-rs/apply-patch/src/invocation.rs :: test_heredoc、test_powershell_heredoc、test_cmd_heredoc_with_cd

text
cd codex-rs
cargo test -p codex-apply-patch --lib invocation::tests::test_heredoc -- --test-threads=1
cargo test -p codex-apply-patch --lib invocation::tests::test_powershell_heredoc -- --test-threads=1
cargo test -p codex-apply-patch --lib invocation::tests::test_cmd_heredoc_with_cd -- --test-threads=1

6.2 负例与验证 ​

负例测试拒绝 ;、||、|、额外 command、多参数 cd;verified 测试拒绝 implicit invocation,并验证有效 patch 进入 filesystem verify。

源码位置:codex-rs/apply-patch/src/invocation.rs :: test_heredoc_with_leading_cd 及 invocation tests

text
cd codex-rs
cargo test -p codex-apply-patch --lib invocation::tests -- --test-threads=1
cargo test -p codex-core --lib tools::handlers::apply_patch::tests -- --test-threads=1

这些测试不证明文件写入已经发生。MaybeApplyPatchVerified::Body 表示变更已根据当前文件内容计算,实际 mutation、symlink 策略和部分失败后的落盘状态属于 apply 阶段。

7. 源码排查 ​

text
rg -n "classify_shell|parse_shell_script|maybe_parse_apply_patch" codex-rs/apply-patch/src/invocation.rs
rg -n "extract_apply_patch_from_bash|APPLY_PATCH_QUERY|ImplicitInvocation" codex-rs/apply-patch/src/invocation.rs
rg -n "test_heredoc|assert_not_match|expected_workdir" codex-rs/apply-patch/src/invocation.rs

Invocation 主线是:先识别直接 argv 或 shell 形状,再用 AST 严格提取 heredoc 和 workdir,最后 parse patch,并用选定 environment 的 filesystem、sandbox 和行尾模式计算 ApplyPatchAction。下一篇 ApplyPatch文件更新算法将分析 context 匹配和新内容生成。