Agent 系统中的批准陷阱:为什么状态转移需要重新验证
揭示 Agent 重启后的关键设计缺陷:人类批准不是永久权限,而是对特定事实时刻的决策。延迟执行期间订单可能已退款、策略可能已变更、角色权限可能已失效。
完整中文译文
人类审批,是针对特定事实条件下某个操作所作出的决定,而不是一个永久有效的权限位。
假设一个 AI Agent 正在准备退款请求:
Order: SO-1001
Amount: CNY 199.00
Reason: Duplicate payment
运行时将该操作判定为高风险,于是暂停任务,并请求人工审批这笔具体请求。
10:00,审批人检查参数后点击 Approve。
任务并没有立即执行。它仍处于暂停状态,在队列中等待,经历了一次协调器重启,最终于 15:00 进入分发阶段。
在这五个小时里,以下任何情况都可能已经发生变化:
- 该订单可能已通过其他渠道完成退款;
- 退款政策现在可能要求增加一次财务审核;
- 审批人可能已经不再具备所需角色;
- 执行操作的主体可能已经离职;
- 任务重建过程中,金额或币种可能发生了偏移;
- 工具实现可能已经发生变化;
- 该审批可能只有 30 分钟的有效期。
系统是否应该仅仅因为数据库中仍有一行 approved = true,就继续执行?
这次审批并不是一项永久授权。它是针对某个具体操作作出的决定:由特定主体代表、使用特定 capability、携带特定参数,并且基于特定政策和一组具体业务事实。
当 Agent 不再只是回答问题,而是开始产生真实的业务后果时,这一区别就变得至关重要。
1. 危险的简化:approval = true
在传统管理软件中,审批与执行往往紧密相连。用户提交表单,经理批准,系统随后很快就会执行相应操作。
这种交互方式很容易让人形成一种过度简化的心智模型:
approval = true
一旦保存了这个值,下游代码就会将该操作视为获得了永久授权。
Agent 任务则有所不同。一个任务可能跨越多个异步边界:
understand intent
-> select a capability
-> construct arguments
-> request approval
-> wait for a human
-> resume the task
-> enter a dispatch queue
-> call the business system
-> record the outcome
整个生命周期可能持续几分钟、几小时,甚至几天。进程可能重启,政策可能重新部署,业务对象也可能通过其他渠道发生变化。
在这种环境中,“审批曾经发生过”只是一项历史事实。它无法证明这个操作现在仍然有效。
生产级设计至少必须区分五个概念:
将这五个概念全部压缩成一个 boolean,会掩盖最重要的故障模式。
2. 审批必须绑定具体操作,而不是模糊意图
像下面这样的审批 prompt 并不足够:
Approve the refund?
它没有告诉审批人:
- 哪个订单将被修改;
- 会转移多少资金;
- 涉及哪种币种;
- Agent 代表谁的权限;
- 将由哪个 capability 执行;
- 是哪个政策版本触发了审批要求;
- 该决定的有效期有多长;
- 任务是否可以执行多次。
一条有效的审批记录应该绑定一个具体的执行信封。根据保障级别,它可以包括:
trusted_subject
capability
canonical_arguments
task_identity
policy_version
approval_time
expiry_time
approver
保障要求更高的部署还可以绑定:
tenant
business_object_version
tool_or_server_artifact
request_purpose
delegation_context
并非所有实现都需要采用相同的表示形式。有些系统可能使用签名对象,有些使用持久化数据库记录,还有些使用外部审批系统。相比具体格式,不变量更重要:
系统必须能够证明,即将执行的操作仍然是当初经过审核的那个操作。
3. 参数哈希保护的是结构,而不是时间
一种常见的保护措施,是对参数进行规范化处理并保存其哈希值:
args_hash = SHA256(canonical_json(arguments))
分发之前,运行时会再次计算哈希。如果值不一致,就不能复用之前的审批。
这可以防止一类危险的参数漂移:
At approval:
order_id = SO-1001
amount = 199.00
At execution:
order_id = SO-1001
amount = 19900.00
但是,即使参数哈希完全一致,也不能证明当前执行仍然安全。
参数可能没有变化,但与此同时:
- 执行操作的主体已经失去访问权限;
- 审批已经过期;
- 当前适用的政策已经变化;
- 订单已经完成退款;
- capability 现在指向了不同的实现。
因此,参数相等是复用审批的必要条件,而不是充分条件。
这正是其中的核心区别:
Structural integrity:
Is this the same request?
Temporal validity:
Is the decision still applicable now?
一个健壮的系统必须同时满足两者。
4. 四种漂移可能使审批失效
审批的新鲜度不是一项单独的检查,而是一组由不同组件负责的检查。
capability、规范化参数、可信主体、tenant 或任务身份,已经与获得审批的执行信封不再匹配。
预期行为:不要分发。将同一个持久化任务重新置为需要审批的状态,或者在分发之前拒绝该任务。
4.2 主体与权限漂移
执行操作的主体或审批人,已经不再具备当前政策所要求的角色、成员资格、委托关系或权限。
预期行为:从权威系统重新解析可信身份和授权。绝不能信任由模型生成的身份字段。
4.3 政策与 capability 漂移
任务暂停期间,风险政策、审批阈值、路由 allowlist、capability 声明或工具实现发生了变化。
预期行为:将审批时的政策与 capability 上下文,同当前上下文进行比较。如果变化具有实质性影响,则必须重新作出决定。
4.4 业务对象漂移
订单、发票、账户、库存项、部署目标或其他业务对象,在审批后发生了变化。
预期行为:必须由业务系统,或者由基于同一权威数据的最新 preflight API,重新评估当前状态和最终权限。
其职责边界可以概括如下:
任何单一的审批服务,都无法安全地凭空构造所有这些事实。
5. 分发才是真正的检查点
最重要的验证时刻,不是人类点击 Approve 的时候,而是在业务操作即将分发之前。
一条保守的恢复路径如下:
1. Load the same durable task identity.
2. Reconstruct the canonical execution envelope.
3. Verify that the capability and arguments still match the approval evidence.
4. Check whether the approval is expired or revoked.
5. Compare the approved policy context with the current policy context.
6. Re-resolve the trusted subject where required.
7. Dispatch with a stable business idempotency identity.
8. Let the business system recheck object state and final authority.
9. Record the outcome against the same task.
如果任何分发前的绑定检查失败,任务都不能悄无声息地继续执行。
正确的结果通常是以下状态之一:
awaiting_reapproval
rejected_pre_dispatch
具体的状态名称取决于实现。真正需要保障的安全属性并不是:
“我们有一条审批记录。”
而是:
“任何业务分发,都不会使用已经失效的审批证据。”
6. 审批过期不是可重试的传输故障
审批过期不能像临时网络错误一样处理。
考虑以下过程:
task T is approved
-> approval expires
-> dispatcher attempts to resume T
-> validation detects expiry
不安全的实现可能会创建一个新任务、自动重试,或者因为参数没有变化而复用旧审批。
这三种行为都会削弱控制边界。
same durable task
-> expired approval detected
-> no dispatch
-> explicit reapproval or terminal rejection
仅仅为了绕过过期决定而创建新任务,会破坏任务的连续性。将过期视为传输重试,是把政策失败与交付失败混为一谈。复用旧决定,则会把有时间限制的审批变成永久权限。
任务身份应该延续,但执行授权未必能够延续。
7. 政策版本是审批上下文的一部分
审批决定通常是在某个政策版本下产生的:
policy_version = refund-policy-2026-08-01
当任务恢复时,运行时应该能够比较:
approved_policy_version
current_policy_version
但是,版本不一致并不总是需要采取相同的处理方式。
可移植契约可以要求实现保留相关政策上下文,但不应试图统一规定每一家企业如何定义“实质性政策变更”。
这项决定属于部署政策,以及拥有该规则的权威主体。
8. 审批的新鲜度不能取代业务状态的新鲜度
即便审批证据完全有效,也无法证明某个订单现在仍然可以退款。
审批层可能知道:
the approver agreed to refund SO-1001 for CNY 199.00
但只有业务领域才能可靠地知道:
whether SO-1001 still exists
whether it belongs to the current tenant
whether CNY 199.00 remains refundable
whether another refund already succeeded
whether the subject may perform this action now
这就是为什么 Agent 治理架构必须保留最终的业务边界。
审批可以证明所要求的人工决定确实发生过,但它不能替代当前的对象级授权、事务约束、tenant 隔离或领域不变量。
Approval controls whether execution may proceed.
The business system controls whether the consequence may exist.
9. 让边界变得可执行
只有架构图还不够。审批有效性应该被表达成不同实现都可以运行的故障场景。
下面是一条很有价值的测试用例:
Scenario: approval expires while a task is paused
Given:
- one durable task identity
- approval evidence bound to a trusted subject,
canonical arguments, capability, and policy version
- implementation-defined validity metadata
When:
- the runtime attempts to resume or dispatch the task
- after the approval is no longer valid
Then:
- expiry is detected before dispatch
- dispatch_count remains 0
- business_effect_count remains 0
- the old approval is not reused
- the same task moves to reapproval or pre-dispatch rejection
测试还应该禁止以下捷径:
- 使用过期审批进行分发;
- 将审批过期视为可重试的传输错误;
- 创建新的持久化任务来规避过期;
- 将过去的审批解释为当前的业务权限。
不同实现可以选择不同的时钟、lease、TTL 格式、状态名称和工作流引擎,但它们仍然应该能够证明同一个外部可观察属性。
这正是“我们支持人工审批”和“我们能够证明审批在故障与延迟下仍然有意义”之间的区别。
10. 哪些内容属于可移植契约,哪些不属于?
人们很容易想通过将所有运行时关注点都加入 capability 声明来解决这个问题:
approval:
required: true
ttl: 30m
workflow: finance-review-v7
approver_query: ...
policy_engine: ...
这样很快就会把一份可移植声明变成某个组织专属的工作流语言。
更清晰的边界划分是:
可移植的 capability 声明
它可以表达某项操作带有审批意图,以及其他稳定的治理语义。
运行时与审批权威
它们负责实现证据绑定、有效性元数据、过期、撤销、暂停与恢复行为,以及政策版本处理。
它会在产生业务后果之前,重新检查当前主体权限、tenant 边界、对象状态、领域不变量和最终许可。
标准应该描述最小的可移植语义。实现应该让操作层面的保障真正成立。业务系统则应该保留对自身状态的权威。
这种职责划分是有意为之。它既能保持契约的互操作性,又不会假装一套 schema 可以取代企业身份系统、审批工作流或业务授权。
11. 实用审查清单
审查一条 Agent 审批路径时,请检查:
- 审批是否绑定了确切的 capability 和规范化参数?
- 可信主体是否来自模型生成输入之外的来源?
- 审批是否包含有效期或撤销语义?
- 是否保留了适用的政策版本?
- 是否会在分发前检测具有实质性影响的政策变化?
- 任务恢复时,是否保留了同一个持久化任务身份?
- 审批过期后,是否会返回重新审批,而不是自动重试?
- 是否将稳定的幂等身份传递到了业务边界?
- 业务系统是否会重新检查当前对象状态和最终权限?
- 测试能否证明无效审批产生的业务影响为零?
如果系统无法回答这些问题,那么 approved = true 就不是治理保障,而只是一个历史标记。
Human-in-the-loop 经常被呈现为一个只有两个按钮的界面:Approve 和 Reject。
真正的工程问题,从点击按钮之后才开始。
审批决定必须始终绑定赋予它意义的主体、capability、参数、任务、政策和有效性条件。当暂停的 Agent 任务恢复时,系统必须判断这些绑定是否仍然成立。如果不成立,执行就必须在分发之前停止。如果仍然成立,业务系统依然必须执行最新、最终的授权检查。
其治理原则非常简单:
审批不是 boolean。它是针对某个具体操作、受时间限制的证据,而且必须在执行时再次证明其有效性。
MCP 工具选择与安全执行之间还缺少什么?
Agent Capability Contract(ACC)
我负责维护 ACC 和 BailingHub。这是两个开源项目,旨在探索可移植的 capability 治理语义,以及面向操作现有业务系统的 Agent、可自行托管的执行控制机制。它们是具体的设计实验,并非唯一有效的架构。业务系统仍然是最终权威。
如需采取进一步措施,可以考虑屏蔽此人和/或举报滥用行为。