Agent 审批防重放:把人工批准绑定到唯一执行与资源状态
用户确认一次高风险动作后,重复点击、队列重投和参数替换都可能复用批准。本文用规范化 action digest、一次性消费、状态版本和幂等执行封闭窗口。
本文要点
- 主题聚焦:AI Agent、MCP、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
审批页面显示“删除 18 个草稿”,不代表后续任意删除都获授权。重复提交、队列重投、参数替换和资源变化,可能让批准执行两次或作用到另一批对象。approved=true 或可复用 ID 无法封闭这些窗口。
Agent 人工审批门 已定义动作范围、期限和实时权限。本篇补齐防重放:批准服务端规范化动作的唯一摘要,消费与建立 execution 必须原子发生。
先生成不可含糊的 action manifest
模型只提交候选计划。可信服务加载资源、授权和策略后生成 manifest,包含租户、run、工具版本、参数、目标 ID 或快照、影响及资源/策略版本。UI 直接渲染这些事实。
{
"tenantId": "tenant-a",
"runId": "run-92",
"tool": "draft.delete.v2",
"arguments": { "projectId": "p-7", "recordIds": ["d-2", "d-9"] },
"expectedState": { "projectVersion": 41, "aclVersion": 12, "recordVersions": [7, 3] },
"impact": { "count": 2, "reversibleUntil": "2026-07-18T10:00:00Z" },
"policyVersion": "approval-v8"
}
对象按 RFC 8785 JCS 生成 canonical bytes 再计算 SHA-256。JCS 不做 Unicode normalization,字符串原样保留;仅接收 I-JSON,parser 先拒绝重复键,数字遵循 IEEE 754/ECMAScript。金额和大整数使用领域规范的十进制字符串。builder 持久化唯一 canonical bytes,UI 与 executor 共用,不能各自序列化 manifest。
批准凭证绑定人、动作和期限
approval artifact 包含 ID、digest、tenant、run、请求者、批准者、期限、nonce、策略、scope、max_uses=1 和 key ID;双人批准需不同身份签同一 digest。
JWT 可作传输格式,但按 RFC 8725 固定 iss、aud、私有 typ=agent-approval+jwt 和非对称 alg allowlist。验证器只从本地可信 key set 解析已知 kid,拒绝 none、对称/非对称算法混淆、未知 kid,以及 token 指定的 jku、x5u 或远程 key。TTL 以分钟计算且不进入模型;撤回、run 取消、权限或策略变化都会 revoke。
人工复核队列 选择审批人,execution binding 防止判断被移植;执行前仍重验身份。
消费与创建执行记录必须原子化
executor 对持久化 canonical bytes 复算 digest,再验证签名、身份、期限、tenant、run、scope 与策略。本地副作用必须在同一事务中锁定 approval、目标资源和授权版本,重查 ACL 与资源版本;匹配时才更新业务行、消费 approval、创建 execution/outbox。若不匹配,则提交 terminal STALE 且不做业务修改,不能留下可复用批准。
BEGIN;
SELECT * FROM approvals WHERE id = $approval FOR UPDATE;
SELECT version, acl_version, tenant_id FROM resources WHERE id = $resource FOR UPDATE;
-- verify live ACL, tenant, digest and expected version
UPDATE resources SET ..., version = version + 1
WHERE id = $resource AND version = $expected; -- exactly one row
UPDATE approvals SET status = 'CONSUMED', execution_id = $execution
WHERE id = $approval AND status = 'APPROVED'; -- exactly one row
INSERT INTO executions (...) VALUES (...);
COMMIT;
任一条件更新不是一行就走 STALE 分支,不执行副作用。唯一约束覆盖 approval 与 digest,工具用 execution id 作幂等键;MCP 超时、重试与幂等 要求重投只查询原 execution。
外部副作用使用 If-Match/ETag、版本 precondition、reservation 或目标系统等价的条件操作,并传 execution id。开始调用前 approval 已进入不可复用的 execution;412、reservation 失效或 ACL 拒绝都终止为 STALE/FAILED,必须重新批准。结果未知进入 RECONCILING,只能按同一 execution id 查询或幂等恢复,不能创建第二次逻辑执行。
资源变化必须要求重新批准
批准后查询可能从 18 条变成 200 条。manifest 应绑定 ID 与版本;必须绑定查询时,保存规范、快照和最大影响数,任一变化都使本次批准终止为 APPROVAL_STALE。
工具版本、金额、收件人、附件哈希或回滚能力变化也使 digest 无效。MCP 审计日志设计 记录 manifest 版本、批准者、execution、结果与补偿。
验证所有重放窗口
验收覆盖双击、并发、重投、过期/撤回、参数替换、字段重排、资源变化、跨租户、换 audience、权限撤销和外部成功但本地超时,并断言副作用次数与状态。
跨语言 golden vectors 覆盖组合字符、不规范 Unicode、重复键、IEEE 754 边界、金额与大整数,并注入事务崩溃、outbox 重发和对账恢复。同一 approval 最多产生一个逻辑 execution。
事故时撤销未消费批准并对账
发现缺陷时停止高风险工具,提升 issuance epoch 使未消费批准失效。对已消费无终态或外部结果未知的 execution 逐项对账,不能批量重试。
回滚到上一版已签名 builder、JCS 库和消费事务,经并发/故障矩阵验证后逐步开放。不得接受旧 digest、延长 TTL 或重置 CONSUMED;旧任务必须重算并重批。批准只有绑定同一动作、状态和唯一执行才是授权。
延伸阅读
Related