第 11 章:把 Patch 作为一等编辑协议
阅读契约: 读这一章时,不要把文件修改和 shell execution 混在一起。跟住五个 owner:patch grammar、verified action、approval decision、executor filesystem、turn diff evidence。读完后,你应该能回答:为什么一次 patch 失败仍然可能携带有用证据?

源码边界: 本章的源码级论断都指向 Codex 固定快照 569ff6a1c400bd514ff79f5f1050a684dc3afde3。文件、函数、enum、request shape、测试行为只有在链接到 pinned source 时才算 verified source。下面的短代码块是裁剪后的源码摘录或 shape-level 示例;完整定义以 pinned anchors 为准。像“patch 是 protocol”这类设计判断,是从可见 handler、parser、runtime、safety 和 diff tracking 代码得出的 surrounding contract inference,不是对 OpenAI 私有服务内部的断言。
第 10 章说明了 shell execution 如何成为受监督的 process。Patch application 看起来离 shell 很近,因为用户和模型经常用 terminal 形状的语法表达编辑。但在 Codex 里,它有不同的 owner。Runtime 不必把 patch 当作发给 shell 的任意 bytes;它可以解析一门很小的编辑语言,验证这次编辑是否匹配当前 filesystem,计算即将写入的 paths,必要时请求 approval,通过当前 turn 的 executor filesystem 写入,然后记录实际 committed 的 delta。
这个区别很实用。直接写文件表达的是:“让这个文件变成这些内容。” Patch 表达的是:“在这些路径上,针对这些旧内容,执行 add、delete、update 或 move hunk。”后者足够窄,能在 mutation 前 review;也足够丰富,能在 failure 后解释发生了什么。
一、Tool Surface 先把 Mutation 说清楚
用户可见的 apply_patch 进入 Codex 时,是一个 custom freeform tool,而不是普通 shell command。Handler 从很小的 ApplyPatchHandler 开始;它的 ToolHandler 实现 会命名这个 tool,暴露 freeform spec,把匹配 payload 识别为 function call,把这次操作标记为 mutating,创建 streaming diff consumer,并为 patch body 提供 pre-tool 与 post-tool hook payload。
关键不在 UI 名字,而在 handler 拥有一条 mutation-specific lifecycle。它可以给 pre-tool 和 post-tool hook 提供 command-shaped patch body,可以把正在生成的 patch hunk 以事件形式流出去,也可以在任何 filesystem write 前拒绝 unsupported payload。
pub struct ApplyPatchHandler;
fn tool_name(&self) -> ToolName { ... }
async fn is_mutating(&self, ...) -> bool { true }
后面的 handle 还会重新 parse 并 verify patch。这个 re-parse 不是形式主义;Codex 需要从即将写入的同一份 body 推导 concrete path permissions 和 patch summary。
二、Grammar 很小,Verification 才是真 Contract

Parser 文件开头给出了可见的 grammar contract:patch 有 begin marker,包含一个或多个 add/delete/update hunk,然后 end。parser module 同时明说,这个 parser 本身不检查 patch 是否真的能应用到 filesystem。这个边界很重要。
在 syntax 层,Hunk enum 让核心形状保持很小。裁剪到 variants 后,它长这样:
pub enum Hunk {
AddFile { ... },
DeleteFile { ... },
UpdateFile { ... },
}
Parsing 把文本变成 hunks;verification 把 hunks 变成 action。maybe_parse_apply_patch_verified 会解析 effective working directory,把 hunk paths 解析到这个目录下,读取被删除文件,基于当前文件内容推导 update diff,并记录 move destination。只要某个 update hunk 找不到期望的旧行,verification 就返回 correctness error,而不是发起 best-effort edit。
用 shape-level 的方式看,转换是这样的:
patch body
-> hunks: add | delete | update | move
-> verified action:
cwd
absolute paths
proposed file changes
raw patch body
这里的 “verified” 要读得很窄:它表示 patch 已经用 executor filesystem 可见的状态核过。它不表示 runtime 已经证明这次编辑在项目语义上正确;那仍然是 review 的问题。
2.1 为什么 Delete 和 Update 需要 Filesystem
Add hunk 基本可以从 patch body 表示。Delete hunk 需要旧内容,才能说明到底删掉了什么。Update hunk 需要当前文件内容,才能推导 concrete unified diff 和最终 new content。所以 verification function 接收的是 ExecutorFileSystem,而不只是一个 string parser。
这能挡住 patch system 里最常见的错误:把 parse success 当成 apply success。Patch 可以语法合法,但仍因为 expected old lines 已经消失、target file 读不出来、destination path 超出 policy boundary 而失败。
三、Shell Compatibility 是 Intercept,不是 Trust

模型和用户经常用 heredoc 表达 patch。Codex 识别的是一组很窄的形式,而不是把所有 shell text 都当作 patch text。maybe_parse_apply_patch 支持 apply_patch <patch> 这种 direct invocation,也支持可抽取的 shell-script forms。extract_apply_patch_from_bash 的注释写得很具体:支持顶层 apply_patch heredoc,以及 cd <path> && apply_patch heredoc。
这个 enum 把边界说清楚:
Body(ApplyPatchArgs)
ShellParseError(...)
NotApplyPatch
Verified path 还多了一道 guard:如果只有 raw patch body,却没有显式 apply_patch invocation,它会被当作 implicit invocation error。这避免了一个 patch-shaped string 仅仅因为出现在 command 位置就被静默应用。
当 shell interception 在主 handler 里成功时,intercept_apply_patch 会记录 model warning,提醒模型直接使用 apply_patch tool。然后它仍然走同一套 permission、event、runtime 和 diff 路径。
值得复用的规则是:可以容忍明确映射到 protocol 的邻近语法,但一旦识别出来,就立刻 normalize 回受治理的 protocol。
四、Safety 在 Write 之前运行

