雨天小六

读懂 Codex(9.11):Apply Patch Grammar、Parser 与安全写入

· 更新于 2026-08-03 · 专栏:读懂 Codex

#Codex#Agent Runtime#Python#软件架构

具体问题与边界

Patch 为什么必须先解析成操作、验证所有目标,再进入可产生副作用的写入阶段?

Mini PatchParser 支持 Add/Update/Delete 子集;PatchApplier 把语法操作转换成完整 PatchPlan;审批后才 stage/replace。Move、模糊 context 和流式 preview 未实现。 本节区分“从官方源码得到的生产事实”和“为教学实现作出的 Python 选择”,不会把后一种包装成 Codex 的等价实现。

状态所有权

对象所有者生命周期持久化
patch textToolCall单次调用随 Call 持久化
PatchOperation/HunkPatchParser解析到计划完成
PatchPlan writes/deletesPatchApplier一次审批/提交
temporary filesPatchApplier.applycommit 期间
workspace files文件系统跨 Turn外部副作用,不在 Rollout 内存储内容

正常路径

Apply Patch Grammar、Parser 与安全写入正常路径图
图 9.11-1:Patch 为什么必须先解析成操作、验证所有目标,再进入可产生副作用的写入阶段?
  1. Parser 强制首尾 marker,并按 header 识别 Add File、Update File 和 Delete File。Add 内容行必须以 + 开头;Update hunk 行必须以空格、+- 开头。
  2. 同一路径在一个 Patch 中只能出现一次。Parser 只建立领域操作,不读取文件系统。
  3. Planner 对每个路径先走 WorkspacePolicy.resolve,再验证存在性;Update 的 old hunk 必须在当前内容中恰好出现一次。
  4. 所有操作都在内存中规划成功后才请求 Approval。因此“后一个文件缺失”不会让前一个 Add 提前落盘。
  5. Commit 为每个 write 在目标目录 stage 临时文件并 fsync,再按顺序 os.replace,最后 delete;发生 OS 错误时报告 partial commit,不声称跨文件事务。

顺序为什么不能交换

Parser 强制首尾 marker,并按 header 识别 Add File、Update File 和 Delete File
→ 同一路径在一个 Patch 中只能出现一次
→ Planner 对每个路径先走 WorkspacePolicy.resolve,再验证存在性;Update 的 old hunk 必须在当前内容中恰好出现一次
→ 所有操作都在内存中规划成功后才请求 Approval
→ Commit 为每个 write 在目标目录 stage 临时文件并 fsync,再按顺序 os.replace,最后 delete;发生 OS 错误时报告 partial commit,不声称跨文件事务

箭头代表可见性与所有权转移,不是松散依赖。Policy、Approval、外部副作用、规范 Item 和 durability 各自有提交点,后一步不能替前一步作更强承诺。

Python 风格伪代码

operations = PatchParser.parse(patch_text)

def plan(operations):
    writes, deletes = [], []
    for op in operations:
        path = workspace.resolve(op.path)
        match op:
            case Add(content):
                require_not_exists(path)
                writes.append((path, content))
            case Update(hunks):
                content = read_existing(path)
                for hunk in hunks:
                    require(content.count(hunk.old) == 1)
                    content = content.replace(hunk.old, hunk.new, 1)
                writes.append((path, content))
            case Delete():
                require_regular_file(path)
                deletes.append(path)
    return PatchPlan(writes, deletes)       # no mutation yet

if await approval(plan.summary):
    staged = [write_temp_and_fsync(target, text) for target, text in plan.writes]
    for temp, target in staged: os.replace(temp, target)
    for target in plan.deletes: target.unlink()

失败、取消与恢复

Apply Patch Grammar、Parser 与安全写入失败路径图
图 9.11-2:每个失败点都列出已经发生的副作用和仍可安全执行的恢复动作。
故障点已留下的状态处理
marker/header 非法没有领域 Operation错误 ToolResult,无写入
hunk 0 次或多次匹配目标位置不唯一规划失败,无写入
路径逃出 workspaceresolve 拒绝规划失败
Approval denyPlan 已存在但无文件副作用模型看到 rejected
第二个操作校验失败第一个操作仍未提交全计划预验证
commit 中途 OS 错误部分 replace 可能已完成报告 partial commit,不伪造 rollback

不变量

语法错误和规划错误必须发生在任何文件修改之前;commit 开始后的跨文件部分失败必须如实暴露。

设计思路与限制

官方测试明确覆盖 failure-after-partial-success;Mini 通过全计划预验证减少一类部分写,但 os.replace 序列仍不是全局事务。Mini 不支持 Move、End of File marker、宽松 shell 截取和 streaming parser。

测试与复现

cd examples/mini-codex
uv run pytest -q -k 'test_patch_parser_plans_all_files_before_commit or test_invalid_late_operation_does_not_apply_earlier_add or test_patch_changes_world_state_before_second_sample'
uv run mypy src
  • test_patch_parser_plans_all_files_before_commit
  • test_invalid_late_operation_does_not_apply_earlier_add
  • test_patch_changes_world_state_before_second_sample

官方源码导航

Mini Codex 对照

  • src/mini_codex/tools/apply_patch.py:Parser、Plan、stage/commit 与 Tool
  • src/mini_codex/policy/workspace.py:路径边界

本节结论

语法错误和规划错误必须发生在任何文件修改之前;commit 开始后的跨文件部分失败必须如实暴露。

阅读导航

上一节:9.10 · 下一节:9.12

评论


← 返回文章列表