跳转到内容

第 19 课:权限与安全模型

设计 coding agent 的 tool allowlist、审批模式、危险命令检测、路径限制、网络限制、写操作确认和权限继承。

权限模型不是给 agent 加一个“安全提示词”就结束了。生产级 coding agent 至少要把权限拆成三层:工具是否可见、工具调用是否需要审批、工具执行时是否被路径和网络边界约束。

本课的核心模型是:先用 tool allowlist 决定 agent 能看见哪些能力,再用审批模式决定每次调用是否放行,最后用工具实现或沙箱把路径、网络和写操作限制落到运行时。权限可以继承,但只能从父任务向子任务收窄,不能让 subagent 自动拿到比主 agent 更高的权限。

阶段二已经用过 readgrepfindlseditwritebash。到了 coding agent 魔改阶段,真正的问题变成:

当用户说“修复这个 bug”时,agent 能不能直接 rm -rf?能不能 git push?能不能写 .env?能不能联网下载依赖?如果主 agent 派出 reviewer 子 agent,reviewer 是否也能写文件?

这节课从 /Users/ryanchen/codespace/pi-agent-course-lab/src/09-permission-modes.ts 出发,把 readonlyask-before-editfull-auto 三种模式扩展成一套可解释、可测试、可继承的安全模型。

权限模型可以按一次工具调用的生命周期理解:

  1. 暴露阶段:系统把当前模式允许的工具放进 system prompt 和 agent tool registry。
  2. 申请阶段:模型产生一个 tool call,例如 bash({ command: "npm test" })
  3. 评估阶段:权限层读取 modetoolNameargscwdtaskScope,判断是否允许、拒绝或要求用户确认。
  4. 执行阶段:工具本身还要做路径解析、文件写入队列、bash timeout、sandbox 网络限制等运行时防护。
  5. 继承阶段:如果这个任务交给子 agent,子 agent 得到的是父任务权限和角色权限的交集。

所以 allowlist 只解决“能不能看见工具”。危险命令检测、路径限制、网络限制、写操作确认和权限继承,分别解决的是“这一次调用能不能执行”“能碰哪里”“能不能出网”“写之前谁确认”“子 agent 能不能越权”。

打开 starter:/Users/ryanchen/codespace/pi-agent-course-lab/src/09-permission-modes.ts。里面的 profiles 把模式映射成工具列表:

readonly -> read, grep, find, ls
ask-before-edit -> read, grep, find, ls, edit, write
full-auto -> read, grep, find, ls, edit, write, bash

这是安全模型的第一道门。模型看不到 bash,就不会正常生成 bash 调用;但这不等于工具实现不需要防护,因为 extension/custom tool、subagent 或 CLI 参数仍可能改变工具集合。

2. 把审批模式从工具名推进到参数

Section titled “2. 把审批模式从工具名推进到参数”

approveToolCall(mode, tool, args) 不能只看 tool === "bash"。同样是 bash,git diffgit push 风险完全不同;同样是 write,写 docs/notes.md 和写 .env 风险也不同。

审批函数应该逐步读这些变量:

  • mode:当前权限档位。
  • tool:能力类别。
  • args:命令、路径、内容摘要。
  • cwd:相对路径要落在哪个项目根。
  • taskId:审批记录要能回放到具体任务。

3. 给危险命令做最低限度的语义分类

Section titled “3. 给危险命令做最低限度的语义分类”

starter 里的 scoreCommand() 已经示范了三类结果:safereviewdanger。生产实现可以继续扩展:

  • safegit statusnpm testrgls
  • reviewnpm installgit push、写 package lock、联网下载。
  • dangerrm -rfsudocurl ... | sh、改权限、删分支、强制 reset。

分类结果不直接等于放行结果,它只是给审批策略输入信号。ask-before-edit 可以自动放行 read-only 命令,但对 review 级命令弹出确认,对 danger 级命令默认拒绝。

Pi 的 writeedit 工具都会把用户传入路径解析到 cwd 下的绝对路径。课程里要进一步加一层 policy:解析后的路径必须落在允许写入范围内,并且不能命中 denylist,例如 .env、密钥、SSH 配置、全局配置目录。

路径检查不能只做字符串前缀比较。要先 normalize、resolve,再判断它是否在 project root 或 worktree root 内。

联网风险主要来自 bash:安装依赖、下载脚本、调用外部 API。Pi 示例里的 sandbox extension 展示了更强的做法:把 bash 包到 OS-level sandbox,并配置 allowedDomainsdenyReadallowWritedenyWrite

课程项目不要求一开始实现完整 OS 沙箱,但要把 policy 接口设计出来:

network: "none" | "allowlisted" | "open"
allowedDomains: string[]

这样后续第 20 课的 worktree sandbox 可以复用同一套策略。

写操作确认应该发生在工具执行前,并记录确认对象:路径、摘要、diff、任务来源、子 agent 角色。子 agent 权限继承用交集规则:

effectiveTools = parentAllowedTools ∩ roleAllowedTools ∩ taskAllowedTools

