5 min read
Day 11:Patch Editing:安全应用 Unified Diff

上一篇:Git 与 Diff Tools:状态、补丁解析与变更摘要
下一篇:Approval 与 Sandbox:写入审批和工作区边界
English version: Patch Editing for AI Coding Agents: Apply Unified Diff Safely

文章介绍

Day 10 我们实现了 git_statusgit_diff,让 agent 能观察当前工作区变化。

今天进入真正的代码编辑执行层:实现 apply_patch

Coding agent 修改代码有两种常见方式:

  • 直接整文件覆盖。
  • 生成 unified diff,再由 harness 应用 patch。

Mini harness 选择第二种。原因是 patch 更适合审查、回滚和失败恢复:模型要说明“哪几行变了”,工具负责验证这些 hunk 是否能应用到当前文件。

Day 11 patch editing flow

今天要解决什么

今天完成五个交付:

  1. 新增 apply_patch 工具。
  2. 支持多文件 unified diff。
  3. 使用 diff 包的 parsePatchapplyPatch,不手写 hunk 匹配。
  4. 在写文件前先完整准备所有变更,避免只写入一半。
  5. 对解析失败、hunk 冲突、不支持的 binary/rename/copy 返回可恢复错误。

Demo 命令:

npm run dev -- "apply a small patch to a fixture file"

手动 dry-run:

npm run dev -- --tool apply_patch --tool-input '{"patch":"diff --git a/demos/day-11/fixture.txt b/demos/day-11/fixture.txt\n--- a/demos/day-11/fixture.txt\n+++ b/demos/day-11/fixture.txt\n@@ -1,3 +1,3 @@\n Day 11 fixture\n-status: pending\n+status: patched\n Keep this file small.","dryRun":true}'

它在 Cursor/Codex 里对应哪一层

今天实现的是 code editing execution layer

在 Cursor、Codex、Claude Code 这类 coding agent 中,模型本身不应该直接“随意写磁盘”。更稳的结构是:

Model proposes a patch
Harness validates paths
Patch engine matches hunks
Harness writes files
Git tools verify resulting diff

也就是说,模型负责意图和 patch 内容,harness 负责执行边界。

Day 11 的 apply_patch 还没有加入审批。审批会放在 Day 12。今天先把 patch 执行能力做出来,并让错误足够清晰,方便模型下一轮修正。

设计思路

1. Patch 比整文件覆盖更适合 agent

整文件覆盖的问题是 blast radius 太大。

如果模型只想改一个函数,却返回整个文件内容,harness 很难判断:

  • 哪些行是模型真正要改的?
  • 哪些行只是模型重写时顺手格式化了?
  • 用户刚刚改过但模型没看到的行是否会被覆盖?
  • 文件很大时,是否有内容在模型输出里被截断?

Unified diff 把变更限定在 hunk 里:

@@ -1,3 +1,3 @@
 Day 11 fixture
-status: pending
+status: patched
 Keep this file small.

应用 patch 时,工具会检查上下文行是否仍然匹配当前文件。匹配不上就失败,而不是硬写。

2. 核心 patch 逻辑交给成熟库

Day 10 已经引入 diff 包。Day 11 继续复用它:

import { applyPatch, parsePatch } from "diff";

流程是:

patch text
  -> parsePatch()
  -> validate file paths
  -> read source files
  -> applyPatch()
  -> write pending results

这里最重要的是:hunk 匹配不是我们手写的。applyPatch 会处理 unified diff 的上下文匹配、行号偏移和换行风格转换。

3. 写入前先准备所有文件

多文件 patch 最怕半成功:

file A applied
file B conflict
file C never tried

如果 file A 已经写入磁盘,这次工具调用就留下了中间状态。

所以 apply_patch 的实现分两步:

prepare phase:
  parse all patches
  read all source files
  apply all hunks in memory
  collect pending writes/deletes

commit phase:
  only if prepare phase has no error
  write files or delete files

这不是完整事务系统,但已经避免了最常见的“一半文件已经被写入”的问题。

4. 错误要可恢复

Patch 冲突不应该让整个 agent run 崩溃。

工具返回:

{
  "applied": false,
  "dryRun": false,
  "error": "Patch hunk failed to apply cleanly: demos/day-11/fixture.txt",
  "files": []
}

模型看到这个结果后,可以选择:

  • 重新读取目标文件。
  • 根据当前内容生成新的 patch。
  • 向用户解释为什么不能自动应用。

这就是 recoverable error。

5. Day 11 支持文本 patch,不支持复杂 Git 操作

今天支持:

  • 修改文件。
  • 新建文件。
  • 删除文件。
  • 多文件 patch。
  • dry-run。
  • 可选 fuzzFactor

今天暂不支持:

  • binary patch。
  • rename patch。
  • copy patch。
  • 文件权限 mode 变更。

