假设 Agent 准备执行下面这个动作:
工具:send_customer_message
工单:T-102
正文:退款申请已进入人工审核
Reviewer 看过预览后点击“批准”。就在工作流恢复前,Agent 又生成了一版语气更强的正文。如果系统只保存了一个 approved = true,新正文也可能带着旧批准被发出去。
这不是 Prompt 写得不够谨慎,而是执行边界没有设计完整:人批准的究竟是一个模糊意图,还是某个工具、某个资源和一组确定参数组成的动作?
Human Control 的目标也不是让人确认每一条命令。它要让系统在风险真正变化的位置暂停,把决定交给有权负责的人;恢复时再证明即将执行的仍是刚才批准的动作。
本文是 AI Agent 工程进阶 第 10 篇。上一篇 Memory Engineering 讨论 Agent 能跨任务保存什么;这一篇把 Context、Tool、Trace、Checkpoint 和 Memory 中分散的安全约束,整理成一条可以运行和测试的控制链。
| 项目 | 说明 |
|---|---|
| 内容类型 | Agent 权限、审批状态机与可复现实验 |
| 适合读者 | 已能运行 Tool-calling Agent,准备接入外部写操作的开发者 |
| 阅读时间 | 约 14-18 分钟 |
| 跟做时间 | 30-45 分钟 |
| 环境要求 | Python 3.10+,零第三方依赖,不需要 API Key |
| 代码检查点 | b0e5315 |
| 实验下载 | agent-security-lab-1.0.0.zip |
| SHA-256 | AE52CC8ABA0090A1961FCB2E05DA9A050683C52F9FC072DB696A56488D1A3E3E |
| 资料核对日期 | 2026-08-07 |
| 实验边界 | 确定性权限策略与副作用夹具;不是渗透测试、IAM 审计或模型安全评测 |
一分钟概览
先记住八个结论:
- 先由工具注册表和业务规则分级,不能让模型临场决定自己的风险。
- 只读、可逆写入、外部发送和关键操作,不应该走同一条控制路径。
- Sandbox、Approval 和 IAM 分别回答“技术上能访问什么”“这一次是否允许”“后端最终允许什么”。
- 批准必须绑定精确动作。 工具、资源、参数、环境或策略变化,都应使旧批准失效。
- 暂停状态要先持久化,再等待人。 否则进程重启后无法判断动作是否已批准、是否已执行。
- 恢复不是继续往下跑,而是重新校验。 检查过期时间、Reviewer 角色、身份新鲜度和动作指纹后,才获取短期凭据。
- 拒绝、过期和取消都是正常终态。 它们不应被当成错误自动重试。
- 审批通过不代表系统安全。 后端仍要做鉴权、幂等、审计和资源边界检查。
图 1:Agent 动作先经过确定性策略闸门;需要审批时,系统把工具、资源、参数指纹、角色和有效期绑定成 Approval Envelope
图 1:Approval 不是一个脱离上下文的按钮。批准分支仍要经过可信执行器复检,拒绝分支必须以零外部副作用停止。封面动画仅用于表现状态流动。
1. 先给动作分级,再谈审批
最容易想到的安全策略是“凡是工具调用都问人”。它看似保守,实际会制造 Approval Fatigue:Reviewer 反复确认低风险动作,最后习惯性点击允许,真正危险的请求反而混在噪声里。
更可用的起点是按最坏可信后果给工具分级:
| 等级 | 典型动作 | 默认路径 | 关键限制 |
|---|---|---|---|
| 只读查询 | 查工单、搜索文档、读构建结果 | 自动执行并留 Trace | 限制租户、字段、网络目标和数据量 |
| 可逆写入 | 加标签、保存草稿、写临时分支 | 策略内自动,或批量审批 | 明确作用域、回滚动作和幂等键 |
| 外部发送 | 发邮件、回客户、公开发布、创建 PR | 展示精确预览并暂停 | 绑定收件人、正文哈希和有效期 |
| 关键操作 | 生产发布、付款、删除、修改 IAM | 指定角色强审批 | 最小权限、短期身份、完整审计 |
图 2:四类 Agent 动作对应自动执行、策略加回滚、人工预览和强审批四条路径
图 2:分级由 Tool Policy Registry 和业务策略定义,不由 Agent 自报。“模型认为风险低”不能成为授予执行权的依据。
这里有三个经常被忽略的细节。
1.1 只读不等于无风险
lookup_ticket 不会修改数据,但仍可能跨租户读取工单、一次导出过多记录,或把结果带入不该出现的 Context。只读动作可以少一道人工审批,不能少数据范围和审计。
1.2 可逆必须有真正的回滚契约
“理论上能改回来”不算可逆。系统至少要知道:
原动作影响了哪个资源
回滚工具是什么
回滚需要哪些原值或回执
重复回滚是否安全
回滚窗口有多长
本篇 Lab 对 update_ticket_label 要求提供同一张工单的 rollback;缺失或指向其他资源都会被拒绝。
1.3 风险属于能力,不属于一句 Prompt
工具的风险等级、允许环境、所需角色和凭据范围应进入版本化注册表。模型只负责提出动作,不应同时充当申请人、风险判定者和授权者。
2. 三层控制,分别回答三个问题
安全边界经常被压缩成一个 allow / deny 开关,随后出现两个误区:开了 Sandbox 就不需要审批,或者有人批准就可以放开系统权限。两者都不成立。
图 3:Sandbox、Policy 与 Approval、IAM 与 Trusted Executor 组成三层独立控制
图 3:任何一层拒绝,动作都必须停下;任何一层给出的“允许”,都不能替其他层授权。Prompt 和 Guardrail 可以帮助识别问题,但不是可执行权限边界。
2.1 Sandbox:运行时技术上能碰到什么
Sandbox 限制文件可写目录、网络目标、进程、挂载和系统资源。Codex 官方把 Sandbox 与 Approval 分开描述:前者约束技术能力,后者决定何时需要用户许可;默认配置会限制工作目录外写入和网络访问。
Claude Code 的官方文档也明确区分 permissions 与 OS 级 sandbox。仅在权限规则里禁止文件读取,并不必然阻止另一个 shell 工具绕路访问;真正的文件与网络隔离要由 Sandbox 承担。
2.2 Policy + Approval:这一份动作是否获准
Policy Registry 先根据工具和业务环境决定:
send_customer_message:
risk: external
approval_required: true
required_role: reviewer
ttl_seconds: 300
需要审批时,系统不执行工具,而是把 ActionProposal 与 Approval Envelope 一起持久化。Reviewer 看到的预览必须来自这份 Proposal,并确认其指纹仍与 Envelope 一致,而不是让模型另写一段摘要代替原参数。
2.3 IAM + Trusted Executor:后端最终允许什么
Agent 运行时不应长期持有生产 Token。审批通过后,由可信执行器重新校验策略,再从 Credential Broker 获取短时、最小 scope 的凭据。外部系统仍要根据身份、资源和环境独立拒绝越权请求。
这也解释了为什么 Guardrail 不能替代授权。Guardrail 可以检查文本是否包含敏感信息、参数是否满足格式,但“检查通过”不等于调用者获得了生产权限。
3. Approval Envelope:人究竟批准了什么
一个够用的 Approval Envelope 至少要绑定这些字段:
{
"approval_id": "approval-4f18...",
"action_id": "act-send-1",
"tool": "send_customer_message",
"resource": "ticket:T-102",
"environment": "support",
"arguments_hash": "7ac9...e12b",
"action_fingerprint": "4f18...92cd",
"requester": "agent",
"required_role": "reviewer",
"requested_at": 100,
"expires_at": 400
}
Lab 用规范化 JSON 计算动作指纹:
def action_fingerprint(action: ActionProposal) -> str:
payload = {
"action_id": action.action_id,
"tool": action.tool,
"resource": action.resource,
"arguments": action.arguments,
"environment": action.environment,
"requester": action.requester,
}
encoded = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
如果正文、收件人、工单号、环境或工具发生变化,恢复阶段计算出的指纹就不同,状态进入 blocked / action_changed。系统应重新生成 Proposal 和 Approval,而不是沿用旧批准。
这一步在防一种典型的时间差问题:检查时是动作 A,使用批准时却变成动作 B。 哈希并不能解决所有攻击,但能让“批准对象不可变”成为可测试契约。
生产实现还要多做一步
Lab 把待审批动作完整保存在受信任的 Checkpoint 中,公开报告只输出哈希和脱敏字段。真实系统应对 Checkpoint 加密、限制访问并设置保留期;分布式审批接口还应显式传递
run_id、approval_id和版本号,避免把决定恢复到错误运行。
4. Pause / Resume 是状态机,不是等待一个布尔值
安全的审批路径可以压缩成八步:
1. Agent 提出 ActionProposal
2. Policy Registry 确定风险和控制路径
3. 生成 ApprovalEnvelope
4. 先持久化 waiting_approval,再向人展示预览
5. 保存 approve / reject / expire / cancel 终态
6. Resume 时重新校验动作、策略、角色和身份新鲜度
7. Trusted Executor 获取短时凭据,幂等执行
8. 保存外部回执,再完成运行
其中最重要的不是第 5 步,而是第 4、6、7 步。
4.1 副作用不能发生在暂停之前
如果节点先发消息再调用 interrupt(),人工看到的不是待批准动作,而是一份已经发生的通知。LangGraph 官方特别提醒:节点恢复时会从节点开头重新执行,放在 interrupt 前的副作用必须是幂等的,或移到单独节点。
4.2 Reviewer 不能等于 Requester
同一身份既申请又批准,会把双人控制退化成装饰。关键操作还应校验角色和身份新鲜度,例如只有 release_manager 能批准生产发布,并要求最近 5 分钟内完成过强身份验证。
4.3 拒绝和过期不能自动换一种说法重试
Reviewer 拒绝了发送动作,Agent 不应通过改写正文、换工具或等待一会儿自动重试。正确处理是终止当前 Proposal;如果业务仍需要继续,由新的证据产生新的 Proposal 和新的审批记录。
4.4 Resume 必须幂等
网络超时可能让同一条“批准”被提交两次。执行器应以稳定 action_id + fingerprint 查询回执:第一次产生副作用,后续恢复只重放 receipt。Lab 的重复恢复案例执行两次,外部 mutation count 仍为 1。
5. 跑一遍 Security Lab
实验代码位于 ai-agent-learn 的 agent-reliability-lab。它不调用模型,专门把权限契约变成可重复的确定性测试。
下载本文附件后运行:
Expand-Archive .\agent-security-lab-1.0.0.zip -DestinationPath .\security-lab
Set-Location .\security-lab\agent-security-lab-1.0.0
python run_lab.py policy-test --output reports/local
python -m unittest discover -s tests -v
预期结果:
Policy test: PASS
Matched outcomes: 8/8
Release checks: 8/8
Ran 100 tests
OK
这里的 100 个测试是整个 agent-reliability-lab 的回归套件,其中 15 个直接覆盖 Security 模块;policy-test 则只评估本篇的 8 个控制案例。任一结果不符合声明时,命令会返回非零退出码,并把差异写入 reports/local/security-failures.md。
图 4:Security Lab 验证八种控制结果和八条发布检查
图 4:PASS 只证明当前确定性夹具满足声明的审批契约。它不代表完成了渗透测试、生产 IAM 审计或模型安全评测。
5.1 八个案例分别在防什么
| 案例 | 预期结果 | 它证明的边界 |
|---|---|---|
read-only-auto | 自动完成,0 写入 | 低风险动作不制造审批噪声 |
reversible-policy-allowed | 完成,1 次写入 | 可逆动作必须带有效 rollback |
external-approved-once | 批准后完成 | 外部发送先审批再执行 |
external-rejected | 拒绝,0 副作用 | 拒绝是终态 |
approval-expired | 过期,0 副作用 | 旧决定不能无限期复用 |
arguments-changed-after-approval | 阻断,0 副作用 | 批准绑定精确参数 |
critical-wrong-reviewer | 拒绝,0 副作用 | 关键动作校验角色 |
duplicate-approved-resume | 完成,1 次副作用 | 重复恢复只重放回执 |
可以直接阅读四个入口:
5.2 两个十分钟故障练习
练习一:让过期批准错误地通过。
打开 datasets/security-cases.jsonl,把 approval-expired 的 decision.at 从 121 改成 119,但保留期望状态 expired。再次执行 policy-test,Release Gate 应失败。它说明测试不是只看程序能否运行,而是在比对声明的控制结果。
练习二:改变已批准参数。
在 arguments-changed-after-approval 中,把 resume_action.arguments.message 改回原文。期望仍保留 blocked,测试应失败。再把期望改为 completed / approved / 1,观察动作为什么恢复为一次执行。
完成后用 Git 恢复夹具,确认 8/8 重新通过。
Lab 没有证明什么
它没有真实 LLM、云端 IAM、Secret Manager、多租户后端或恶意输入,也没有模拟 Reviewer 账号被盗。
reviewer这样的字符串只是教学用角色。生产系统还需要身份提供商、短时凭据、资源级鉴权、速率限制、告警、渗透测试和事件响应。
6. 映射到 Codex、OpenAI Agents SDK、Claude 与 LangGraph
这些框架提供的原语不同,但工程问题相通。
| 平台 | 可用原语 | 仍需自己定义的部分 |
|---|---|---|
| Codex | Sandbox、Approval、网络与目录边界 | 业务动作分级、Reviewer 角色、外部系统 IAM |
| OpenAI Agents SDK | needs_approval、interruptions、RunState 恢复 | Approval Envelope、策略注册表、业务审计与凭据代理 |
| Claude Code | deny / ask / allow permissions、Sandbox | 业务资源范围、审批职责分离、后端鉴权 |
| LangGraph | interrupt()、checkpointer、thread ID、resume | 风险策略、身份系统、副作用幂等和审批 UI |
OpenAI Agents SDK 的 HITL 流程会在工具需要批准时返回 interruption,并通过保存的 RunState 继续同一次运行。这解决了“如何暂停与恢复”的框架问题,不会自动替你决定“谁可以批准付款、批准绑定哪些参数”。
同理,Codex 或 Claude Code 弹出一次 permission prompt,表示当前客户端根据配置请求许可;业务系统仍然要验证后端身份和资源权限。不要把界面上的 Allow 当成生产系统的授权证明。
7. 一份可复用的 Tool Policy 模板
给新工具接入 Agent 前,可以先填写这份模板:
tool: send_customer_message
owner: support-platform
risk: external
scope:
environments: [support]
resources: [ticket]
allowed_fields: [ticket_id, message]
approval:
required: true
reviewer_role: reviewer
ttl_seconds: 300
separation_of_duties: true
bind: [tool, resource, arguments, environment, requester]
execution:
credential_scope: message:send
idempotency_key: action_id
receipt_required: true
timeout_seconds: 15
audit:
redact: [credential, message]
retain_days: 30
failure:
reject_is_terminal: true
expire_is_terminal: true
retry_requires_new_proposal: true
真正评审时,再追问七个问题:
- 这个工具最坏能造成什么后果,风险由谁维护?
- 只读范围是否限制到租户、资源、字段和数量?
- 可逆写入是否真的有可测试的回滚和幂等键?
- Reviewer 看到的是精确动作,还是模型生成的模糊摘要?
- 参数、环境、策略或身份变化后,旧批准会不会自动失效?
- 凭据是在 Agent Context 里,还是只在可信执行器中短暂出现?
- 拒绝、过期、进程重启和重复恢复是否都能保持零额外副作用?
如果其中任何一项只能回答“Prompt 会提醒模型注意”,控制链还没有闭合。
收藏清单
读完本文,可以把下面这组最小实现带走:
[ ] Tool Policy Registry 由代码和业务规则维护
[ ] 四级动作对应不同控制路径
[ ] Sandbox、Approval、IAM 三层独立生效
[ ] Approval Envelope 绑定精确动作与有效期
[ ] waiting_approval 在展示审批前持久化
[ ] Resume 重新检查指纹、策略、角色与身份
[ ] 凭据只在 Trusted Executor 注入
[ ] side effect 使用稳定 action_id 幂等
[ ] reject / expire / cancel 以零副作用终止
[ ] Checkpoint 受保护,公开 Audit 默认脱敏
Human Control 的判断标准不是“页面上有没有审批按钮”,而是:当人说允许时,系统能否证明执行的是他刚刚看到的那一个动作;当人说不允许时,系统能否证明没有发生外部副作用。
下一篇进入 Agent Production Loop:当权限边界已经建立,怎样继续管理生产信号、降级、Canary 与回滚,并把确认后的事故沉淀成下一版回归评估。