Agent Approval Replay Protection: Binding Human Approval to a Unique Execution and Resource State
After a user confirms a high-risk action once, repeated clicks, queue re-sends, and parameter substitution can all reuse the approval. This article closes those windows with a canonical action digest, one-time consumption, state versions, and idempotent execution.
Key Points
- Topic Focus: AI Agent、MCP、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
An approval page showing "delete 18 drafts" does not mean any subsequent arbitrary deletion is authorized. Repeated submissions, queue re-sends, parameter substitution, and resource changes may cause the approval to execute twice or apply to a different set of objects. approved=true or a reusable ID cannot close these windows.
Agent Human Approval Gates already define action scope, deadlines, and real-time permissions. This article completes the picture with replay protection: the approval server canonicalizes the unique digest of the action, and consuming the approval and creating an execution must happen atomically.
First Generate an Unambiguous Action Manifest
The model only submits candidate plans. After loading resources, authorization, and policies, a trusted service generates the manifest, which includes tenant, run, tool version, arguments, target IDs or snapshots, impact, and resource/policy versions. The UI renders these facts directly.
{
"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"
}
Use RFC 8785 JCS to produce canonical bytes for the object, then compute the SHA-256. JCS performs no Unicode normalization; strings are preserved as-is; only I-JSON is accepted, the parser rejects duplicate keys first, and numbers follow IEEE 754/ECMAScript. Amounts and large integers use domain-canonical decimal strings. The builder persists the unique canonical bytes, shared by both the UI and the executor; neither should serialize the manifest on its own.
Approval Credentials Bind Person, Action, and Deadline
The approval artifact contains ID, digest, tenant, run, requester, approver, deadline, nonce, policy, scope, max_uses=1, and key ID; two-person approval requires different identities to sign the same digest.
A JWT can serve as the transport format, but per RFC 8725 fix iss, aud, a private typ=agent-approval+jwt, and an asymmetric alg allowlist. The verifier resolves only a known kid from a local trusted key set, rejecting none, symmetric/asymmetric algorithm confusion, unknown kid, and any jku, x5u, or remote key specified by the token. The TTL is counted in minutes and never reaches the model; withdrawal, run cancellation, or permission/policy changes all revoke the approval.
Human Review Queues select approvers, while execution binding prevents the decision from being transplanted; identity is still re-verified before execution.
Consumption and Execution Record Creation Must Be Atomic
The executor recomputes the digest over the persisted canonical bytes, then verifies signature, identity, deadline, tenant, run, scope, and policy. Local side effects must lock the approval, target resource, and authorization version within the same transaction, re-querying the ACL and resource version; only on match do they update business rows, consume the approval, and create execution/outbox records. On mismatch, commit a terminal STALE and make no business changes, leaving no reusable approval behind.
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;
If any conditional update does not affect exactly one row, the STALE branch is taken and no side effects are executed. Unique constraints cover approvals and digests, and tools use the execution id as the idempotency key; MCP Timeout, Retry, and Idempotency requires re-sends to only query the original execution.
External side effects use If-Match/ETag, version preconditions, reservations, or the target system's equivalent conditional operations, and pass the execution id. Before the call begins, the approval has already been committed to an unreusable execution; a 412, an invalidated reservation, or an ACL rejection all terminate as STALE/FAILED, requiring re-approval. An unknown result enters RECONCILING, which can only be queried or idempotently recovered under the same execution id; a second logical execution must not be created.
Resource Changes Must Require Re-approval
After approval, a query can grow from 18 records to 200. The manifest should bind IDs and versions; when a query must be bound, the canonical form, snapshot, and maximum impact count are stored, and any change terminates the current approval as APPROVAL_STALE.
Changes to the tool version, amount, recipient, attachment hash, or rollback capability also invalidate the digest. MCP Audit Logging Design records manifest version, approver, execution, result, and compensation.
Verify All Replay Windows
Acceptance tests cover double-clicks, concurrency, re-sends, expiry/revocation, parameter substitution, field reordering, resource changes, cross-tenant, audience swapping, permission revocation, and external success with local timeout, asserting side-effect counts and statuses.
Cross-language golden vectors cover combining characters, non-canonical Unicode, duplicate keys, IEEE 754 boundaries, amounts, and large integers, and inject transaction crashes, outbox re-sends, and reconciliation recovery. The same approval produces at most one logical execution.
During an Incident, Revoke Unconsumed Approvals and Reconcile
When a defect is found, stop high-risk tools and bump the issuance epoch to invalidate unconsumed approvals. For executions that are consumed without a terminal state or whose external result is unknown, reconcile item by item; do not retry in bulk.
Roll back to the previous signed builder, JCS library, and consumption transaction, and gradually reopen after validating against the concurrency/failure matrix. Never accept old digests, extend TTL, or reset CONSUMED; old tasks must be recomputed and re-approved. An approval is authorized only when it binds the same action, state, and unique execution.
Further Reading
Related
Related Content
Article
From Copilot to Codex: AI Coding Tools Enter the Agent Era
Technical trends, engineering practices, and indie-developer perspectives around AI, AI Agents, and Vibe Coding.
Article
Why Does MCP Matter? Understanding the AI Tool Protocol
Technical trends, engineering practices, and an independent developer perspective on MCP and AI Agents.
Article
Core Architecture of AI Agents: Planning, Memory, Tools, and Execution
Technical trends, engineering practices, and independent developer perspectives around AI Agents and large language models.
Article
How Should RAG and Agents Work Together?
An independent developer's perspective on technical trends and engineering practices involving RAG and AI agents.