Handler 会在 runtime execution 前计算受影响的 absolute paths 和 additional permissions。file_paths_for_action 不只收 source path,也会把 move destination 纳入考虑。write_permissions_for_paths 会为当前 sandbox policy 下还不可写的路径推导 read-write roots。
Safety decision 本身在 assess_patch_safety。它的输出形状刻意很小:
pub enum SafetyCheck {
AutoApprove { ... },
AskUser,
Reject { reason: String },
}
这个阶段挡住两个 shortcut。第一个 shortcut 是以为 patch 有结构,所以天然安全。不是;structured edit 仍可能指向危险路径。第二个 shortcut 是以为 sandbox 可以替代 approval。也不是;sandbox availability 和 approval policy 会共同决定 Codex 能 auto-approve、ask,还是 reject。
源码里还特别提到 hard link 风险:即使 patch 看起来只落在 writable paths 里,runtime 仍可能需要在 sandbox 中运行,因为这些 path 可能是指向 writable roots 外文件的 hard links。这是一个很好的例子:local filesystem fact 不能靠漂亮 diff UI 解决。
五、Runtime 通过 Owner Filesystem 写入
Approval 本身不会直接写文件。被批准的 request 会进入 ApplyPatchRuntime,request 携带 verified action、affected paths、protocol changes、approval requirement 和 additional permissions。
pub struct ApplyPatchRequest {
pub action: ApplyPatchAction,
pub file_paths: Vec<AbsolutePathBuf>,
...
}
在 run 里,runtime 取 primary turn environment,拿到它的 filesystem,为当前 attempt 构造 filesystem sandbox context,然后调用 apply-patch library。这就是 patch application 能成为 turn-owned operation 的原因:local turn 可以给 local filesystem,remote turn 可以给 remote filesystem,patch protocol 不需要假设自己写的是哪一个。
Apply library 随后通过 filesystem abstraction 完成 read、write、directory creation 和 remove。apply_hunks_to_files 分别处理 add、delete、update、move hunk,并在 work commit 时更新 AppliedPatchDelta。
这解释了为什么 patch failure 不总是空的。一次 move 可能已经写入 destination,然后在 remove source 时失败。一次 write 也可能在 truncate 后失败。Library 因此会返回带有 committed delta 的 ApplyPatchFailure,表示 failure boundary 前已经确定发生的文本 mutation。Runtime 会 append 这份 committed delta,并让 event emitter 用现有 evidence 结束。
六、Diff Tracking 是 Evidence,不是 Decoration

AppliedPatchDelta 保存 committed changes 和 exact flag。TurnDiffTracker 保存 baselines、current content、rename origins 和 validity bit。
pub struct TurnDiffTracker {
valid: bool,
baseline_by_path: HashMap<...>,
current_by_path: HashMap<...>,
}
Tracker 只接受 exact delta。track_delta 在 delta 不 exact 时直接 invalidate。get_unified_diff 会在 tracker invalid 时返回 nothing。
这就是 patch 是 protocol 的最后一层理由:输出不只是 success 或 failure,而是一份带 honesty boundary 的 structured mutation record。Evidence exact 时,Codex 可以展示 net diff;evidence 不 exact 时,正确做法是不再展示 confident diff,而不是 reread unknown state 然后假装 turn-level proof 还完整。
七、这套设计换来了什么
| Pressure | 简单做法 | Codex patch design | 保护的 invariant |
|---|---|---|---|
| 模型输出 shell heredoc | 直接执行 shell command | 识别窄形式并转入 patch protocol | Mutation 仍受治理 |
| Patch syntax parse 成功 | 立刻 apply | 先验证旧内容与 path | 不从 stale context 做 best-effort edit |
| Path 超出 writable roots | 让 sandbox 后置兜底 | Runtime 前先 assess patch safety | Approval 与 policy 可见 |
| Local vs remote workspace | 假设本地文件 | 使用 turn environment filesystem | Edit 落到 owner workspace |
| Partial failure | 只报告失败 | 保留 committed delta 和 exactness | Failure 后 evidence 仍诚实 |
| UI 需要 diff | 永远显示 diff | 只在 tracker valid 时渲染 | Diff 是 proof,不是 decoration |
应用到实践
- Mutation 前先结构化 edit intent。 一个可治理的 edit action 应该命名 paths、operations 和 expected old content。
- 把兼容语法当 input adapter。 只有 shell-like form 明确映射到 protocol 时才 intercept,然后立即 normalize。
- Safety 放在 runtime 之前。 Approval policy、writable roots、sandbox availability 不应该等 failure 后才补救。
- 通过 owner 写入。 Runtime 应使用拥有当前 turn 的 filesystem,无论 local 还是 remote。
- 保留 exact evidence,不可证明就 invalidate。 无法证明的 diff 应该消失,而不是变成表演。
第 12 章会从 patch lane 向外扩展,解释围绕所有 side effects 的人类和自动化关卡:hooks、approval requests、Guardian review,以及等待决策时可能暂停 execution 的 client surfaces。
源码地图
| 概念 | 源码锚点 |
|---|---|
| Patch handler struct 与 tool surface | ApplyPatchHandler, ToolHandler impl |
| Handler verification and orchestration | ApplyPatchHandler::handle |
| Shell interception path | intercept_apply_patch |
| Patch grammar 与 hunk model | parser.rs |
| Invocation verifier | maybe_parse_apply_patch_verified |
| Patch safety assessment | assess_patch_safety |
| Patch runtime | ApplyPatchRuntime::run |
| Hunk application and committed delta | apply_hunks_to_files |
| Turn diff tracker | turn_diff_tracker.rs |