apply_patch 协议
apply_patch 是 codex 用来改文件的自定义文本补丁格式,模型在 tool call 里生成一段 *** Begin Patch ... *** End Patch 文本,客户端解析成 hunks,落到 exec-server 的文件系统上。它比 unified diff 更宽松:支持 add/delete/update 三类 hunk,update 用 @@ context + +/-/ 行表示替换,还能用 *** Move to: 做 rename。整套逻辑在 apply-patch/ crate 里,core/src/apply_patch.rs 是 core 侧的安全门和协议转换层。
职责
- 解析 patch 文本:
parse_patch把字符串拆成Hunk枚举(AddFile / DeleteFile / UpdateFile),UpdateFile 内含UpdateFileChunk列表,每个 chunk 有 change_context + old_lines + new_lines (codex-rs/apply-patch/src/parser.rs:130-137)。 - 识别调用形态:
maybe_parse_apply_patch区分两种 argv——直接apply_patch <body>或 shell heredoc 形式bash -c "apply_patch <<'EOF' ... EOF",后者要用 tree-sitter bash 解析提取 heredoc (codex-rs/apply-patch/src/invocation.rs:112-137)。 - 校验路径与 cwd:
try_verify_apply_patch_args把每个 hunk 的相对路径用effective_cwd解析成PathUri,workdir 来自 heredoc 前缀的cd <path> &&(codex-rs/apply-patch/src/invocation.rs:180-200)。 - 应用到文件系统:
apply_patch/apply_hunks通过ExecutorFileSystemtrait 在沙箱里执行 add/delete/update,产出AppliedPatchDelta(codex-rs/apply-patch/src/lib.rs:276-347)。 - core 侧做安全分类:
core/src/apply_patch.rs::apply_patch把 action 交给assess_patch_safety,分AutoApprove/AskUser/Reject三类,再决定是直接执行还是走 runtime approval (codex-rs/core/src/apply_patch.rs:34-75)。
设计动机
为什么不直接用 unified diff?两个原因。第一,unified diff 的 hunk header(@@ -a,b +c,d @@)要求模型精确算行号,模型不擅长。apply_patch 的 update 用 @@ context 标记位置,允许 seek_sequence 在原文里搜上下文行定位,容忍行号偏移。第二,unified diff 不原生支持"新建文件"和"删除文件"作为同一 patch 的一部分,而 apply_patch 用 *** Add File: / *** Delete File: 显式区分。
为什么还要 core/src/apply_patch.rs 这一层?因为 apply-patch crate 是纯文件操作,不知道 sandbox policy、approval、exec_approval_requirement 这些 core 概念。core 这一层负责把一个 ApplyPatchAction 翻译成 InternalApplyPatchInvocation:要么 Output(直接返回结果,用户已显式批准)、要么 DelegateToRuntime(交给 orchestrator 走沙箱或 approval 流程)。
ImplicitInvocation 错误是个有意思的设计:如果模型直接把 patch 文本当成 argv[0] 传进来(没包 apply_patch 命令),maybe_parse_apply_patch_verified 不会默默执行,而是返回 ImplicitInvocation 错误,提示"必须用 ["apply_patch", "<patch>"] 形式"。这避免了把 patch 内容误当成普通 shell 命令执行的安全风险。
关键文件
codex-rs/apply-patch/src/parser.rs:64-109 — Hunk 枚举,三类 hunk 的数据结构,resolve_path 用 cwd 解析。codex-rs/apply-patch/src/parser.rs:178-200 — parse_patch_text 主体,支持 Strict / Lenient 两种模式。codex-rs/apply-patch/src/streaming_parser.rs:22-46 — StreamingPatchParser,逐行状态机解析,适合大 patch。codex-rs/apply-patch/src/invocation.rs:36-52 — MaybeApplyPatch / ExtractHeredocError,识别 shell 包裹的 patch。codex-rs/apply-patch/src/invocation.rs:141-166 — maybe_parse_apply_patch_verified,先拒绝裸 patch,再走 verify。codex-rs/apply-patch/src/lib.rs:276-312 — apply_patch,把 patch 文本落到文件系统。codex-rs/apply-patch/src/lib.rs:361-389 — apply_hunks_to_files,逐 hunk 执行,try_write! 宏在写失败时把 delta 标为非 exact。codex-rs/apply-patch/src/lib.rs:675-711 — derive_new_contents_from_chunks,读原文件、算 replacement、生成新内容。codex-rs/core/src/apply_patch.rs:34-75 — core 侧 apply_patch,做安全分类后决定执行路径。三类 hunk 的结构很直接,UpdateFile 额外带 move_path 做 rename:
// parser.rs:64-82 — 三类 hunk,UpdateFile 支持多 chunk + rename
pub enum Hunk {
AddFile {
path: PathBuf,
contents: String,
},
DeleteFile {
path: PathBuf,
},
UpdateFile {
path: PathBuf,
move_path: Option<PathBuf>,
chunks: Vec<UpdateFileChunk>,
},
}core 侧的安全判定是关键分支点。assess_patch_safety 根据 approval policy、permission_profile、sandbox policy 综合判断,把 patch 分成"自动放行"、"问用户"、"直接拒"三类。拒绝时直接把 reason 包进 FunctionCallError::RespondToModel,让模型知道为什么没改:
// core/src/apply_patch.rs:39-74 — 三路分流,Output 表示不走 runtime
match assess_patch_safety(&action, ...) {
SafetyCheck::AutoApprove { user_explicitly_approved, .. } => {
InternalApplyPatchInvocation::DelegateToRuntime(ApplyPatchRuntimeInvocation {
action,
auto_approved: !user_explicitly_approved,
exec_approval_requirement: ExecApprovalRequirement::Skip { .. },
})
}
SafetyCheck::AskUser => InternalApplyPatchInvocation::DelegateToRuntime(..),
SafetyCheck::Reject { reason } => InternalApplyPatchInvocation::Output(Err(
FunctionCallError::RespondToModel(format!("patch rejected: {reason}")),
)),
}文件写入失败时,try_write! 宏会把 delta.exact 标为 false——意思是"我们记录了改动,但可能不完整"。比如 ENOSPC 时文件可能已经被 truncate 但没写完,这时 delta 还要保留,因为 rollback 需要知道原状:
// lib.rs:378-388 — 写失败时 delta 立即失真
macro_rules! try_write {
($result:expr) => {
match $result {
Ok(value) => value,
Err(error) => {
delta.exact = false;
return Err(anyhow::Error::from(error));
}
}
};
}数据流
边界与失败
- Lenient 模式默认开启:
PARSE_IN_STRICT_MODE = false,因为 gpt-4.1 会把 patch 包在<<'EOF' ... EOFheredoc 里直接当 argv 传,strict 模式会全部拒掉 (codex-rs/apply-patch/src/parser.rs:47-53)。 - 裸 patch 拒绝执行:argv 只有一个元素且能解析成 patch 时,返回
ImplicitInvocation错误,提示 rerun 成["apply_patch", "<patch>"],防止把 patch 当 shell 命令误执行 (codex-rs/apply-patch/src/invocation.rs:147-158)。 - UpdateFile 空 chunks 报错:
StreamingPatchParser::ensure_update_hunk_is_not_empty检查 update hunk 必须有内容,空 hunk 直接报InvalidHunkError(codex-rs/apply-patch/src/streaming_parser.rs:53-82)。 - 写失败但 delta 保留:ENOSPC 等错误发生时,文件可能已被 truncate 但写入未完成,
delta.exact = false标记改动不确定,供后续 rollback 或审计参考 (codex-rs/apply-patch/src/lib.rs:375-388)。 - DeleteFile 区分目录:删除前调
ensure_not_directory确认目标是文件不是目录,避免误删目录 (codex-rs/apply-patch/src/lib.rs:423-430)。
小结
apply_patch 是 codex 改文件的标准协议,parser 负责把文本拆成 hunks,core 侧负责安全分类,exec-server 负责实际写入。整条链路里最容易出问题的是路径解析和 heredoc 识别——模型生成的 shell wrapper 形态各异,Lenient 模式就是为此存在。要看 patch 执行时怎么过沙箱,接 沙箱与命令隔离;要看 tool call 怎么被分发到 apply_patch,接 工具调用与 function_tool。