权限模型不是给 agent 加一个“安全提示词”就结束了。生产级 coding agent 至少要把权限拆成三层:工具是否可见、工具调用是否需要审批、工具执行时是否被路径和网络边界约束。
本课的核心模型是:先用 tool allowlist 决定 agent 能看见哪些能力,再用审批模式决定每次调用是否放行,最后用工具实现或沙箱把路径、网络和写操作限制落到运行时。权限可以继承,但只能从父任务向子任务收窄,不能让 subagent 自动拿到比主 agent 更高的权限。
阶段二已经用过 read、grep、find、ls、edit、write、bash。到了 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 出发,把 readonly、ask-before-edit、full-auto 三种模式扩展成一套可解释、可测试、可继承的安全模型。
权限模型可以按一次工具调用的生命周期理解:
- 暴露阶段:系统把当前模式允许的工具放进 system prompt 和 agent tool registry。
- 申请阶段:模型产生一个 tool call,例如
bash({ command: "npm test" })。 - 评估阶段:权限层读取
mode、toolName、args、cwd、taskScope,判断是否允许、拒绝或要求用户确认。 - 执行阶段:工具本身还要做路径解析、文件写入队列、bash timeout、sandbox 网络限制等运行时防护。
- 继承阶段:如果这个任务交给子 agent,子 agent 得到的是父任务权限和角色权限的交集。
所以 allowlist 只解决“能不能看见工具”。危险命令检测、路径限制、网络限制、写操作确认和权限继承,分别解决的是“这一次调用能不能执行”“能碰哪里”“能不能出网”“写之前谁确认”“子 agent 能不能越权”。
1. 从工具可见性开始
Section titled “1. 从工具可见性开始”打开 starter:/Users/ryanchen/codespace/pi-agent-course-lab/src/09-permission-modes.ts。里面的 profiles 把模式映射成工具列表:
readonly -> read, grep, find, lsask-before-edit -> read, grep, find, ls, edit, writefull-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 diff 和 git push 风险完全不同;同样是 write,写 docs/notes.md 和写 .env 风险也不同。
审批函数应该逐步读这些变量:
mode:当前权限档位。tool:能力类别。args:命令、路径、内容摘要。cwd:相对路径要落在哪个项目根。taskId:审批记录要能回放到具体任务。
3. 给危险命令做最低限度的语义分类
Section titled “3. 给危险命令做最低限度的语义分类”starter 里的 scoreCommand() 已经示范了三类结果:safe、review、danger。生产实现可以继续扩展:
safe:git status、npm test、rg、ls。review:npm install、git push、写 package lock、联网下载。danger:rm -rf、sudo、curl ... | sh、改权限、删分支、强制 reset。
分类结果不直接等于放行结果,它只是给审批策略输入信号。ask-before-edit 可以自动放行 read-only 命令,但对 review 级命令弹出确认,对 danger 级命令默认拒绝。
4. 路径限制要落到工具边界
Section titled “4. 路径限制要落到工具边界”Pi 的 write 和 edit 工具都会把用户传入路径解析到 cwd 下的绝对路径。课程里要进一步加一层 policy:解析后的路径必须落在允许写入范围内,并且不能命中 denylist,例如 .env、密钥、SSH 配置、全局配置目录。
路径检查不能只做字符串前缀比较。要先 normalize、resolve,再判断它是否在 project root 或 worktree root 内。
5. 网络限制不能只靠提示词
Section titled “5. 网络限制不能只靠提示词”联网风险主要来自 bash:安装依赖、下载脚本、调用外部 API。Pi 示例里的 sandbox extension 展示了更强的做法:把 bash 包到 OS-level sandbox,并配置 allowedDomains、denyRead、allowWrite、denyWrite。
课程项目不要求一开始实现完整 OS 沙箱,但要把 policy 接口设计出来:
network: "none" | "allowlisted" | "open"allowedDomains: string[]这样后续第 20 课的 worktree sandbox 可以复用同一套策略。
6. 写操作确认和权限继承
Section titled “6. 写操作确认和权限继承”写操作确认应该发生在工具执行前,并记录确认对象:路径、摘要、diff、任务来源、子 agent 角色。子 agent 权限继承用交集规则:
effectiveTools = parentAllowedTools ∩ roleAllowedTools ∩ taskAllowedTools例如主 agent 是 ask-before-edit,reviewer 角色只允许 read、grep、bash 的只读命令,那么 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,否则拒绝或等待用户确认。
课堂代码推演
Section titled “课堂代码推演”课堂上用三次调用走一遍权限决策:
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.tsat61babc2:CreateAgentSessionOptions.tools和noTools是 tool allowlist 的 SDK 入口。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/agent-session.tsat61babc2:allowedToolNames、_refreshToolRegistry()和setActiveToolsByName()决定哪些工具进入 registry 和 system prompt。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/index.tsat61babc2:内置工具集合和createReadOnlyToolDefinitions()、createAllToolDefinitions()。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/permission-gate.tsat61babc2:通过tool_callhook 拦截危险 bash 命令,并在无 UI 时默认拒绝。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/plan-mode/utils.tsat61babc2:读模式下的安全 bash allowlist 和破坏性命令模式。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/src/core/tools/bash.tsat61babc2: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.tsat61babc2:写文件和编辑文件的路径解析、写入队列和工具执行边界。/Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/examples/extensions/sandbox/index.tsat61babc2: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.tsat61babc2:验证 allowlist 会过滤 built-in 和 extension tools。
学完本课后,你应该能做到:
- 设计
readonly、ask-before-edit、full-auto三种权限模式,并说明每种模式暴露哪些工具。 - 区分 tool allowlist、审批模式、危险命令检测、路径限制和网络限制各自解决的问题。
- 写出一个工具调用审批函数,能读取
mode、toolName、args和路径 scope。 - 解释为什么
bash需要命令风险分类,为什么写操作需要确认。 - 说明子 agent 权限为什么只能继承交集,而不能自动继承父 agent 的全部权限。
- 实践题:扩展
/Users/ryanchen/codespace/pi-agent-course-lab/src/09-permission-modes.ts,加入isPathAllowed()、networkPolicy和Decision返回类型,并为git push、npm install、写.env各写一个测试用例。 - 思考题:如果
full-auto允许bash,你还会不会保留危险命令检测和路径限制?为什么?