第 4 章 · Permissions 与安全权限

01 · 为什么需要权限系统

Coding Agent 能写文件、跑命令、改 git。没有权限边界,一句话就可能把仓库删了。

能力越强,风险越大

上一章结束时,agent 已经有了完整的工具箱:能搜索代码、读写文件、执行命令、查看 Git 变更。这些能力组合起来,agent 可以完成相当复杂的开发任务。

但问题也出在这里——这些能力全部"裸奔"着,没有任何安全控制。

看一下 agent 现在能做什么:

工具能力潜在风险
write_file创建或覆写任意文件覆盖重要配置、删除文件内容
patch_file修改文件的任意部分破坏性修改、引入安全漏洞
run_command执行任意终端命令删除文件、安装恶意包、泄露数据
git push --force通过 run_command 强制推送覆盖远端代码、丢失团队工作

每一个工具都是必要的——没有 run_command,agent 就没法跑测试;没有 write_file,agent 就没法创建文件。但正因为它们能做"真正的事",所以也就能造成"真正的破坏"。

一句话就可能出事

这不是假设。考虑几个真实场景:

场景一:模型产生了"清理"的幻觉。 用户让 agent "清理一下项目",模型决定执行 rm -rf node_modules && rm -rf src。它以为自己在清理构建产物,但实际上把源代码也删了。

场景二:命令拼接出错。 用户让 agent "找一下所有 TODO 注释",模型执行 grep -r "TODO" .,但如果拼接参数出错,可能变成 rm -rf /

场景三:误操作 Git。 模型想查看状态,执行了 git reset --hard HEAD~5,结果丢掉了最近五次提交。

这些场景的共同点是:模型做了决策,代码无条件执行了。 在模型和工具执行之间,没有任何检查点。

最小权限原则

信息安全里有一个核心原则叫最小权限(Principle of Least Privilege):只给主体完成当前任务所需的最少权限,不多给。

把这个原则应用到 agent 场景:

  • 搜索代码、读文件——这些是只读操作,不会改变任何东西,应该自动放行
  • 修改文件、执行命令——这些会改变系统状态,应该经过确认
  • 危险操作(rm -rfgit push --force)——后果不可逆,应该直接拒绝

这就是权限系统要做的事:在模型决策和工具执行之间,插入一道检查。

检查点:模型决策 → 权限检查 → 执行

改造前后的 agent 循环对比:

改造前(第 1-3 章):

模型决定调用工具 → 直接执行 → 结果返回模型

模型说什么就做什么,没有任何中间环节。

改造后(第 4 章):

模型决定调用工具 → 权限检查 → 执行(或拒绝)→ 结果返回模型

权限检查有三种可能的结果:

  • 放行(allow):只读操作,安全,直接执行
  • 询问(ask):需要用户确认,终端弹出提示
  • 拒绝(deny):危险操作,直接拦截,不执行

插入点在哪里

看一下 agent 循环中权限检查的位置。在 src/agent.ts 的工具执行循环中:

/** 依次执行每个工具调用 */
for (const toolCall of response.toolCalls) {
  const tool = allTools.find((t) => t.name === toolCall.name);
  if (!tool) {
    allMessages.push({
      role: "tool_result",
      toolCallId: toolCall.id,
      result: `错误:未知工具 ${toolCall.name}`,
    });
    continue;
  }

  /**
   * 权限检查(第 4 章新增)
   *
   * 如果提供了 permissionGuard,在执行工具前检查权限:
   * - allow: 直接执行
   * - ask: 调用确认回调,由用户决定
   * - deny: 直接拒绝,不执行
   *
   * 如果没有提供 permissionGuard,所有调用自动放行(向后兼容第 1-3 章)。
   */
  if (options?.permissionGuard) {
    const { approved, reason } = await options.permissionGuard.check(
      toolCall.name,
      toolCall.arguments,
    );
    if (!approved) {
      allMessages.push({
        role: "tool_result",
        toolCallId: toolCall.id,
        result: `操作被拒绝: ${reason}`,
      });
      continue;
    }
  }

  const result = await tool.execute(toolCall.arguments, state);
  stats.toolCallCount++;
  allMessages.push({
    role: "tool_result",
    toolCallId: toolCall.id,
    result,
  });
}

关键设计点:

检查在执行之前。 permissionGuard.check()tool.execute() 之前调用。如果检查不通过,continue 跳过执行,直接处理下一个工具调用。

拒绝不是静默丢弃。 被拒绝的操作会把 "操作被拒绝" 作为工具结果返回给模型。模型能看到自己被拒绝了,并且知道原因——它可以根据这个信息调整策略。

向后兼容。 如果没有提供 permissionGuard,所有工具调用自动放行。这意味着第 1-3 章的代码不需要任何修改,权限系统是可选的新增层。

完整流程图

加入权限检查后,agent 循环变成这样:

和第 2 章的 ReAct 循环相比,唯一的变化是在"模型返回工具调用"和"执行工具"之间多了一步"权限检查"。这个插入点选得很精准:

  • 它在模型决策之后——模型有完全的自由决定调用什么工具
  • 它在工具执行之前——有机会拦截不应该执行的操作
  • 它的结果会回到对话历史中——模型能感知到被拒绝,并据此调整

RunAgentOptions 的变化

为了把权限守卫传入 agent 循环,RunAgentOptions 新增了一个字段:

export interface RunAgentOptions {
  maxIterations?: number;
  /** 权限守卫,如果不传则所有工具调用自动放行(向后兼容) */
  permissionGuard?: PermissionGuard;
}

permissionGuard 是可选的。不传的话,agent 的行为和第 1-3 章完全一样。传入后,每次工具执行前都会走一遍权限检查。

main.ts 中的调用方式:

const permissionGuard = new PermissionGuard({
  confirm: async (tool, args, reason) => {
    /** ... 用户确认逻辑 ... */
  },
});

const result = await runAgent(state, model, tools, { permissionGuard });

权限守卫的实例化在 agent 循环外部完成,通过 options 传入。agent 循环本身不需要知道权限检查的具体规则——它只关心"通过"或"拒绝"。

权限系统要解决的问题

总结一下,这一章的权限系统要解决三个问题:

1. 自动放行安全的操作。 搜索、读文件、查看 Git 状态——这些是只读操作,不会改变任何东西。每次都让用户确认会严重影响体验。

2. 需要确认的操作要问用户。 修改文件、执行命令——这些会改变系统状态。agent 应该告诉用户"我要做什么",让用户决定是否允许。

3. 直接拦截危险操作。 rm -rfgit push --forcesudo——这些操作的后果不可逆。不应该让用户来做这个判断,系统应该自动拦截。

下一节会详细讲解三级权限(allow / ask / deny)的具体实现。

登录以继续阅读

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

立即登录

On this page