Problem
Approval systems often ask a person or policy service to approve a proposed tool call, then execute it later. Between those steps, the request can be reconstructed, transformed, retried, delegated, or resumed from stored state. A seemingly valid approval may therefore be applied to a materially different action: another recipient, target, operation, payload, actor, acting surface, or policy version.
Ordinary audit logs show what was approved and what was executed, but comparison after the fact does not prevent the mismatch. Tool-level allowlists and human approval gates decide whether an action may proceed; unless the acting boundary verifies the exact approved action, they do not close this approval-to-execution time-of-check/time-of-use gap.
This is narrower than deciding where approval is required or which policy allows a tool call. It binds one authenticated approval to the authorization-relevant representation that a correctly mediated adapter will consume.
Solution
Bind authorization to a deterministic identity of the complete authorization-relevant representation presented for approval, and require the acting adapter to rederive that identity immediately before execution.
- Define the authorization-relevant fields, such as task, approver, actor, acting surface, operation, resolved target identity, arguments, governing policy, expiry, and a nonce or single-use identifier.
- Canonicalize that representation with a documented profile. Compute a versioned, collision-resistant, domain-separated digest and bind the profile identifier into the authorization.
- Issue an authenticated, short-lived authorization envelope. A signature or MAC, protected opaque capability, or trusted transactional lookup must bind the issuer, digest, profile, expiry, nonce, policy, and intended enforcement surface.
- At the execution boundary, authenticate the envelope and reconstruct the action from the immutable values the adapter will actually consume. Canonicalize independently and compare the digest.
- Atomically compare-and-consume the single-use authorization before execution. Deny mismatches, expiry, replay, or integrity failure. Any material transformation after comparison requires a new authorization.
- Record authorization match, execution attempt, and independently observed external effect as separate claims. Digest equality correlates records; it does not prove that execution occurred or succeeded.
profile = "example.action-binding/v1+sha256"
approved = canonicalize(profile, {
task_id, actor, acting_surface, operation,
target, arguments, policy_id, expires_at, nonce
})
authorization = authenticated_approve(
domain_digest(profile, approved), profile, expires_at, nonce
)
# Later, inside the adapter that will perform the action:
verify_envelope_integrity_and_issuer(authorization)
presented = canonicalize(profile, immutable_values_used_by_adapter())
if domain_digest(profile, presented) != authorization.action_digest:
deny("approved action changed")
atomic_compare_and_consume(authorization) # fails on expiry, replay, or race
result = execute(presented)
record_attempt(authorization.id, result)
# Resolve a stable external handle or read-back separately before claiming effect.
The adapter is the enforcement point. A prompt instruction to "execute only what was approved" is not this pattern because the same model can reinterpret both the approval and the action. If the adapter crashes after atomic consumption, reconcile through an idempotency key or authoritative status lookup; otherwise report the effect as unknown rather than replaying blindly.
How to use it
- Apply it to actions where post-approval drift would matter: payments, messages, deployments, permission changes, destructive operations, or externally visible commitments.
- Include every field that could change the meaning or blast radius. Hashing only the tool name while leaving recipient or arguments outside the identity creates approval theater. Resolve aliases before approval when later resolution could change the resource.
- Use a documented canonicalization profile. Reject duplicate keys, unsupported numeric forms, ambiguous paths, and unknown fields rather than normalizing them differently across services.
- Keep authorization short-lived and preferably single-use. Authenticate it and bind it to the acting surface and policy version so it cannot be replayed through a broader adapter.
- Make alternative execution paths pass through the same verification point. An unmediated CLI, browser, or fallback API can bypass an otherwise correct contract.
- Test mutations and races deliberately: change one target, argument, actor, operation, policy, expiry, or nonce; then make two workers consume the same authorization. Prove rejection before duplicate execution.
- After a matching attempt, verify the stated success criterion through an appropriately trusted status lookup, stable handle, or independent read-back. Report
unknownwhen observation cannot resolve the outcome; do not reuse the authorization digest as success evidence.
Trade-offs
- Pros: Detects and rejects committed-field drift on correctly mediated paths; makes approval scope machine-checkable; localizes enforcement at the acting boundary; improves correlation among approval, attempt, and observation records.
- Cons: Requires authenticated envelopes, strict canonicalization, atomic consumption, crash reconciliation, and mediation of every acting path; changed requests incur another approval; aliases and unchecked post-comparison transformations can still defeat naive implementations.
- Limits: A digest is an integrity identity, not authentication, authorization, or execution proof. A compromised approver, adapter, canonicalizer, or signing key remains dangerous. The pattern does not establish that supplied facts were true, that the action was wise, or that the external effect occurred.