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 -rf、git 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 -rf、git push --force、sudo——这些操作的后果不可逆。不应该让用户来做这个判断,系统应该自动拦截。
下一节会详细讲解三级权限(allow / ask / deny)的具体实现。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。