已发布资料基础官方资料 + 个人实践
·9 分钟0 次阅读AI Agent 工程进阶
文章/AI 与智能体

Human Control:Agent 的权限、审批与执行边界

AI Agent 工程进阶第 10 篇:用动作分级、Sandbox、Approval Envelope、IAM 与可恢复状态机建立 Human Control,并通过零依赖 Security Lab 验证拒绝、过期、参数漂移和重复恢复等边界。

#Agent Security#Human-in-the-loop#Approval#Sandbox#Codex
文章目录
  1. 一分钟概览
  2. 1. 先给动作分级,再谈审批
  3. 1.1 只读不等于无风险
  4. 1.2 可逆必须有真正的回滚契约
  5. 1.3 风险属于能力,不属于一句 Prompt
  6. 2. 三层控制,分别回答三个问题
  7. 2.1 Sandbox:运行时技术上能碰到什么
  8. 2.2 Policy + Approval:这一份动作是否获准
  9. 2.3 IAM + Trusted Executor:后端最终允许什么
  10. 3. Approval Envelope:人究竟批准了什么
  11. 4. Pause / Resume 是状态机,不是等待一个布尔值
  12. 4.1 副作用不能发生在暂停之前
  13. 4.2 Reviewer 不能等于 Requester
  14. 4.3 拒绝和过期不能自动换一种说法重试
  15. 4.4 Resume 必须幂等
  16. 5. 跑一遍 Security Lab
  17. 5.1 八个案例分别在防什么
  18. 5.2 两个十分钟故障练习
  19. 6. 映射到 Codex、OpenAI Agents SDK、Claude 与 LangGraph
  20. 7. 一份可复用的 Tool Policy 模板
  21. 收藏清单
  22. 参考资料

假设 Agent 准备执行下面这个动作:

text
工具: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-256AE52CC8ABA0090A1961FCB2E05DA9A050683C52F9FC072DB696A56488D1A3E3E
资料核对日期2026-08-07
实验边界确定性权限策略与副作用夹具;不是渗透测试、IAM 审计或模型安全评测

一分钟概览

先记住八个结论:

  1. 先由工具注册表和业务规则分级,不能让模型临场决定自己的风险。
  2. 只读、可逆写入、外部发送和关键操作,不应该走同一条控制路径。
  3. Sandbox、Approval 和 IAM 分别回答“技术上能访问什么”“这一次是否允许”“后端最终允许什么”。
  4. 批准必须绑定精确动作。 工具、资源、参数、环境或策略变化,都应使旧批准失效。
  5. 暂停状态要先持久化,再等待人。 否则进程重启后无法判断动作是否已批准、是否已执行。
  6. 恢复不是继续往下跑,而是重新校验。 检查过期时间、Reviewer 角色、身份新鲜度和动作指纹后,才获取短期凭据。
  7. 拒绝、过期和取消都是正常终态。 它们不应被当成错误自动重试。
  8. 审批通过不代表系统安全。 后端仍要做鉴权、幂等、审计和资源边界检查。

图 1:Agent 动作先经过确定性策略闸门;需要审批时,系统把工具、资源、参数指纹、角色和有效期绑定成 Approval Envelope图 1:Agent 动作先经过确定性策略闸门;需要审批时,系统把工具、资源、参数指纹、角色和有效期绑定成 Approval Envelope

图 1:Approval 不是一个脱离上下文的按钮。批准分支仍要经过可信执行器复检,拒绝分支必须以零外部副作用停止。封面动画仅用于表现状态流动。

1. 先给动作分级,再谈审批

最容易想到的安全策略是“凡是工具调用都问人”。它看似保守,实际会制造 Approval Fatigue:Reviewer 反复确认低风险动作,最后习惯性点击允许,真正危险的请求反而混在噪声里。

更可用的起点是按最坏可信后果给工具分级:

等级典型动作默认路径关键限制
只读查询查工单、搜索文档、读构建结果自动执行并留 Trace限制租户、字段、网络目标和数据量
可逆写入加标签、保存草稿、写临时分支策略内自动,或批量审批明确作用域、回滚动作和幂等键
外部发送发邮件、回客户、公开发布、创建 PR展示精确预览并暂停绑定收件人、正文哈希和有效期
关键操作生产发布、付款、删除、修改 IAM指定角色强审批最小权限、短期身份、完整审计

