English 书架

第 11 章:把 Patch 作为一等编辑协议

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

Patch protocol workbench:把 patch grammar、verified action、safety gate、executor filesystem、turn diff 与 failure feedback 放进同一条受治理编辑链路
Patch handling 不是 shell shortcut,而是一条受治理的编辑链路:解析 grammar、验证 action、做 safety decision、通过 owner filesystem 应用,并保留 evidence。

源码边界: 本章的源码级论断都指向 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

Patch grammar 经过 parser 进入 action ledger:path pins 与 filesystem verification 把 hunk 转成 verified action
Grammar 故意很小。Verified action 更丰富:path 被解析,update chunk 变成 diff,delete 读取旧内容,move 同时携带 source 与 destination。

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

Shell heredoc 与 workdir patch 形式经过 parser gate 进入 patch action;无关 shell command 被挡在 patch 链路外
被识别出的 shell 形式只是兼容输入。一旦识别成功,它们会按 patch action 治理,而不是按普通 shell text 执行。

模型和用户经常用 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 之前运行

Patch action 进入 safety gate:检查 writable roots,必要时 ask user,reject,或通过 sandbox 走向 executor filesystem
Patch safety 是 path-aware 的。Codex 会先把 verified action 与 writable roots、approval policy 对齐,再让 executor filesystem 写入。

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

Committed patch deltas 进入 turn diff tracker;exact evidence 渲染 diff,不确定 evidence 让 display invalidate
Turn diff tracker 很保守:delta exact 时才渲染;一旦 evidence 不能证明净 diff,就 invalidate。

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 protocolMutation 仍受治理
Patch syntax parse 成功立刻 apply先验证旧内容与 path不从 stale context 做 best-effort edit
Path 超出 writable roots让 sandbox 后置兜底Runtime 前先 assess patch safetyApproval 与 policy 可见
Local vs remote workspace假设本地文件使用 turn environment filesystemEdit 落到 owner workspace
Partial failure只报告失败保留 committed delta 和 exactnessFailure 后 evidence 仍诚实
UI 需要 diff永远显示 diff只在 tracker valid 时渲染Diff 是 proof,不是 decoration

应用到实践

  1. Mutation 前先结构化 edit intent。 一个可治理的 edit action 应该命名 paths、operations 和 expected old content。
  2. 把兼容语法当 input adapter。 只有 shell-like form 明确映射到 protocol 时才 intercept,然后立即 normalize。
  3. Safety 放在 runtime 之前。 Approval policy、writable roots、sandbox availability 不应该等 failure 后才补救。
  4. 通过 owner 写入。 Runtime 应使用拥有当前 turn 的 filesystem,无论 local 还是 remote。
  5. 保留 exact evidence,不可证明就 invalidate。 无法证明的 diff 应该消失,而不是变成表演。

第 12 章会从 patch lane 向外扩展,解释围绕所有 side effects 的人类和自动化关卡:hooks、approval requests、Guardian review,以及等待决策时可能暂停 execution 的 client surfaces。

源码地图

概念源码锚点
Patch handler struct 与 tool surfaceApplyPatchHandler, ToolHandler impl
Handler verification and orchestrationApplyPatchHandler::handle
Shell interception pathintercept_apply_patch
Patch grammar 与 hunk modelparser.rs
Invocation verifiermaybe_parse_apply_patch_verified
Patch safety assessmentassess_patch_safety
Patch runtimeApplyPatchRuntime::run
Hunk application and committed deltaapply_hunks_to_files
Turn diff trackerturn_diff_tracker.rs