这些能力要么需要更严格的安全策略,要么和 Day 12 approval/sandbox 更相关。

实现步骤

1. 新增 patch tool

文件:apps/mini-harness/src/tools/patch.ts

注册函数:

export function registerPatchTools(registry: ToolRegistry, workspaceRoot: string): void {
  const root = resolve(workspaceRoot);

  for (const tool of createPatchTools(root)) {
    registry.register(tool);
  }
}

工具 schema:

inputSchema: {
  type: "object",
  properties: {
    patch: { type: "string" },
    dryRun: { type: "boolean" },
    fuzzFactor: { type: "number" },
  },
  required: ["patch"],
}

2. 解析 patch

const parsed = parsePatch(patchText);

如果解析失败,直接返回:

{
  "applied": false,
  "error": "Patch parse failed: ..."
}

这里不 throw,是因为对 agent 来说,patch 格式错误是可修复输入错误。

3. 校验 patch 类型

当前版本会拒绝:

patch.isBinary
patch.isRename
patch.isCopy

原因是这些不是简单文本 hunk 写入。后续可以扩展,但 Day 11 先保持边界清晰。

4. 校验 workspace path

每个 patch path 都会先去掉 Git 的 a/b/ 前缀:

path.replace(/^[ab]\//, "")

然后 resolve 到 workspace root 内:

const absolutePath = resolve(workspaceRoot, inputPath);
const relativePath = relative(workspaceRoot, absolutePath);

如果路径逃出 workspace,直接拒绝:

Path is outside workspace: ...

5. 应用 hunk

核心调用:

const patchedContent = applyPatch(sourceContent, patch, {
  fuzzFactor,
  autoConvertLineEndings: true,
});

如果返回 false,说明 hunk 无法匹配当前文件:

{
  "applied": false,
  "error": "Patch hunk failed to apply cleanly: ..."
}

6. 注册到默认工具表

文件:apps/mini-harness/src/tools/registry.ts

现在默认 registry 里有:

registerLocalFileTools(registry, workspaceRoot);
registerGitTools(registry, workspaceRoot);
registerPatchTools(registry, workspaceRoot);

这意味着 agent loop、manual tool dispatch、transcript logging、context report 都能看到 apply_patch

7. 更新 mock provider

文件:apps/mini-harness/src/model/provider.ts

当任务包含 patchapply_patcheditmodify 等关键词时,mock provider 会返回一个 dry-run patch:

TOOL_CALL {"name":"apply_patch","input":{"patch":"...","dryRun":true}}

这样 Day 11 demo 可重复运行,不会反复改写 fixture 文件。

Demo

文件:demos/day-11/README.md

运行:

npm run dev -- "apply a small patch to a fixture file"

你会看到类似:

[mini-harness] Step 1 [model]: Model requested tool: apply_patch
[mini-harness] Step 1 [tool]: Tool completed: apply_patch
[mini-harness] Step 2 [final]: Model returned a final answer.

手动 dry-run 返回:

{
  "applied": true,
  "dryRun": true,
  "fileCount": 1,
  "files": [
    {
      "path": "demos/day-11/fixture.txt",
      "operation": "modify",
      "additions": 1,
      "deletions": 1,
      "hunks": 1
    }
  ]
}

如果你把 patch 里的上下文改错,会得到 recoverable error,而不是进程崩溃。

当前系统能力变化

Day 11 后,mini harness 已经具备最小编辑闭环:

read_file/search_text
  -> git_status/git_diff
  -> model proposes unified diff
  -> apply_patch
  -> git_diff verifies result

这一步让 agent 从“理解代码”进入“能修改代码”。

遇到的问题

1. 为什么 demo 默认 dry-run

Demo 要能重复运行。如果每次都真实写 fixture,第二次运行时原 patch 就会因为内容已经变化而冲突。

所以 mock provider 使用 dryRun: true。真实模型或手动命令可以把它改成 false,工具就会写文件。

2. 为什么不支持 rename/copy

Rename/copy patch 涉及多路径操作、删除旧文件、创建新文件、甚至文件交换。这个能力可以做,但它需要更严格的写入审批和错误恢复策略。

Day 11 先聚焦文本 hunk 应用。Day 12 加入 approval/sandbox 后,再扩展更危险的写入操作会更合理。

3. 为什么路径越界直接失败

Patch 工具是写文件能力,不能允许 ../ 或绝对路径逃出 workspace。

后续审批机制不能替代路径边界。路径边界是工具层的基本安全约束。

明天做什么

Day 12 会加入 Approval and Sandbox。

到那时,apply_patch 这种写入工具会进入更严格的执行策略:

model proposes patch
  -> harness classifies write action
  -> approval gate
  -> workspace sandbox
  -> apply_patch

这会让 mini harness 更接近真实 coding agent 的安全模型。

Comments

  • Loading comments…

Comments are posted immediately and emailed to the site owner. No account needed.