图 2:四类 Agent 动作对应自动执行、策略加回滚、人工预览和强审批四条路径图 2:四类 Agent 动作对应自动执行、策略加回滚、人工预览和强审批四条路径

图 2:分级由 Tool Policy Registry 和业务策略定义,不由 Agent 自报。“模型认为风险低”不能成为授予执行权的依据。

这里有三个经常被忽略的细节。

1.1 只读不等于无风险

lookup_ticket 不会修改数据,但仍可能跨租户读取工单、一次导出过多记录,或把结果带入不该出现的 Context。只读动作可以少一道人工审批,不能少数据范围和审计。

1.2 可逆必须有真正的回滚契约

“理论上能改回来”不算可逆。系统至少要知道:

text
原动作影响了哪个资源
回滚工具是什么
回滚需要哪些原值或回执
重复回滚是否安全
回滚窗口有多长

本篇 Lab 对 update_ticket_label 要求提供同一张工单的 rollback;缺失或指向其他资源都会被拒绝。

1.3 风险属于能力,不属于一句 Prompt

工具的风险等级、允许环境、所需角色和凭据范围应进入版本化注册表。模型只负责提出动作,不应同时充当申请人、风险判定者和授权者。

2. 三层控制,分别回答三个问题

安全边界经常被压缩成一个 allow / deny 开关,随后出现两个误区:开了 Sandbox 就不需要审批,或者有人批准就可以放开系统权限。两者都不成立。

图 3:Sandbox、Policy 与 Approval、IAM 与 Trusted Executor 组成三层独立控制图 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 先根据工具和业务环境决定:

yaml
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 至少要绑定这些字段:

json
{
  "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 计算动作指纹:

python
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_idapproval_id 和版本号,避免把决定恢复到错误运行。

4. Pause / Resume 是状态机,不是等待一个布尔值

安全的审批路径可以压缩成八步:

text
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-learnagent-reliability-lab。它不调用模型,专门把权限契约变成可重复的确定性测试。

下载本文附件后运行:

powershell
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

预期结果:

text
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: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-expireddecision.at121 改成 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

这些框架提供的原语不同,但工程问题相通。

平台可用原语仍需自己定义的部分
CodexSandbox、Approval、网络与目录边界业务动作分级、Reviewer 角色、外部系统 IAM
OpenAI Agents SDKneeds_approval、interruptions、RunState 恢复Approval Envelope、策略注册表、业务审计与凭据代理
Claude Codedeny / ask / allow permissions、Sandbox业务资源范围、审批职责分离、后端鉴权
LangGraphinterrupt()、checkpointer、thread ID、resume风险策略、身份系统、副作用幂等和审批 UI

OpenAI Agents SDK 的 HITL 流程会在工具需要批准时返回 interruption,并通过保存的 RunState 继续同一次运行。这解决了“如何暂停与恢复”的框架问题,不会自动替你决定“谁可以批准付款、批准绑定哪些参数”。

同理,Codex 或 Claude Code 弹出一次 permission prompt,表示当前客户端根据配置请求许可;业务系统仍然要验证后端身份和资源权限。不要把界面上的 Allow 当成生产系统的授权证明。

7. 一份可复用的 Tool Policy 模板

给新工具接入 Agent 前,可以先填写这份模板:

yaml
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

真正评审时,再追问七个问题:

  1. 这个工具最坏能造成什么后果,风险由谁维护?
  2. 只读范围是否限制到租户、资源、字段和数量?
  3. 可逆写入是否真的有可测试的回滚和幂等键?
  4. Reviewer 看到的是精确动作,还是模型生成的模糊摘要?
  5. 参数、环境、策略或身份变化后,旧批准会不会自动失效?
  6. 凭据是在 Agent Context 里,还是只在可信执行器中短暂出现?
  7. 拒绝、过期、进程重启和重复恢复是否都能保持零额外副作用?

如果其中任何一项只能回答“Prompt 会提醒模型注意”,控制链还没有闭合。

收藏清单

读完本文,可以把下面这组最小实现带走:

text
[ ] 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 与回滚,并把确认后的事故沉淀成下一版回归评估。

参考资料

继续阅读