5 min read
Day 12:Approval 与 Sandbox:写入审批和工作区边界

上一篇:Patch Editing:安全应用 Unified Diff
下一篇:AGENTS.md:把项目规则注入 Agent 上下文
English version: AI Coding Agent Security: Approval Gates and Workspace Sandbox in TypeScript

文章介绍

Day 11 的 mini harness 已经能解析 unified diff,并通过 apply_patch 修改文件。能力一旦从“读取代码”升级到“写入代码”,安全模型就必须跟上。

今天实现两个独立边界:

  • Approval Gate:写文件和命令类工具必须获得显式授权。
  • Workspace Sandbox:工具只能访问当前 workspace 内的路径。

它们解决的问题不同。Approval 决定“这个动作是否允许执行”,workspace sandbox 决定“这个动作最多能影响哪里”。即使用户批准了一次写入,也不能因此允许 patch 逃到 ../ 目录。

AI coding agent approval gate and workspace sandbox flow

今天要解决什么

今天完成六个交付:

  1. 新增统一的工具风险分类与审批决策。
  2. 默认拒绝真实 apply_patch 写入。
  3. 允许 apply_patch dry-run 无审批执行。
  4. 新增 --allow-writes,为可信运行提供显式授权。
  5. 把 agent loop 和手动 tool dispatch 接入同一审批门禁。
  6. 把文件、Git、context 和 patch 的路径限制收敛到统一 workspace guard。

最小演示:

npm run dev -- "show approval sandbox for a write"

默认情况下,mock model 会请求一次真实写入,但 harness 会在工具执行前拒绝它。

它在 Cursor/Codex 里对应哪一层

今天实现的是 coding agent 的 Safety Layer

一个工具调用从模型到执行器,至少要经过这几个判断:

Model proposes tool call
  -> classify risk
  -> check approval
  -> validate workspace path
  -> execute tool
  -> record result

模型只负责提出动作,不拥有最终执行权。是否允许写文件、运行命令或访问 workspace 外部路径,都由 harness 决定。

这也是 approval 不能只写在 prompt 里的原因。Prompt 中的“请先询问用户”只是给模型的行为建议;真正的安全边界必须存在于模型无法绕过的执行层。

威胁模型

1. 未授权写入

模型可能在用户只要求“解释代码”时错误调用写入工具,也可能因为上下文误判而修改错误文件。

默认拒绝 write-class tool,可以让错误停在执行前。

2. 路径逃逸

下面的路径看起来只是普通字符串,但解析后会离开 workspace:

../../.ssh/config
/tmp/agent-output.txt

所以工具不能简单拼接路径。它必须先把输入转换为绝对路径,再计算它相对于 workspace root 的位置。

3. 命令执行

当前项目还没有注册 shell command 工具,但安全策略已经把常见 command tool 名称归为 command 风险。以后加入 run_command 时,它会默认进入审批门禁,而不是默认执行。

4. Approval 不是无限授权

--allow-writes 只对当前 CLI 运行生效,不写入全局配置,也不会关闭 workspace path guard。

换句话说:

approval controls whether
sandbox controls where

设计思路

1. 默认拒绝有副作用的动作

CLI 的默认审批模式是:

type ApprovalMode = "deny-writes" | "allow-writes";

没有显式参数时使用 deny-writes。这能保证旧的读取、搜索、Git diff 和 dry-run demo 正常运行,同时真实写入不会静默发生。

2. Dry-run 属于只读风险

apply_patch 有两种行为:

dryRun: true   -> parse + validate + hunk match
dryRun: false  -> parse + validate + write files

前者不会修改磁盘,因此可以自动放行。后者是 write-class action,必须审批。

这种判断不能只看工具名,还要看输入参数。同一个工具可能因为执行模式不同而具有不同风险。

3. 拒绝也要成为结构化 ToolResult

审批失败不是进程异常,而是 agent 可以观察和处理的执行结果:

{
  "name": "apply_patch",
  "output": {
    "approved": false,
    "required": true,
    "risk": "write"
  },
  "error": "Approval required for write tool `apply_patch`..."
}

Agent loop 会把这个结果追加到对话中。模型可以解释限制、改用 dry-run,或者等待用户重新发起一次带授权的运行。

4. Agent 与手动调用共用策略

如果只在 agent loop 里检查 approval,用户仍可通过下面的入口绕开:

npm run dev -- --tool apply_patch --tool-input '{...}'

所以两个入口都调用相同的 evaluateToolApproval()

agent tool call -----+
                     +-> approval policy -> registry.dispatch()
manual tool call ----+

实现步骤

1. 定义审批策略

