第 21 章 权限与审批:谁有权干什么、谁点头
隔离完了,还剩"谁有权干什么"
第 20 章把"读"和"干"分开了。但权限问题还没答完:Agent 具体能调用哪些工具?哪些动作必须有人点头? 一个能访问工具服务器的 Agent,理论上能带任何参数调任何工具,中间没有任何强制层。这不行。
这一章讲四个模式,从"要人审"到"精确到动作"到"统一门禁"到"策略化授权":
- Human-in-the-Loop Approval Framework(人在回路审批):高风险动作插人工审批门。
- Exact-Action Authorization Binding(精确动作授权绑定):把审批绑定到完整动作,执行前重新比对。
- Policy-Gated Tool Proxy(策略门控工具代理):Agent 和工具之间插一个透明代理,每次调用过策略引擎。
- Sandboxed Tool Authorization(沙箱授权):模式匹配 + 默认拒绝 + 层级继承的策略。
模式一:人在回路审批(Human-in-the-Loop Approval Framework)
问题
自主 Agent 需要执行高风险或不可逆操作(数据库修改、生产部署、系统配置、API 调用),但让它们无监督执行,会带来不可接受的安全和合规风险。把所有操作都拦住又违背了 Agent 自动化的初衷。
方案
为指定的高风险函数系统化地插入人工审批门,同时保持安全操作的 Agent 自主性。 建轻量反馈循环,让"时间敏感的人工决策"能发生,而不阻塞整个 Agent 工作流。
核心组件:
风险分类:
- 识别需要人工审批的函数。
- 定义审批标准(成本阈值、数据敏感性、可逆性)。
- 按风险级别给操作分类。
多渠道审批界面:
- Slack 集成:实时通知。
- 邮件:异步审批。
- SMS:紧急/关键操作。
- Web 仪表盘:批量审查。
审批工作流:
- Agent 执行风险函数前请求许可。
- 人类收到上下文丰富的审批请求。
- 快速批准/拒绝/修改。
- Agent 根据响应继续或调整。
- 超时处理带可配置升级(推荐默认拒绝)。
审计轨迹:
- 记录所有审批请求和响应。
- 追踪谁在何时批准了什么。
- 支持合规和调试。
Agent → 框架: 请求 DROP old_users 表
框架 → 分类: 高风险
框架 → Slack: 发审批请求(带上下文)
Slack → 人: 带批准/拒绝按钮的通知
批准: 人点击批准 → 框架授权 → Agent 执行 DROP → 记录审批和执行
拒绝: 人点击拒绝+理由 → 框架拒绝+理由 → Agent 调整计划(跳过或找替代)证据
- 证据等级:生产验证(
validated-in-production),来自 Dexter Horthy(HumanLayer)。 - 学术支撑:Beurer-Kellner 等人(2025)把审批系统当成安全模式做学术处理,包括"提议和执行分离"。
怎么用
什么时候用:
- 生产数据库操作(DELETE、DROP、ALTER)。
- 有副作用的外部 API 调用(支付、邮件、webhook)。
- 系统配置变更(防火墙规则、权限)。
- 破坏性文件操作(批量删除、覆盖)。
- 合规敏感操作(GDPR、HIPAA、SOC2)。
实现示例:
装饰器模式(HumanLayer):
from humanlayer import HumanLayer
hl = HumanLayer()
@hl.require_approval(channel="slack")
def delete_user_data(user_id: str):
"""删除用户所有数据,需要审批"""
return db.users.delete(user_id)
# Agent 正常调用函数
delete_user_data("user_123")
# 执行暂停,审批请求发到 Slack
# 人工批准/拒绝后恢复中断模式(LangGraph):
from langgraph.types import interrupt
def risky_operation(state):
approval = interrupt({
"question": "要我继续这个操作吗?",
"operation": state["message"]
})
return {"user_approval": approval}
# 用 checkpointer 编译,保留状态
app = workflow.compile(checkpointer=MemorySaver())配审批渠道:
approval_channels:
high_risk:
slack: "#agent-approvals"
sms: "+1234567890" # 紧急用
medium_risk:
slack: "#agent-review"
low_risk:
email: "team@company.com"审批请求要带的上下文: 为什么需要这个操作?会影响哪些数据?可逆吗?有什么替代方案?
取舍
- 好处:让高风险操作能安全自主执行;在最要紧的地方保留人工监督;集成轻量(Slack 按钮,不是复杂 UI);审计轨迹支持合规和调试;减少 Agent 对犯错的不安;信任可以随时间逐步扩大。
- 代价:要求人类随时在线、响应及时;审批慢会卡住 Agent 工作流;基础设施复杂(通知系统、状态管理);有审批疲劳导致"橡皮图章"的风险;要清楚划分什么需要审批;频繁请求会打断人的专注。
模式二:精确动作授权绑定(Exact-Action Authorization Binding)
问题
审批系统常让人或策略服务批准一个提议的工具调用,然后稍后再执行。这中间,请求可能被重建、转换、重试、委派、或从存储状态恢复。于是一个看起来有效的批准,可能被用在一个实质不同的动作上:不同的接收者、目标、操作、载荷、执行者、执行面、或策略版本。
普通审计日志显示"批准了什么"和"执行了什么",但事后对比阻止不了不匹配。工具级白名单和人工审批门决定"动作能不能进行",但除非执行边界验证的是"被批准的精确动作",否则它们关不上这个"批准到执行"的 TOCTOU(时间检查到时间使用)缺口。
方案
把授权绑定到"提交审批的完整授权相关表示"的确定性身份上,并要求执行适配器在执行前立即重新推导这个身份。
- 定义授权相关字段:任务、批准人、执行者、执行面、操作、解析后的目标身份、参数、治理策略、过期时间、nonce 或一次性标识。
- 用文档化的 profile 规范化这个表示。算一个带版本、抗碰撞、域分离的摘要,把 profile 标识绑定进授权。
- 签发认证过的短期授权信封。签名或 MAC、受保护的不透明能力、或可信事务查找,必须绑定签发者、摘要、profile、过期时间、nonce、策略、预期强制面。
- 执行边界处,认证信封,从适配器实际要消费的不可变值重建动作。独立规范化并比对摘要。
- 执行前原子地 compare-and-consume 一次性授权。不匹配、过期、重放、完整性失败都拒绝。比对之后任何实质性转换都要新授权。
- 把授权匹配、执行尝试、独立观察到的外部效果记录成三条独立声明。摘要相等只关联记录,不证明执行发生过或成功过。
profile = "example.action-binding/v1+sha256"
approved = canonicalize(profile, {
task_id, actor, acting_surface, operation,
target, arguments, policy_id, expires_at, nonce
})
authorization = authenticated_approve(
domain_digest(profile, approved), profile, expires_at, nonce
)
# 稍后,在真正执行动作的适配器里:
verify_envelope_integrity_and_issuer(authorization)
presented = canonicalize(profile, immutable_values_used_by_adapter())
if domain_digest(profile, presented) != authorization.action_digest:
deny("被批准的动作变了")
atomic_compare_and_consume(authorization) # 过期、重放、竞态都失败
result = execute(presented)
record_attempt(authorization.id, result)适配器是强制点。 一条"只执行被批准的"的提示词指令不是这个模式,因为同一个模型可以重新解释批准和动作两者。适配器原子消费后崩溃,用幂等键或权威状态查找来对账;否则把效果报成"未知",而不是盲目重放。
证据
- 证据等级:混合(
mixed)。 - 有价值发现:NOA Action Digest 定义了从授权记录、参数承诺、执行授权、一次性 nonce 推导的尝试级关联构造,同时明确说它不定义执行绑定,相等也不证明执行或效果;SCITT AI-agent 动作收据 profile 把签发者认证的动作记录和先前授权、控制器报告结果、外部世界效果声明分开;Google AIP-151 支持稳定操作句柄供后续状态查询,Kubernetes API 约定区分期望规格和观察状态。
- 未验证:失败关闭的适配器比对是这个模式的综合,不是被引用规范保证的;对比生产证据还没建立。安全依赖认证授权、抗碰撞域分离规范化、原子消费、比对后的不可变值、完整中介。
怎么用
- 用在"批准后漂移会出事"的动作上:支付、消息、部署、权限变更、破坏性操作、外部可见承诺。
- 每个能改变含义或爆炸半径的字段都进身份。只哈希工具名、把接收者或参数留在身份外,是"审批表演"。批准前解析别名,避免事后解析改变资源。
- 用文档化的规范化 profile。拒绝重复键、不支持的数值形式、歧义路径、未知字段,而不是在服务间各按各的归一化。
- 授权保持短时有效、最好一次性。认证它,绑定到执行面和策略版本,防止通过更宽的适配器重放。
- 让替代执行路径也走同一个验证点。一个不经中介的 CLI、浏览器、回退 API 都能绕过本来正确的契约。
- 刻意测突变和竞态:改一个目标、参数、执行者、操作、策略、过期时间或 nonce;再让两个 worker 消费同一个授权。证明重复执行前被拒绝。
- 匹配尝试后,通过适当可信的状态查询、稳定句柄或独立读回验证声明的成功标准。观察无法解决结果时报告
unknown,别拿授权摘要当成功证据。
取舍
- 好处:在正确中介的路径上检测并拒绝"提交字段漂移";让审批范围机器可检查;把强制定位在执行边界;改善批准、尝试、观察记录之间的关联。
- 代价:要认证信封、严格规范化、原子消费、崩溃对账、每条执行路径都中介;变更请求要再走一次审批;别名和比对后未检查的转换仍能打败幼稚实现。
- 局限:摘要是完整性身份,不是认证、授权或执行证明。被攻破的批准人、适配器、规范化器或签名密钥仍然危险。这个模式不建立"提供的字段为真""动作明智""外部效果发生"。
模式三:策略门控工具代理(Policy-Gated Tool Proxy)
问题
AI Agent 通过 MCP 这类协议调用外部工具(数据库、API、文件系统)。一旦 Agent 能访问工具服务器,它就能带任何参数调任何工具。 "Agent 决定调这个工具"和"工具执行了"之间没有任何强制层。这造成三个缺口:
- 没有访问控制:Agent 能调不该调的工具(比如生产库删除,而它只被授权读)。
- 没有审计轨迹:出事时,没有记录"哪些 Agent 用什么参数调了哪些工具"。
- 没有策略强制:合规规则(数据驻留、PII 处理、限流)没法在工具调用边界强制。
方案
在 Agent 和工具服务器之间放一个透明代理。 代理拦截每次工具调用,用策略引擎评估,然后放行、阻止、或转人工审批流程。每个决策都记录进追加式审计轨迹。
核心组件:
- 代理层:坐在 Agent 和工具服务器之间,两侧说同样的协议(MCP 进、MCP 出)。Agent 不知道自己在对代理说话。
- 策略引擎:对照声明式规则评估每次工具调用。规则可以匹配工具名、参数值、调用者身份、时间、限流、自定义谓词。
- 决策结果:允许、拒绝、需要审批(转人工)、转换(转发前修改参数)。
- 审计日志:每次工具调用、策略评估结果、执行结果的追加式记录。理想情况哈希链起来,防篡改。
Agent → 代理: call_tool(name, args)
代理 → 策略引擎: evaluate(tool, args, caller)
允许: 策略引擎→ALLOW → 代理转发 → 工具返回 → 记录 → Agent 拿结果
拒绝: 策略引擎→DENY(原因) → 记录 → Agent 收到策略违规错误
需要审批: 策略引擎→REQUIRE_APPROVAL → 代理请求人批准 → 批准则转发,拒绝则回绝证据
- 证据等级:新兴(
emerging),来自 SidClaw 团队。 - 学术基础:Beurer-Kellner 等人(2025)形式化了 Agent 安全里的"提议和执行分离"。
怎么用
什么时候用:
- 多 Agent 系统,不同 Agent 需要不同工具权限。
- 受监管环境(金融、医疗)要求审计轨迹。
- 生产部署里某些工具调用要人签字。
- 任何"Agent 有 MCP 访问权"这个粒度太粗的场景。
实现方式:
- 独立代理进程:作为单独服务跑。Agent 连代理,代理连工具服务器。和任何 Agent 框架兼容。
- 中间件/包装:包工具服务器 SDK。基础设施少,但耦合特定语言或框架。
- 网关模式:一个代理前置多个工具服务器,跨所有工具提供统一策略和审计。
策略规则示例:
rules:
- match: { tool: "database_query", args.query: "DELETE *" }
action: deny
reason: "批量删除需要人工执行"
- match: { tool: "send_email", rate: "> 10/hour" }
action: deny
reason: "超过邮件限流"
- match: { tool: "deploy_production" }
action: require_approval
channel: slack取舍
- 好处:对 Agent 和工具服务器都透明,两边都不用改代码;策略声明式、可审计,不埋在 Agent 提示词里;哈希链审计日志提供防篡改的合规记录;能在边界强制限流、数据驻留、PII 规则;和其它模式(人在回路、可观测性)可组合。
- 代价:每次工具调用加延迟(策略评估 + 日志);策略规则要随工具和需求演进维护;人工审批流程引入阻塞等待和可用性依赖;抓不住跨多次工具调用的策略违规(那需要更上层的编排);代理要和它前置的工具服务器一样可用。
模式四:沙箱授权(Sandboxed Tool Authorization)
问题
工具授权既要灵活又要安全。静态白名单在多个维度上撑不住:
- 多环境:开发(宽松)vs 生产(严格)。
- 不同 Agent 角色:编码 Agent 需要文件系统访问,消息 Agent 不该有。
- 层级委派:子 Agent 应该继承父级限制,但加更多约束。
- 插件生态:外部工具要动态纳入,不能手动更新白名单。
Agent 需要一个支持模式匹配、默认拒绝语义、层级继承的策略系统。
方案
基于模式匹配的策略 + 默认拒绝 + 继承。 工具通过匹配编译后的模式(精确、正则、通配)授权,拒绝列表优先于允许列表。子 Agent 继承父策略并加额外限制,基于 profile 的层级给常见 Agent 类型提供预设。
这和学术上的 Action Selector 模式(Beurer-Kellner 等人,2025)一致:把 LLM 当指令解码器而不是活体控制器,执行前用严格 schema 验证工具参数,防止工具输出不经额外验证就重新进入选择器提示词。
核心概念:
- 模式匹配:精确匹配(
exec)、通配(fs:*)、正则式(*test*)。 - 默认拒绝:空允许列表拒绝所有工具;显式允许列表只允许匹配的工具。
- 拒绝优先:先算拒绝列表,匹配拒绝模式的工具不管允许列表都拦。
- 相关工具继承:某些工具隐式授予相关权限(比如
exec允许apply_patch)。 - 层级策略继承:子 Agent 策略继承父级,加额外拒绝规则。
- Profile 分层:预定义 profile(
minimal、coding、messaging、full)快速配置。
function makeToolPolicyMatcher(policy: ToolPolicy) {
const deny = compilePatterns(policy.deny);
const allow = compilePatterns(policy.allow);
return (name: string) => {
const normalized = normalizeToolName(name);
// 拒绝优先
if (matchesAny(normalized, deny)) return false;
// 空允许 = 全放行(默认拒绝由调用方处理)
if (allow.length === 0) return true;
// 需要显式允许
if (matchesAny(normalized, allow)) return true;
// 相关工具继承
if (normalized === "apply_patch" && matchesAny("exec", allow)) return true;
return false;
};
}Profile 预设:
const TOOL_PROFILES = {
minimal: { allow: ["session_status"] }, // 最少
coding: { allow: ["group:fs", "group:runtime", "group:sessions", "group:memory", "image"] },
messaging: { allow: ["group:messaging", "sessions_list", "sessions_history", "sessions_send", "session_status"] },
full: {}, // 空策略 = 全放行
};子 Agent 策略继承:
const DEFAULT_SUBAGENT_TOOL_DENY = [
// 会话管理,主 Agent 管
"sessions_list", "sessions_history", "sessions_send", "sessions_spawn",
// 系统管理,子 Agent 调太危险
"gateway", "agents_list",
// 状态/调度,主 Agent 协调
"session_status", "cron",
];
function resolveSubagentToolPolicy(config) {
const deny = [...DEFAULT_SUBAGENT_TOOL_DENY, ...(config?.deny ?? [])];
return { allow: config?.allow, deny };
}证据
- 证据等级:生产验证(
validated-in-production),来自 Clawdbot。 - 学术基础:Beurer-Kellner 等人的 Action Selector 模式。
怎么用
- 定义工具组:把相关工具分组(
group:fs、group:runtime),批量写策略。 - 选 profile:拿预定义 profile(
minimal、coding、messaging、full)当基线。 - 加显式规则:在 profile 上叠加 allow/deny 规则。
- 配子 Agent 限制:给派生的 Agent 定义额外 deny 规则。
- 运行时过滤:用策略匹配器在把工具交给 Agent 前先过滤一遍。
要避开的坑:
- 过宽的模式:
*这种通配可能意外授予过多权限,偏好具体模式。 - 漏了拒绝优先:总是先算 deny 再算 allow,否则 allow 规则能绕过安全意图。
- 忘了相关工具:允许了
exec,记得apply_patch也该放(它是文件操作)。 - 继承混淆:子 Agent 策略是在父策略之上加限制,不是整个替换。
取舍
- 好处:模式灵活,通配和分组让大工具集策略写得很简洁;默认拒绝,防止意外授权;层级控制,子 Agent 不用改父策略就能进一步限制;profile 预设,常见 Agent 类型开箱即用;插件支持,工具组能通过动态发现纳入插件工具。
- 代价:模式复杂,正则式可能让人困惑,语法错误可能授予意外访问;策略爆炸,很多 Agent 各自策略难以管理和审计;求值顺序必须一致(deny 先于 allow),bug 会引发安全问题;"哪些工具相关"(比如
exec→apply_patch)是主观判断,可能覆盖不全。
四个模式怎么选
| 场景 | 推荐模式 |
|---|---|
| 高风险动作要人点头 | 人在回路审批 |
| 审批后可能被篡改,要精确绑定 | 精确动作授权绑定 |
| 多 Agent / 受监管环境,要统一门禁 + 审计 | 策略门控工具代理 |
| 要灵活的策略 + 默认拒绝 + 层级继承 | 沙箱授权 |
四个模式是权限与审批的四件套:人在回路解决"谁点头",精确绑定解决"批的和干的必须是一个",策略代理解决"统一门禁 + 审计",沙箱授权解决"策略怎么写得灵活又安全"。通常组合用:沙箱授权定策略,策略代理做强制,人在回路补高风险,精确绑定堵篡改。
实践清单
- 高风险函数(DB 删改、生产部署、破坏性操作)插人工审批门,默认拒绝超时
- 审批请求带足上下文:为什么、影响什么、可逆吗、有什么替代
- 审批要绑定到完整动作(目标/参数/执行者/策略/过期/nonce),执行前重新推导比对
- 批准后动作字段变了就拒绝,原子消费防重放,比对后转换要重新授权
- Agent 和工具之间插策略代理:允许/拒绝/需审批/转换四种结果,全记录
- 策略代理审计日志哈希链防篡改,跨所有工具统一策略
- 工具授权默认拒绝,deny 优先于 allow,用模式(精确/通配/正则)和分组写策略
- 子 Agent 继承父策略 + 额外 deny,别整个替换
- 让所有替代执行路径(CLI、浏览器、回退 API)也走同一个验证点
本章小结
- 人在回路审批:高风险函数插审批门,Slack/邮件/SMS 多渠道,审计轨迹完整。
- 精确动作授权绑定:批准绑定到完整动作的确定性身份,执行前重新比对,TOCTOU 缺口关闭。
- 策略门控工具代理:透明代理 + 策略引擎 + 哈希链审计,统一强制和记录。
- 沙箱授权:模式匹配 + 默认拒绝 + 层级继承 + profile 预设,策略灵活又安全。
- 权限与审批把"Agent 能干什么"从拍脑袋变成可配置、可审计、可强制。
下一章讲凭据与出口:Agent 手里的钥匙和门。本地优先凭据代理、非托管支出控制、出口封锁、零知识出口。