例如主 agent 是 ask-before-edit,reviewer 角色只允许 readgrepbash 的只读命令,那么 reviewer 不能因为父任务允许写文件就拿到 edit

本课实践基于 /Users/ryanchen/codespace/pi-agent-course-lab/src/09-permission-modes.ts。先运行 starter,观察 resolveTools("readonly") 只返回读工具。然后把 approveToolCall() 扩展成更接近真实系统的 evaluateToolCall()

type Decision =
| { action: "allow"; reason: string }
| { action: "confirm"; reason: string; summary: string }
| { action: "deny"; reason: string };
function evaluateToolCall(mode, tool, args, scope): Decision {
if (!resolveTools(mode).includes(tool)) {
return { action: "deny", reason: `${tool} is not visible in ${mode}` };
}
if ((tool === "edit" || tool === "write") && !isPathAllowed(args.path, scope)) {
return { action: "deny", reason: "path is outside writable scope" };
}
if (tool === "bash") {
const risk = scoreCommand(args.command);
if (risk.level === "danger") return { action: "deny", reason: risk.reason };
if (risk.level === "review") return { action: "confirm", reason: risk.reason, summary: args.command };
}
if (tool === "edit" || tool === "write") {
return { action: "confirm", reason: "file mutation requires review", summary: args.path };
}
return { action: "allow", reason: "read-only or low-risk tool call" };
}

观察变量流:mode 先决定工具集合;tool 决定进入哪类规则;args 让规则能看见命令和路径;scope 把项目根、允许写路径、denylist 和网络策略带进来。控制点只有一个:工具执行前必须拿到 allow,否则拒绝或等待用户确认。

课堂上用三次调用走一遍权限决策:

const scope = {
root: "/repo",
writable: ["/repo/src", "/repo/tests"],
denyWrite: ["/repo/.env"],
network: "allowlisted",
};
evaluateToolCall("readonly", "edit", { path: "src/a.ts" }, scope);
// deny:edit 不在 readonly 的 allowlist 里。
evaluateToolCall("ask-before-edit", "write", { path: ".env" }, scope);
// deny:write 在模式里可见,但路径命中 denyWrite。
evaluateToolCall("full-auto", "bash", { command: "npm install" }, scope);
// confirm 或 deny:取决于当前策略是否允许修改依赖和联网。

这里要让学生注意顺序:先判断工具是否可见,再判断参数是否越界,最后才决定是否确认。不要反过来先弹确认,因为用户点了确认也不能让越权路径变安全。

再推一遍权限继承:

const parent = ["read", "grep", "find", "ls", "edit", "write"];
const reviewer = ["read", "grep", "find", "ls", "bash"];
const task = ["read", "grep", "find", "ls"];
const effective = intersect(parent, reviewer, task);
// reviewer 最终只能读,不能写,也不能 bash。

这说明权限继承不是复制父权限,而是逐层收窄。后续第 21 课的 subagent 架构会复用这个规则。

  • /Users/ryanchen/codespace/pi-agent-course-lab/src/09-permission-modes.ts:本课 lab starter,展示 Mode、tool profiles、scoreCommand()approveToolCall()
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/sdk.ts at 61babc2CreateAgentSessionOptions.toolsnoTools 是 tool allowlist 的 SDK 入口。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.ts at 61babc2allowedToolNames_refreshToolRegistry()setActiveToolsByName() 决定哪些工具进入 registry 和 system prompt。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/index.ts at 61babc2:内置工具集合和 createReadOnlyToolDefinitions()createAllToolDefinitions()
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/permission-gate.ts at 61babc2:通过 tool_call hook 拦截危险 bash 命令,并在无 UI 时默认拒绝。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/utils.ts at 61babc2:读模式下的安全 bash allowlist 和破坏性命令模式。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/bash.ts at 61babc2:bash 工具的 cwd、timeout、abort、输出截断和可替换 BashOperations 边界。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/write.ts/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/edit.ts at 61babc2:写文件和编辑文件的路径解析、写入队列和工具执行边界。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/sandbox/index.ts at 61babc2:OS-level sandbox 示例,包含网络 allowed domains、filesystem deny/allow write 配置和 sandboxed bash operations。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts at 61babc2:验证 allowlist 会过滤 built-in 和 extension tools。

学完本课后,你应该能做到:

  • 设计 readonlyask-before-editfull-auto 三种权限模式,并说明每种模式暴露哪些工具。
  • 区分 tool allowlist、审批模式、危险命令检测、路径限制和网络限制各自解决的问题。
  • 写出一个工具调用审批函数,能读取 modetoolNameargs 和路径 scope。
  • 解释为什么 bash 需要命令风险分类,为什么写操作需要确认。
  • 说明子 agent 权限为什么只能继承交集,而不能自动继承父 agent 的全部权限。
  1. 实践题:扩展 /Users/ryanchen/codespace/pi-agent-course-lab/src/09-permission-modes.ts,加入 isPathAllowed()networkPolicyDecision 返回类型,并为 git pushnpm install、写 .env 各写一个测试用例。
  2. 思考题:如果 full-auto 允许 bash,你还会不会保留危险命令检测和路径限制?为什么?