文件:apps/mini-harness/src/safety/approval.ts

核心决策结构:

export type ApprovalDecision = {
  approved: boolean;
  required: boolean;
  risk: "read" | "write" | "command";
  reason: string;
};

分类规则保持最小:

if (COMMAND_TOOLS.has(toolCall.name)) {
  return "command";
}

if (toolCall.name === "apply_patch" && !isDryRun(toolCall.input)) {
  return "write";
}

return "read";

真实产品通常会把风险元数据放到 tool definition 中。这个 mini harness 目前工具数量很少,先让策略集中在一个模块,避免过早扩展共享协议。

2. 在 agent loop 执行前审批

文件:apps/mini-harness/src/agent/run.ts

工具执行前先得到 decision:

const approval = evaluateToolApproval(input.toolCall, input.approvalMode);

if (!approval.approved) {
  return createApprovalDeniedResult(input.toolCall, approval);
}

只有 approved: true 才会继续调用 registry 和 timeout wrapper。因此审批发生在任何写入之前。

3. 为 CLI 增加显式授权参数

文件:apps/mini-harness/src/cli.ts

CLI 新增:

--allow-writes

它把当前运行的模式从 deny-writes 切换为 allow-writes

approvalMode: options.allowWrites ? "allow-writes" : "deny-writes"

这个参数同时作用于 agent 发起的工具调用和 --tool 手动调用。

4. 统一 workspace path guard

文件:apps/mini-harness/src/safety/workspace.ts

路径解析先做 lexical check,再使用 realpath 检查已存在的目标或最近的已存在父目录:

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

assertInsideWorkspace(relativePath, inputPath);

const canonicalRoot = await realpath(root);
const canonicalPath = await resolveCanonicalTarget(absolutePath);
assertInsideWorkspace(relative(canonicalRoot, canonicalPath), inputPath);

统一 guard 被以下模块复用:

  • list_filesread_filesearch_text
  • git_statusgit_diff
  • context selected files
  • apply_patch

这样路径规则不会在每个工具里复制一份,也不会因为某个工具忘记校验而留下旁路。第二次 canonical check 还会拒绝 workspace 内指向外部目录的软链接。

5. 更新 mock provider

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

当任务包含 approvalsandboxpermission 时,mock provider 会请求一次 dryRun: false 的 patch。默认运行会收到 approval error,然后给出最终说明。

这个 demo 不会修改 fixture,因此可以重复运行。

Demo

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

1. 验证默认拒绝写入

npm run dev -- "show approval sandbox for a write"

关键输出:

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

2. 验证 dry-run 自动放行

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

Day 11 的 mock patch 使用 dryRun: true,所以它仍然可以执行。

3. 验证显式授权

真实写入必须明确增加:

npm run dev -- --allow-writes --tool apply_patch --tool-input '{"patch":"...","dryRun":false}'

授权只对这次进程有效。路径仍然必须位于 workspace 内。

4. 验证路径越界拒绝

npm run dev -- --tool read_file --tool-input '{"path":"../outside.txt"}'

返回结果包含:

Path is outside workspace: ../outside.txt

当前系统能力变化

Day 12 后,写入闭环变成:

read/search
  -> inspect git diff
  -> propose patch
  -> classify risk
  -> require explicit approval
  -> validate workspace path
  -> apply patch
  -> record transcript

现在模型可以提出修改,但 harness 保留最终控制权。

边界与限制

1. 这不是操作系统级 sandbox

当前实现是 symlink-aware workspace path guard,不是容器、虚拟机、macOS sandbox profile 或 Linux namespace。它限制 harness 自己传给工具的路径,但不能替代 OS 隔离,也不能彻底消除路径检查与实际写入之间的 TOCTOU 竞态。

2. 当前授权粒度是整次运行

--allow-writes 是非交互 CLI 的最小实现。生产级产品通常会提供逐次确认、规则匹配、可信命令前缀和 session 级授权。

3. 命令工具尚未注册

策略已经定义 command 风险,但项目目前没有 shell execution tool。未来加入命令执行时,还需要命令解析、cwd 限制、环境变量过滤、超时和 OS sandbox。

4. 路径检查不能替代审批

workspace 内的写入仍然可能删除重要代码。路径合法只说明“位置允许”,不说明“动作合理”。

明天做什么

Day 13 会实现 Project Rules。

Mini harness 将读取 AGENTS.md 风格的项目规则,并把根目录指导注入 context builder:

workspace rules
  -> context builder
  -> model decision
  -> approval/sandbox
  -> tool execution

这会把“能安全执行”继续升级为“理解项目约束后再执行”。

Comments

  • Loading comments…

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