第 4 章 · Permissions 与安全权限

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次]

这就是权限系统的第一个设计目标——安全操作不打扰用户globread_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_fileallow无需操作
patch_file (第 1 次)ask确认
patch_file (第 2 次)ask确认
run_commandask确认

只读操作 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 需要用户确认"

模型收到"操作被拒绝"的反馈后,不会崩溃,也不会重复尝试同一个被拒绝的操作。它可能会:

  1. 换一种方式完成任务。 比如被拒绝 patch_file 后,模型可能改为用文字描述应该怎么改,让用户自己动手。
  2. 跳过这一步,继续后面的步骤。 如果修改不是任务的核心部分。
  3. 承认无法完成。 如果没有替代方案。

这就是为什么拒绝原因作为 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_filepatch_file 同样需要用户确认。用户会看到"需要修改文件"的提示,可以选择拒绝。权限系统不是只防一次,而是防每一层。

审计日志:任务结束后回溯

任务完成后,PermissionGuardgetAuditLog() 返回完整记录:

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 -rfgit push --forcesudo 等高危命令
  • 确认回调:通过 ConfirmationCallback 把最终决定权交给用户
  • 审计日志:每次权限决策都被记录,任务结束后可回溯
  • agent 循环集成:权限检查在工具执行之前,通过可选参数注入,不传则全部放行

从第 3 章的"能做就做",到第 4 章的"该问就问、该拦就拦",agent 的安全性上了一个台阶。下一章开始建设流式输出能力——让用户在模型思考的同时就能看到中间结果。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page