05 · 实战:权限系统如何拦截和放行
用真实运行结果展示权限系统在实际任务中的表现:只读操作静默通过、写入操作等待确认、危险命令被拦截。
在真实任务中观察权限系统
前四节分别讲了权限系统的各个组件。这一节用两个真实任务跑一遍,看权限系统在实际运行中的表现。
任务一:只读操作——静默通过
给 agent 一个纯只读任务:
> 列出 src/tools/ 下所有 .ts 文件,然后读取 src/tools/search.ts 的前 5 行执行过程:
工具调用: glob({ pattern: "src/tools/*.ts" })
→ allow: 只读操作,自动放行
工具调用: read_file({ path: "src/tools/search.ts" })
→ allow: 只读操作,自动放行
工具调用: read_file({ path: "src/tools/search.ts" })
→ allow: 只读操作,自动放行用户视角: 终端中没有出现任何确认提示。agent 搜索文件、读取内容,整个过程流畅自然,和没有权限系统时一样。
审计日志: 3 次工具调用,全部 allow,全部 approved: true。
[模型调用: 3次 | 工具调用: 3次]这就是权限系统的第一个设计目标——安全操作不打扰用户。glob 和 read_file 都是只读工具,在 DEFAULT_RULES 中配置为 allow,不会触发任何确认。
任务二:写入操作——等待确认
换一个需要修改文件的任务:
> 在 src/tools/search.ts 的 parameters 中添加一个 countOnly 布尔参数执行过程:
工具调用: read_file({ path: "src/tools/search.ts" })
→ allow: 只读操作,自动放行
工具调用: patch_file({ path: "src/tools/search.ts", ... })
→ ask: patch_file 需要用户确认
需要修改文件: src/tools/search.ts
原因: patch_file 需要用户确认
允许? (y/n) y ← 用户确认
工具调用: patch_file({ path: "src/tools/search.ts", ... })
→ ask: patch_file 需要用户确认
需要修改文件: src/tools/search.ts
原因: patch_file 需要用户确认
允许? (y/n) y ← 用户确认
工具调用: run_command({ command: "npx vitest run" })
→ ask: run_command 需要用户确认
需要执行命令: npx vitest run
原因: run_command 需要用户确认
允许? (y/n) y ← 用户确认用户视角: 终端中出现了三次确认提示——两次修改文件、一次运行命令。用户看到 agent 要修改哪个文件、要执行什么命令,逐一确认后 agent 才继续。
权限决策拆解:
| 工具调用 | 权限级别 | 用户操作 |
|---|---|---|
| read_file | allow | 无需操作 |
| patch_file (第 1 次) | ask | 确认 |
| patch_file (第 2 次) | ask | 确认 |
| run_command | ask | 确认 |
只读操作 read_file 一路绿灯,写入操作 patch_file 和执行操作 run_command 都需要用户确认。这就是权限系统的第二个设计目标——有风险的操作必须经过用户同意。
如果用户拒绝会怎样
当用户在确认提示中输入 n:
需要修改文件: src/tools/search.ts
原因: patch_file 需要用户确认
允许? (y/n) n
已拒绝权限守卫返回 { approved: false, reason: "patch_file 需要用户确认" }。这个结果作为 tool_result 追加到对话历史中:
工具结果: "操作被拒绝: patch_file 需要用户确认"模型收到"操作被拒绝"的反馈后,不会崩溃,也不会重复尝试同一个被拒绝的操作。它可能会:
- 换一种方式完成任务。 比如被拒绝
patch_file后,模型可能改为用文字描述应该怎么改,让用户自己动手。 - 跳过这一步,继续后面的步骤。 如果修改不是任务的核心部分。
- 承认无法完成。 如果没有替代方案。
这就是为什么拒绝原因作为 tool_result 而不是错误返回——它需要回到模型的上下文中,让模型自己决定下一步怎么做。
危险命令:根本不会到达用户
如果模型尝试执行 rm -rf node_modules,这条命令会在 resolveLevel 的第一步就被拦截:
resolveLevel("run_command", { command: "rm -rf node_modules" })
→ 第一步: isDangerousCommand("rm -rf node_modules")
→ 匹配 /\brm\s+(-\w*[rf]\w*|--recursive|--force)/
→ 返回 { level: "deny", reason: "危险命令被拦截: rm -rf node_modules" }用户根本不会看到确认提示。confirmCallback 不会被调用。命令直接被拒绝,拒绝原因返回给模型。
审计日志记录:{ tool: "run_command", level: "deny", approved: false, reason: "危险命令被拦截: ..." }。
拦截后的模型行为
一个有趣的观察:当 run_command 被拒绝时,模型可能会改用 patch_file 来完成类似的工作。比如模型想通过 rm 删除文件被拒绝后,可能会用 write_file 覆写一个空文件来"清空"内容。
这不是安全漏洞——write_file 和 patch_file 同样需要用户确认。用户会看到"需要修改文件"的提示,可以选择拒绝。权限系统不是只防一次,而是防每一层。
审计日志:任务结束后回溯
任务完成后,PermissionGuard 的 getAuditLog() 返回完整记录:
const result = await runAgent(state, model, tools, { permissionGuard });
/** 查看审计日志 */
const auditLog = permissionGuard.getAuditLog();
// [
// { tool: "glob", level: "allow", approved: true, reason: "只读操作,自动放行" },
// { tool: "read_file", level: "allow", approved: true, reason: "只读操作,自动放行" },
// { tool: "patch_file", level: "ask", approved: true, reason: "patch_file 需要用户确认" },
// { tool: "patch_file", level: "ask", approved: true, reason: "patch_file 需要用户确认" },
// { tool: "run_command", level: "ask", approved: true, reason: "run_command 需要用户确认" },
// ]每一行都是一个完整的权限决策记录。可以统计哪些工具被确认、哪些被拒绝、用户的确认率是多少。这些数据能帮助调整权限规则——如果某个工具的确认率接近 100%,可以考虑把它改成 allow。
注意:在当前实现中,PermissionGuard 在整个会话中只创建一次(在 REPL 循环外部)。这意味着审计日志是 session 级别的——多轮对话的权限决策会累积在同一个日志中。如果你需要按任务隔离审计日志,可以在每次 runAgent 调用前创建新的 PermissionGuard 实例。
权限检查在 agent 循环中的位置
把权限检查放回完整的 agent 循环中看:
权限检查是 agent 循环中的一个"关卡"。无论通过还是拒绝,结果都回到对话中,模型继续推理。循环不会因为拒绝而中断——这是和传统权限系统最大的不同。
这一章的代码结构
完成第 4 章后,项目结构变为:
mini-coding-agent/
├── src/
│ ├── types.ts # 类型定义(新增 PermissionLevel, PermissionRule 等)
│ ├── model.ts # 模型层(不变)
│ ├── todo.ts # Todo 管理器(不变)
│ ├── permissions/
│ │ └── index.ts # 权限守卫(新增)
│ ├── agent.ts # Agent Loop(升级:权限检查)
│ ├── main.ts # CLI 入口(升级:权限确认交互)
│ └── tools/ # 工具集(不变)
├── test/
│ ├── permissions/
│ │ └── index.test.ts # 权限守卫测试(新增)
│ └── ...
├── package.json
└── tsconfig.json这一章做了什么
回顾第 4 章的成果:
- PermissionGuard 类:封装了完整的权限判断逻辑,通过构造函数注入规则和确认回调
- 三级权限模型:
allow(自动放行只读工具)、ask(用户确认写入/执行工具)、deny(直接拒绝危险命令) - 危险命令检测:用正则模式匹配拦截
rm -rf、git push --force、sudo等高危命令 - 确认回调:通过
ConfirmationCallback把最终决定权交给用户 - 审计日志:每次权限决策都被记录,任务结束后可回溯
- agent 循环集成:权限检查在工具执行之前,通过可选参数注入,不传则全部放行
从第 3 章的"能做就做",到第 4 章的"该问就问、该拦就拦",agent 的安全性上了一个台阶。下一章开始建设流式输出能力——让用户在模型思考的同时就能看到中间结果。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。