AI Agent 的工具调用前授权
工具调用前授权会在 AI Agent 执行动作前检查权限,控制它能读什么、跑什么、改什么,以及循环何时停止。
TL;DR — 工具调用前授权,指的是 agent 已经决定要做某个动作,但工具还没真正执行之前,由 runtime 做一次硬检查。它回答的不是“模型想不想做”,而是“这个 agent 现在能不能读这份数据、能不能在这里跑代码、能不能改这个系统、还能不能继续循环”。MCP 负责把工具接进来,sandbox 负责把执行隔离起来,pre-action authorization 就是夹在两者之间的门禁。
工具调用让 agent 真正有用了。也让 agent 开始有了真实的破坏力。
很多事故并不复杂。不是高深的越狱,也不是特别精巧的 prompt injection。更多时候只是模型做了一个看起来合理、但时机不对的动作:读了任务范围外的文件,把 PR 开到了错误的仓库,对同一个浏览器动作重试了 80 次,或者让一段生成代码带着网络权限跑起来。
所以生产级 agent 不能只靠“工具列表”。它需要工具调用前授权。不是写在文档里的权限模型,而是每一次关键动作执行前,runtime 真的拦下来检查一次。
它应该放在什么位置
一次 agent 动作大概有 5 步:
- 模型判断下一步想做什么。
- runtime 把这个意图转成工具调用。
- 授权层检查工具、参数、身份、任务范围和剩余预算。
- runtime 在合适的环境里执行工具。
- 可观测系统记录发生了什么。
很多 demo 会直接从第 2 步跳到第 4 步。生产系统不能这么干。
MCP tools specification 解决的是工具如何暴露、如何被调用。这个标准很重要,但它不会替你判断:这个 agent、这个用户、这个 workspace、这个任务,现在到底能不能调用这个工具。这个判断必须放在 runtime 里。
你可以把它理解成 MCP 执行边界 和 agent runtime 层 之间的一个具体落点。MCP 让工具可用,runtime 负责执行,pre-action authorization 判断下一次调用能不能越过边界。
问题不是“有没有这个工具”
“这个 agent 有没有 GitHub 工具”这个问题太粗了。
把 GitHub 工具拆开看,风险完全不同:
| 动作 | 风险类型 | 默认策略 |
|---|---|---|
| 列 issue | 读取 | 仓库范围内允许 |
| 读文件 | 读取 | 任务 workspace 内允许 |
| 跑测试 | 计算 | sandbox 内允许 |
| 生成 patch | 草稿 | 允许,但只停留在本地 |
| 打开 PR | 外部状态变更 | 需要策略或审批 |
push 到 main | 高风险变更 | 默认拒绝 |
| 删除仓库 | 不可逆 | 直接拒绝 |
这些不能被混成一个权限。否则最后只会得到两种系统:要么 agent 什么都干不了,要么 agent 什么都敢干。
更合理的做法是把能力和上下文一起判断:
{
"agent_id": "coding-agent",
"task_id": "fix-login-test",
"user_id": "u_123",
"workspace": "repo:sandbase-demo",
"tool": "github.open_pull_request",
"arguments": {
"repo": "sandbase-demo",
"branch": "agent/fix-login-test",
"base": "main"
},
"risk": "external_mutation",
"budget": {
"remaining_tool_calls": 8,
"remaining_seconds": 420
}
}
这才足够做判断。同一个工具,换了仓库,可能要拒绝。同一个仓库,分支不对,也要拒绝。任务预算已经耗尽,就应该停。这个动作已经被 maintainer 明确批准过,可以放行,但要记审计日志。
先做 5 个门禁
一开始不需要做一个巨大的权限系统。先把 agent 最常见的翻车点拦住就够了。
| 门禁 | 检查什么 | 典型拒绝 |
|---|---|---|
| Scope gate | 这个任务能不能用这个工具 | support agent 想调用 deploy 工具 |
| Argument gate | 参数有没有越界 | 文件路径逃出 /workspace |
| Execution gate | 动作会在哪里执行 | 生成的 Python 想要宿主机网络 |
| Mutation gate | 会不会改外部状态 | agent 想发邮件或开 PR |
| Loop gate | 循环还在预算内吗 | 同一个工具被重复调用太多次 |
大多数团队会从 scope gate 开始。真正容易踩坑的是 argument gate。
读文件工具本身看起来安全,直到参数变成 ../../.env。浏览器工具看起来安全,直到 URL 指向内部后台。代码执行工具看起来安全,直到生成脚本要求网络权限和挂载凭证。授权层不能只看工具名,必须看参数。
门禁要放在 sandbox 前面
Sandbox 仍然是底线。只要 agent 能运行生成代码,就应该跑在隔离环境里,有资源限制,有网络规则。Claude 的 Managed Agents 发布文章 也说明了同一个方向:长时间运行的 agent 需要 sandboxed execution、scoped permissions 和 tracing,这些应该是产品基础设施,而不是每个应用临时拼出来的胶水。
但 sandbox 不能替代授权。
Sandbox 回答的是:如果这段东西跑起来,影响范围有多大?
Pre-action authorization 回答的是:这段东西现在该不该跑?
两个都要有。明显不该执行的动作,在进入 sandbox 前就拒掉。允许执行的计算,也仍然放进 sandbox。这样才有两道防线:
- policy 拦住明显错误;
- isolation 兜住 policy 没拦住的错误。
我不太喜欢那种“直接给模型一个通用 shell,然后把安全全压到容器里”的设计。容器能限制爆炸半径,但它不知道 git push origin main 对当前任务是不是合理。这是 policy 的问题。
人工审批要少,但位置要准
一说授权,很多人会想到“每次工具调用都弹确认”。这会把产品体验毁掉。
正确做法是把审批收窄:
| 动作类型 | 推荐默认值 |
|---|---|
| 读取限定范围内的数据 | 自动允许并记录 |
| 在 sandbox 里跑计算 | 预算内自动允许 |
| 生成文本或代码草稿 | 自动允许,但保持 staged |
| 修改外部系统 | 需要策略或审批 |
| 不可逆动作 | 必须人工确认 |
Agent 应该能自己完成大部分工作:看文件、跑测试、准备 patch、草拟客服回复、总结日志、生成报告。这些不应该每一步都打断用户。
审批应该放在“草稿变成外部状态”的那条线上。比如发出回复、合并 patch、部署生产、扣款、删数据。这个位置才需要人或更严格的策略介入。
Anthropic 的 Claude Code hooks 文档 也指向同一个模式:PreToolUse 这类 hook 的价值,就是让系统在工具执行前检查和拦截动作。产品形态可能不同,但模式很稳定:副作用发生前,先拦一下。
一个最小可用的策略模型
最早可以做得很朴素:
type Risk = "read" | "compute" | "draft" | "mutation" | "irreversible";
type Action = {
agentId: string;
taskType: "code" | "support" | "research";
tool: string;
risk: Risk;
args: Record<string, unknown>;
workspaceId: string;
remainingSteps: number;
};
type Decision =
| { allow: true; audit: boolean }
| { allow: false; reason: string }
| { allow: false; reason: string; approvalRequired: true };
const taskTools = {
code: ["repo.read", "tests.run", "sandbox.exec", "pull_request.draft"],
support: ["ticket.read", "docs.search", "reply.draft"],
research: ["web.search", "page.fetch", "notes.write"],
};
export function authorize(action: Action): Decision {
if (action.remainingSteps <= 0) {
return { allow: false, reason: "step budget exhausted" };
}
if (!taskTools[action.taskType].includes(action.tool)) {
return { allow: false, reason: "tool not allowed for task type" };
}
if (action.risk === "irreversible") {
return {
allow: false,
reason: "irreversible action requires approval",
approvalRequired: true,
};
}
if (action.risk === "mutation") {
return {
allow: false,
reason: "external mutation requires approval",
approvalRequired: true,
};
}
return { allow: true, audit: action.risk !== "read" };
}
这段代码故意很普通。重点不是发明一门 policy 语言,而是让每个工具调用都经过一个单一、可测试、可审计的决策点。
后面你可以把硬编码规则换成 Open Policy Agent、Cedar、Zanzibar 风格的关系检查,或者公司内部已有的授权服务。但接口最好保持不变:输入 action,输出 decision,然后写审计日志。
审计日志要记什么
没有审计的授权,最后会变成客服噩梦。用户问“为什么 agent 停了”,你不能只回答“policy 拒绝了”。
至少要记录这些字段:
| 字段 | 作用 |
|---|---|
trace_id | 关联到这次 agent run |
agent_id 和 user_id | 知道是谁代表谁在操作 |
tool 和标准化后的参数 | 复现这次判断 |
risk | 解释为什么需要审批 |
decision | allow、deny 或 approval required |
reason | 给人看的拒绝理由 |
policy_version | policy 改版后方便排查 |
这和 agent observability 是一件事。一次 trace 里应该能看到模型调用、它想调用的工具、授权决策、执行结果,以及可能的审批事件。否则你只能猜:到底是模型选错工具,policy 拦了,还是 sandbox 挂了。
最容易翻车的地方
最常见的失败不是放得太松,而是拦得太死。
团队上线了一个严格 policy,结果 agent 每 20 秒就让用户点一次确认。用户很快疲劳,开始闭眼批准一切。系统表面上有权限模型,实际上没有判断力。
解法是渐进式自治:
- 读取和 sandbox 内计算,默认自动允许。
- 草稿类产物保持 staged,不直接改外部状态。
- 只有外部 mutation 需要审批。
- 观察一段时间后,把反复安全的动作提升为 policy。
- 不可逆动作永远保留人工确认。
信任不是靠一句“我们的 agent 很安全”建立的,而是靠系统让安全路径比危险路径更顺手。
FAQ
工具调用前授权和 guardrails 是一回事吗?
不是。Guardrails 是更大的控制体系,pre-action authorization 是其中一个具体环节:工具执行前由 runtime 做决定。完整控制栈可以看 production agent guardrails guide。
每个工具调用都要人工确认吗?
不需要。人工审批应该留给外部状态变更和不可逆操作。读取、sandbox 内计算、草稿生成,通常可以在范围和预算内自动执行。
MCP 会帮我处理授权吗?
不会。MCP 规范的是工具如何暴露、如何调用。具体某个 agent 能不能在某个 workspace 里用某个参数调用某个工具,还是 runtime 的责任。
授权检查应该放在哪里?
放在 runtime 里,位于工具选择和真正执行之间。不要把它藏在 prompt 里。模型可以解释意图,但执行策略必须由 runtime 强制执行。
结论
Pre-action authorization 最好的地方,就是它足够无聊。它把“模型想做某件事”,变成“系统在副作用发生前做一次明确判断”。
如果你在做生产级 agent,先实现 5 个门禁:scope、arguments、execution、mutation、loop。安全的工作尽量自动化,外部状态变更再审批。每一次判断都写日志。
这就是“agent 能调用工具”和“agent 可以持续、安全地调用工具”之间的区别。


