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
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
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
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
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
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
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
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
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=16.2 负例与验证
负例测试拒绝 ;、||、|、额外 command、多参数 cd;verified 测试拒绝 implicit invocation,并验证有效 patch 进入 filesystem verify。
源码位置:codex-rs/apply-patch/src/invocation.rs :: test_heredoc_with_leading_cd 及 invocation tests
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. 源码排查
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.rsInvocation 主线是:先识别直接 argv 或 shell 形状,再用 AST 严格提取 heredoc 和 workdir,最后 parse patch,并用选定 environment 的 filesystem、sandbox 和行尾模式计算 ApplyPatchAction。下一篇 ApplyPatch文件更新算法将分析 context 匹配和新内容生成。
