Skip to content
Metamynd

Open protocol

Agentic Governance Protocol

Version 1.0. Every authorization decision Metamynd makes is defined here. Implement it, verify against it, or re-implement the gate yourself — no licence required, and no account needed to call any endpoint the spec names as public.

npm i @metamynd/agentsafe-guardor scaffold a governed agent with npx create-metamynd-agent

MetaMynd Agentic Governance Protocol (MAGP)

Specification — Version 1.0 Status: Stable · Date: 2026-08-20 · Editors: MetaMynd

A protocol for authorizing the actions of autonomous AI agents. An agent presents a signed description of what it is about to do; a verifier evaluates that action against the agent's identity, the authority delegated to it, and the policies in force; and returns a decision the agent must obey. Every decision produces evidence a third party can verify without trusting the verifier.


Status of this document

Version 1.0 supersedes the 0.x drafts. It describes what is built and running: every normative clause below corresponds to code in the reference implementation, and the order of checks in §8.5 is transcribed from the gate itself rather than designed on paper.

The 0.x drafts tagged individual clauses [IMPLEMENTED] or [PROPOSED]. That made sense while the delta was large; it no longer does, and a document where most sentences carry a maturity tag reads as provisional even when it is not. Clauses that were still unbuilt at 1.0 have been moved out of the normative text into Appendix D — Future directions. If it is in the body, it exists.

Scope

MAGP is the interoperability surface. The test for inclusion is narrow and worth stating, because it is what keeps this document implementable:

A thing belongs in MAGP if an independent implementer needs it to reach the same verdict on the same request, or to verify the same evidence without access to the platform that produced it.

That test admits identity, the canonical signed message, the order of checks, reason codes, mandate and delegation semantics, policy-bundle distribution, and the evidence and proof formats. It excludes the governance surfaces a platform builds on top — supervisory access for regulators, sandbox programmes, incident routing, the trust graph, document ingestion. Those are real and specified in the companion document MetaMynd Platform Governance; they are not things a second implementation must reproduce to interoperate.

Conformance language

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.


Table of contents

  1. Terminology
  2. Roles and entities
  3. Architecture
  4. Identity
  5. Authority: mandates, resource scope, delegation
  6. Policy artifacts
  7. Agent posture: containment, operating mode, reassessment
  8. The authorize protocol
  9. Secondary controls: rate limits and circuit breakers
  10. Escalation resolution
  11. Payment execution binding (x402)
  12. Effects and reconciliation
  13. Evidence and anchoring
  14. Decision records and the governance envelope
  15. Privacy-preserving verification
  16. Discovery and channels
  17. Security considerations
  18. Extensibility and versioning
  19. Conformance
  20. Reference endpoints
  • Appendix A — Reason codes
  • Appendix B — Canonical examples
  • Appendix C — Atom catalog
  • Appendix D — Future directions

1. Terminology

Agent — an autonomous software actor that proposes and performs actions. Identified by a DID.

Principal — the legal person or organisation on whose behalf an agent acts. The delegation root: the chain of authority terminates here, at a named legal entity whose verification status — and how it was verified — is reported on every read (§4.4).

Owner — the tenant account that administers an agent. Distinct from the principal: an owner may administer agents acting for several principals.

Mandate — the authority delegated to an agent, expressed as an ODRL policy and issued as a verifiable credential. Says which actions, within which constraints, for how long.

Standard — a published, versioned rule pack an organisation binds agents to. Enforced.

SOP — a standard operating procedure: an organisation's own rule pack, scoped to its agents.

Molecule — one rule within a Standard or SOP: a set of atoms (pure predicates over request context) and the decision to emit when they all hold.

Gate — the verifier. Evaluates a proposed action and returns a verdict. Server- authoritative: the agent does not decide, and a guard that evaluates locally reaches the same verdict from the same signed inputs.

Verdict — the gate's answer: a disposition plus a reason code (§8.6, Appendix A).

Evidence — the record of a governance decision, in a form a third party can verify independently of the platform that produced it.


2. Roles and entities

Role Holds Responsibility
Principal A legal identity, verified (KYC/KYB) before it may back a mainnet agent (§4.4) Issues mandates; answerable for what its agents do
Agent A DID and a private key Signs and submits proposed actions
Issuer Signing keys for credentials and bundles Issues mandates; publishes and signs policy bundles
Gate The evaluation logic and state Returns verdicts; reserves budget; records evidence
Guard A copy of the deterministic evaluator Refuses locally; re-checks at a counterparty
Counterparty Its own DID May independently re-evaluate a caller's authority
Approver A platform identity and role Resolves escalations

An MCP server is a counterparty that exposes tools. Where it holds its own DID and mandate it is simultaneously an agent, and this protocol treats it as one.


3. Architecture

Two planes.

The control plane issues and publishes: identities, mandates, standards, SOPs, signed policy bundles. It is not on the hot path.

The data plane is the authorize exchange. It MAY be executed:

  • against the gate — the central verifier, which owns the stateful parts (nonce consumption, spend reservation, evidence);
  • at the edge by a guard — deterministic evaluation against a signed policy bundle, with no round trip;
  • at the counterparty — a trustless re-check, so an agent that skips its own guard still cannot get the counterparty to act.

All three MUST produce the same disposition from the same inputs. Where they cannot — because a check is inherently stateful — the edge MUST fail closed and defer (§8.5).


4. Identity

4.1 Agent DID methods

An implementation MUST support at least one of:

  • did:hedera — anchored on an HCS topic. The DID document is derived from the anchor; the agent's Ed25519 public key is the verification method.
  • did:key — self-certifying. The Ed25519 key is the identifier; no anchor, no issuance ceremony, control provable from the identifier alone.

4.2 Proof of possession

4.2.1. An agent whose key was supplied by its holder (BYOK) MUST prove control before the gate accepts any action from it. The gate issues a challenge; the agent signs it; the gate verifies against the key in the DID and stamps the identity as proven.

4.2.2. Until control is proven the gate MUST deny with AGENT_KEY_UNVERIFIED. This is not advisory: an unproven key means the signature check (§8.5 step 5) is meaningless, because nobody has demonstrated that the key belongs to the party claiming it.

4.2.3. Where the platform generated the keypair and returned the private key once, control is established at issuance and no challenge is required.

4.2.4. Proof is per key. A DID minted by rotation or for a second network has its own keypair, so the gate MUST judge possession of the key the request is verified with — that DID's own proof — not the proof recorded for the agent's original key. (A network identity recorded before per-key proofs existed carries neither a proof nor a challenge; it keeps the agent-level proof.)

4.3 Resolution

4.3.1. A verifier MUST resolve the agent's public key from the DID rather than trusting any key material in the request.

4.3.2. GET /did/{did} returns a W3C DID document for both supported methods.

4.3.3. Identity rotation. Rotating an agent's identity on a network (key-loss recovery) replaces its DID and key there. Mandates are bound to a DID, so every mandate active under the old DID is reissued under the new one — same terms, and the same cumulative spend (the new mandate supersedes the old) — and the old one is revoked as superseded (10.4.2); pending reviews do not migrate. A delegated mandate is reissued delegated, re-validated against its delegator, or else revoked (5.4.11). The rotation's response reports mandateReissue: reissuedCount, failedCount, revokedCount and revoked (the delegated mandates revoked instead of reissued). The old DID no longer verifies: the gate refuses a request signed as it IDENTITY_ROTATED, and GET /did/{did} answers it 410 Gone with a DID resolution result whose didDocumentMetadata says deactivated: true (the HTTP binding of W3C DID Resolution). Its document still lists the old key, which is derived from the DID itself; the metadata is what tells a verifier to stop trusting it. The public Authority Manifest of the old DID carries retired: { reasonCode: "IDENTITY_ROTATED" } (null for a current DID). Neither public read names the DID that replaced it: the IDENTITY_ROTATED refusal carries it as replacementDid, outside the digested verdict like hint, and only to a request validly signed with the replaced key — so the old→new link reaches whoever runs the old config, not anyone who merely names the old DID.

4.4 Principal assurance

4.4.1. Every mandate MUST name an authorizing principal, and an absent one is refused. Beyond that, how much the principal's verification matters depends on the network the agent is on, and this section states what the reference implementation does rather than a uniform rule:

  • Testnet and did:key agents — verification is reported, not required. The principal's authority holds whether or not it is verified, and a sandbox or evaluation needs no KYC/KYB. The verdict and the registry carry the principal's status (4.4.2) so a counterparty applies its own risk policy.
  • Mainnet agents — the principal MUST be verified by a mainnet-eligible provider to create the identity at all, and the principal's authority holds only within the free-tier caps (1 mainnet agent per owner, 5 active mandates on the agent). Beyond either cap the gate refuses with PRINCIPAL_UNVERIFIED, a code whose name predates this model. A testnet or did:key agent is never refused with it.

The mainnet check is unconditional and precedes signature verification, because it is a question about the policy rather than about the requester.

4.4.2. The registry (GET /magp/manifest/{did}, GET /policy/mandate/{ref}/status) MUST expose the authorizing principal's current verification status on every read, and every authorize verdict MUST carry it alongside the decision — so a counterparty applies its own risk policy to an unverified-backed mandate rather than assuming verification.

4.4.2a. The public Authority Manifest (GET /magp/manifest/{did}) MUST also expose the agent's live containment (7.1) as a sibling of the manifest — containment: { status, since }, status one of suspended, quarantined, decommissioned, or null when the agent is active. The manifest states the authority the agent was granted; containment does not change that, it stops the agent using any of it, so a rendering of a contained agent's manifest MUST NOT present its granted actions as currently permitted. The containment reason is the owner's and MUST NOT be published. The manifest endpoint itself MUST NOT be served from a cache; a cached public projection of it (a README badge) SHOULD reflect a containment within a minute: it SHOULD read the manifest fresh and be cached once, briefly (the reference badge: 30 seconds), never through two caches in series.

4.4.3. Verified does not mean identity-checked. The registry's principal block reports not only verified but the basis of the verification — the weakest across the principals behind the agent's active mandates:

basis Meaning
provider An automated third-party provider verified the principal (KYC/KYB).
manual_review A person on the platform's compliance team reviewed and decided.
self_approved The platform approved the principal automatically (sandbox and beta self-serve). No identity check ran. Such a principal cannot create a mainnet agent.
unrecorded Verified, but the record does not say by whom. Never upgraded to a stronger basis.

A counterparty that needs a real identity check MUST require provider or manual_review, not merely verified: true. The block also carries identityChecked, which is true only when verified is true and the basis is provider or manual_review. A self-approved principal reads verified: true, basis: "self_approved", identityChecked: false.


5. Authority

5.1 Mandates

5.1.1. A mandate is an ODRL policy carried in a verifiable credential. It names a target action and carries permissions and prohibitions, each with constraints.

5.1.2. Evaluation order within a mandate is: validity window, then prohibitions, then a matching permission whose constraints all hold. A prohibition that fires outranks any permission.

5.2 Constraints

A constraint is { leftOperand, operator, rightOperand }. Operators are a fixed set — new scenarios add operands, not operators:

eq · neq · lt · lteq · gt · gteq · isAnyOf · isNoneOf · isPartOf · before · after

5.2.1. An unknown operator MUST fail closed.

5.2.2. Well-known operands map to stable reason codes so a denial names its cause: mm:payAmount and mm:cumulativeSpend → SPEND_LIMIT_EXCEEDED; mm:merchant → MERCHANT_NOT_ALLOWED; mm:route → ROUTE_NOT_ALLOWED; mm:counterparty → COUNTERPARTY_NOT_ALLOWED; mm:jurisdiction → JURISDICTION_REQUIRED when the request has no effective jurisdiction, JURISDICTION_NOT_ALLOWED when it has one outside the list (§5.2.3). Any other operand denies with CONSTRAINT_FAILED:<operand>.

5.2.2a. A cumulative cap is per currency. Its limit is a bare number, so the committed spend it is checked against MUST count only holds in the request's currency (compared case-insensitively); summing two currencies has no meaning without a rate, and none is applied. A hold with no recorded currency counts against every currency, so the check fails closed. This changes nothing for a single-currency mandate; for one that allows several currencies (or any), the cap applies to each separately.

5.2.3. Allowed jurisdictions. A mandate MAY restrict the jurisdictions an action may be performed in. Issuance (POST /policy/mandate) takes an optional allowedJurisdictions: a list of ISO 3166-1 alpha-2 codes, trimmed, upper-cased, de-duplicated and sorted; the group code EU expands to the 27 member states. An empty or malformed list is refused (400 MALFORMED_REQUEST); omitting it means no restriction. The term is stored as an ordinary constraint on the permission:

{ "leftOperand": "mm:jurisdiction", "operator": "isAnyOf", "rightOperand": ["DE", "FR", "SG"] }
  • It is judged on the request's effective jurisdiction (§8.3.12) — the country the owner registered for the payee, else the signed top-level jurisdiction — never on an itinerary value. With no effective jurisdiction the request is blocked JURISDICTION_REQUIRED; outside the list, JURISDICTION_NOT_ALLOWED.
  • GET /policy/mandate/{ref}/status returns it as allowedJurisdictions (null = no restriction).
  • A reissue (identity rotation, resource-scope pin, payload-binding toggle) MUST carry the term. A jurisdiction constraint that cannot be read back as an allow-list (a hand-authored isNoneOf, say) refuses the reissue of that mandate rather than dropping the restriction.
  • A delegated mandate inherits or narrows it (§5.4.5).
  • The zero-knowledge path proves membership in the same list (§15.1.2).

5.3 Resource scope

5.3.1. Authority over actions does not answer which systems and data an agent may reach. A resource scope is expressed as an ordinary constraint over the resource operand, which the request declares in its context:

{ "leftOperand": "resource", "operator": "isAnyOf",
  "rightOperand": ["crm.contacts", "ledger.postings"] }

5.3.2. The resource is part of the signed message (§8.3): it is cryptographically committed the same way merchant already is (and a top-level jurisdiction is, §8.3.12), not merely asserted the way tool or an itinerary value is. A counterparty relaying a buildSignedRequest()-built request cannot substitute a different resource without invalidating the signature.

5.3.3. An action that declares no resource, evaluated against a mandate carrying a resource constraint, MUST be denied: an action that will not say what it touches cannot be checked against a scope.

5.4 Delegation

5.4.1. An agent MAY delegate a narrowed subset of its own authority to another agent. The delegated mandate is an ordinary mandate; nothing downstream special-cases it.

5.4.2. Narrowing invariant. A delegated mandate MUST NOT widen its parent. Narrowing is asymmetric per operator, and an implementation that gets this wrong grants authority nobody issued:

Operator Narrower means
lteq, lt a lower ceiling
gteq, gt a higher floor
isAnyOf, isPartOf a smaller allow-list (child ⊆ parent)
isNoneOf a larger deny-list (child ⊇ parent)
before an earlier deadline
after a later start
eq the identical value

5.4.3. A parent constraint the child omits is a widening, not a simplification. A child that drops its parent's spend cap holds more authority than its parent.

5.4.4. A child MUST NOT outlive its parent's validity window, and MUST retain every prohibition the parent carried.

5.4.5. Constraints the parent did not have MAY be added freely: they can only remove authority. The same holds for the permission-level settings that are not constraints: a child MUST NOT lower or drop its parent's riskTier (§6.3), nor drop its parent's requirePayloadBinding (§8.3.9), and a value of either that is not a recognised one is refused rather than read as "not set".

  • Allowed jurisdictions (§5.2.3). A child that states no allowedJurisdictions inherits its parent's list, and the implementation MUST store it on the child explicitly (so the child's own document carries the restriction, and a later change to the parent cannot widen it). A child that states a list has it normalised as at issuance (EU expanded) and it MUST be a subset of the parent's (isAnyOf narrowing, 5.4.2) — otherwise the delegation is refused CONSTRAINT_WIDENED (operand mm:jurisdiction). A child of an unrestricted parent MAY add a list. A reissue of a delegated mandate carries its list and re-checks it against the parent's current terms (5.4.11): a list no longer within the parent's is DELEGATION_NARROWING_FAILED.

5.4.6. The chain MUST terminate at a bounded depth (reference implementation: 3). Depth SHOULD be stored rather than derived by walking, so the bound is checkable without following the chain and a cycle cannot hang a verifier.

5.4.7. The delegation root MUST remain the original principal at every depth, so an accountability walk from any descendant terminates at the principal that issued the root mandate — a named legal entity, whose verification status and basis (§4.4.3) are reported.

5.4.8. Narrowing SHOULD be verified at issuance. A stored child that has been proven no wider than its parent costs nothing extra to evaluate.

5.4.9. Revoking a mandate revokes everything delegated from it. A child holds authority only because its parent did, so revoking a mandate — directly (POST /policy/mandate/{ref}/revoke), as a chain (POST /policy/mandate/{policyId}/revoke-chain), or by decommissioning its agent — MUST revoke every mandate delegated from it, at every depth, and each one MUST be revoked exactly as a directly revoked mandate is (8.7.15): made inactive, its pending reviews closed (10.4.2), its unclaimed holds released. A revoke that only deactivated the delegated mandates would leave their holds claimable. This holds for a reissue too (identity rotation, resource-scope pin, payload-binding toggle): the superseded mandate's own reviews close MANDATE_SUPERSEDED, but nothing was delegated from the new mandate, so every mandate delegated from the old one is revoked — its reviews close MANDATE_REVOKED — and the delegator re-delegates if it still wants to. (A reissue could otherwise leave a child wider than its new parent.)

5.4.10. A delegated mandate is only as live as its chain. Independently of the cascade, a gate MUST refuse to use a delegated mandate unless every mandate it was delegated from, up to the root, is active — at authorize, at an escalation approval or a reviewer's MODIFY (10.4.1), and on a claim or a capture of an unclaimed hold under it (8.7.15): MANDATE_REVOKED when an ancestor is inactive, and DELEGATION_CHAIN_BROKEN when the chain cannot be walked to a root (a parent that no longer exists, a cycle, or a chain deeper than the bound of 5.4.6). The walk MUST be bounded and cycle-safe (the reference implementation: one recursive query, at most the depth bound, never revisiting a mandate).

5.4.11. A reissued delegated mandate stays delegated. When a delegated mandate is reissued — the delegate's identity is rotated (4.3.3), a resource scope is pinned into it, its payload-binding requirement is switched — the new mandate MUST keep the delegation: the same parent (and delegating agent), the same depth, the delegator's tenant as its owner and the same root principal (5.4.7). Its terms are the delegated mandate's own terms with only that change applied, not a document rebuilt from what an ordinary mandate can express. Before it is issued it MUST be re-validated as a delegation is at issuance: every mandate above it active (5.4.10), and the changed terms a narrowing (5.4.2–5.4.5) of the parent's current terms at the same depth. The failure codes are MANDATE_REVOKED (an inactive ancestor), DELEGATION_CHAIN_BROKEN (a chain that cannot be walked to its root) and DELEGATION_NARROWING_FAILED (a widening, with the refusal and operand), each reported as { policyId, reasonCode, detail }. If either check fails, nothing is issued, and:

  • an owner's change — a resource pin, a payload-binding toggle — MUST be refused, and nothing changes: the delegated mandate stays active, unrevoked, as the agent's authority. The refusal is all-or-nothing across the agent's mandates: every delegated mandate is checked before anything is written, and if any fails, none is reissued. The answer is 409 in the standard refusal body — the code as message, data.reasonCode (the first failing mandate's code), data.detail, and data.refused listing every failing mandate;
  • an identity rotation MUST instead revoke the delegated mandate (8.7.15) with that code — the old DID is gone, and the mandate may not be carried forward as ordinary authority — and report it in mandateReissue.revoked (with revokedCount).

Issuing checks, in the same transaction as the insert and under a lock on both rows, that the parent and the mandate being replaced are still active, so a reissue that races a revoke of its delegator either lands before the revoke (and the revoke's cascade reaches it) or is refused MANDATE_REVOKED. A reissue is never a way out of a chain: carried forward as an ordinary mandate, it would escape both the revoke cascade (5.4.9) and the chain check (5.4.10).

Up to v1.69.2 the chain revoke deactivated the delegated mandates and closed their reviews but left their unclaimed holds held — still claimable and capturable, their budget still counted — and a direct revoke of a delegator did not reach its delegated mandates at all. Revoking the delegator again (or its chain) finishes such a revoke: an already-inactive mandate is still swept. The reference implementation also finishes them itself, once, at start-up (8.7.15).

Up to v1.69.2 a reissue of a delegated mandate came out an ordinary one: no parent, depth 0, owned by whoever reissued it (for a rotation, the delegate's tenant) — so after a delegate rotated its key, revoking the delegator no longer reached the delegate's authority, which kept authorizing. At start-up the reference implementation finds every active mandate without a parent whose reissue lineage (supersedes) leads back to a delegated mandate, and re-links it (parent, depth, delegating agent, owner) when its chain is live, it has no delegated mandates of its own, and its terms narrow both the delegated mandate it replaced and the parent's current terms; otherwise it revokes it with the reason above. A reissue made before the lineage was recorded (before v1.68.0) cannot be traced and is not found.


6. Policy artifacts

6.1 Standards and SOPs

6.1.1. A rule pack is a set of molecules. A molecule fires when all its atoms hold, and emits its declared decision and reason code.

6.1.2. Atoms are pure predicates over request context — no clock, no network, no model. Determinism is what allows the edge and the gate to agree. The catalog is Appendix C.

6.1.3. Across all molecules of all bound packs the most restrictive decision wins.

6.2 Signed policy bundles

6.2.1. GET /policy/bundle/{did} returns the agent's evaluable policy, signed by the issuer. GET /magp/policy/pubkey publishes the verification key.

6.2.2. A guard MUST verify the signature before evaluating, and MUST reject a bundle whose signature is invalid for any action. A guard holding a pinned verification key MUST also reject a bundle whose signature is absent, for any action — an amount-0 action included. The issuer never serves an unsigned bundle, so under a pin one can only have been stripped in transit, and an amount-0 action is not necessarily a read: a non-financial agent's every action carries no amount.

6.2.3. Bundles carry a maxStaleness. A value-bearing action MUST fail closed on an unsigned, tampered or stale bundle (POLICY_BUNDLE_UNSIGNED, POLICY_BUNDLE_SIGNATURE_INVALID, POLICY_BUNDLE_STALE). A guard holding a pinned key MUST fail closed on a stale bundle for any action, since the issuer re-issues every copy it serves and a stale one is a replay of authority that may since have been withdrawn. Only a guard with no pinned key (6.2.4) MAY let an amount-0 action proceed on an unsigned or stale bundle.

6.2.4. A guard that has no pinned verification key cannot perform 6.2.2 and is trusting the transport instead. It MUST NOT permit a value-bearing action on a bundle that arrived over an unauthenticated transport (plain http://) — POLICY_BUNDLE_UNVERIFIED — unless its operator has explicitly opted out for local development. Over TLS it SHOULD warn that the bundle is trusted on the strength of TLS alone.

6.2.5. maxStaleness bounds the age of the copy a guard holds, measured from the bundle's issuedAt. The issuer therefore MUST serve a bundle whose issuedAt is the time it is served, signed at that time — not the time its rules were last compiled, which can be arbitrarily old for rules that have not changed. A compiled bundle is re-issued on every fetch: issuedAt is the serve time, and compiledAt and compiledSignature (the compile's own signature, the one anchored on the agent's topic as policy-update.sigDigest, §5.3.1) are carried inside the re-signed content. The compiled bundle is recovered by setting issuedAt to compiledAt, removing those two fields and verifying compiledSignature over the result; a guard confirming the bundle is the latest anchored one compares the anchor with sha256(compiledSignature), or with sha256(proof.signature) for a bundle that carries no compiledSignature.

6.2.6. Revoked actions. A bundle MAY carry revokedActions: the actions the agent was granted a mandate for and no longer holds one for, every mandate for them having been revoked. It is part of the signed content, and the issuer omits it when it would be empty. A guard that finds no mandate for an action listed there MUST refuse it MANDATE_REVOKED rather than NO_MANDATE or NO_PERMISSION_FOR_ACTION, as the gate does (§8.5): the authority was granted and withdrawn, which is not the same fact as never granted. The field names the refusal and never decides it, since the action is refused either way. A guard reading a bundle without it refuses as before. An action granted again is in mandates and is not listed.

6.2.7. Owner. A bundle carries ownerPrincipal: the public DID of the principal that owns the agent — its organisation principal, else its owner's oldest verified principal — signed with the rest, and omitted only when the agent has none (it then cannot act: the gate requires a verified principal). It is how a Service bound to the owner of its credentials (16.3) checks that an admitted agent belongs to that owner. It never carries the platform's internal owner identifier.

6.3 Context provenance

6.3.1. The context the atoms judge is assembled from sources of very different trust, and a rule that judges what the agent said about itself has judged nothing. A signature does not help: it says who asserted a value, not that it is true, and the counterparty re-check reads the same claim. So every context field MUST carry a provenance — where its value came from — attached only by the party assembling the context, never by the request:

Provenance Source Rank
agent_asserted the agent's own unsigned context (itinerary) 0
agent_signed covered by the agent's signature: attributable, not verified 1
gateway_derived derived by the authenticated counterparty from the real request 2
authoritative derived by the issuer (spend, call count, trust) or set by the mandate's owner 3
attested backed by a verified attestation or credential 4

An unlabelled field is agent_asserted: no context, however it was built, reads as more trusted than the agent's own word. An agent cannot set a label — provenance is not part of any request, and a value the agent sends under a key named "provenance" is just another unsigned field.

6.3.2. Assembly. Sources are applied in order of trust, each later source winning a collision: unsigned < signed < gateway-derived < server-derived. This is §17.2 extended to the two derived sources.

6.3.3. Risk is a floor, never a ceiling. riskLevel is an ordered, restrictive field, so it is merged, not overridden: the effective value is the maximum of every trusted source and the agent's own claim. Trusted sources are the mandate owner's riskTier on the permission (authoritative), a value the gateway derived from the request (gateway_derived) and a server-derived one (authoritative). An agent may raise its stated risk; it can never lower it below what a trusted source says. The effective value takes the provenance of the most trusted source present. A trusted source that states an unusable riskLevel has said nothing: it neither overwrites the agent's claim nor lends it the trusted source's label. riskTier is part of the signed mandate, so the gate and every guard apply the same floor, and a delegated mandate (§5.4) MUST NOT drop or lower its parent's tier — a lower tier is less scrutiny, a widening — nor carry a tier that is not a recognised level (refused, never read as "no floor"). riskLevel is read case- and whitespace-insensitively ("HIGH " is high); the levels are low, medium, high, critical.

6.3.4. Fail closed on the atoms that judge risk. A molecule containing a risk-at-or-above atom MUST NOT pass when riskLevel is missing, or is present but not one of the four levels: it MUST decide at least escalate, with reason code CONTEXT_UNVERIFIABLE, whatever its combinator (a none rule must not read absence as "nothing wrong"). If the molecule fires on its own, its own decision and reason code stand (an observe is raised to escalate). This is deliberately conservative: a molecule that could not be judged escalates even if another of its atoms would have kept it from firing — the implementation does not try to prove the rule could not have fired. An implementation's scenario simulation MUST report the same outcome as the gate, or it would show a control as "not fired" where the gate holds the request.

6.3.5. Policies declare the trust they need. A molecule MAY declare requireProvenance: { field: level }. For each named field the molecule needs a well-formed value whose provenance is at least level; otherwise it decides at least escalate (CONTEXT_UNVERIFIABLE). An unknown level is read as the strictest (attested), and a document declaring one is rejected at authoring, so a typo can only make a rule refuse more. A molecule that declares nothing behaves exactly as before, except as 6.3.4 requires.

6.3.6. Counterparty derivation. A guard SHOULD let the service supply trustedContext — context the service derived from the real call, never from the agent — which is applied over the agent's claim and labelled gateway_derived. A trustedContext the service configured but that yields nothing usable — not an object, or a riskLevel that is not a level (a lookup that missed and returned nothing) — means its deriver is broken, and the request MUST be refused; it MUST NOT fall back to the agent's word. The operating-mode gate (§7.2) judges the effective risk, so an agent in supervised mode cannot skip the high-risk escalation by claiming low.

6.3.7. What this does and does not establish. It closes omission, garbling and understating where a trusted source exists. It does not make an agent's claim true: with no owner riskTier, no gateway derivation and no requireProvenance, riskLevel is still the agent's word — only hiding or garbling it is now refused. The other agent-supplied fields (tool, model, consent, …) keep their documented absent-field behaviour (Appendix C); a rule that must not depend on the agent's word about them declares requireProvenance. jurisdiction is no longer one of them: the gate enforces only the effective jurisdiction of §8.3.12 (registered payee country, else the signed field), and an itinerary jurisdiction is ignored.

6.3.8. Owner-keyed operations (non-financial risk). A financial action has risk the issuer derives from facts the agent cannot fake (the amount against the cap, a first payment to an unvetted merchant). A non-financial action has no amount, so unless the owner configures otherwise, whether it escalates depends on the riskLevel the agent declares — an agent that labels op: "delete-all" as low is not reviewed. The owner closes this by keying a rule on what is being done: the context-value-in atom fires when a context field (a dot path such as op or params.op) has one of the owner's values, and context-value-not-in fires when it has a value not on an allow-list (Appendix C). Both are pure, so the gate and every guard bundle reach the same verdict. Values are compared after Unicode NFKC, removal of invisible characters, lower-casing, folding of common Cyrillic/Greek look-alikes of Latin letters, and removal of whitespace, hyphens and underscores; a value that is an object or a nested list is unjudgeable and MUST fire; a list fires if any element would; a field that is absent fires only when the atom is configured missing: "fire". A deny-list cannot foresee every spelling, so an allow-list is the stronger form when the safe operations are known. When a refusing or escalating molecule fires on such an atom, the gate SHOULD report it with the decision as a risk signal { signal: "context-value", level: "high", detail } (e.g. op=delete-all is owner-marked for review), alongside the financial signals, so the review shows the risk was derived from the request rather than declared by the agent. The SOP compiler drafts OPERATION_NEEDS_APPROVAL / OPERATION_NOT_ALLOWED rules of this kind. The field is only as good as the executor's binding of it: it must be the field the executor acts on, and the approved context is bound at claim (§8.6).


7. Agent posture

Three per-agent states that bias or override evaluation. All are gate-side facts; a guard learns them through the bundle.

7.1 Containment

7.1.1. A contained agent is denied all actions until reinstated — AGENT_SUSPENDED, AGENT_QUARANTINED, or AGENT_DECOMMISSIONED. Containment is checked before any action-specific evaluation.

7.1.2. Containment MAY be applied automatically by a firing rule (suspend/quarantine), or manually by an owner (suspend/quarantine/decommission). Reinstating a suspended or quarantined agent restores gate access, but not mandates or SOP assignments revoked meanwhile. Decommission retires the agent: it revokes every active mandate and unassigns every SOP, and it is final — an implementation MUST NOT reinstate a decommissioned agent, nor lower its containment to suspended or quarantined (which would make it reinstatable); such a request is refused AGENT_DECOMMISSIONED (HTTP 409). Its work is taken over by a newly provisioned agent.

7.1.3. "All actions" includes the ones already authorized: an unclaimed hold of a contained agent cannot be claimed or captured, and is voided, so a reinstatement never revives an authorization granted before the containment (8.7.17).

7.2 Operating modes

7.2.1. Four rungs, least to most autonomous: read_only · restricted · supervised · autonomous.

7.2.2. read_only denies any value-bearing action up front (MODE_READ_ONLY).

7.2.3. restricted and supervised do not deny. They impose an escalate floor applied after deterministic evaluation: an action the rules would otherwise permit is routed to a human instead. A rule-driven block or escalate already outranks the floor, so applying it earlier would only mask a more specific reason.

7.2.4. The effective rung is the operator's manual pin when set, otherwise the trust-derived rung. Mode changes SHOULD be audited.

7.3 Reassessment

7.3.1. A material change to an agent's authority (for example a large increase in a payment cap) invalidates its current assurance. A flagged agent's value-bearing actions escalate with REASSESSMENT_PENDING until an owner clears the flag.


8. The authorize protocol

8.1 Endpoint

POST /policy/mandate/authorize                          (PUBLIC, unauthenticated)
POST /policy/mandate/authorize/{authorizationId}/capture
POST /policy/mandate/authorize/{authorizationId}/void

The gate is public by design: any counterparty may check any agent's authority. Authority to act comes from the mandate, not from an API key.

8.2 Request

{
  "agentDid":  "did:hedera:testnet:...",
  "action":    "flight-purchase",
  "amount":    150,
  "currency":  "USD",
  "merchant":  "skyward-air",
  "resource":  "crm.contacts",
  "jurisdiction": "SG",
  "itinerary": { "tool": "book-flight", "riskLevel": "low" },
  "nonce":     "b1c2…(8–128 chars)",
  "issuedAt":  "2026-07-10T05:12:00Z",
  "signature": "<hex Ed25519>"
}
  • merchant and resource are top-level signed fields (§8.3.1), each optional and each the empty string in the signed message when absent. resource names what the action touches and is checked against a resource-scoped mandate. The signed values are applied last, so they always take precedence over a same-named key in the unsigned itinerary: a resource placed there cannot satisfy a resource scope.
  • jurisdiction (optional, ISO 3166-1 alpha-2) is a top-level signed field too; a request that carries it is signed over the v2 message (§8.3.12). It is the only jurisdiction the gate enforces — one placed in the itinerary is ignored.
  • itinerary carries request context — the atom inputs beyond the signed core. It is unsigned: every value in it, riskLevel included, is an agent's own claim and is treated as one (§6.3).
  • amount and currency may be omitted together for a non-financial action; a mandate that carries a spend constraint still requires a real amount.
  • nonce is single-use; issuedAt bounds freshness.
  • An optional trace object correlates a multi-step workflow.

8.3 Canonical signed message

8.3.1. The signature MUST be an Ed25519 signature over the UTF-8 string formed by joining these eight fields, in this order, with | (U+007C), substituting the empty string for an absent merchant or resource:

agentDid | action | amount | currency | merchant | resource | nonce | issuedAt

8.3.2. Verifiers MUST reconstruct the message from the fields received, never trust a client-supplied message, and verify against the key resolved from agentDid.

The three clauses that follow exist because each fails as SIGNATURE_INVALID and as nothing else — a well-formed request, a correct key, and bytes that simply differ from the ones the verifier rebuilds.

8.3.3. Private key encoding. Managed provisioning issues the key as hex-encoded DER PKCS#8 (ASN.1 SEQUENCE tag 30), not a raw 32-byte seed. An implementation SHOULD accept either and MUST NOT assume the raw form.

8.3.4. Number stringification. amount is signed as text, so its rendering is part of the protocol. The canonical rendering is ECMAScript String(n): an integral value carries no fractional part (150.0 → "150"). Languages whose default float formatting keeps a trailing .0 MUST normalise. The disagreement is not only the trailing .0: outside 1e-6 ≤ |n| < 1e21 ECMAScript uses exponent form with its own spelling (5e-7 is "5e-7", 1e21 is "1e+21"), and below 1e-4 a language's default may already differ — Python's repr(0.00005) is "5e-05" where ECMAScript writes "0.00005". A verifier reconstructs this text from the JSON number it parsed, so an integer beyond 2^53 is signed as the double a JSON parser holds, not as its exact digits. Values ≥ 1e21 SHOULD NOT be sent.

8.3.5. Field identity. Every field is signed as the literal string transmitted. Any RFC 3339 rendering of issuedAt a verifier can parse is acceptable, but the signed and transmitted renderings MUST be byte-identical. Formatting a timestamp twice — once for the message, once for the body — is the common violation, and at second precision it fails only when a second happens to tick between the two calls.

8.3.6. A verifier MAY attach a non-normative hint to a SIGNATURE_INVALID verdict naming §8.3.3–§8.3.5 as usual causes. The hint is diagnostic only and MUST NOT enter the digested decision content.

8.3.7. A verifier MAY distinguish a request that verifies against a prior canonical message shape (e.g. the seven-field message from before resource existed) from a genuinely invalid signature, returning CLIENT_PROTOCOL_VERSION_UNSUPPORTED instead of SIGNATURE_INVALID — still refusing the request either way, but naming the real cause (an outdated client) rather than implying a broken or rotated key.

8.3.8. Conformance vectors. docs/protocol/authorize-vectors.json publishes known-answer vectors for the signed message: for each, the eight input fields, the exact message, and the Ed25519 signature a fixed public test key produces over its UTF-8 bytes (Ed25519 is deterministic, so every implementation must reproduce the same bytes). The vectors cover the cases that have each broken a client — an integral float (150.0 → 150), a fractional amount, a sub-cent amount (0.00005), the exponent forms (1.5e-7, 1e+21), an integer beyond 2^53, an amount near 1e21, an absent merchant and resource, resource alone, a literal | and \ in a field, and non-ASCII text. An implementation SHOULD reproduce every vector before it is trusted with a real request. The reference implementation checks them in its own test suite, and so does the reference Python client, against the same file: a message format that changes in one place and not the other fails a test instead of shipping — which is what happened when a client signed seven fields against a gate that required eight.

8.3.9. Payload binding (optional). The eight signed fields do not cover a payee, an account number, a passenger list, a route — every other field a real tool takes. A gateway can compare amount, merchant and resource in what it forwards, but a field the signature never mentioned is either refused (the tool cannot be used) or allowed and unbound. An agent that wants the WHOLE payload held to what it authorized signs a digest of it:

payloadDigest    = "sha256:" + lower-case-hex( SHA-256( UTF-8( canonical(payload) ) ) )
payloadSignature = Ed25519( UTF-8( MAGP-PAYLOAD-v1 | agentDid | action | nonce | issuedAt | payloadDigest ) )
  • canonical is RFC 8785 (JSON Canonicalization Scheme): object keys sorted by UTF-16 code unit, no insignificant whitespace, ECMAScript number and string serialisation (§8.3.4 applies to every number in the payload). The input is JSON. undefined, NaN, Infinity, functions, BigInt and an unpaired surrogate are refused, not normalised: two languages would normalise them differently, and a digest they compute differently is a binding that silently never matches. A payload nested deeper than 32 levels or larger than 256 KiB once canonical is refused too. The digest is of the JSON the executing side RECEIVES, so a value a language holds differently from JSON (a Python 250.0, an integer beyond 2^53) is digested as the JSON number it becomes.
  • The fields are escaped exactly as in §8.3.1, and the message is domain-separated from the §8.3.1 message and from the service messages of §8.7.6: a signature made for one purpose cannot be replayed for another. It names the agent, action, nonce and issuedAt of the authorize request it accompanies, so a digest cannot be lifted from one authorization onto another.
  • payloadDigest and payloadSignature are two optional top-level request fields, both or neither. They are not a ninth field of §8.3.1, which every existing client signs as eight; a request that carries neither is exactly what it always was.
  • A verifier MUST, once the §8.3.1 signature verifies (check 5, §8.5), verify the payloadSignature under the same key, and MUST block a request whose binding is present but not valid — a digest with no signature, a signature with no digest, a digest that is not sha256: and 64 lower-case hex, or a signature that does not verify — with PAYLOAD_BINDING_INVALID. A digest nobody signed binds nothing, so it is refused, never stored.
  • The verifier MUST store the digest with the hold, and with an escalation when the action is held for a human, so that an approved hold keeps it.
  • Acknowledgement. An allow, observe or escalate verdict for a request that carried a binding MUST state the digest the authorization is bound to (data.payloadDigest, null for an unbound one), outside the digested decision content. The binding is not covered by the §8.3.1 signature, so a hop that strips the two fields would otherwise leave the agent believing an unbound authorization is bound, as would an issuer that predates this section and ignores fields it does not know. An agent that sent a digest MUST treat a permit or escalation whose payloadDigest is not the digest it sent — including an absent one — as not bound: refuse it (PAYLOAD_BINDING_NOT_CONFIRMED) and release the hold it was given.
  • A reviewer's MODIFY decision (§10) changes the action the agent signed, so the hold it mints — or the one minted when the re-entered review is approved — carries no digest at first: it is unbound until the agent binds the payload of the action that WILL run (§8.3.11). An executor that requires binding does not claim it before then; one that does not require it would run the modified action without the guarantee. The status a polling agent reads for such a review reports whether the hold it was handed carries a digest (payloadBound, a boolean and never the digest itself, present only when there is a hold), so the agent knows it still has to bind.
  • Owner requirement. A mandate's permission may carry requirePayloadBinding: true (issued with the mandate, carried in the signed mandate, never dropped by a delegation, §5.4). For such an action the verifier MUST block a request that carries no binding — after the §8.3.1 signature verifies, so only the agent learns the setting — with PAYLOAD_BINDING_REQUIRED, and MUST refuse to grant a claim (§8.7.11) on a hold that carries no digest, with the same code, until the agent has bound one (§8.3.11). Without it, binding stays the agent's or the executor's choice: an agent, or a hop that strips the two fields, can leave a hold unbound.
  • The comparison that gives the binding its force is made at the claim (§8.7.11).

8.3.10. Payload-binding vectors. docs/protocol/payload-binding-vectors.json publishes known-answer vectors for §8.3.9: canonical forms and digests (key order, whitespace, numbers in every exponent form, integers beyond 2^53, string escapes, non-ASCII text, key order beyond the Basic Multilingual Plane), inputs that MUST be refused, and complete bindings — message and the Ed25519 signature a fixed public test key produces over it. The reference implementation checks them in its test suite, and so does the reference Python client, against the same file. The same file holds the vectors for §8.3.11.

8.3.11. Binding a payload to a hold that already exists. A hold can exist with no digest: a reviewer's MODIFY changes the action the agent signed (§8.3.9), so the hold minted for the modified action has none. The agent binds the payload of the action that will run with

POST /policy/mandate/authorize/{authorizationId}/payload-binding
{ agentDid, action, nonce, issuedAt, payloadDigest, payloadSignature }
payloadSignature = Ed25519( UTF-8( MAGP-PAYLOAD-REBIND-v1 | agentDid | action | authorizationId | nonce | issuedAt | payloadDigest ) )

The endpoint is public: the agent's signature is the credential, exactly as for /authorize.

  • The message is the §8.3.9 message with the authorization id added, under a domain of its own (MAGP-PAYLOAD-REBIND-v1, escaped as §8.3.1). It names the hold, so a digest cannot be lifted onto another one, and no signature made for one of the two messages can verify as the other.
  • The verifier MUST check that the hold exists; that payloadDigest is sha256: and 64 lower-case hex; and that payloadSignature verifies under the key agentDid resolves to — and only THEN say anything about the hold: that it was issued to agentDid and (when its mandate names an action) to action, so an unauthenticated caller cannot probe a hold's owner or action by authorization id. It MUST then check that issuedAt is fresh and consume nonce once, in that order, so a forged request cannot burn a victim's nonce.
  • An agent that is contained (suspended, quarantined or decommissioned, §7.1) is refused with its containment code (AGENT_SUSPENDED, AGENT_QUARANTINED, AGENT_DECOMMISSIONED, 403): it is denied everything at /authorize and does not get to keep arranging what its existing holds will execute. Checked once the signature has proven it is that agent asking, so a stranger learns nothing about the agent's status.
  • It MUST apply the digest with one conditional update that changes the hold only while it is live (held, and inside the same hold window a claim is held to), unclaimed and carrying no digest. That single statement is what makes the binding atomic against a claim and against a second binding. A digest is never overwritten.
  • The same digest already on the hold is an idempotent success (200, alreadyBound: true): a retry of a binding whose response was lost — but only for a hold that is otherwise still live; a voided or lapsed hold is reported as such first. A different digest is refused.
  • The response echoes the digest it now holds (data.payloadDigest). An agent MUST treat a success that does not echo the digest it sent — an issuer that predates this clause — as not bound.
Refusal HTTP Meaning
AUTHORIZATION_NOT_FOUND 404 no such hold
AUTHORIZATION_AGENT_MISMATCH · AUTHORIZATION_ACTION_MISMATCH 403 the hold is another agent's, or for another action
PAYLOAD_BINDING_INVALID 403 a malformed digest of up to 80 characters, an unresolvable key, or a signature that does not verify (a non-string or longer payloadDigest fails request-schema validation with 400)
AGENT_SUSPENDED · AGENT_QUARANTINED · AGENT_DECOMMISSIONED 403 the agent is contained (checked after the signature)
REQUEST_EXPIRED 403 issuedAt outside the freshness window
REPLAY_DETECTED 409 the nonce was already used
PAYLOAD_ALREADY_BOUND 409 the hold already carries a different digest
AUTHORIZATION_ALREADY_CLAIMED · AUTHORIZATION_VOIDED · AUTHORIZATION_EXPIRED 409 the hold is no longer unclaimed and live

A hold that was bound at authorize time cannot be re-bound. Binding does not change what the hold authorizes (amount, merchant, action): it only names the payload the agent is willing to have executed under it.

8.3.12. Signed jurisdiction (message v2). A request MAY carry a top-level jurisdiction: an ISO 3166-1 alpha-2 code, sent upper-case. A request that carries it MUST be signed over the v2 message — the eight §8.3.1 fields, then the literal version tag MAGP-AUTH-v2, then the jurisdiction exactly as transmitted, ten fields, each escaped as in §8.3.1:

agentDid | action | amount | currency | merchant | resource | nonce | issuedAt | MAGP-AUTH-v2 | jurisdiction
  • A request that does not carry it is signed over the §8.3.1 message, byte for byte as before: every existing client keeps verifying.
  • A verifier MUST choose the message from the request itself — jurisdiction present → v2, absent → v1 — and MUST NOT try the other. Adding the field to a v1-signed request, changing it, or stripping it from a v2-signed one therefore fails SIGNATURE_INVALID. A present value that is not two ASCII letters is refused (MALFORMED_REQUEST).
  • The effective jurisdiction of a request is, in order: the country the mandate's owner registered for the request's payee (the signed merchant's active entries in the owner's payee directory, PUT /policy/payees/country), when it has one — the registry is authoritative; else its signed jurisdiction, upper-cased; else none. It is the ONLY value the gate enforces: the mandate's allowed-jurisdictions term (an mm:jurisdiction isAnyOf constraint) and the jurisdiction-not-allowed atom are both judged on it. An itinerary jurisdiction / mm:jurisdiction is the agent's unsigned word and is ignored for enforcement.
  • A registered country stands in for a missing signed value (so a registered payee satisfies the requirement below). A signed jurisdiction that differs from it — or entries for one payee that name more than one country — is blocked JURISDICTION_MISMATCH, a hard block, whether or not the mandate restricts jurisdictions. The decision record's jurisdiction control names the source (signed | registered), and the rule context labels the value agent_signed or authoritative accordingly.
  • When the mandate restricts jurisdictions for the action, or an enforced (non-observe) SOP or Standard molecule judges jurisdiction-not-allowed, a request with no effective jurisdiction is blocked JURISDICTION_REQUIRED; one outside the mandate's list is blocked JURISDICTION_NOT_ALLOWED. Both are hard blocks and outrank an escalate (§8.5). With neither a restriction nor such a rule, a request behaves exactly as before, with or without the field.
  • The verifier MUST store the signed jurisdiction with an escalation (and commit it in the row's integrity hash); a reviewer cannot change it. An approval or a MODIFY (§10) resolves the effective jurisdiction again from it and the payee registry AS OF the approval — so an owner who changed the payee's country since the review opened is heard: a stored signed value that now contradicts it is refused JURISDICTION_MISMATCH. With no effective jurisdiction, a review under a jurisdiction-restricted mandate, or under an enforced jurisdiction rule, is refused APPROVAL_INPUT_UNAVAILABLE.
  • docs/protocol/authorize-v2-vectors.json publishes known-answer vectors for the v2 message under the §8.3.8 test key; a separate file, so a client that pins the eight-field vectors keeps passing until it implements v2.
  • POST /magp/private-authorize (§15) is unchanged: it never sees the jurisdiction, and proves membership in the mandate's list in zero knowledge.

8.3.13. Context signature (optional). The itinerary, trace and materiality are not in the signed message, so anything between the agent and a verifier can rewrite them. A request MAY carry envelopeSignature: an Ed25519 signature, by the same key as signature, over the UTF-8 bytes of the request's envelope hash — the lower-case hex SHA-256 of the GovernanceEnvelope built from the request as transmitted (no defaults applied; context = the itinerary, omitted when absent or null, present when {}), minus its integrity block, with object keys sorted recursively and undefined values dropped. resource and jurisdiction are not in it; the §8.3.1 / §8.3.12 message signs them.

  • A verifier MUST check a present envelopeSignature after signature (§8.5 step 5) and before any rule reads the context, over the very itinerary object it will evaluate. One that does not verify — the context was altered after signing, or it was signed with another key — is refused CONTEXT_SIGNATURE_INVALID, fail closed.
  • Absent, the request is judged as before: the signature is optional on the wire (the reference clients send it by default — agentsafe-guard ≥ 0.17.0, metamynd-client ≥ 0.7.0 — and opt out with signContext: false / sign_context=False). A relay can also strip it, so a counterparty that relies on the context SHOULD require it: the reference receivers (agentsafe-mcp-guard ≥ 0.17.0, agentsafe-a2a-guard ≥ 0.12.0, agentsafe-http-gateway ≥ 0.15.0) refuse an unsigned context with CONTEXT_SIGNATURE_REQUIRED when built with requireContextSignature: true. That code is receiver-only: the gate has no such option.
  • It attributes the context to the agent; it does not make it true (§17.4).
  • docs/protocol/context-signature-vectors.json publishes known-answer vectors under the §8.3.8 test key: the request, the canonical envelope, its hash and the signature.

8.4 Response

200 on a permitting disposition; 403 on a refusing one. Body is { success, message, data } where data is the verdict:

{ "decision": "allow", "reasonCode": "AUTHORIZED",
  "authorizationId": "…", "remaining": 350, "proofRef": "…",
  "expiresAt": null, "escalationId": null,
  "envelopeId": "env:…", "envelopeHash": "sha256:…" }

proofRef is the ledger anchor of the hold. An implementation MAY anchor after answering, so that the ledger round trip is not part of the request; proofRef is then null in the response, and the anchor is recorded on the hold when it lands. The reference implementation does this by default (LEDGER_ANCHOR_MODE=sync waits for the anchor and returns it). The same applies to proofRef on capture. A hold refused at the cap check is not anchored.

An implementation that anchors after answering MUST NOT lose an anchor it owes. The reference implementation records each owed anchor durably before writing the row it describes, and embeds an anchorId in the ledger message. A sweep settles any anchor still owed after a grace period. If the row was never written, the anchor is abandoned. If a message with that anchorId is already on the ledger, it is recorded. Only otherwise is it submitted again. A ledger that cannot be searched at that moment is not taken as "absent", and the anchor is retried later.

8.5 Order of checks (normative)

A conformant evaluation MUST apply these in order and MUST stop at the first that fails, returning its reason code — with one exception: an escalate at steps 11–12 is not a failure that stops evaluation (see Escalation never outranks a hard block below).

# Check On failure
1 Mandate exists for (agentDid, action) MANDATE_REVOKED (every mandate for the action was revoked, §6.2.6) · NO_PERMISSION_FOR_ACTION (the agent holds mandates for other actions) · NO_MANDATE (it holds none)
2 Principal authority in force — mainnet free-tier caps (§4.4.1); verification is reported, not required, on testnet PRINCIPAL_UNVERIFIED
3 Compliance with every enforced Standard STANDARD_NONCOMPLIANT
4 Agent exists; key proven AGENT_NOT_FOUND · AGENT_KEY_UNVERIFIED
5 Signature valid over §8.3; then a present context signature (§8.3.13) SIGNATURE_INVALID · CONTEXT_SIGNATURE_INVALID
6 Not contained AGENT_SUSPENDED · AGENT_QUARANTINED · AGENT_DECOMMISSIONED
7 Operating-mode block tier MODE_READ_ONLY
8 Freshness, then nonce consumed atomically; then the effective jurisdiction resolved (§8.3.12) REQUEST_EXPIRED · REPLAY_DETECTED · JURISDICTION_MISMATCH
9 Rate limit, if enabled RATE_LIMIT_EXCEEDED
10 Circuit breakers, if enabled CIRCUIT_BREAKER_OPEN
11 Standard rule molecules the firing molecule's code
12 SOP molecules; then, if an enforced Standard or SOP rule judges jurisdiction-not-allowed and there is no effective jurisdiction the firing molecule's code · JURISDICTION_REQUIRED
13 Mandate evaluation (§5), the allowed-jurisdictions term on the effective jurisdiction e.g. SPEND_LIMIT_EXCEEDED · JURISDICTION_REQUIRED · JURISDICTION_NOT_ALLOWED; success AUTHORIZED
14 Fold in OBSERVE (§8.6.2) allow → observe
15 Operating-mode escalate floor; reassessment floor; spend-anomaly floor (graph-backed, opt-in); runtime assurance floor (opt-in) MODE_* · REASSESSMENT_PENDING · SPEND_PATTERN_ANOMALY · AGENT_NOT_CERTIFIED · ASSURANCE_STALE
16 Atomic spend reservation CAP_EXCEEDED

Notes on the ordering, each of which is a security property rather than an accident:

  • Steps 1–3 precede signature verification because they are questions about the policy, not the requester. They cost one query each and reveal nothing.

  • Nonce consumption (8) MUST follow signature verification (5), or a forged request could burn a victim's nonce.

  • Containment (6) precedes everything action-specific, so a contained agent cannot reach evaluation at all.

  • The mode escalate floor (15) follows deterministic evaluation, so a specific rule denial is reported instead of a generic mode reason.

  • Escalation never outranks a hard block. A Standard or SOP molecule that escalates (step 11 or 12) — including CONTEXT_UNVERIFIABLE, a rule that could not get a trustworthy value for a field it needs, such as a missing riskLevel (§6.4.3) — MUST NOT end evaluation. The escalate is carried; the remaining rule stages and the mandate evaluation (13) still run, and any block, suspend or quarantine among them (an SOP cap, the mandate's per-transaction or cumulative cap, an action the mandate never granted) is the verdict. The carried escalate is the verdict only when everything after it would otherwise permit (or itself escalates), and it keeps its own reason code. Escalation can therefore only lift a would-be permit to human review, never downgrade a block: otherwise an agent could turn any hard cap into a review by omitting a field. (Through v1.69.0 a Standards-stage escalate returned at once, so a request with no riskLevel over an SOP or mandate cap was escalated rather than blocked.) A molecule's own block still stops evaluation at its stage, as before.

  • Jurisdiction is one resolved value, judged in three places. After the signature (5) — which covers a top-level jurisdiction through the v2 message (§8.3.12), a malformed value being MALFORMED_REQUEST — the gate resolves the request's effective jurisdiction, in this order: (a) the country the mandate's owner registered for the signed merchant in the payee directory, when it has one — authoritative; (b) else the signed jurisdiction, upper-cased; (c) else none. The itinerary's jurisdiction / mm:jurisdiction is removed before any rule or the mandate sees the context, so it can neither satisfy nor fail a check. Three refusals follow, all hard blocks that outrank a carried escalate:

    • JURISDICTION_MISMATCH (step 8, before any rule runs): a signed value that differs from the registered country, or a registry naming two countries for one payee. It applies whether or not anything restricts jurisdictions.
    • JURISDICTION_REQUIRED: no effective jurisdiction while one is needed — the mandate's allowed-jurisdictions term (step 13), or an enforced (non-observe) Standard or SOP rule that judges jurisdiction-not-allowed (end of step 12; the atom itself does not fire on a missing value, so without this the rule would pass silently).
    • JURISDICTION_NOT_ALLOWED (step 13): an effective jurisdiction outside the mandate's list. (An effective jurisdiction outside an SOP/Standard allow-list fires the molecule, with that molecule's own disposition and code.)

    Compatibility guarantee. A mandate with no allowed-jurisdictions term, for an agent bound to no enforced jurisdiction rule, is judged exactly as before this field existed — for a v1 client and a v2 client alike, with or without the field — with one exception: a request that signs a jurisdiction contradicting the country the owner registered for its payee is refused JURISDICTION_MISMATCH. A request that signs nothing is never refused for the registry's sake, and one to an unregistered payee (or a payee with no country) is never refused JURISDICTION_MISMATCH.

  • An approval cannot exceed the hard caps either (§10). Approving an escalation, or a reviewer's MODIFY that re-enters the gate, re-runs the deterministic layers on the request as it stands at the moment of the decision, with every input the live gate has — the mandate (1: still active and still the agent's current one for the action), the principal (2), Standards compliance (3), the agent's key, containment and mode (4, 6, 7), the rate window (9) including the callCount a rule reads, circuit breakers (10), committed spend, counterparty trust, the Standard and SOP molecules (11, 12) and the mandate (13) with the signed operands, resource included. Only transport authentication (5, 8) is not repeated. Any block-class verdict refuses the approval with that reason code — the review stays pending, and the reviewer may still deny it — and only escalates are disregarded, being what the reviewer is approving. An input that cannot be reconstructed refuses the approval (APPROVAL_INPUT_UNAVAILABLE); it is never skipped. The atomic reservation (16) then still applies (CAP_EXCEEDED). (Through v1.69.1 the approval's rule context carried the committed spend alone, so a call-count or trust rule could not fire at approval, and the rate ceiling, breakers, mode, compliance and a revoked mandate were not consulted.)

  • The spend-anomaly floor (15, opt-in) only ever lifts a permit to escalate, never downgrades a stricter verdict — same precedence discipline as the mode/reassessment floors it shares a step with. It answers a different question than rate limits (9) or circuit breakers (10): those enforce a threshold a human pre-set; this flags a deviation from the agent's OWN recorded history, which no fixed threshold could have anticipated.

  • The runtime assurance floor (15, opt-in) escalates on a blocked or expired deployment-assurance certificate (issued out-of-band via POST /assurance/deployment/:ref; not otherwise specified here). Same precedence discipline: only ever lifts an otherwise-permit verdict to escalate, never a hard block — an agent that has been operating is not cut off the instant its certificate lapses; a human reviews before the next action refuses outright. An agent never issued a certificate is unaffected (this floor is opt-in per agent, not retroactive against the whole fleet).

  • Reservation (16) is last and atomic: it is the only step that mutates shared budget, and a lost race MUST return CAP_EXCEEDED.

  • Steps 9 and 10 are secondary controls and MUST fail open on internal error (§9): a supporting control must not take the gate down.

8.6 Dispositions

8.6.1. Six: allow · observe · block · escalate · suspend · quarantine.

8.6.2. observe permits the action and flags it for monitoring. It does not fail fast; it is collected across the rule and SOP layers and folded in most-restrictive-wins, lifting an otherwise-allow verdict.

8.6.3. allow and observe permit execution. Everything else MUST stop the action.

8.6.4. escalate is a hold, not a denial — the action waits for a human (§10). A caller MUST NOT treat it as permission, and an agent MUST NOT self-approve.

8.6.5. An unknown disposition MUST be treated as a block.

8.7 Spend cap and settlement

8.7.1. On a permitting verdict for a value-bearing action the evaluator MUST atomically reserve the amount against the mandate's cap.

8.7.2. After the real-world charge settles the actor SHOULD capture with the actual amount. A hold that no counterparty has claimed expires and auto-voids.

8.7.3. A claimed hold is a commitment. Once a counterparty has claimed a hold for execution (POST /policy/mandate/authorize/{id}/effect/dispatching, effect state dispatching or later) the effect may already have happened, so the reservation MUST remain against the cap until it is settled or the claimer proves it did not happen. It MUST NOT lapse with the hold TTL, and it MUST NOT be releasable by any caller that is not the claimer.

8.7.4. The capture and void endpoints are public and keyed only by the authorization id, which the agent holds. A settled amount below the authorized amount is therefore a claim by an untrusted party. An implementation MUST NOT accept it for a claimed hold, nor void a claimed hold, unless the caller presents the claim token returned by that hold's successful claim (data.claimToken, in the body as claimToken). Settling at the full authorized amount needs no token. Before any claim, nothing can have been executed, and a lower capture or a void makes the hold unclaimable — but the authorization id travels (the agent hands it to every Service it asks to execute), so it is not a credential. A capture or void of a hold nobody has claimed MUST come from one of: the hold's own agent, with agentProof: { agentDid, nonce, issuedAt, signature } in the body — its signature over MAGP-SETTLE-v1|verb|agentDid|authorizationId|nonce|issuedAt|...fields (verb capture or void; the fields those of the call as in 8.7.6: capture amountCharged, bookingRef, settlementTxHash; void reason; each escaped as in 8.3.1), fresh and single-use like an authorize request; or a counterparty signing as itself (8.7.6) that the owner registered and whose registry entry would let it claim this hold. On every owner — one who registered no claimant included — an anonymous capture or void of an unclaimed hold MUST be refused: unlike a claim, which can only execute what the hold allows, a settlement keyed by an id that travels would let a stranger void the agent's hold or commit a fraction of it. Anything else is refused with the code a claim by that caller gets (COUNTERPARTY_AUTH_REQUIRED, COUNTERPARTY_NOT_REGISTERED, COUNTERPARTY_NOT_ALLOWED_FOR_MERCHANT, HTTP 403), and an agentProof that does not verify AGENT_SETTLE_SIGNATURE_INVALID (401); the hold is untouched either way.

8.7.5. A claim is single-use and MUST be arbitrated atomically: of any number of simultaneous claimants exactly one succeeds. Every other claim of that authorization — a simultaneous loser, a later replay, a claim after the effect was dispatched, went unknown or was settled — MUST be refused with the one stable code AUTHORIZATION_ALREADY_CLAIMED (HTTP 409), carrying no claim token. EFFECT_TRANSITION_CONTENDED is reserved for a claim that lost for an unexplained reason and left the hold claimable, or that overlapped an earlier attempt still being recorded (a claimant SHOULD retry it once); AUTHORIZATION_VOIDED and AUTHORIZATION_EXPIRED keep their own meaning. The hold window is enforced in the claim itself, not only in an earlier read, so a hold that lapses between the two is not claimed. No response may carry storage-layer error text.

8.7.6. Counterparty identity. A claim token (8.7.4) is a bearer secret: it proves "I made the claim", not who the claimer is. A counterparty that holds a key SHOULD instead sign the settlement calls with a did:key or did:hedera identity it controls. The signed message is MAGP-SERVICE-v1|action|authorizationId|field…|nonce|issuedAt, where action is one of claim, dispatched, unknown, capture, void, refund, reconcile; the fields are those of the call (claim: the Idempotency-Key of 8.7.7 when one is sent, then payload=<digest> of 8.7.11 when a digest is stated; capture: amount, bookingRef, settlementTxHash; unknown/void: reason; refund: amount — the empty string for a full refund — then reason; reconcile: none), each escaped for \ and |; and the signature is Ed25519, carried in the headers x-magp-service-did, x-magp-service-nonce, x-magp-service-issued-at and x-magp-service-signature.

Refund. POST .../refund { amount?, reason?, claimToken? } records a reversal of a captured hold: no money moves, but the cumulative cap counts less. Because it frees budget, it MUST be accepted only from the hold's claimer — its signature under the refund action, or for an anonymous claim its claim token — the owner of the hold's mandate, or a platform admin; anyone else MUST be refused COUNTERPARTY_MISMATCH (HTTP 403, the bare code as the message and in data.reasonCode). A hold captured without a claim has no claimer: only its owner or an admin may refund it. refund is its own action, so a void signature can never be replayed as a refund, nor a refund as a void (earlier drafts signed a refund as void; such a signature is refused COUNTERPARTY_SIGNATURE_INVALID).

  • The verifier MUST check the signature against the key the DID itself commits to, reject a stale or future issuedAt, and consume the nonce once. A malformed, unsupported, expired, replayed or badly signed identity MUST be rejected with a COUNTERPARTY_* reason code; it MUST NOT fall back to anonymous handling.
  • The identity that signs the claim is recorded as the claimer. Every later capture, void or unknown for that hold that lowers or releases the reservation MUST be signed by the same identity. No claim token is issued to an authenticated claimer, so there is nothing to leak to the agent.
  • An implementation MAY require an authenticated claimer for every claim (MANDATE_REQUIRE_COUNTERPARTY_AUTH=true); a claim with no identity is then refused with COUNTERPARTY_AUTH_REQUIRED.
  • Scope: this proves the claimer controls a key. On its own it does not prove the key belongs to a counterparty the mandate's owner trusts — an agent could claim under a key of its own. Binding the claimer to a counterparty the owner has registered is the trusted-counterparty registry of 8.7.10, applied at the claim: an owner with any registry entry (and every hold of a mainnet agent, or every owner when the deployment requires it) accepts only a claim signed by an active, registered identity.
  • A settlement call that lowers or releases a claimed hold and is not made by its claimer is refused COUNTERPARTY_MISMATCH (HTTP 403) — for void as for unknown (8.7.12).

8.7.7. Idempotent claim. A claim whose response is lost is ambiguous to its claimant: it cannot tell "my claim landed" from "someone else claimed", and without more it must abandon a hold it actually owns, leaving it committed with nobody executing it. A claimant MAY therefore send an Idempotency-Key header with a claim: 32–128 characters of [A-Za-z0-9._:~-] (128 bits of randomness, e.g. 32 hex), chosen per claim call and reused only for retries of that call. A malformed key — including one shorter than 32 characters or one that contains the authorization id, the value the agent is certain to know — is refused (400 IDEMPOTENCY_KEY_INVALID), never ignored.

  • The issuer records only a hash of the key, bound to the authorization, inside the tamper-evident effect chain; the key itself is never stored or returned.
  • A retry is answered as a replay — 200 with the original grant and replayed: true, no second claim recorded — only if all hold: the effect is still dispatching on both the chain and the hold row, and the hold is still held (not settled, released or parked unknown); the chain's claim carries this key's hash; and it was made by the same identity. Otherwise it is an ordinary AUTHORIZATION_ALREADY_CLAIMED. A replay after the effect was settled, released or went unknown is therefore refused: a late "claim" MUST NOT invite a second execution. A retry that overlaps its own first attempt (claimed on the row, not yet written to the chain) is answered EFFECT_TRANSITION_CONTENDED, which the claimant retries.
  • A claimant that is executing still reads dispatching, so a delayed duplicate of the same call can be answered with the grant. Single execution rests on the claimant using a fresh key per claim call and never reusing one for a second execution, and on the key staying secret from the agent — for an anonymous claim it is the only thing that lets a retry recover the claim token, which is why an authenticated claim (8.7.6) is preferred.
  • For an authenticated claim the key is part of the signed fields (8.7.6), so it can be neither added nor stripped in flight.
  • The authorization id is the effect's natural idempotency key for the upstream. A gateway SHOULD send it to the upstream as Idempotency-Key, and MUST NOT forward an Idempotency-Key the agent supplied (it is replaced when a hold was claimed and removed when none was), so an upstream that de-duplicates on it makes the effect exactly-once even if the gateway is asked to run the same call twice. It is not a valid key for a claim (above): the agent knows it.

8.7.8. Outcome lookup. GET /policy/mandate/authorize/{id}/effect (public, keyed by the authorization id) answers what became of an authorization, in addition to its effect state:

outcome meaning nothingExecuted retrySafe
not_started authorized, unclaimed, inside its hold window true false
expired unclaimed, past its window; budget already returned true true
not_executed voided before a claim, cancelled by its claimer, or failed clean true true
in_flight claimed (dispatching / dispatched) false false
settled settled; the spend is committed false false
unknown ambiguous or under reconciliation false false
reversing / reversed / reversal_failed settled, then compensated false false

The two bits answer different questions. nothingExecuted is a fact about now: nothing has executed so far. retrySafe is a promise about the future — nothing can execute later either — and is the only safe basis for issuing a fresh authorization for the same intent. It is false for not_started: that hold is still claimable until its window closes, so a request queued behind a slow gateway could still claim and run it while a re-issued authorization runs too. To retry a not_started hold, void it first (a void is atomic against a claim) and read not_executed. An unknown or in_flight outcome MUST NOT be retried blindly.

The effect chain is authoritative (a mandate revoke voids the row without touching the chain, so a claimed hold revoked mid-flight still reads in_flight), except that a hold row ahead of the chain — a claim decided on the row but not yet recorded — is honoured and reads in_flight. An unrecognised state fails closed to unknown. The response also carries claimed, claimedAt, currency, updatedAt, authorizedAmount (before and after settlement, 8.7.9), settledAmount and settlementEvidence. It never carries a claim token or the claimant's identity.

Capture and void are not idempotent the way a claim is (8.7.7). Retrying either after it already succeeded does not replay the original response: POST .../capture on a hold that is no longer held answers 409 NOT_HELD; POST .../void on one answers 200 with { voided: false, reasonCode: "NOT_HELD" }. Both look like failures to a naive retry-with-backoff loop, but usually mean the first call already landed — a connection that dropped after the write, not before it. A caller that retries capture or void after a timeout, a dropped connection, or a 5xx MUST read GET .../effect before concluding the call failed: outcome: settled after a capture, or an outcome consistent with what the void was attempting, confirms it already succeeded. Only a genuine, confirmed failure is safe to retry.

A refused settlement call is a 4xx, never a 5xx, and has one shape. A refused capture, void or refund answers with the bare reason code as the message and data: { captured | voided | refunded: false, authorizationId, reasonCode, detail }, where detail is an explanatory sentence for people — a client MUST branch on reasonCode (or message), never on detail. Every refusal of these three calls has a code, and its HTTP status follows from the code alone — the same code is the same status on every call it can occur on:

reasonCode HTTP on when
AUTHORIZATION_NOT_FOUND 404 capture · void · refund no hold under this id (the same answer the effect/* routes give)
HOLD_UNDER_RECONCILIATION 409 capture · void the owner forced the hold into reconciliation (8.7.13)
EFFECT_NOT_VOIDABLE 409 void the effect is dispatched or unknown (or later): it may have happened, so it is reconciled, never voided
EFFECT_NOT_CAPTURABLE 409 capture the effect is unknown (or already resolved): it is reconciled, never captured on faith
CLAIMED 409 void a claim landed while the void was in flight
COUNTERPARTY_AUTH_REQUIRED · COUNTERPARTY_NOT_REGISTERED · COUNTERPARTY_NOT_ALLOWED_FOR_MERCHANT 403 capture · void the hold is unclaimed and the caller has no authority over it: not its agent, and not a counterparty the owner's registry would let claim it (8.7.4)
AGENT_SETTLE_SIGNATURE_INVALID · AUTHORIZATION_AGENT_MISMATCH 401 · 403 capture · void an agentProof that does not verify over MAGP-SETTLE-v1, or that is another agent's (8.7.4); REQUEST_EXPIRED (401) and REPLAY_DETECTED (409) as for an authorize request
COUNTERPARTY_MISMATCH 403 capture · void · refund the hold is claimed and the caller is not its claimer (8.7.4, 8.7.6) — for capture, one settling below the authorized amount; for refund, not its claimer, owner or an admin
SETTLEMENT_NOT_CONFIRMED · SETTLEMENT_AMOUNT_MISMATCH · SETTLEMENT_NOT_FOUND 400 capture · void a lowered settlement, or a claimer's release, was not supported by the settlement observer (8.7.9)
PAYEE_NOT_REGISTERED · PAYEE_NOT_VERIFIED 400 capture a lowered settlement named no registered payee, or the payee directory could not be read (8.7.14)
NOT_HELD 409 capture the hold is already settled or released (usually: an earlier capture landed — read the outcome)
AUTHORIZATION_EXPIRED 409 capture an unclaimed hold past its window; its budget already went back to the cap (the code a claim of it gets)
AMOUNT_EXCEEDS_AUTHORIZED 400 capture amountCharged is more than the hold (8.7.9). Refused; a larger settlement observed by reconciliation is SETTLED_OVER_AUTHORIZED instead
AMOUNT_INVALID 400 capture · refund an amount that is not a finite number in range
PASSPORT_EXPIRED · PASSPORT_ALREADY_CONSUMED · PASSPORT_REVOKED 400 capture the hold's Action Passport is no longer usable (§13). Checked on a counterparty's capture only: reconciliation (8.7.12, 8.7.13, an operator's resolution) records what happened remotely and is never refused on the passport
HOLD_STATE_CHANGED 409 capture · refund the call lost a race — the hold was claimed, settled, voided or refunded between the read and the write; nothing was applied
NOT_CAPTURED 409 refund the hold is not captured
ALREADY_REFUNDED 409 refund nothing remains to refund
AMOUNT_EXCEEDS_REFUNDABLE 400 refund a partial refund larger than what remains captured
MANDATE_REVOKED 409 capture the hold is unclaimed and its mandate — or a mandate it was delegated from (5.4.10) — has been revoked (8.7.15): it can only be voided. A claimed hold keeps the claimed-hold rules
DELEGATION_CHAIN_BROKEN 409 capture the hold is unclaimed and its delegated mandate's chain cannot be walked to a root (5.4.10): fail closed; it can only be voided
MALFORMED_REQUEST 400 capture · void · refund the authorization id is not a UUID, or the body failed validation (the issues in data.issues)

NOT_HELD on void (already settled or released) remains a 200 with voided: false, as above: repeating a void that already happened is not an error. Any other failure is unexpected and is a 5xx with a generic message. The effect/* routes (8.7.5, 8.7.8, 8.7.12, 8.7.13) answer a refusal in the same shape less the captured | voided | refunded flag — data: { …, authorizationId, reasonCode, detail } — and an unknown id is 404 AUTHORIZATION_NOT_FOUND on every one of them, the outcome lookup included. Where reconciliation or an operator's resolution settles a hold and the capture is refused, the route answers 409 with the capture's code. A claim of an unclaimed hold under a revoked mandate is 409 MANDATE_REVOKED (8.7.15), or 409 DELEGATION_CHAIN_BROKEN (5.4.10).

The platform-admin routes — effect/resolve, effect/compensate/begin, effect/compensate/complete and effect/reconcile-sweep — answer in the same shape: a signed-in caller who is not a platform admin is 403 FORBIDDEN; a malformed authorization id, an unknown resolution or outcome, or a settledAmount that is not a non-negative number is 400 MALFORMED_REQUEST with the validation issues in data.issues; an unknown id is 404 AUTHORIZATION_NOT_FOUND; and a refusal by the hold's effect state keeps its code and its 409. No session at all is still 401. Up to v1.69.2 these routes put a sentence in message ("Platform admin required", "resolution must be …", "settledAmount must be …", "outcome must be …") with data: null, answered a service refusal with data carrying no authorizationId or detail, and passed a malformed id to the database (a 500).

Before v1.69.1 a refused capture carried the sentence in message and only { reasonCode } in data, and an unknown id answered 400 "Authorization not found" on all three calls. Up to v1.69.1 the refusals decided by the hold's state rather than by the observer or the owner — already settled, expired, over the authorized amount, lowered by a non-claimer (then a 400), an unusable Action Passport, a lost race, and every refund state refusal — still carried the sentence in message with data: null, all as 400s (already settled, expired, a lost race, not captured and already refunded are 409 now: they are the hold's state); a capture of an unknown effect was a 500; a malformed id answered 400 "Invalid authorization id"; effect/unknown and effect/reconcile answered 403 for an unknown id; and the outcome lookup answered 404 "Not found".

8.7.9. Authorized amount, settled amount, and what the settled amount rests on. The gate MUST record the amount it authorized once, with the hold, and MUST NOT overwrite it at settlement: the settled amount is what the cumulative cap then counts, and a settlement that replaced the authorization would leave nothing to compare it with. A settlement MUST be recorded with the evidence it rests on, one of:

settlementEvidence meaning
unattested The full authorized amount, counted for a caller that is not the claiming counterparty. It asserts nothing about what was charged; it is only the most the authorization allowed.
unclaimed_lowered Settled below the authorization before any claim, by whoever held the authorization id. Nothing can have executed (the hold cannot be claimed once settled), so it is a release, not a report of a charge — kept distinct from unattested so that the label for the safe direction is never read on a row whose budget was handed back.
counterparty_attested The claiming counterparty (8.7.6) reported the amount. Believed because of who said it; nothing confirmed it.
independently_confirmed A connector reconciler observed the remote system and it reported this amount.
operator_resolved A platform operator, resolving an ambiguous effect, decided the amount.
reconciled_at_authorized The remote system confirmed the effect occurred but reported no amount, so the full authorized amount is counted.

A settled amount MUST NOT exceed the authorized amount. Where an independent observer of the remote system is configured, a claimer settling below the authorized amount MUST be supported by it: the observed amount must equal the reported one, and the lower figure is refused — never believed — when the observer is unreachable, still undecided, finds no settlement, or confirms a settlement without stating its amount (SETTLEMENT_NOT_CONFIRMED, SETTLEMENT_AMOUNT_MISMATCH, SETTLEMENT_NOT_FOUND, HTTP 400, the code as the message and in data.reasonCode); the claimer may still settle in full. The amount recorded is the one the observer reported, not the one the claimer asked for. A refusal names the reason code and never the observer's own figures or wording, which would be an oracle for what the remote system recorded against an authorization id. Settling at the full amount needs no confirmation, because it can only spend more.

Releasing a claimed hold is the same act as settling it at zero and needs the same support: where an observer is configured, a claimer MUST NOT void a claimed hold unless the observer affirms that nothing was settled (an explicit failed / rejected record). Silence, an absent record and an unreachable observer are not that. An absent record in particular is never proof that nothing happened: a facilitator may not yet know the id, or not have the route at all. The claimer can still capture in full, or mark the effect unknown and leave it to reconciliation. The refusal is SETTLEMENT_NOT_CONFIRMED, HTTP 400, the status it has on a lowered capture.

Consequence for x402-bound holds (intended, fail-closed). A release carries no settlement transaction id, so the Hedera mirror-node observer cannot run for it, and the x402 facilitator observer affirms a failure only from a per-authorization settlement record (GET {facilitator}/settlement/{authorizationId} answering failed or rejected) that the standard x402 facilitator routes (/verify, /settle, /supported) do not provide — every probe then reads as absent, and absence is not proof. So where a facilitator is configured, a claimer cannot void a claimed x402-bound hold: every attempt is refused SETTLEMENT_NOT_CONFIRMED. This is the intended behaviour, not a gap. A claimer whose x402 call did not go through MUST instead capture (in full, or lower with the observer's support) or mark the effect unknown (POST .../effect/unknown) and let reconciliation decide. Reconciliation of an ambiguous effect settles at the amount the remote system (or the operator) reports, not blindly at the authorization, and MUST NOT absorb a settlement larger than the authorization: that effect goes to compensation (SETTLED_OVER_AUTHORIZED) for a person to decide. A hold captured before the authorized amount was stored reports authorizedAmount: null — unknown, neither zero nor the settled amount.

8.7.10. Trusted counterparties. Authenticating a counterparty (8.7.6) proves who signed a claim; it does not say whether the mandate's owner trusts that identity, and the claimer is the only party that may then settle the hold lower, release it, or mark it unknown. An owner therefore maintains a registry of the counterparty DIDs that may claim their holds (GET|POST /policy/counterparties, DELETE /policy/counterparties/{id} — authenticated and owner-scoped; a DID must be self-verifying, did:key or did:hedera). An entry may name the merchants it may act for; an entry naming none may claim for any merchant.

Registering an owner's first entry is not like registering their second: it is the call that flips every one of that owner's holds from open to registered-only (below), which can silently strand an existing counterparty that was never asked to register. POST /policy/counterparties therefore MUST refuse a first registration that does not carry confirmEnforcementChange: true (409 ENFORCEMENT_CHANGE_NOT_CONFIRMED, naming the effect in its message) — a second and later registration needs no such flag, since enforcement is already in effect.

The rule is applied at the claim, against the registry of the owner of the hold's mandate:

  • An owner with no entries is open — any authenticated counterparty may claim — unless the deployment sets MANDATE_REQUIRE_REGISTERED_COUNTERPARTY=true (every owner registered-only), or the hold's agent is a mainnet identity, which is always registered-only regardless of what the owner has (or has not) registered. The registry-default decision (release-blockers-open-items.md) settled on scoping enforcement to mainnet rather than every deployment or every owner: it protects real settlement without adding friction to the free, no-account testnet sandbox every fresh integration starts in.
  • An owner with any entry, of any status, is registered-only, and it fails closed: revoking the last entry does not reopen the owner's holds to anyone, it stops every claim until an entry is registered again.
  • Under enforcement an anonymous claim is refused (COUNTERPARTY_AUTH_REQUIRED, HTTP 403), the claimant must be an active entry (COUNTERPARTY_NOT_REGISTERED, 403), and a merchant-scoped entry may claim only holds for its merchants — a hold with no merchant cannot satisfy a scoped entry (COUNTERPARTY_NOT_ALLOWED_FOR_MERCHANT, 403). A refused claim leaves the hold unclaimed and claimable by a trusted counterparty.
  • A registry that cannot be read is an error, never "no registry".
  • An entry has a purpose: claim (the default, everything above) or report — a Service registered only to report what it did (16.4), such as a non-financial agent's gateway, which claims no ordinary hold. A report entry MUST NOT be accepted as a claimant and MUST NOT count toward registered-only enforcement, so registering one changes nothing about who may claim the owner's holds. Registering an active claim entry again as report keeps it a claimant. The one exception is an approval claim (8.7.18): a claim that asks for a person's approval of a hold whose authorized amount is 0 and which a person approved. An active, trusted report entry MAY make that claim only for a hold of an agent it was registered for (agents, the agent DIDs the service acts for); an entry registered for no agents makes none, so one of an owner's gateways can never take — and so deny — another agent's approval. It is the only way a value-less tool's gateway, which runs without claiming its allowed calls, can run an approved escalation. The claim is still single-use, bound to the hold's agent and values, and moves no money.
  • Proof of control. A registration MUST carry the service's own acceptance: the owner obtains a challenge (POST /policy/counterparties/challenge, for one DID and purpose, valid 10 minutes), the service signs its message MAGP-COUNTERPARTY-ACCEPT-v1|<registrant principal DID>|<service DID>|<purpose>|<nonce>|<expiresAt> (fields escaped as in 8.3) with the key its DID embeds, and the registration presents the challenge token and that signature. The issuer binds the challenge to the registering owner, so an acceptance obtained by one owner is useless to another, and refuses a missing, foreign, expired or wrong-purpose proof (COUNTERPARTY_PROOF_REQUIRED / _INVALID / _EXPIRED). Without it any owner could name another tenant's gateway as its own counterparty and have it claim and run that owner's holds. A service MUST sign only a well-formed acceptance naming its own DID — never free text (16.2). Entries registered before this rule are kept but marked unproven; a deployment that sets COUNTERPARTY_REQUIRE_PROOF trusts only proven entries, while unproven ones still keep the owner registered-only.

Registration governs who may claim. Once claimed, the claimer's identity is the one recorded on the effect chain and the only one that may settle lower or release (8.7.6, 8.7.9); revoking that identity afterwards does not transfer the hold, which is then resolved by reconciliation.

8.7.11. Payload comparison at the claim. A hold whose authorization carried a payload binding (§8.3.9) is claimed with the digest of exactly what the claimant is about to execute. The claimant sends it in the x-magp-payload-digest header, and — when it authenticates (§8.7.6) — the digest is also one more signed field of the claim message, payload=<digest>, so it can be neither added nor stripped in flight. The issuer compares it with the digest it stored at authorize time:

  • Equal: the claim proceeds as before, and the grant reports the digest the hold is bound to (data.payloadDigest; null for an unbound hold).
  • Different: the claim is refused — PAYLOAD_DIGEST_MISMATCH, HTTP 403 — and refused rather than granted and then checked: the hold stays unclaimed and claimable by an honest executor.
  • The hold is bound and the claimant states no digest: refused, PAYLOAD_DIGEST_REQUIRED (403). A claimant cannot ignore the binding by omission.
  • The hold is unbound and the claimant states a digest: refused, PAYLOAD_NOT_BOUND_AT_AUTHORIZE (403). The agent never bound a payload, and a claim must not assert that it did.
  • The hold is unbound and its mandate requires binding (§8.3.9): refused, PAYLOAD_BINDING_REQUIRED (409, the hold's state). The hold stays unclaimed, and is claimable once the agent has bound a digest (§8.3.11). Decided after the two comparisons above, so a claimant that states a digest against an unbound hold is told the more specific PAYLOAD_NOT_BOUND_AT_AUTHORIZE.
  • A digest that is not sha256: and 64 lower-case hex is a malformed request (400 PAYLOAD_DIGEST_INVALID).
  • Evidence. A granted claim records the digest it was granted against beside the claim on the effect chain (§8.7.5) as the evidence ref payload:<digest> (with the idempotency-key hash when the claim sent one). The hold's own digest column is set by a binding; the chain entry is the tamper-evident record of which payload the claimant was held to. An unbound claim records none.
  • The comparison is the same whether the claimant is authenticated or anonymous. It is made with the registry check of §8.7.10, before a claim is recorded: no refusal above records one.
  • The claim is decided against the digest it read, and commits only against that digest: the conditional update that records the claim (§8.7.5) also requires the hold to still carry exactly the digest — or the absence of one — the decision was made on. A binding (§8.3.11) that lands between the decision and the write therefore makes the claim contend (EFFECT_TRANSITION_CONTENDED, which the claimant retries, §8.7.5) instead of granting a claim that ran unbound; the retry meets the digest that is now there.

The comparison is made against the digest the ISSUER stored, not against a request the claimant was handed. A gateway that only compared the body it forwards with the signed request in its header could be given a different signed request — a fresh nonce, a different digest — for the same authorization id by an agent that signs whatever it likes. Only the issuer knows which digest the authorization was actually granted for.

The executing side SHOULD also compare, before it claims, the digest of what it will execute with the one in the signed request it was handed, and refuse a difference without claiming: cheaper, and it leaves the hold untouched. This is an optimisation and never the authority; a verifier that omits it is still protected by the comparison above. An executor whose deployment requires binding SHOULD refuse a request whose authorization carries none (PAYLOAD_BINDING_REQUIRED), and MUST refuse, rather than skip the check for, a payload it cannot digest (PAYLOAD_NOT_CANONICALIZABLE; a gateway that received a body for a request that bound a payload and cannot show it is that payload answers PAYLOAD_NOT_BOUND). The binding covers the payload, not the address it is sent to: an executor that forwards an HTTP request MUST NOT forward a URL query string — or a query, path parameter or fragment encoded into the path — that the signature does not cover, and refuses it before claiming (QUERY_NOT_BOUND), unless its operator has listed the exact keys it may forward unbound (never a signed value field).

What is digested must mean the same thing to every reader of the bytes that are forwarded. An executor that digests a body it received as JSON MUST refuse, rather than digest, a body that is ambiguous between readers: a repeated object key (a first-wins reader runs a different value from a last-wins one), a number a double cannot carry exactly (an integer of magnitude 2^53 or more, or a non-integer with more than 15 significant digits — send large identifiers and amounts as strings), invalid UTF-8, or a byte-order mark. Otherwise two different bodies share a digest, and the binding covers less than it appears to.

8.7.12. Who may push a claimed hold along. Marking an outcome unknown (POST .../effect/unknown) and asking for reconciliation (POST .../effect/reconcile) freeze a hold out of capture and void, so a gate MUST accept them only from the claimer (its signed call — action unknown or reconcile — or, for an anonymous claim, its claim token), the owner of the hold's mandate, or an operator. The authorization id alone is not enough (COUNTERPARTY_MISMATCH, RECONCILE_NOT_PERMITTED).

8.7.13. Owner-forced reconciliation. An owner who no longer trusts the counterparty holding a claim (typically one just revoked, 8.7.10) MAY force the hold into reconciliation (POST .../effect/force-reconcile). The hold moves to reconciling with the owner as the actor, its budget stays committed, and from then on the claimer holds none of its powers: no caller-side capture or void applies to it at all — not even at the full amount, which would end reconciliation before it decided (HOLD_UNDER_RECONCILIATION, HTTP 409 on both capture and void, the code as the message and in data.reasonCode). It settles only on what the settlement observer establishes or an operator decides. Nothing is released or reassigned: a hold that may have executed must not run twice.

8.7.14. Payee directory. A claimer settling below the authorization names the account it paid (payTo), and the observer checks that account was credited — which proves the claimed account was paid, not that it is the right one. An owner MAY register the accounts each merchant is paid into (/policy/payees). For a merchant with an active entry, a lowered settlement MUST name one of them or be refused (PAYEE_NOT_REGISTERED); a gate that cannot read the directory refuses the lowered settlement rather than skip the check (PAYEE_NOT_VERIFIED). A merchant with no entry, and a settlement at the full amount, are unaffected.

8.7.15. Revocation and holds. Every path that revokes a mandate — the owner's or an admin's revoke, a delegation-chain revoke, the cascade to delegated mandates (5.4.9, including from a mandate superseded by a reissue), decommissioning or a failed provisioning (which revoke all of an agent's mandates), and the revoke of a mandate superseded by a reissue — MUST do the same thing to it:

  • make it inactive and close every review pending under it, in one transaction (10.4.2) — from then on it authorizes nothing (MANDATE_REVOKED, unless the action is granted again), and no approval can mint a hold on it (MANDATE_REVOKED);
  • release every hold under it that no counterparty has claimed: the hold becomes voided (effect state cancelled, reason HOLD_VOIDED:<revoke reason>), its budget returns to the cap, and its Action Passport is revoked. The release is conditional on the hold still being unclaimed, so a claim that lands first wins and the hold is treated as claimed;
  • leave every claimed hold (8.7.3) as it is: still held, its budget still committed, its passport usable, settled the ordinary way — its claimer captures it or releases it (8.7.9), or it is reconciled, or the owner forces it into reconciliation (8.7.13). A revoke never releases a hold that may have executed;
  • anchor the revoke and record it as evidence once, when it makes the mandate inactive.

An already-inactive mandate is still swept (reviews closed, unclaimed holds released) but not revoked twice. Independently of the sweep, a gate MUST NOT let an unclaimed hold under an inactive mandate — or under a delegated mandate with an inactive ancestor (5.4.10) — become an execution: a claim of it is refused MANDATE_REVOKED (409, the hold stays unclaimed; or DELEGATION_CHAIN_BROKEN for a chain that cannot be walked), and so is a counterparty's capture of it (409, at any amount); it can still be voided. The effect chain is not written by the release (8.7.8).

Revokes made before this rule (v1.69.2 and earlier: a chain revoke that left unclaimed holds held, a direct revoke that did not reach delegated mandates, a revoke that left reviews pending) are finished by the reference implementation once, at start-up: every inactive mandate that still has an active delegated mandate, an unclaimed held hold or a pending review is revoked again through the same routine (reason REVOKE_SWEEP), with the evidence, ledger anchor and Action Passport revocation a live revoke emits. It is idempotent and logged.

8.7.16. Rules changed while a hold waits. A change to the agent's SOPs or bound Standards takes effect immediately, including for holds already granted but not yet claimed. Before an unclaimed hold moves to dispatching, the gate MUST evaluate the SOP and Standard molecules bound to the agent at the time of the claim, deterministically and most-restrictive-wins as at authorize (6.1, 8.5), over the context the hold was granted on:

  • the signed values the hold carries — the action of its mandate, the agent, the authorized amount and currency;
  • the rest of the rule context the permit was decided on, recorded with the hold: the agent's unsigned context, a signed or registered jurisdiction, and the request's call count (a claim is not another call). The owner's risk tier for the action floors riskLevel, as at authorize;
  • committed spend and counterparty trust, read again at the claim, as an approval reads them (10.4.1). Committed spend excludes the hold itself, as it did at authorize.

If the current rules no longer permit the hold — a block, suspend or quarantine from either layer; an enforced jurisdiction rule with no jurisdiction; or an escalate, unless the hold was minted by a person's approval or MODIFY (an approval disregards escalates, 10.4) — the claim MUST be refused AUTHORIZATION_RULES_CHANGED (409) and the hold voided: effect state cancelled, reason HOLD_VOIDED:<the firing rule's code>, its budget returned to the cap and its Action Passport revoked. Nothing executes; the agent asks again under the new rules. A claim or capture refused this way MUST also be recorded as an evidence event (block, AUTHORIZATION_RULES_CHANGED, the mandate's standardRef), so the evidence trail shows the action was stopped, not only that it was authorized. An observe permits. If the rules cannot be evaluated the claim MUST be refused (CONTROL_UNAVAILABLE, 409) and the hold left unclaimed, so it can be claimed once they can. A hold minted before its rule context was recorded is judged on the values its row carries, and an escalate is not a refusal for it (it may only mean a field the claim cannot see was not sent). The recorded context carries the agent's own itinerary, so it SHOULD be stored encrypted and bound to its hold, and it MUST be bounded in size without trimming: a request whose context is over the bound is refused at authorize (CONTEXT_TOO_LARGE), since a trimmed context could drop a field a rule reads. A recorded context that cannot be read back at the claim MUST fail closed (CONTROL_UNAVAILABLE, hold left unclaimed), never be judged as unrecorded. The check runs after the counterparty and payload checks (8.7.6, 8.3.9), so only an otherwise-grantable claim can void a hold, and only while the hold is live (a lapsed one is answered AUTHORIZATION_EXPIRED).

A hold already claimed (8.7.3) is never re-judged: it may be executing, and settles by the claimed-hold rules. The mandate's own terms are not re-judged here: they cannot change under a hold, because changing them reissues the mandate, and the revoke of the old one releases its unclaimed holds (8.7.15).

A capture of an unclaimed hold — an executor that settles without claiming first — MUST be re-judged the same way before it commits anything, with the same outcomes: refused AUTHORIZATION_RULES_CHANGED (409) and the hold voided, or refused CONTROL_UNAVAILABLE (409) with the hold unchanged. A capture of zero (a release) and a capture by reconciliation are not re-judged.

A change to the agent's SOPs or bound Standards SHOULD also re-judge the agent's live unclaimed holds as soon as it commits, voiding those the new rules no longer permit exactly as a claim would. Each void is conditional on the hold still being unclaimed, as each claim is, so a change that commits while a claim is between its re-check and its commit voids the hold unless the claim committed first; a claim that committed first is a commitment, like a claim that beat a revoke (8.7.15), and settles by the claimed-hold rules. A hold whose rules cannot be read during this re-judgment is left as it is; the claim-time check still applies to it.

8.7.17. Containment and holds. Containment (suspend, quarantine, decommission — 7.1) stops what the agent was already allowed to do, not only what it asks for next. Before an unclaimed hold moves to dispatching, and before a capture of an unclaimed hold commits anything, the gate MUST read the containment state of the agent the hold was issued to (by its mandate's stable identity, so a rotated DID cannot escape it; by the hold's DID only for a mandate that names no identity). Any state other than none that is not a known containment state MUST fail closed (CONTROL_UNAVAILABLE). The write that commits the claim, or the unclaimed capture, MUST itself be conditional on the agent not being contained, so a containment that commits after the gate read the agent as active, but before the claim's write, still stops it — answered and voided exactly as if the read had seen it. If the agent is contained, the claim or capture MUST be refused with the containment code — AGENT_SUSPENDED, AGENT_QUARANTINED or AGENT_DECOMMISSIONED (409) — and the hold voided: effect state cancelled, reason HOLD_VOIDED:<that code>, its budget returned to the cap and its Action Passport revoked. A claim or capture refused this way MUST also be recorded as an evidence event (block, the containment code), as for 8.7.16. A reinstatement therefore never revives an authorization granted before the containment; the agent asks again. The check runs before WHO is claiming is considered (8.7.6) and before the rules are re-judged (8.7.16): a contained agent may not act, whoever offers to execute for it. If the containment state cannot be read, the claim or capture MUST be refused CONTROL_UNAVAILABLE (409) with the hold left unclaimed. A lapsed hold is answered AUTHORIZATION_EXPIRED, and a capture of zero (a release) is not refused.

Every path that contains an agent — the owner's or an admin's containment, and the gate's own suspend/quarantine verdicts — SHOULD also void the agent's live unclaimed holds as soon as the containment commits, after writing it, returning their budget at once and leaving nothing for a reinstatement to revive; the claim-time check is the backstop for any hold the sweep does not reach. Each void is conditional on the hold still being unclaimed. Only a claim whose write committed before the containment did is a commitment, as for a revoke (8.7.15): it may be executing, is left held, and settles by the claimed-hold rules. Because "all actions" includes these, an unclaimed claimant that presents a contained agent's hold learns the containment code rather than a counterparty refusal; the hold is voided either way and nothing executes.

8.7.18. Executing an approved escalation. An executor that re-evaluates the agent's policy (8.7.6) judges a request the agent signed with the context that escalated it — and so reaches escalate again, even after the owner approved that very action and the approval minted a hold (10.4). The approval, not the request, settles it: when the executor's own verdict is escalate and the request carries an authorizationId, the executor MAY claim that hold stating requireHumanApproval: true. The issuer MUST then grant the claim only for a hold minted by a person's approval (ESCALATION_APPROVED or ESCALATION_MODIFIED_APPROVED), and otherwise refuse ESCALATION_NOT_APPROVED (409) leaving the hold unclaimed and unchanged — the request still needs a person, so the executor answers escalate. Every claim grant carries approvedByHuman; the executor MUST permit only on approvedByHuman: true, and on a grant without it (an issuer that does not implement this section ignores the flag) MUST release the claim and keep the escalate. Every other check on the claim applies unchanged — the agent, action, amount, currency, merchant and payload binding (8.7.6, 8.3.9), the rules as they stand now (8.7.16) and containment (8.7.17) — so the approval executes exactly what the person approved, once. A modified approval carries no payload digest (the agent never signed the modified action), so under requireHumanApproval it MUST be refused PAYLOAD_BINDING_REQUIRED until the agent binds the payload of what will run (8.3.11). The requireHumanApproval check runs after the counterparty checks (8.7.6), so only a claimer allowed to claim learns how a hold came about.

Because approvedByHuman in the grant is what lifts the verdict, the executor MUST take this path only over an issuer channel whose responses are integrity-protected — TLS, or loopback for a local development issuer — and otherwise answer escalate. Only an escalate is lifted: a block, suspend or quarantine from the executor's own evaluation is never overridden by an approval given before it. The approval lifts any escalate the executor reaches for that request (a rule, an operating-mode floor, a review flag) — consistent with 8.7.16, where an approved hold is not refused for an escalate either; anything stronger still refuses.

8.7.19. Claim expectations. An executor checks a claim's grant against what it is about to execute — the agent, action, amount, currency and merchant of the request it verified — and refuses on any difference. Checked only after the grant, a mismatch leaves the hold claimed with nothing executed: anyone who learns an authorization id could present it at the hold's gateway under another agent and strand that agent's hold against its cap. So a claim SHOULD state those values as expect: { agentDid, action, amount, currency, merchant }, and the issuer MUST compare them with the hold before granting the claim — after the counterparty checks (8.7.6), so only a claimer allowed to claim learns anything of the hold — refusing a difference with AUTHORIZATION_AGENT_MISMATCH, AUTHORIZATION_ACTION_MISMATCH, AUTHORIZATION_AMOUNT_MISMATCH, AUTHORIZATION_CURRENCY_MISMATCH or AUTHORIZATION_MERCHANT_MISMATCH (403) and leaving the hold unclaimed and unchanged. A value the hold does not carry is not compared, as the executor skips a field the grant omits; amounts compare as numbers, and a field sent as null is not stated. A malformed expect (a field of the wrong type, a non-finite amount) MUST be refused CLAIM_EXPECTATION_INVALID (400), never partly applied; a key the issuer does not know is ignored, so a later field can be introduced while the executor's own post-grant check still covers it. An executor that still finds a mismatch after a grant (an issuer that does not implement this section) MUST release the claim (8.7.4, with its claim token) before refusing, so the hold is not stranded — on such an issuer that release voids the hold (its budget returns; the agent asks again), which is the lesser harm.

8.7.20. Approved-context binding. A hold a person approved (an escalation's approval, or a MODIFY→allow re-entry) is for the request that person reviewed — including its context (itinerary), not only what it spends. The values of 8.7.19 bind amount, currency and merchant; for an action that spends nothing they bind little more than its name, so an approval of { target: "record-A", op: "read" } could otherwise be executed as { target: "record-B", op: "delete-all" }. The approved context's digest is sha256: + hex SHA-256 of the RFC 8785 canonical JSON of { "MAGP-APPROVED-CONTEXT-v1": context } (an absent context is {}; for a MODIFY that changed the itinerary, the reviewer's itinerary), pinned by docs/protocol/resume-binding-vectors.json (contextVectors). A claim MUST state the digest of the context of the request it is about to execute as expect.contextDigest, and for a person-approved hold the issuer MUST compare it with the approved context's digest before granting — whatever the executor's own judgement of the request, since a request re-signed at a lower risk still spends the approval — refusing a difference AUTHORIZATION_CONTEXT_MISMATCH (403), a claim that states none AUTHORIZATION_CONTEXT_REQUIRED (403), and a hold whose approving review cannot be read AUTHORIZATION_CONTEXT_UNAVAILABLE, each with the hold left unclaimed. A refusal AUTHORIZATION_CONTEXT_REQUIRED MUST carry a detail naming the executor versions that state the digest — only an executor that predates this section states none — and an executor SHOULD log a refused claim's detail. The grant of a person-approved hold names the digest it compared (contextDigest), and an executor MUST release the claim and refuse when it differs from its own or is absent (an issuer that predates this section). The escalation status of an approved review carries the same digest as contextDigest, and an SDK resuming an approved escalation in-process MUST refuse arguments whose context does not reproduce it (ESCALATION_REQUEST_MISMATCH) before anything runs. The digest binds the agent's SIGNED itinerary; what an executor actually runs is tied to the request only by payload binding (8.3.9). An executor that runs a body other than the itinerary therefore SHOULD require payload binding (requirePayloadBinding, as the scaffolded gateways do), or derive the operation from the itinerary itself, so an approved context cannot be presented beside a different body.

8.7.21. In-process resume claim. An SDK that runs an approved escalation's tool in its own process has no gateway claim (8.7.4) between the arguments and the tool, so before it runs it takes the approval's one resume at the issuer: POST /policy/escalations/{escalationId}/resume-claim with { agentDid, nonce, issuedAt, requestDigest, contextDigest, signature }. requestDigest is the resume binding digest (MAGP-RESUME-BIND-v1) and contextDigest the approved-context digest (8.7.20) of the request the SDK is about to run — computed from its arguments, never copied from the status. The agent signs, with its DID key, the UTF-8 message MAGP-RESUME-CLAIM-v2|escalationId|authorizationId|agentDid|requestDigest|contextDigest|nonce|issuedAt (each field escaped as in 8.3), pinned by docs/protocol/resume-binding-vectors.json (claimVectors). The issuer verifies the signature and freshness (as an authorize request) and compares both digests with those of the approved review before consuming the nonce or the resume, refusing a request difference ESCALATION_REQUEST_MISMATCH (403) and a context difference AUTHORIZATION_CONTEXT_MISMATCH (403); of any number of valid claims exactly one is granted, the others AUTHORIZATION_IN_USE (409), and a granted resume is never handed out again, with one exception: when, after it was granted, an authenticated claim of the approval's hold is refused AUTHORIZATION_CONTEXT_REQUIRED (an executor that predates 8.7.20 ran nothing) and the hold is still held and unclaimed, the issuer re-opens the resume once, so the agent resumes again after upgrading the executor instead of its owner approving the same request again. An SDK whose tool fails with AUTHORIZATION_CONTEXT_REQUIRED therefore MUST NOT void the approval's hold, and SHOULD say which executor to upgrade; an SDK that finds an approval's hold voided reports AUTHORIZATION_VOIDED (the owner must approve again), not AUTHORIZATION_ALREADY_USED. A claim that arrives after the resume was taken (and not re-opened) says what became of the approval, as the SDK's own check would: AUTHORIZATION_IN_USE while its hold is still unsettled (the one resume may still be running), AUTHORIZATION_VOIDED once the hold was voided, and AUTHORIZATION_ALREADY_USED (409) once it is settled, claimed or lapsed. An SDK settles the hold of a call that ran even when the tool maps no amount (at 0), so a repeat resume reads as finished. Every escalation that reaches approved was approved by a person, so a MAGP-RESUME-CLAIM-v1 claim (the same message without the two digests, which binds neither) MUST be refused AUTHORIZATION_CONTEXT_REQUIRED (403) with a detail naming the SDK versions that sign v2 — the SDK-side checks of 8.7.20 exist only in current SDKs, and an outdated one otherwise ran an approval of one context as another. An issuer that requires v2 says so on an approved escalation's status (resumeClaimVersion: 2); an SDK MUST sign v2 when it is present and MAY sign v1 for an issuer that does not state it (one that predates this section, and accepts nothing else).

9. Secondary controls

Controls that protect the system rather than express authority. Both are optional, and by default both MUST fail open on internal error — a rate counter or breaker lookup that errors must not deny an otherwise-valid action. A deployment MAY instead configure its secondary controls to fail closed (reference implementation: SECONDARY_CONTROLS_FAIL_CLOSED=true): a rate counter or breaker lookup that errors then blocks with CONTROL_UNAVAILABLE, and the spend-anomaly and assurance floors, which only ever escalate, escalate with it.

A request MUST NOT be able to cause the internal error itself. A breaker scope value the request supplies (the itinerary's tool or counterpartyDid) that contains a control character cannot be looked up, so it is refused MALFORMED_REQUEST before the lookup — otherwise the fail-open path would skip every breaker for that request, the agent's and tenant's included.

9.1 Rate limits

9.1.1. Per agent, max calls per rolling windowSec. max = 0 disables it.

9.1.2. The running count is server-derived and MUST be injected into rule context signed-last, so a rule-authored rate atom evaluates the same number the built-in ceiling does and an unsigned itinerary cannot shadow it.

9.2 Circuit breakers

9.2.1. Scoped to agent, tenant, tool or counterparty. An open breaker denies with CIRCUIT_BREAKER_OPEN.


10. Escalation resolution

10.1. escalate parks a pending record. No budget is held.

10.2. GET /policy/escalations/{id}/status is public and keyed by the escalation id, which is itself the capability. An agent holds no user token and needs none to ask about its own held action. It returns status, reasonCode, authorizationId, expiresAt, approvals, required. Everything it returns is the agent's own request state, which the agent needs to resume (the authorization and the digests of 9a.5 included); it names no principal and no budget. With an authorization it also reports what became of it: executed (something ran, or may have run, under the approval — a gateway claimed or settled its hold, or an in-process resume took it and the hold was not given back), executedAt, and outcome (the hold's effect outcome, as GET /policy/mandate/authorize/{id}/effect reports it). status stays approved after the action ran; the execution fields are added beside it rather than a new status, because SDKs key on approved.

10.2a. GET /policy/mandate/{ref}/status is public as a registry read (4.4.2), and minimized: an unauthenticated caller gets policyId, agentDid, status, principalVerified, principalTierReason, allowedJurisdictions, vcHash, mandateStoreRef, anchorRef and expiresAt. The principal's DID and the budget (cap, spent, remaining, byCurrency) are returned only to the mandate's owner or a platform admin. The response says which it is (view: "public" | "owner"). No SDK reads this route.

10.3. Approval requires M of N distinct approvers, configurable per agent, defaulting to

  1. An approver MAY be required to hold a named role and a minimum authority level.

10.4. On approval the gate re-runs the deterministic checks and mints an authorizationId. Approval does not bypass the budget, nor any hard block: containment, an action the mandate does not grant, a Standard or SOP block (e.g. an SOP per-transaction cap) and the mandate's per-transaction and cumulative caps are all re-evaluated on the escalated request, and any of them refuses the approval with its own reason code (the cumulative cap as CAP_EXCEEDED); the escalation stays pending and may still be denied. Escalates are disregarded — they are what is being approved (§8.5). The minted hold starts its effect lifecycle at authorized, so it is claimable like any other. A MODIFY re-enters the gate under the same precedence.

10.4.1. The re-evaluation uses the gate's inputs as of the approval, never those of the moment the review opened, and every one the live gate uses except transport authentication (signature, freshness, nonce — satisfied by the agent's original request):

  • The mandate. It MUST still be active, and for a delegated mandate so must every mandate above it (MANDATE_REVOKED otherwise, DELEGATION_CHAIN_BROKEN for a chain that cannot be walked, 5.4.10), and still the agent's current mandate for the action (MANDATE_SUPERSEDED if a newer one has been issued). Reviews do not migrate to a reissued mandate: a review raised under the old one cannot be approved, and the agent asks again under the new one (10.4.2).
  • Principal, compliance and the agent — steps 2, 3, 4, 6 and 7 of §8.5 as they stand now.
  • The rate window (§9.1): the gate's own ceiling (RATE_LIMIT_EXCEEDED) and the callCount given to rules. A request is counted once in the window it arrives in. An approval in that same window reads the count (the request is already in it); an approval in a later window is counted in that window, as the live gate counts a request — before judging, so a refused approval there counts too — and a hold granted in a window is therefore always one of its calls.
  • Circuit breakers (§9.2).
  • Committed spend and counterparty trust, applied as server-derived fields (§6.4.2), so the review's stored itinerary cannot supply cumulativeSpend, callCount or holTrustScore.
  • The signed operands, resource included: the gate keeps the signed resource with the review and applies it last; the unsigned itinerary never stands in for it.
  • The jurisdiction (§8.3.12). The gate stores the request's signed jurisdiction with the review, committed in the row's integrity hash; a reviewer cannot change it, and a MODIFY cannot move the action to another jurisdiction (an itinerary value is ignored, as at authorize). The effective jurisdiction is resolved again from the stored signed value and the payee registry as of the decision: an owner who registered or changed the payee's country since the review opened is heard, and a stored signed value that now contradicts it refuses the approval JURISDICTION_MISMATCH. The mandate's allowed-jurisdictions term and any enforced jurisdiction rule are then judged on it (JURISDICTION_NOT_ALLOWED, or the molecule's own code). With no effective jurisdiction at the decision — a review stored before the signed jurisdiction was kept, or one whose payee's registered country has since been cleared — a review under either is refused APPROVAL_INPUT_UNAVAILABLE, not JURISDICTION_REQUIRED; the review stays pending and the agent may ask again.

An input that cannot be reconstructed — the rate counter, the breaker store or the payee registry unreadable, a review stored before the signed resource was kept, under a mandate that constrains resource, or the missing jurisdiction above — refuses the approval with APPROVAL_INPUT_UNAVAILABLE; the review stays pending. A MODIFY receives the same inputs; its block closes the review as modified rather than leaving it pending.

One known approximation: if the agent's rate limit is switched on after its request escalated, within the same window, that request was never counted, and an approval in that window reads the count one call low. Accepted — it is bounded to one call, in one window, once.

10.4.2. Revoking a mandate closes its pending reviews. In the same transaction that makes the mandate inactive, every review still pending under it becomes denied, decided by system, with the reason MANDATE_REVOKED — or MANDATE_SUPERSEDED when the mandate was revoked because it was reissued (identity rotation, resource-scope pin, payload-binding toggle). This holds for every revoke path: the owner's or an admin's revoke, decommissioning (which revokes all of an agent's mandates), a reissue, and a delegation-chain revoke. The closure emits what a reviewer's deny emits (an anchored block evidence event with that reason code, and the review's trust-graph snapshot) but no signed vote, since no person decided. The status route reports the closure code as the review's reasonCode, so a polling agent learns it must ask again. A closed review can never be approved (ALREADY_DENIED), and no hold is minted on a revoked mandate: the hold for an approval is inserted only if the mandate is still active, checked under the same lock the revoke takes, so an approval that races a revoke is refused MANDATE_REVOKED. The same holds for a live authorize: a revoke that commits after the gate read the mandate as active, but before its hold is inserted, refuses that authorize MANDATE_REVOKED and mints no hold.

10.5. An unresolved escalation expires — ESCALATION_EXPIRED. The expiry (24 hours by default) bounds how stale a review can be; nothing else about its age is checked, because everything the approval depends on is re-read at approval time (10.4.1), and the hold it mints carries its own TTL from the moment of approval.

10.6. Callers MUST poll to resolution and act only on an approved status carrying an authorizationId.


11. Payment execution binding (x402)

11.1. Authorize before pay. Payment MUST be bound to an authorizationId issued by a permitting verdict.

11.2. The binding is amount-exact and single-use. A payment whose amount differs from the authorized amount MUST be refused with AMOUNT_MISMATCH; a reused authorization MUST be refused.

11.3. Settlement is reconciled by capture, carrying the settlement reference.

11.4. No custody. The protocol never holds funds.


12. Effects and reconciliation

12.1. A permitted action has a real-world effect whose outcome may not be known synchronously. Effect state transitions are recorded on a hash-chained ledger: each transition commits to the digest of the previous one plus its own canonical content.

12.2. A verifier MUST reject an invalid transition, a sequence gap, a broken chain link or a digest mismatch — INVALID_EFFECT_TRANSITION, SEQUENCE_GAP, CHAIN_LINK_BROKEN, DIGEST_MISMATCH.

12.3. An effect whose outcome is genuinely unknown is recorded as such rather than guessed, and resolved by reconciliation.


13. Evidence and anchoring

13.1. Every decision MUST produce an evidence record.

13.2. Records MAY be anchored individually or batched. In batch mode records are accumulated, hashed into a Merkle tree, and a single root is anchored.

13.3. GET /magp/evidence/{id}/proof returns an inclusion proof: the leaf, the path, and the anchored root.

13.4. A verifier MUST be able to check inclusion offline, given only the leaf, the proof and the anchored root. Verification MUST NOT require the platform.

13.5. Leaf stability. The set of fields in a leaf is part of the protocol. Adding one changes every digest and invalidates previously anchored proofs; a change to leaf composition is therefore a breaking change (§18).

13.6. Execution Receipt. A second, separate signed-proof artifact, issued after a value-bearing action settles: POST .../capture best-effort issues one binding what was actually charged back to the Action Passport (§8) the action was authorized against (actionPassportId, authorizedPayloadDigest), alongside a digest of what was actually executed (executedPayloadDigest) and whether the two are within the authorized bound (payloadMatch). Unlike the Merkle-anchored evidence above, a receipt is a standalone Ed25519 signature over a canonical (sorted-key JSON) message — verifiable with no anchored root, no Merkle path, just the issuer's published public key (GET /action-passport/pubkey).

  • GET /action-passport/receipt/verify/{authorizationId} (public) answers integrityValid (the signature checks out), payloadMatch, outcomeStatus, and settlementEvidence (below) for any relying party, with no authentication and no state about who is asking.
  • receiptVersion. Same discipline as leaf stability (13.5), applied to a signed message instead of an anchored leaf: the receipt's field set is versioned, and a version bump adds fields only to future receipts. A verifier reconstructs the exact message a given row was originally signed with — an older row's reconstruction MUST NOT gain the newer version's fields (even as null/a default), because that is not the string that was actually signed and would make every historical receipt fail verification. Receipt version 2 added settlementEvidence; version 1's 9-field message (executionReceiptId, actionPassportId, authorizationId, agentDid, authorizedPayloadDigest, executedPayloadDigest, payloadMatch, outcomeStatus, issuedAt) is unchanged and still verifies.
  • settlementEvidence (receipt version 2). What the capture producing this receipt rested on — the same taxonomy §8.7.9 defines for the hold itself (unattested, unclaimed_lowered, counterparty_attested, independently_confirmed, operator_resolved, reconciled_at_authorized), folded into the signed payload rather than left as an unsigned, mutable side-channel: a relying party checking the receipt's signature also gets a tamper-evident answer to how sure the platform was this settlement was real.

14. Decision records and the governance envelope

14.1. Alongside the anchored leaf, an implementation SHOULD record an off-leaf decision record: per-control results, stage timestamps, and the names of context fields evaluated.

14.2. Context field names are recorded; values MUST NOT be. A conformance assessment needs to distinguish "the control held" from "the situation never arose" — a spend-cap clause that never saw an amount was not tested, and scoring it as passed would be a false assurance. The values themselves are the tenant's.

14.3. The governance envelope is a canonical view over the request, carrying an envelopeId and an integrity hash so a caller can bind a decision to the exact action submitted.

14.4. Decision records are best-effort and MUST NOT affect the verdict.

14.5. Where the anchored leaf carries a commitment to the decision record (a single hash covering both, rather than anchoring twice), and the request carried a payload binding (§8.3.9) that has been VERIFIED — never one merely claimed — the payload digest SHOULD be folded into that same commitment, so a verifier holding the anchored digest can also prove which payload the decision was bound to. A new field folded into an existing digest MUST be additive: omitted from the hashed content entirely when absent (never written as an explicit null), so a record with nothing to add digests identically to one computed before the field existed. Anything covered by this digest that has not yet been proven true — e.g. PAYLOAD_BINDING_INVALID, where the very point of the code is that the claimed digest was never shown to be the agent's — MUST NOT be included.


15. Privacy-preserving verification

Optional. Where a counterparty must verify a property without learning the underlying value.

15.1. Range proofs. An amount may be proven within a mandate's cap without revealing it (ZK_AMOUNT_PROOF_INVALID, ZK_JURISDICTION_PROOF_INVALID, ZK_POLICY_OK).

15.1.1. Who may ask, and against which terms. A private-authorize request (POST /magp/private-authorize) MUST be signed by the agent's own DID key, with the same key resolution (including rotated and promoted DIDs), single-use nonce and freshness window as a cleartext authorization (§8.3). The signed message is MAGP-PRIVATE-AUTHORIZE-v1| followed by the hex SHA-256 of the canonical JSON (RFC 8785-style) of agentDid, action, nonce, issuedAt, currency, both commitments, both proofs, transactionCommitment and sessionBindingDigest (absent members as null). Its own domain means a signature made for it never verifies as an authorize signature, or the reverse. transactionCommitment is REQUIRED: a request without one is refused (ZK_TRANSACTION_COMMITMENT_REQUIRED, HTTP 400) before anything else is checked, so every permitting verdict carries a capability bound to the transaction it authorizes (§15.3).

15.1.2. The cap and the jurisdiction allow-list MUST be read from the agent's active mandate for the action, never from the request. The cap is the permission's per-transaction bound (mm:payAmount lteq, else mm:cumulativeSpend), floored to whole units of its currency; the allow-list is an mm:jurisdiction isAnyOf constraint, in its stored order — the allowed-jurisdictions term of §5.2.3, so a mandate issued with allowedJurisdictions (or a delegated mandate that inherited one) gives the blind path a real list to prove membership in; before that term existed, a mandate issued the standard way carried none and only the amount was proven. The list is the same one the cleartext gate enforces, but the blind path proves the agent's committed jurisdiction against it: it does not consult the payee registry, and JURISDICTION_MISMATCH does not arise there (it names no payee). A request that carries its own cap or allowedJurisdictions MUST be refused (ZK_CALLER_POLICY_REJECTED), not silently re-interpreted. A range proof built against any other cap is refused (ZK_CAP_MISMATCH); a term the mandate carries whose proof is missing is refused (ZK_AMOUNT_PROOF_REQUIRED, ZK_JURISDICTION_PROOF_REQUIRED). A mandate with no jurisdiction term has nothing to prove about jurisdiction — a membership proof is then not evaluated, and the response and capability say so; a mandate with neither term, or with one in a form set membership or a range proof cannot express, is refused (ZK_MANDATE_TERMS_UNSUPPORTED).

15.1.3. A contained agent, an inactive (never issued, or revoked) or expired mandate, a principal whose authority does not hold, and an unmet enforced Standard are refused with the codes a cleartext authorization uses. Because the amount is hidden and nothing is held, an agent whose operating mode routes value to a human is refused (MODE_READ_ONLY, ZK_HUMAN_REVIEW_UNAVAILABLE), as is an action whose mandate requires payload binding (PAYLOAD_BINDING_REQUIRED) — a transaction commitment is not a payload binding.

15.1.4. What a private-authorize verdict does not establish, and MUST state: it reserves no hold, so it does not draw down or check the cumulative budget; it does not evaluate merchant, resource or other constraints, prohibitions, Standards or SOP rules. The response lists these under notEvaluated. A counterparty that relies on it still has to enforce them.

15.2. Selective disclosure. BBS presentations prove selected credential attributes — for example that assurance meets a threshold — without disclosing the rest (BBS_PROOF_INVALID, BBS_KYC_LEVEL_TOO_LOW, BBS_PRESENTATION_OK).

15.3. Capability tokens. A permitting verdict MAY be returned as a signed capability bound to a transaction commitment and a session, verifiable offline. Authorizing transaction A and executing transaction B MUST fail with COMMITMENT_MISMATCH; a capability used outside its session fails with SESSION_MISMATCH; an expired one with CAPABILITY_EXPIRED.

15.3.1. A capability minted by a private authorization (payload v: 2) also names its subject: agentDid, mandateId, action, currency, network, and the mandate terms proven (cap, jurisdictions; null = the mandate had no such term). A verifier SHOULD check at least that agentDid is the agent it authenticated; presented by another agent the capability fails with CAPABILITY_AGENT_MISMATCH, and against a different expected mandate, action or currency with CAPABILITY_SCOPE_MISMATCH. A verifier that requires a subject field MUST fail a capability that lacks it (a v: 1 capability) rather than accept it.


16. Discovery and channels

16.1. HTTPS is the baseline transport. A DID document's serviceEndpoint locates a peer.

16.2. Peers MUST mutually verify DIDs before transacting. A counterparty SHOULD independently re-evaluate a caller's authority rather than trusting that the caller's own guard ran (§3). Mutual verification has each peer sign the OTHER peer's nonce with its own key, so a peer MUST sign only a nonce that is a plain random token (16–128 characters of A–Z a–z 0–9 _ -) and refuse anything else: a nonce that is free text makes the signer an oracle — a malicious peer sends a canonical authorize message (8.3), or a MAGP-SERVICE-v1 claim, as its nonce and obtains a valid signature on it. Every MAGP message that authorizes anything contains |, which a token cannot.

16.3. Whom a Service acts for. A counterparty's re-evaluation (16.2) judges the caller by the caller's own policy — written by the caller's owner. It answers "may this agent do this?", never "is this an agent I act for?". A Service that executes with one owner's credentials (an agent's own tool gateway, an executor holding one owner's API key) MUST therefore also establish that the owner of those credentials admitted the caller:

  • Admission. The Service MUST be configured with the agents it acts for, and MUST refuse a validly signed request from any other agent with AGENT_NOT_ADMITTED — after verifying the request signature, and before fetching the caller's policy or claiming any authorization (8.7.6). An unconfigured admission list is a startup error, never "admit everyone"; a malformed one likewise.
  • Owner binding. The Service MUST be bound to the principal that owns its credentials (gatewayOwnerPrincipal), a startup error when absent (GATEWAY_OWNER_UNBOUND). The policy bundle names the agent's owning principal (ownerPrincipal, signed with the rest); an admitted agent whose bundle names any other owner, or none, MUST be refused GATEWAY_OWNER_MISMATCH before any claim. An agent of another owner can be admitted only by a bilateral delegation the gateway owner grants — not by its own mandate, and not by being listed.
  • Credential profiles. Admission to the Service is not access to every credential it holds: a route, tool or skill MAY admit fewer agents, and an admitted agent outside it MUST be refused CREDENTIAL_PROFILE_NOT_PERMITTED, also before its policy is fetched or anything claimed.

Without these, any agent on the platform whose own owner granted it an action of the same name executes through that Service, with that Service's credentials, under rules the victim never saw; on the stateful path the claim does not stop it, because the hold belongs to the caller and the caller's owner chooses its counterparties (8.7.6). A Service that holds no one owner's credentials — a public tool, or one that resolves each caller's own credential (the Credential Vault) — MAY admit every governed agent, but only by saying so explicitly ('any'), never by omission, and it then takes no owner binding. The reference guards take allowedAgents (agent DIDs, or 'any'), gatewayOwnerPrincipal, and a per-route / per-tool / per-skill allowedAgents; the reference gateway reads AGENTSAFE_ALLOWED_AGENTS and AGENTSAFE_GATEWAY_OWNER; create-metamynd-agent admits the agent it provisioned and binds the gateway to that agent's owner.

16.4. Reporting what a Service did. The issuer sees an execution only when a Service claims an authorization (8.7.6); a Service that executes without one (a bundle-only, non-financial gateway) or refuses a request itself (16.3, a rule it evaluated, a binding failure) is otherwise invisible to the owner whose credentials it holds. Such a Service SHOULD report each governed request it answers to POST /policy/gateway-reports: { servedAgentDid, outcome: "executed" | "refused", reasonCode, httpStatus, request, reportId, occurrences }, where request is the agent's signed request as received (8.3), carrying its payloadDigest when it had one, the authorization the request named as presentedAuthorizationId, and authorizationId only when the Service claimed that authorization for this request. A Service MUST NOT report as authorizationId an authorization it did not claim, and the issuer MUST link a report to an authorization only when the effect chain shows that the reporting Service claimed it; any other id is recorded as presented (an authorizationId from an older Service is read as presented). A request allowed on its own merits that merely names an authorization — an approved escalation's, say — never ran under it. servedAgentDid is the agent the Service acts for — the caller when it is served, else another agent it admits. reportId (8–64 characters of [A-Za-z0-9_-]) is the Service's own id for the report, kept across retries; occurrences is how many identical refusals the report stands for (an executed report is always 1, REPORT_OCCURRENCES_INVALID otherwise). The report is signed as the Service (MAGP-SERVICE-v1, action report, the request's nonce in the authorization-id slot, fields servedAgentDid | callerAgentDid | action | outcome | reasonCode | httpStatus | reportId | occurrences). The issuer MUST record it only when that signature verifies and the served agent's owner has registered the signing DID as an active counterparty — of either purpose; a Service that only reports is registered as report (8.7.6), which does not make the owner registered-only; where the issuer requires proof of control (8.7.6), only a proven entry counts — and MUST file it under that owner — never under the caller's — so a refused foreign agent appears in the victim's audit trail and a party that registers someone else's Service gains nothing. When the caller's signature verifies and its agent belongs to another owner, the issuer MUST also show the report to that owner (the caller's side), without naming which of the gateway owner's agents the Service acts for. An executed report MUST carry a request whose own agent signature verifies (REPORT_CALLER_UNVERIFIED otherwise), checked against the agent's registered key; a refusal is recorded either way, marked unverified. An unknown served agent is answered exactly as an unregistered Service is (COUNTERPARTY_NOT_REGISTERED). The issuer consumes nothing before the row is written: a resent report (the same service DID and nonce, or the same service DID and reportId re-signed later) is answered ALREADY_RECORDED only when it was recorded, so a report that failed may be resent — a Service SHOULD keep one the issuer could not take (unreachable, 429, 5xx) and resend it under its reportId (the reference gateway's reportSpool). A refusal's request is whatever a caller sent, so a Service SHOULD aggregate repeated refusals rather than drop them (the reference gateway: the first per caller and reason at once, the rest of a one-minute window as one report with their occurrences), and the issuer MAY limit reports per Service (REPORT_RATE_LIMITED, 429). An execution under a claimed authorization is not reported: the claim already records it. Reports are visibility only — they never change a decision, a hold or a cap — and are not anchored.


17. Security considerations

17.1. Fail closed. An unreachable gate means block. A guard that cannot verify a bundle for a value-bearing action means block. Only the secondary controls of §9 fail open, only on internal error, and only where the deployment has not configured them to fail closed.

17.2. Signed-last precedence. Server-derived, gateway-derived and signed operands MUST be applied over the unsigned itinerary, so a client-supplied key cannot shadow a signed amount or a server-counted rate (§6.3.2).

17.3. Nonce discipline. Single-use, consumed atomically, only after signature verification.

17.4. The unsigned context is an assertion. tool, jurisdiction and riskLevel are claims by the agent (resource is signed, §8.3; a jurisdiction the gate enforces is the signed top-level one, §8.3.12, never the itinerary's). A signature attributes a claim; it does not verify it, and the counterparty re-check reads the same claim — so a re-check does not, by itself, catch a lie. The defence is §6.3: every field carries a provenance, an owner-set riskTier and a gateway-derived value are floors the agent cannot lower, a rule can require a trusted source with requireProvenance, and a missing or unrecognised risk fails closed. The optional context signature (§8.3.13) stops a relay rewriting the claim; it does not make it true. Adding these fields to the signed message would break every deployed client and every anchored proof, and would still only attribute them; it is not proposed.

17.5. Escalation is not permission. See §8.6.4.

17.6. Delegation cannot widen. See §5.4. An implementation that reverses the isNoneOf rule silently grants authority nobody issued.


18. Extensibility and versioning

18.1. New operands and atoms extend the protocol. New operators do not.

18.2. An unknown reason code MUST be treated as a block (§8.6.5). An unknown atom MUST NOT silently pass.

18.3. Additive fields on the request or verdict are minor changes. Changes to the canonical message (§8.3), to leaf composition (§13.5), or to the order of checks (§8.5) are breaking.

18.4. GET /magp/schema publishes the request, verdict and rule-pack JSON Schemas, the reason-code registry with SAFR aliases, the control namespaces and the atom catalog. It is the machine-readable companion to this document.


19. Conformance

An implementation is conformant if it:

  1. resolves agent keys from the DID (§4.3.1);
  2. rebuilds and verifies the canonical message exactly (§8.3);
  3. applies the order of checks in §8.5, stopping at the first failure;
  4. treats allow and observe as the only permitting dispositions (§8.6.3);
  5. never treats escalate as permission (§8.6.4);
  6. fails closed as §17.1 requires;
  7. enforces the narrowing invariant on any delegated mandate it accepts (§5.4);
  8. emits reason codes from Appendix A, or documents extensions;
  9. produces evidence verifiable offline (§13.4).

A guard need not implement the stateful steps (8, 9, 10, 16). It MUST defer them to the gate and MUST NOT report a permitting verdict on their behalf for a value-bearing action.


20. Reference endpoints

Endpoint Auth Purpose
POST /policy/mandate/authorize public The gate
POST /policy/mandate/authorize/{id}/capture public Reconcile a hold
POST /policy/mandate/authorize/{id}/void public Release a hold
GET /policy/escalations/{id}/status public Follow a held action
GET /policy/bundle/{did} public Signed policy bundle
GET /magp/policy/pubkey public Bundle verification key
GET /did/{did} public DID document
GET /magp/evidence/{id}/proof public Inclusion proof
GET /magp/schema public Machine-readable protocol surface
GET /standards/atoms authenticated Atom catalog with required context

Appendix A — Reason codes

Generated from the reference implementation's registry, which is also published at GET /magp/schema. The SAFR §30 alias column maps each code to the canonical SAFR vocabulary where one exists; — marks a permit/lifecycle code or a MetaMynd-specific extension beyond the SAFR floor.

Permit / lifecycle (no SAFR deny alias)

Code Decision Meaning SAFR §30 alias
AUTHORIZED allow All checks passed. —
OBSERVED observe Permitted but flagged for monitoring (SAFR §11). —
STANDARD_OBSERVE observe A Standard molecule marked the action for monitoring. —
SOP_OBSERVE observe An SOP molecule marked the action for monitoring. —

Request validation

Code Decision Meaning SAFR §30 alias
MALFORMED_REQUEST block The request body failed schema validation (missing/invalid field, e.g. a negative amount) before any policy evaluation ran. Returned in the same verdict envelope shape as every other decision (decision/reasonCode), not a raw validation-issue array, so a caller checking decision/reasonCode doesn't need a second response shape for this one path. —

Identity (SAFR §6)

Code Decision Meaning SAFR §30 alias
PRINCIPAL_UNVERIFIED block The authorizing principal's authority does not currently hold: a mainnet agent beyond the free-tier caps (1 mainnet agent, 5 mandates). Testnet and did:key agents are not blocked on principal verification — it is reported on the verdict and in the registry, for a counterparty's own risk policy. (The code's name predates the free-tier model.) IDENTITY_UNVERIFIED
AGENT_NOT_FOUND block Unknown agent DID. IDENTITY_UNVERIFIED
AGENT_KEY_UNVERIFIED block BYOK key not yet proven. IDENTITY_UNVERIFIED
IDENTITY_ROTATED block The request is signed as a DID its agent no longer uses: the owner rotated the agent's key (Rotate key), which issued a new DID and moved the agent's mandates to it. The old DID and key stop working at once. IDENTITY_REVOKED
IDENTITY_KEY_MISMATCH block Registered public key does not match the key embedded in the agent's own self-certifying DID. IDENTITY_UNVERIFIED
MANDATE_REVOKED block Mandate has been revoked. The gate and every guard refuse an action with it when every mandate for that action was revoked (§8.5 step 1; a guard reads the bundle's revokedActions, §6.2.6). Also the reason, decided by system, on every review that was pending under it (§10.4.2), an approval refused because the mandate is no longer active, and a claim or capture of an unclaimed hold under a revoked mandate (§8.7.15). A delegated mandate is refused it too when any mandate it was delegated from is inactive (§5.4.9), and an owner's resource pin or payload-binding toggle that would reissue such a mandate is refused with it (409, nothing changes; §5.4.11). IDENTITY_REVOKED
DELEGATION_CHAIN_BROKEN block A delegated mandate whose chain cannot be walked to its root — a parent mandate is missing, the chain cycles, or it is deeper than the delegation bound (§5.4.9). Fails closed at authorize, at an approval, and on a claim or capture of an unclaimed hold under it, and refuses (409) an owner's resource pin or payload-binding toggle that would reissue it (§5.4.11). IDENTITY_REVOKED
DELEGATION_NARROWING_FAILED block A reissue of a delegated mandate whose changed terms would not be a narrowing of its delegator's current terms (§5.4.2–§5.4.5, §5.4.11). An owner's resource pin or payload-binding toggle is refused with it (409, nothing changes); an identity rotation revokes the delegated mandate with it instead of carrying it forward. Also the reason the start-up repair revokes a pre-fix reissue that is wider than its delegation. MANDATE_SCOPE_VIOLATION

Envelope integrity (SAFR §5)

Code Decision Meaning SAFR §30 alias
SIGNATURE_INVALID block Ed25519 verification failed. ENVELOPE_INTEGRITY_FAILURE
CLIENT_PROTOCOL_VERSION_UNSUPPORTED block The request genuinely verifies against the pre-resource (7-field) canonical message, not the current 8-field one — a real, valid key on an SDK old enough to predate resource (MAGP §8.3), not a broken or rotated key. Still blocked (the old message can't carry a signed resource-scope commitment), but distinguished from SIGNATURE_INVALID so the operator upgrades the client instead of rotating a key that was never the problem. ENVELOPE_INTEGRITY_FAILURE
CONTEXT_SIGNATURE_INVALID block Optional envelope signature present (Tier 1 context-claim binding, SAFR §5) but does not verify against the GovernanceEnvelope hash — the context was altered after the agent signed it, or signed with the wrong key. ENVELOPE_INTEGRITY_FAILURE
PAYLOAD_BINDING_INVALID block Payload binding (§8.3.9, or a late binding §8.3.11) present but not valid: a payloadDigest without a payloadSignature (or the reverse), a digest that is not "sha256:" + 64 lower-case hex, or a signature that does not verify under the agent key over MAGP-PAYLOAD-v1 / agentDid / action / nonce / issuedAt / payloadDigest (at authorize) or MAGP-PAYLOAD-REBIND-v1 / agentDid / action / authorizationId / nonce / issuedAt / payloadDigest (a hold that already exists). A digest nobody signed binds nothing, so it is refused, never stored. ENVELOPE_INTEGRITY_FAILURE
PAYLOAD_BINDING_REQUIRED block The mandate's owner requires this action's payload to be bound (requirePayloadBinding, §8.3.9) and the request carries no signed payload digest — or, at a claim, the hold carries none yet (a reviewer's MODIFY produces one) and must be bound (§8.3.11) before it can be claimed. Never granted unbound. Fix: send the payload with the request so the agent signs its digest, or bind the hold (POST /policy/mandate/authorize/:id/payload-binding) and then claim. ENVELOPE_INTEGRITY_FAILURE
QUERY_NOT_BOUND block Executor-side (an HTTP gateway, §8.3.9): the request to a governed route carries a URL query string — or a query, path parameter or fragment encoded into its path (%3F, ;, %23) — that the agent's signature and payload binding do not cover. Refused before anything is claimed, so no hold is consumed. A route may list the exact query keys it forwards (agentsafe-http-gateway allowedQuery, never a signed value field); those are forwarded unbound. ENVELOPE_INTEGRITY_FAILURE
REPLAY_DETECTED block Nonce already consumed. ENVELOPE_INTEGRITY_FAILURE
REQUEST_EXPIRED block issuedAt outside the freshness window. ENVELOPE_INVALID

Mandate (SAFR §7)

Code Decision Meaning SAFR §30 alias
NO_MANDATE block The agent holds no mandate at all, and none was ever revoked for this action (that is MANDATE_REVOKED). MANDATE_MISSING
MANDATE_EXPIRED block Outside mandate validity window. MANDATE_EXPIRED
MANDATE_NOT_YET_VALID block Before the mandate validity window. MANDATE_EXPIRED
NO_PERMISSION_FOR_ACTION block Action not granted by the mandate. ACTION_NOT_PERMITTED
PROHIBITED block A mandate prohibition fired. REGULATORY_PROHIBITION
CONSTRAINT_FAILED block A mandate constraint failed. MANDATE_SCOPE_VIOLATION
JURISDICTION_REQUIRED block The request signed no jurisdiction (§8.3.12) while one is required: the mandate restricts jurisdictions for this action (its allowed-jurisdictions term), or an enforced SOP or Standard rule judges jurisdiction-not-allowed. Only the SIGNED top-level jurisdiction counts — an itinerary jurisdiction / mm:jurisdiction is the agent's unsigned word and is ignored. A hard block: it outranks an escalate. EVIDENCE_MISSING
JURISDICTION_NOT_ALLOWED block The request's signed jurisdiction (§8.3.12) is not in the mandate's allowed-jurisdictions list for this action. MANDATE_SCOPE_VIOLATION
JURISDICTION_MISMATCH block The request's signed jurisdiction (§8.3.12) differs from the country the owner registered for the payee (payee directory, PUT /policy/payees/country) — or the owner's entries for that payee name more than one country. A registered country is authoritative: it is the effective jurisdiction, so the agent's contrary word is refused. Judged at authorize and again, on the registry as of that moment, at an approval or MODIFY. A hard block: it outranks an escalate. COUNTERPARTY_NOT_ALLOWED

Exposure / counterparty (SAFR §22/§23)

Code Decision Meaning SAFR §30 alias
SPEND_LIMIT_EXCEEDED block Mandate budget constraint failed. EXPOSURE_THRESHOLD_EXCEEDED
CAP_EXCEEDED block The mandate's spend cap, enforced atomically as the hold is written: committed spend plus this amount is over the cap. The rules passed a moment earlier, so this is almost always a concurrent authorize that took the rest of the budget in between. The same exhausted budget met one request at a time is refused earlier, by the rules: SOP_BUDGET_CAP (a rule pack's budget) or SPEND_LIMIT_EXCEEDED (the mandate's own constraint). Same cause, different control; none of them is retryable until spend is released. EXPOSURE_THRESHOLD_EXCEEDED
CURRENCY_NOT_ALLOWED block The request's currency is not one a cap is denominated in: a mandate spend constraint, or an SOP or rule-pack cap scoped to a currency (amount-over / cumulative-over with currency). The cap cannot judge an amount in another currency, so it refuses (fail closed) whatever the amount. Remedy: request in the cap's currency, or have the owner add the currency to the mandate and its caps. MANDATE_SCOPE_VIOLATION
AMOUNT_NOT_DETERMINABLE block The gate could not establish how much value the action moves, so no cap could be applied to it (amount-unknown atom). EVIDENCE_MISSING
MERCHANT_NOT_ALLOWED block Merchant outside mandate. COUNTERPARTY_NOT_ALLOWED
COUNTERPARTY_NOT_ALLOWED block Counterparty outside mandate. COUNTERPARTY_NOT_ALLOWED
ROUTE_NOT_ALLOWED block Route outside mandate. ACTION_NOT_PERMITTED
RATE_LIMIT_EXCEEDED block / escalate / observe A time-windowed rate limit fired (rate-limit-exceeded atom). RATE_LIMIT_EXCEEDED
CONTROL_UNAVAILABLE block / escalate A secondary control could not run and the deployment is configured to fail closed on them (SECONDARY_CONTROLS_FAIL_CLOSED): the rate window or circuit-breaker lookup blocks; the spend-anomaly check or assurance lookup escalates to a person. Off by default, where those controls fail open (§9). EVIDENCE_MISSING

Policy / SOP / standards (SAFR §8)

Code Decision Meaning SAFR §30 alias
UNAUTHENTICATED_MCP_CALL block An MCP tool call arrived without an authenticated principal for the gate to attribute it to (seeded MCP default policies). IDENTITY_UNVERIFIED
STANDARD_NONCOMPLIANT block Missing/stale pin for an enforced Standard. POLICY_VIOLATION
STANDARD_RULE block / escalate / suspend / quarantine A bound Standard molecule fired. POLICY_VIOLATION
SOP_RULE block / escalate / suspend / quarantine A bound SOP molecule fired. SOP_VIOLATION
AUTHORIZATION_RULES_CHANGED block A claim (or a capture without a claim) of an unclaimed hold refused because the agent's CURRENT SOP or bound-Standard rules no longer permit it (§8.7.16): the rules were tightened after the hold was granted. The hold is voided — its budget returns to the cap, the firing rule's own code is recorded as HOLD_VOIDED:<code> — and nothing executes; the agent asks again under the new rules. A hold already claimed is never re-judged. POLICY_VIOLATION
ESCALATION_NOT_APPROVED escalate A claim that asked to execute only a hold a person approved (§8.7.18) — the executor's own policy judged the request as needing review — presented a hold that no approval minted. The hold is left unclaimed and unchanged; the request still needs a person, so the agent escalates and waits for the decision. HUMAN_REVIEW_REQUIRED
AUTHORIZATION_CONTEXT_MISMATCH block A claim of a hold a PERSON approved stated a context (itinerary) other than the one that person approved (§9a.5): an approval of one operation was presented for another. Refused before the claim, so the hold is left unclaimed for the approved request; an executor that finds the mismatch only on the grant releases the claim and refuses. The issuer refuses an in-process resume claim (MAGP-RESUME-CLAIM-v2, §9a.6) for another context the same way, with the one resume left untaken. Resume with the same arguments the escalation was raised with. MANDATE_SCOPE_VIOLATION
AUTHORIZATION_CONTEXT_REQUIRED block A claim of a hold a PERSON approved did not state the context it is about to execute (§9a.5), so the issuer cannot tell whether it is the context that was approved. Refused, the hold untouched. The executor predates the approved-context binding: upgrade it (agentsafe-mcp-guard 0.27.0+, agentsafe-a2a-guard 0.18.0+, agentsafe-http-gateway 0.26.0+). Also the answer to an in-process resume claim signed as MAGP-RESUME-CLAIM-v1 (§9a.6), which names no request or context: upgrade the SDK (@metamynd/agentsafe-guard 0.32.0+, metamynd-client 0.19.0+), which signs v2. A gateway's refusal carries that upgrade in its detail; an SDK (guard 0.34.0+, client 0.20.0+) keeps the approval's hold rather than voiding it, and a signed claim refused this way re-opens the approval's one resume while the hold is still held and unclaimed, so the agent resumes again after upgrading the gateway instead of its owner approving again. ENVELOPE_INVALID
AUTHORIZATION_CONTEXT_UNAVAILABLE block A claim of a hold a PERSON approved, whose approving review could not be found to read the approved context from (§9a.5). Refused rather than claimed unchecked; the hold is left as it was. POLICY_VIOLATION
ESCALATION_REQUEST_MISMATCH block Raised by the SDK's resume() (Node guard 0.24.0+, Python client 0.14.0+) and, since MAGP-RESUME-CLAIM-v2 (§9a.6), by the issuer on the resume claim: the arguments it was given do not reproduce the request the owner approved — its amount, currency, merchant, resource, payload, or (guard 0.29.0+ / client 0.18.0+) its context (§9a.5). Nothing runs and the approval is left for the right request; resume with the arguments the escalation was raised with. MANDATE_SCOPE_VIOLATION
CONTEXT_TOO_LARGE block An agent with bound SOP or Standard rules sent a rule context (its itinerary and envelope metadata) too large to record whole with the hold (§8.7.16). Refused rather than trimmed: a trimmed context could drop a field a rule reads and let the hold slip the claim-time re-check. Send a smaller context. ENVELOPE_INVALID
SANDBOX_CALLER_SHARE_EXCEEDED block The shared public sandbox only: this caller (told apart by a keyed hash of its network) already holds its share of the sandbox's rolling spend budget, so one visitor cannot keep the budget drained for everyone else. The budget heals as the window rolls past; a real tenant's mandate never returns this. POLICY_VIOLATION

Evidence-quality controls (SAFR §24). Author-facing codes for the evidence atoms.

Code Decision Meaning SAFR §30 alias
EVIDENCE_INSUFFICIENT block / escalate A required evidence type was not attested (evidence-requirement atom). EVIDENCE_MISSING
EVIDENCE_CONFIDENCE_LOW block / escalate Attested evidence confidence below the required minimum (evidence-confidence-below atom). EVIDENCE_QUALITY_LOW

Containment / circuit breaker (SAFR §21)

Code Decision Meaning SAFR §30 alias
AGENT_SUSPENDED suspend Agent contained (suspended) by a prior firing rule — reinstate to act again. CIRCUIT_BREAKER_ACTIVE
AGENT_QUARANTINED quarantine Agent contained (quarantined) by a prior firing rule — reinstate to act again. CIRCUIT_BREAKER_ACTIVE
AGENT_DECOMMISSIONED decommission Agent decommissioned (retired) by its owner — final: it is never reinstated; a new agent takes over its work. The owner's reason, if they gave one, is the record's note. CIRCUIT_BREAKER_ACTIVE
CIRCUIT_BREAKER_OPEN block A scoped circuit breaker (agent/tenant/tool/counterparty) is open — tripped manually or by failure rate. CIRCUIT_BREAKER_ACTIVE

Human review (SAFR §12)

Code Decision Meaning SAFR §30 alias
MODE_READ_ONLY block READ_ONLY operating mode denies a value-bearing action. (The owner's act of pinning that mode is recorded under the same code, as MODE_AUTONOMOUS / MODE_SUPERVISED / MODE_RESTRICTED are.) ACTION_NOT_PERMITTED
MODE_REVIEW escalate Operating-mode floor routed to human review. HUMAN_REVIEW_REQUIRED
MODE_RESTRICTED_REVIEW escalate RESTRICTED mode: every value-bearing action needs a human. HUMAN_REVIEW_REQUIRED
MODE_SUPERVISED_REVIEW escalate SUPERVISED mode: high-consequence action needs a human. HUMAN_REVIEW_REQUIRED
CONTEXT_UNVERIFIABLE escalate A rule needed a context field (e.g. riskLevel) and could not get a well-formed value from a source it trusts — missing, unrecognised, or less trusted than the rule requires (spec §6.3). HUMAN_REVIEW_REQUIRED
REASSESSMENT_PENDING escalate A material authority change awaits owner review. HUMAN_REVIEW_REQUIRED
SPEND_PATTERN_ANOMALY escalate Amount is far above the agent's own recent payments for this action (a graph-backed baseline of at least 5, not a preset cap). An amount within a share of the mandate's per-transaction cap (25% by default) is not checked: the cap and budget already bound it. HUMAN_REVIEW_REQUIRED
AGENT_NOT_CERTIFIED escalate The agent's latest deployment-assurance certificate is 'blocked' (a required assurance gate failed or was never assessed). HUMAN_REVIEW_REQUIRED
ASSURANCE_STALE escalate The agent's latest deployment-assurance certificate has passed its validUntil — the assessed facts are no longer current. HUMAN_REVIEW_REQUIRED
ESCALATION_PENDING escalate Held awaiting approver resolution (§9a). HUMAN_REVIEW_REQUIRED
ESCALATION_EXPIRED block Escalation TTL elapsed (§9a.4). HUMAN_REVIEW_TIMEOUT
ESCALATION_CONTEXT_ALTERED block The pending escalation's stored request context no longer matches its row-integrity commitment made at escalate time (Tier 2 context-claim binding, SAFR §5) — reservation refused. ENVELOPE_INTEGRITY_FAILURE
MANDATE_SUPERSEDED block Approval refused: the mandate the review was raised under is no longer the agent's current mandate for the action (a newer one has been issued) — the agent asks again (§10.4.1). Also the reason a reissue closes the reviews pending under the mandate it replaced (§10.4.2). MANDATE_MISSING
APPROVAL_INPUT_UNAVAILABLE block Approval refused: an input the gate judges the request on could not be read or reconstructed at approval time (rate counter, breaker store, or the signed resource or signed jurisdiction of a review stored before it was kept) — refused, never skipped; the review stays pending (§10.4.1). EVIDENCE_MISSING
ESCALATION_APPROVED allow Approver upgraded the held request (§9a). —
ESCALATION_DENIED block Approver denied the held request (§9a). —

Decision token / capability binding (SAFR §20)

Code Decision Meaning SAFR §30 alias
AMOUNT_MISMATCH block x402 amount exceeds the authorized amount (§7a). DECISION_TOKEN_INVALID
COMMITMENT_MISMATCH block Executed transaction does not match the authorized commitment. DECISION_TOKEN_INVALID
SESSION_MISMATCH block Capability bound to a different agent↔MCP session. DECISION_TOKEN_INVALID
CAPABILITY_EXPIRED block Capability past its expiry (§7.7). DECISION_TOKEN_INVALID
CAPABILITY_SIGNATURE_INVALID block Capability signature invalid or wrong issuer key. DECISION_TOKEN_INVALID
CAPABILITY_OK allow Capability signature + transaction/session binding verified. —
CAPABILITY_AGENT_MISMATCH block The capability was issued to a different agent DID than the one presenting it — or carries no agent at all (a v1 capability) while the verifier requires one (§15.3). DECISION_TOKEN_INVALID
CAPABILITY_SCOPE_MISMATCH block The capability names a different mandate, action or currency than the verifier expects, or names none (a v1 capability) where one is required (§15.3). DECISION_TOKEN_INVALID

Evidence quality (SAFR §24)

Code Decision Meaning SAFR §30 alias
BBS_KYC_LEVEL_TOO_LOW block Disclosed KYC level below the required floor. EVIDENCE_QUALITY_LOW

MetaMynd-specific extensions beyond the SAFR floor (no §30 alias)

Code Decision Meaning SAFR §30 alias
HOLD_VOIDED void Reservation expired or explicitly voided (§7a.4). —
SETTLEMENT_FAILED block Facilitator could not verify/settle (§7a). —
AGENT_NOT_ADMITTED block A Service-side guard refused a validly signed request from an agent it does not admit (allowedAgents, §16.3) — before fetching its policy or claiming anything. COUNTERPARTY_NOT_ALLOWED
CREDENTIAL_PROFILE_NOT_PERMITTED block The agent is admitted to the Service but not to this credential profile (a route, tool or skill that admits fewer agents, §16.3) — refused before its policy is fetched or anything claimed. COUNTERPARTY_NOT_ALLOWED
GATEWAY_OWNER_MISMATCH block The agent's signed bundle names an owner other than the principal that owns this Service's credentials (gatewayOwnerPrincipal, §16.3), or names none — refused before any claim. COUNTERPARTY_NOT_ALLOWED
GATE_UNREACHABLE block Guard fail-closed on transport error: the issuer could not be reached (a network failure, or a 5xx). A Service-side guard returns it when the policy bundle fetch fails that way; a 4xx answer is not an outage. —
ZK_POLICY_OK allow All zero-knowledge predicates verified blind. —
ZK_AMOUNT_PROOF_INVALID block Bulletproofs range proof (amount ≤ cap) failed. EXPOSURE_THRESHOLD_EXCEEDED
ZK_JURISDICTION_PROOF_INVALID block Set-membership proof (jurisdiction ∈ allow-list) failed. REGULATORY_PROHIBITION
ZK_CALLER_POLICY_REJECTED block A private-authorize request carried its own policy (cap, allowedJurisdictions). The terms are read from the agent's active mandate; a caller-chosen policy is refused (HTTP 400) rather than ignored, so an old client fails loudly (§15.1). ENVELOPE_INVALID
ZK_TRANSACTION_COMMITMENT_REQUIRED block A private-authorize request carried no transactionCommitment (HTTP 400). Every allow returns a capability bound to the transaction it authorizes, so a request with no transaction to bind to is refused rather than answered with an unbound verdict (§15.1). ENVELOPE_INVALID
ZK_CAP_MISMATCH block The range proof was built against a cap other than the agent's mandate cap, so it proves nothing about this mandate. Rebuild it against the cap in the agent's policy bundle (§15.1). ENVELOPE_INVALID
ZK_AMOUNT_PROOF_REQUIRED block The mandate caps this action but the private-authorize request carries no amount commitment + range proof (§15.1). EVIDENCE_MISSING
ZK_JURISDICTION_PROOF_REQUIRED block The mandate restricts jurisdictions for this action but the private-authorize request carries no jurisdiction commitment + membership proof (§15.1). EVIDENCE_MISSING
ZK_MANDATE_TERMS_UNSUPPORTED block The mandate has no term the zero-knowledge path can prove for this action (no spend cap, no jurisdiction allow-list), or has one in a form it cannot prove (a non-upper-bound cap, a jurisdiction exclusion). Use /policy/mandate/authorize (§15.1). MANDATE_SCOPE_VIOLATION
ZK_CURRENCY_NOT_ALLOWED block The private-authorize request names a currency outside the mandate cap's currencies, or names none where the cap allows several (§15.1). MANDATE_SCOPE_VIOLATION
ZK_HUMAN_REVIEW_UNAVAILABLE block The agent's operating mode (SUPERVISED/RESTRICTED) routes value-bearing actions to a human, and the zero-knowledge path can neither see the amount nor hold anything for a reviewer — so it refuses. Use /policy/mandate/authorize (§15.1). HUMAN_REVIEW_REQUIRED
BBS_PRESENTATION_OK allow Issuer-signed attributes verified via BBS selective disclosure. —
BBS_PROOF_INVALID block BBS proof invalid, wrong issuer key, or nonce mismatch (replay). ENVELOPE_INTEGRITY_FAILURE
POLICY_BUNDLE_OK allow Policy bundle signature + freshness verified. —
POLICY_BUNDLE_STALE block Bundle older than maxStaleness — fails closed for any action under a pinned key, else for a value-bearing one. POLICY_VIOLATION
POLICY_BUNDLE_UNSIGNED block Bundle carries no signature — fails closed for any action under a pinned key, else for a value-bearing one. POLICY_VIOLATION
POLICY_BUNDLE_SIGNATURE_INVALID block Bundle signature invalid or tampered (any tier). ENVELOPE_INTEGRITY_FAILURE
POLICY_BUNDLE_UNVERIFIED block No pinned policy key and the bundle came over plain http — nothing authenticates it, so a value-bearing action fails closed. ENVELOPE_INTEGRITY_FAILURE

Action Passport (MetaMynd Governed Execution scope, Module E/F)

Code Decision Meaning SAFR §30 alias
PASSPORT_EXPIRED block The Action Passport bound to this authorization has passed its expiry — capture refused. MANDATE_EXPIRED
PASSPORT_ALREADY_CONSUMED block The Action Passport was already consumed by a prior capture — single-use, replay refused. DECISION_TOKEN_INVALID
PASSPORT_REVOKED block The Action Passport was revoked (its hold was voided) before capture. DECISION_TOKEN_INVALID

Platform rule packs

Code Decision Meaning SAFR §30 alias
SOP_SPEND_CAP block A starter SOP or rule pack's per-transaction cap: the amount is over the cap in the cap's currency. EXPOSURE_THRESHOLD_EXCEEDED
SOP_BUDGET_CAP block A rule pack's total budget: committed spend plus this amount is over the budget. EXPOSURE_THRESHOLD_EXCEEDED
RISK_REVIEW escalate A starter SOP or rule pack's risk review: the effective risk level is at or above the pack's threshold. HUMAN_REVIEW_REQUIRED
PII_REVIEW escalate A rule pack sends a request that says it carries personal data to a human. HUMAN_REVIEW_REQUIRED
CONTENT_REVIEW escalate A rule pack's starter term list matched the request text (a STARTER list, meant to be replaced with the owner's own). HUMAN_REVIEW_REQUIRED
JURISDICTION_BLOCKED block A rule pack's jurisdiction list does not include the request's jurisdiction. MANDATE_SCOPE_VIOLATION
DATA_RESIDENCY_REVIEW escalate A rule pack's data-residency list does not include the request's data region. HUMAN_REVIEW_REQUIRED
EVIDENCE_INCOMPLETE escalate A rule pack requires evidence types the request did not carry. EVIDENCE_MISSING
EVIDENCE_CONFIDENCE_REVIEW escalate The request's evidence confidence is below a rule pack's minimum. HUMAN_REVIEW_REQUIRED
COUNTERPARTY_TRUST_REVIEW escalate The counterparty's trust score is below a rule pack's review threshold. HUMAN_REVIEW_REQUIRED
RATE_LIMIT_REVIEW escalate The agent's call count in its rate-limit window passed a rule pack's early-review mark (before the hard RATE_LIMIT_EXCEEDED ceiling). HUMAN_REVIEW_REQUIRED

Automated containment (MetaMynd Governed Execution scope, Module H)

Code Decision Meaning SAFR §30 alias
REPEATED_DENIALS_CONTAINED quarantine Automated containment: this agent crossed the repeated-denial threshold within the rolling window and was quarantined (AUTO_CONTAINMENT_MODE). CIRCUIT_BREAKER_ACTIVE
MANUAL_CONTAINMENT suspend / quarantine The agent was contained (suspended or quarantined) by its owner or an operator from the dashboard or API. The owner's reason, if they gave one, is the record's note. Reinstate it to act again. CIRCUIT_BREAKER_ACTIVE
REINSTATED_AFTER_REGRESSION_CHECK allow The agent's owner reinstated it after its current policy passed every Scenario Bank set that applies to it (the sets checked are on the record); it may act again. —

Claim, resume and settlement (an executor's or SDK's refusal)

Code Decision Meaning SAFR §30 alias
AUTHORIZATION_REQUIRED block The executor requires an authorization (an authorizationId from an allow or an approval) and the request carried none. DECISION_TOKEN_INVALID
AUTHORIZATION_NOT_FOUND block No hold exists for the authorizationId presented. DECISION_TOKEN_INVALID
AUTHORIZATION_EXPIRED block The hold was not claimed within its time-to-live; ask again. MANDATE_EXPIRED
AUTHORIZATION_VOIDED block The hold was released (voided) before this claim — by its agent, its owner, a revoke, a containment, a rule change or a change to the mandate limits. Also raised for an approval whose hold was voided: by the issuer on any resume claim of it, first or repeat (§9a.6), and by the SDK resume() (guard 0.34.0+, client 0.20.0+). Nothing can run under it, and its owner must approve the request again. DECISION_TOKEN_INVALID
AUTHORIZATION_ALREADY_CLAIMED block Another executor (or an earlier attempt) already claimed this single-use hold. DECISION_TOKEN_INVALID
AUTHORIZATION_ALREADY_USED block A resume of an approval that is already finished with: its authorization was settled, claimed by a gateway, or expired, so the tool is not run again. Raised by the SDK's resume() and, for a repeat claim of a resume already taken, by the issuer (§9a.6). A hold that was voided is reported AUTHORIZATION_VOIDED instead (guard 0.34.0+, client 0.20.0+). DECISION_TOKEN_INVALID
AUTHORIZATION_IN_USE block The one resume of this approval is already taken and may still be running — by a concurrent resume in this process or another one (§9a.6); its authorization has not settled yet. Exactly one runs. DECISION_TOKEN_INVALID
AUTHORIZATION_AGENT_MISMATCH block The hold belongs to another agent than the one whose request presents it (§8.7.19). IDENTITY_UNVERIFIED
AUTHORIZATION_ACTION_MISMATCH block The hold authorizes another action than the request's (§8.7.19). MANDATE_SCOPE_VIOLATION
AUTHORIZATION_AMOUNT_MISMATCH block The hold authorizes another amount than the request's (§8.7.19). MANDATE_SCOPE_VIOLATION
AUTHORIZATION_CURRENCY_MISMATCH block The hold authorizes another currency than the request's (§8.7.19). MANDATE_SCOPE_VIOLATION
AUTHORIZATION_MERCHANT_MISMATCH block The hold authorizes another merchant than the request's (§8.7.19). COUNTERPARTY_NOT_ALLOWED
AUTHORIZATION_CLAIM_UNREACHABLE block The executor could not reach the issuer to claim the hold, so it did not execute (fail closed). —
COUNTERPARTY_AUTH_REQUIRED block A claim or settlement of a hold was not signed by the agent or by a counterparty the owner registered: an authorizationId alone settles nothing. IDENTITY_UNVERIFIED
COUNTERPARTY_MISMATCH block Only the counterparty that claimed the hold may settle it lower, void it or mark it unknown. COUNTERPARTY_NOT_ALLOWED
PAYLOAD_DIGEST_INVALID block The payload digest presented is not a well-formed sha256 digest. ENVELOPE_INVALID
PAYLOAD_DIGEST_MISMATCH block What the executor is about to run is not the payload the agent signed (payload binding, §8.3.9). ENVELOPE_INTEGRITY_FAILURE
PAYLOAD_NOT_BOUND block The executor requires the agent to bind its payload (§8.3.9) and the request bound none. ENVELOPE_INVALID
PAYLOAD_NOT_CANONICALIZABLE block The payload cannot be canonicalised (a value JSON cannot carry, too deep or too large), so it cannot be bound or compared. ENVELOPE_INVALID
PAYLOAD_ALREADY_BOUND block A late payload binding (§8.3.11) was refused: the hold is already bound to another payload. ENVELOPE_INTEGRITY_FAILURE
PAYLOAD_BINDING_UNSUPPORTED block Raised by the SDK: its key provider cannot sign a payload binding, so the request is not sent unbound. —
PAYLOAD_BINDING_NOT_CONFIRMED block Raised by the SDK: the issuer answered a late binding without confirming the digest (it predates §8.3.11), so the hold is treated as unbound. —
HOLD_UNDER_RECONCILIATION block The hold is being reconciled (its outcome is unknown) and cannot be claimed or settled until that resolves. —
EFFECT_TRANSITION_CONTENDED block An overlapping attempt on the same hold is in progress; retry the same request (not a refusal of it). —
AMOUNT_CHARGED_REQUIRED block A settlement must state the amount actually charged. —
REFUND_AMOUNT_INVALID block A refund must be a positive amount no greater than what was captured. —
SETTLEMENT_REUSED block A settlement proof (x402 payment) was already used for another authorization. DECISION_TOKEN_INVALID
ESCALATION_MODIFIED_APPROVED allow A reviewer MODIFIED the escalated action and the modified action passed the gate: a hold was minted on the reviewer's values. —

Fail-closed executor and guard errors

Code Decision Meaning SAFR §30 alias
GUARD_ERROR block The executor guard failed while verifying the request (not an issuer outage); nothing executed. —
LOCAL_EVAL_ERROR block The agent's guard failed while evaluating its cached rules locally; nothing ran. —
ISSUER_UNREACHABLE block The executor could not reach the issuer it needs (bundle, claim or status), so it did not execute. —
ISSUER_API_REQUIRED block The executor is configured to claim or settle but was given no issuer API to do it with. —
NO_ISSUER block No issuer is configured for an operation that needs one. —
BUNDLE_SUBJECT_MISMATCH block The policy bundle fetched is for another agent than the request's. ENVELOPE_INTEGRITY_FAILURE
MAGP_ENVELOPE_MISSING block An A2A task arrived without the MAGP extension envelope (the signed request). ENVELOPE_INVALID
CAPABILITY_REQUIRED block The tool requires a capability credential the request did not present. DECISION_TOKEN_INVALID
CAPABILITY_CHECK_ERROR block The capability credential could not be checked, so the call was refused. —
SERVICE_IDENTITY_REQUIRED block The executor has no service identity (serviceDid / key) for an operation that must be signed. —
SERVICE_SIGNING_FAILED block The executor could not sign with its service key. —
NO_SERVED_AGENT block The executor was asked to act for an agent it does not serve. IDENTITY_UNVERIFIED
NO_FACILITATOR block An x402 payment needs a facilitator and none is configured. —
REPORT_OUTCOME_INVALID block An executor's outcome report was malformed and was not sent. —
REPORT_UNREACHABLE block An executor's outcome report could not be delivered (it is kept and re-sent). —

Rules MetaMynd writes for you: the Controlled Testing plans and the SOP compiler's drafts

Code Decision Meaning SAFR §30 alias
SANDBOX_PER_TXN_CAP block Controlled Testing baseline plan: an amount over its per-transaction cap. EXPOSURE_THRESHOLD_EXCEEDED
SANDBOX_CUMULATIVE_CAP block Controlled Testing baseline plan: committed spend over its cumulative cap. EXPOSURE_THRESHOLD_EXCEEDED
SANDBOX_RISK_REVIEW escalate Controlled Testing baseline plan: a high-risk action sent for review. HUMAN_REVIEW_REQUIRED
SANDBOX_JURISDICTION block Controlled Testing baseline plan: a jurisdiction outside the plan's list. MANDATE_SCOPE_VIOLATION
SANDBOX_TOOL_NOT_ALLOWED block Controlled Testing baseline plan: a tool outside the plan's allow-list. ACTION_NOT_PERMITTED
SANDBOX_PII_DISCLOSURE block Controlled Testing baseline plan: a request that discloses personal data. POLICY_VIOLATION
SANDBOX_EVIDENCE_CONFIDENCE escalate Controlled Testing baseline plan: evidence confidence below the plan's minimum. EVIDENCE_QUALITY_LOW
SANDBOX_RATE_EXCURSION observe Controlled Testing baseline plan: call rate above the plan's watch mark (flagged, not refused). —
PROBE_CAP block Governance Respect Probes: the over-cap probe was refused, as the probe expects (the agent must then stop). EXPOSURE_THRESHOLD_EXCEEDED
PROBE_ESCALATE escalate Governance Respect Probes: the high-risk probe was escalated, as the probe expects (the agent must then wait). HUMAN_REVIEW_REQUIRED
AMOUNT_ABOVE_LIMIT block A rule the SOP compiler drafted from "no payment may exceed X": the amount is over X. EXPOSURE_THRESHOLD_EXCEEDED
AMOUNT_NEEDS_APPROVAL escalate A rule the SOP compiler drafted from "payments over X need approval": the amount is over X. HUMAN_REVIEW_REQUIRED
BUDGET_EXCEEDED block A rule the SOP compiler drafted from a total budget: committed spend plus this amount is over it. EXPOSURE_THRESHOLD_EXCEEDED
BUDGET_NEEDS_APPROVAL escalate A rule the SOP compiler drafted from a total budget that needs approval beyond it. HUMAN_REVIEW_REQUIRED
NO_FINANCIAL_AUTHORITY block A rule drafted from "the agent has no financial authority": any request with a positive amount is refused (one with no amount is not). ACTION_NOT_PERMITTED
CONSENT_MISSING block A rule the SOP compiler drafted from a consent requirement: the request says consent was not given. POLICY_VIOLATION
PII_PRESENT block A rule the SOP compiler drafted from "personal data must not be shared": the request carries personal data. POLICY_VIOLATION
PII_NEEDS_REVIEW escalate A rule the SOP compiler drafted that sends a request carrying personal data to a human. HUMAN_REVIEW_REQUIRED
PROHIBITED_TERM block A rule the SOP compiler drafted from a list of banned words: the request text contains one. POLICY_VIOLATION
PROHIBITED_CONTENT block A rule the SOP compiler drafted from a prohibited-content requirement: the request text matched. POLICY_VIOLATION
PROHIBITED_CONTENT_NEEDS_REVIEW escalate A rule the SOP compiler drafted that sends prohibited or abusive content to a human. HUMAN_REVIEW_REQUIRED
RISK_TOO_HIGH block A rule the SOP compiler drafted that refuses actions at or above a risk level. POLICY_VIOLATION
RISK_NEEDS_REVIEW escalate A rule the SOP compiler drafted that sends actions at or above a risk level to a human. HUMAN_REVIEW_REQUIRED
OPERATION_NEEDS_APPROVAL escalate An owner-keyed operation rule: the request context names an operation (e.g. op=delete-all) the owner sends to a human, whatever risk level the agent declared. HUMAN_REVIEW_REQUIRED
OPERATION_NOT_ALLOWED block An owner-keyed operation rule: the request context names an operation the owner does not allow, or omits a field the rule requires. ACTION_NOT_PERMITTED
SCENARIO_PER_TXN_CEILING block A Scenario Bank set's per-transaction ceiling: the amount is over it. Remedy: lower the amount, or change the owner's cap the set checks. EXPOSURE_THRESHOLD_EXCEEDED
SCENARIO_CUMULATIVE_CEILING block A Scenario Bank set's cumulative ceiling: committed spend plus this amount is over it. EXPOSURE_THRESHOLD_EXCEEDED
SCENARIO_RATE_CEILING block A Scenario Bank set's rate ceiling: the agent's calls in the window are over it. RATE_LIMIT_EXCEEDED
SCENARIO_CONSENT_MISSING block A Scenario Bank set: the request says consent was not given for the data it uses. POLICY_VIOLATION
SCENARIO_SOURCE_NOT_APPROVED block A Scenario Bank set: the request's data source is not on the approved list. POLICY_VIOLATION
SCENARIO_DATA_RESIDENCY block A Scenario Bank set: the request's data region is outside the allowed regions. REGULATORY_PROHIBITION
SCENARIO_TOOL_NOT_ALLOWED block A Scenario Bank set: the tool is not on the set's allow-list. ACTION_NOT_PERMITTED
SCENARIO_MODEL_NOT_ALLOWED block A Scenario Bank set: the model is not on the set's allow-list. ACTION_NOT_PERMITTED
SCENARIO_HIGH_RISK_REVIEW escalate A Scenario Bank set: a high-risk action is sent to a human. HUMAN_REVIEW_REQUIRED

MCP tool-call policies (the built-in policy catalog)

Code Decision Meaning SAFR §30 alias
APPROVED allow An MCP tool-call policy governs this action and none of its rules refused it: the call may run. —
NO_POLICY allow No active MCP tool-call policy governs this action (the verdict's standardRef is UNGOVERNED): the call is allowed ungoverned. Remedy: attach a policy if the tool should be governed. —
DATA_SOURCE_NOT_APPROVED block A catalog policy: the tool call's data source is not on the policy's approved list. POLICY_VIOLATION
RISK_THRESHOLD escalate A catalog policy: a high-risk tool call is sent to a human. HUMAN_REVIEW_REQUIRED
PROHIBITED_CLAIM block A catalog policy: the text makes a prohibited financial claim (e.g. "guaranteed returns", "risk-free"). REGULATORY_PROHIBITION
SENSITIVE_ADVICE escalate A catalog policy: the text reads as medical or legal advice, so a human reviews it. HUMAN_REVIEW_REQUIRED

HTTP gateway refusals (agentsafe-http-gateway)

Code Decision Meaning SAFR §30 alias
MISSING_GOVERNANCE block HTTP 401: a governed route was called without a signed MAGP request (by default the x-magp-request header). Remedy: sign the call with the agent's guard (buildSignedRequest) and send it in that header, or in whatever the gateway's extractGovernance reads. ENVELOPE_INVALID
GATEWAY_ACTION_MISMATCH block HTTP 403: the signed request names a different action than the one this gateway route is pinned to (http-gateway 0.29.0+; it used to surface as SIGNATURE_INVALID, because the gateway substituted the route's action before verifying). Nothing is claimed and no nonce is consumed. Remedy: sign the call with the route's action, or call the route that serves the action you signed. ACTION_NOT_PERMITTED
PAYLOAD_UNBINDABLE block HTTP 403: the JSON body carries none of the governed fields (amount, currency, merchant) at its top level — nested, renamed, differently cased or an array — so the gateway cannot show the signature covers it. Remedy: put the governed fields at the body's top level, or give the route its own bind. ENVELOPE_INVALID
BIND_ERROR block HTTP 502: the route's payload binder threw reading the body, so the gateway cannot show the signature covers it (fail closed). Remedy: fix the route's bind function or the body it is given. —
GOVERNANCE_ERROR block HTTP 502: verifying the request threw inside the gateway — the guard failed, or the route's trustedContext deriver threw or returned nothing usable (fail closed). The error says which. Remedy: fix the deriver or the guard's configuration; it is not a policy verdict. —
CREDENTIAL_UNAVAILABLE block HTTP 502: the gateway's credential vault released no credential for this route, so the call was not forwarded and the hold it claimed was released. Remedy: configure the credential, or mark a route that needs none credential: false. —
UPSTREAM_UNREACHABLE block HTTP 502 (http-gateway 0.32.2+): the permitted call could not be forwarded because the connection to the upstream was never made (refused, no such host, connect timeout), so nothing was sent and nothing executed. The hold it claimed was released with this reason, and the body says executed: false. Remedy: start or fix the upstream (AGENTSAFE_UPSTREAM); a retry is safe once it is reachable. —
GATEWAY_ERROR block HTTP 502: the gateway's HTTP server caught an unexpected error handling the call (fail closed). Check the gateway's log; the request may be retried. —
BLOCKED block HTTP 403: the guard refused the call without naming a code (an older or custom guard). Treated as a block (§13.3). —
EXECUTED allow An outcome report's code: the gateway forwarded the permitted call and the upstream answered (the report's httpStatus says how). —
UPSTREAM_ERROR allow An outcome report's code, and the reason a claimed hold is marked unknown: the permitted call was forwarded and the upstream failed without an HTTP answer, so whether it took effect is unknown until reconciled. —
RESPONSE_LOST allow The reason recorded when a claimed hold's effect is marked unknown (effect/unknown), shown as RESPONSE_LOST or RESPONSE_LOST:<executor's reason> (e.g. RESPONSE_LOST:UPSTREAM_ERROR, RESPONSE_LOST:UPSTREAM_HTTP_503): the call was forwarded and its outcome could not be established, so the hold stays committed to the cap and cannot be captured or released until it is reconciled. Remedy: ask for reconciliation (POST …/effect/reconcile, by the claimer, the owner or an admin), which settles on what the remote system reports. http-gateway 0.32.2+ no longer parks a call whose upstream connection was refused here: it releases it (UPSTREAM_UNREACHABLE). —
SETTLEMENT_THREW block The gateway's settlement call to the issuer (capture, void or mark-unknown) threw. It is retried as transient; after the last attempt it is logged and the claimed hold stays committed to the cap until it is reconciled. —

Guard and SDK fail-closed refusals

Code Decision Meaning SAFR §30 alias
CONTEXT_SIGNATURE_REQUIRED block A service-side guard run with requireContextSignature received a request whose context carries no envelope signature (or had it stripped). Remedy: have the agent sign its context (the SDKs do by default; do not pass signContext: false). ENVELOPE_INVALID
CONTEXT_SIGNING_UNSUPPORTED block Raised by the SDK: its key provider (or the agentsafe-signer daemon) cannot sign the request's context, so the request is not sent unsigned. Remedy: upgrade the signer, or pass signContext: false. —
JURISDICTION_SIGNING_UNSUPPORTED block Raised by the SDK: the agentsafe-signer daemon predates signed jurisdictions (signer 0.18.0), so the request is not sent. Remedy: upgrade the signer, or omit jurisdiction. —
SIGNER_UNREACHABLE block Raised by the SDK: the agentsafe-signer daemon holding the agent's key could not be reached or failed, so nothing was signed or sent (not a gate outage). Remedy: start the daemon or fix its socket path. —
CAPABILITY_INVALID block The MCP guard's capability verifier rejected the capability credential the call presented, without naming a more specific code. DECISION_TOKEN_INVALID
NOT_APPLIED block A service-side guard's settlement call (capture, void, refund or mark-unknown) was refused by the issuer without a reasonCode; nothing changed on the hold. The HTTP status and the issuer's message ride along. —
REFUSED block Raised by the Python client: a settlement or late payload-binding call was refused and the issuer's answer carried no reasonCode or message. —
UNKNOWN block Raised by the Python client: the gate's answer carried no reasonCode (an older or non-MAGP endpoint). Read per §13.3: a non-allow decision without a known code is a block. —

Claim payload and x402 settlement binding

Code Decision Meaning SAFR §30 alias
PAYLOAD_DIGEST_REQUIRED block A claim of a hold whose authorization bound a payload digest presented none: an executor cannot skip the binding by omission (§8.3.9). Nothing is claimed. Remedy: the executor states the digest of what it is about to run (guard payloadDigest). ENVELOPE_INVALID
PAYLOAD_NOT_BOUND_AT_AUTHORIZE block A claim presented a payload digest for a hold whose authorization bound none: the executor believes the payload is bound and it is not (§8.3.9). Nothing is claimed. Remedy: the agent binds its payload when it asks (or binds it late, §8.3.11). ENVELOPE_INVALID
MISSING_BINDING block x402: the payment requirements carry no MAGP authorization (extra.magpAuthorizationId), so the payment cannot be tied to a hold (§7a.2). DECISION_TOKEN_INVALID
AUTHORIZATION_MISMATCH block x402: the payment is bound to another authorization than the one being settled (§7a.2). DECISION_TOKEN_INVALID
AMOUNT_INVALID block An amount that is not a usable number: an x402 paid amount that is not a non-negative integer of minor units (§7a.2), a capture whose amountCharged is negative or not a number, or a refund that is not positive. —
BINDING_OK allow x402: the payment is bound to this authorization and pays no more than it authorized (§7a.2). —

Capture, release and refund of a hold

Code Decision Meaning SAFR §30 alias
AMOUNT_EXCEEDS_AUTHORIZED block A capture whose amountCharged is over the authorized hold. Nothing is captured; an overspend goes through reconciliation, not capture. EXPOSURE_THRESHOLD_EXCEEDED
AGENT_SETTLE_SIGNATURE_INVALID block The agent's signature on a settlement of its own unclaimed hold does not verify over MAGP-SETTLE-v1 for this call. ENVELOPE_INTEGRITY_FAILURE
PAYEE_NOT_REGISTERED block A settlement below the hold named an account paid that is not one the owner registered for this merchant (payee directory), so the lower amount is not accepted. COUNTERPARTY_NOT_ALLOWED
PAYEE_NOT_VERIFIED block The payee directory could not be read to confirm the account paid. Remedy: settle at the full authorized amount, or retry. —
SETTLEMENT_NOT_CONFIRMED block The remote system could not confirm a lower settled amount, or (on a release after a claim) that nothing was settled. Remedy: settle at the authorized amount, mark the effect unknown for reconciliation, or retry once it can confirm. —
SETTLEMENT_AMOUNT_MISMATCH block The remote system reports a different settled amount than the settlement states. —
SETTLEMENT_NOT_FOUND block The remote system reports no settlement for this authorization, so a lower settled amount cannot be confirmed. —
EFFECT_NOT_CAPTURABLE block The hold's effect is in a state that cannot be captured (unknown, reconciling, ...): reconcile it first. —
EFFECT_NOT_VOIDABLE block The hold's effect is in a state that cannot be released (unknown, reconciling, ...): reconcile it first. —
INVALID_EFFECT_TRANSITION block The requested move of the hold's effect (e.g. to reconciling or compensating) is not allowed from its current state; read its state and act on that. —
HOLD_STATE_CHANGED block The hold was claimed, settled or voided by another caller while this capture or refund ran; nothing was applied. Read its current state and retry. —
NOT_CAPTURED block Only a captured (settled) authorization can be refunded; this one is not. —
ALREADY_REFUNDED block Nothing remains to refund: the capture was already refunded in full. —
AMOUNT_EXCEEDS_REFUNDABLE block The refund is more than what remains to refund of the capture. —
CLAIM_EXPECTATION_INVALID block HTTP 400: a claim's expectation (the action, amount, currency or merchant the executor expects the hold to authorize) is malformed. ENVELOPE_INVALID
IDEMPOTENCY_KEY_INVALID block HTTP 400: the Idempotency-Key on a hold operation is malformed. —
MANDATE_BLOCK block An approved escalation, re-judged against the mandate and rules as they stand at reservation, no longer passes and no more specific code was produced. Nothing is reserved; ask again. POLICY_VIOLATION
MODIFIED_APPROVED allow A reviewer-modified action, re-judged against the rules as they stand, passed every check (the hold is then minted as ESCALATION_MODIFIED_APPROVED). —
COMPENSATION allow The separately authorized $0 effect opened to compensate (reverse) an earlier one; its booking reference names the effect it compensates. —

Counterparty authentication (a claim, settlement or outcome report signed by an executor)

Code Decision Meaning SAFR §30 alias
COUNTERPARTY_AUTH_MALFORMED block The executor's counterparty signature headers are incomplete or malformed (nonce, issuedAt or signature missing or out of range). ENVELOPE_INVALID
COUNTERPARTY_DID_UNSUPPORTED block The executor's service DID is not one whose key the issuer can resolve (use did:key). IDENTITY_UNVERIFIED
COUNTERPARTY_REQUEST_EXPIRED block The executor's signed request is outside the freshness window (too old, or issued too far in the future). Check the executor's clock. ENVELOPE_INVALID
COUNTERPARTY_SIGNATURE_INVALID block The executor's counterparty signature does not verify against its service DID. ENVELOPE_INTEGRITY_FAILURE
COUNTERPARTY_REPLAY_DETECTED block The executor's signed request reuses a nonce already seen: a replay is refused. ENVELOPE_INTEGRITY_FAILURE
COUNTERPARTY_NOT_REGISTERED block The executor's service DID is not an active counterparty the agent's owner registered (or not a trusted entry where a proof of control is required). COUNTERPARTY_NOT_ALLOWED
COUNTERPARTY_NOT_ALLOWED_FOR_MERCHANT block The registered counterparty is scoped to other merchants than this hold's. COUNTERPARTY_NOT_ALLOWED
COUNTERPARTY_PROOF_REQUIRED block Registering the counterparty needs a proof of control of its DID (a signed challenge) and none was given. IDENTITY_UNVERIFIED
COUNTERPARTY_PROOF_INVALID block The proof of control of the counterparty DID is malformed or does not verify. IDENTITY_UNVERIFIED
COUNTERPARTY_PROOF_EXPIRED block The challenge the proof of control signs has expired; fetch a new one. IDENTITY_UNVERIFIED
REPORT_CALLER_UNVERIFIED block HTTP 400: an 'executed' outcome report must carry a caller request that verifies; this one does not. ENVELOPE_INTEGRITY_FAILURE
REPORT_OCCURRENCES_INVALID block HTTP 400: an 'executed' outcome report counts exactly one occurrence (only refusals are aggregated). ENVELOPE_INVALID
REPORT_RATE_LIMITED block HTTP 429: the executor sent outcome reports faster than the issuer accepts them; retry later (the guard keeps and re-sends them). RATE_LIMIT_EXCEEDED

Effect reconciliation and compensation

Code Decision Meaning SAFR §30 alias
NOT_RECONCILABLE block HTTP 409: the effect is neither unknown nor reconciling, so there is nothing to reconcile; its current effectState is returned. —
NOT_RECONCILING block An operator resolution needs the effect to be reconciling; it is not. —
NOT_COMPENSABLE block Compensation can begin only for an effect in compensation_required; this one is not. —
NOT_COMPENSATING block Compensation can be completed only for an effect that is compensating; this one is not. —
RECONCILE_COOLDOWN block HTTP 429: this effect was probed moments ago; retry after the time in detail. RATE_LIMIT_EXCEEDED
RECONCILE_NOT_PERMITTED block HTTP 403: the caller is not one that may resolve this hold (its claimer, or a signed-in user who may manage it). —
NEEDS_HUMAN escalate Reconciliation could not decide what happened (no reconciler for this effect, or the remote was inconclusive): the effect stays reconciling for an operator to resolve. HUMAN_REVIEW_REQUIRED
RECONCILED_SUCCEEDED allow Reconciliation found the effect happened: the hold is captured at the amount the remote system (or the operator) says was settled. —
RECONCILED_FAILED block Reconciliation found the effect did not happen: the hold is released and the effect is failed_final. —
COMPENSATION_REQUIRED escalate The effect happened but must be reversed (it violates a postcondition or policy): it waits for a compensating action. HUMAN_REVIEW_REQUIRED
SETTLED_OVER_AUTHORIZED escalate Reconciliation found more settled than was authorized: the cap counts the real amount and the effect waits for compensation. EXPOSURE_THRESHOLD_EXCEEDED
COMPENSATION_STARTED escalate A separately authorized compensating effect was opened for this one, which is now compensating. —
COMPENSATED allow The compensating action succeeded: the original effect is compensated. —
COMPENSATION_FAILED block The compensating action failed: the original effect is compensation_failed and needs a person. —
EFFECT_RECONCILE_NO_HANDLER escalate Evidence row: no reconciler can probe this effect, so it waits for an operator (the answer is NEEDS_HUMAN). HUMAN_REVIEW_REQUIRED
EFFECT_RECONCILE_UNRESOLVED escalate Evidence row: the reconciler could not decide what happened, so it waits for an operator (the answer is NEEDS_HUMAN). HUMAN_REVIEW_REQUIRED
EFFECT_RECONCILED_SUCCEEDED allow Evidence row of RECONCILED_SUCCEEDED. —
EFFECT_RECONCILED_FAILED block Evidence row of RECONCILED_FAILED. —
EFFECT_COMPENSATION_REQUIRED escalate Evidence row of COMPENSATION_REQUIRED. HUMAN_REVIEW_REQUIRED
EFFECT_SETTLED_OVER_AUTHORIZED escalate Evidence row of SETTLED_OVER_AUTHORIZED. EXPOSURE_THRESHOLD_EXCEEDED

Owner actions on the evidence trail

Code Decision Meaning SAFR §30 alias
MODE_AUTONOMOUS allow The owner pinned the agent's operating mode to autonomous. —
MODE_SUPERVISED allow The owner pinned the agent's operating mode to supervised. —
MODE_RESTRICTED allow The owner pinned the agent's operating mode to restricted. —
MODE_PIN_CLEARED allow The owner cleared the agent's operating-mode pin; the derived mode applies again. —
REASSESSMENT_REQUIRED escalate The owner recorded a change to the agent that needs its assurance reassessed before it is trusted as before. HUMAN_REVIEW_REQUIRED
REASSESSED allow The owner cleared a pending reassessment of the agent. —
REGRESSION_CHECK_PASSED allow The agent version's current policy passed every Scenario Bank set that applies to it (the sets checked are on the record). —
TRANSCRIPT allow An agent-chat session transcript was anchored as evidence (hashed on-chain, encrypted off-chain). —

Appendix B — Canonical examples

Canonical message

did:hedera:testnet:zEEZ…|flight-purchase|150|USD|skyward-air|9f2c…|2026-07-10T05:12:00Z

A narrowing delegation — parent caps at 500 across two merchants; the child caps at 50 against one of them, and repeats the parent's cumulative constraint because omitting it would widen:

{ "target": "flight-purchase",
  "permission": [{ "target": "flight-purchase", "constraint": [
    { "leftOperand": "mm:payAmount",       "operator": "lteq",    "rightOperand": 50 },
    { "leftOperand": "mm:cumulativeSpend", "operator": "lteq",    "rightOperand": 500 },
    { "leftOperand": "mm:merchant",        "operator": "isAnyOf", "rightOperand": ["skyward-air"] }
  ]}]}

A refused widening — the same child with mm:payAmount raised to 5000 is rejected at issuance with CONSTRAINT_WIDENED; omitting mm:payAmount entirely is rejected with CONSTRAINT_DROPPED.


Appendix C — Atom catalog

Atoms are pure predicates over request context. Generated from the reference implementation; GET /standards/atoms returns the live catalog and is authoritative. New governance requirements are added as new atoms, which does not require a protocol version bump (§18.1).

Predicate Required context Fires when
amount-over amount Fires when a single action amount exceeds a configured limit (per-transaction cap).
amount-unknown amount Fires when the action carries no usable amount, or a NEGATIVE one — the gate cannot trust either for capping. A deny-by-default control for value-moving actions: author it with BLOCK ahead of a spend cap, otherwise an amount that is missing, unparseable, or negative passes the cap untested (amount-over only ever fires above the limit, so a negative amount clears every positive cap). A genuine $0 amount does NOT fire this — only attach it to actions that must always carry a real, non-negative amount.
cumulative-over amount, cumulativeSpend Fires when cumulative spend (already-spent + this transaction) exceeds a configured total budget.
risk-at-or-above riskLevel Fires when the assessed risk level is at or above the configured threshold.
data-source-not-approved dataSourceId Fires when the action uses a data source not on the approved list.
consent-missing consent Fires when explicit consent is absent for the action.
text-matches prompt, output Fires when the prompt or output contains any of the configured terms.
jurisdiction-not-allowed jurisdiction Fires when the action's jurisdiction is not on the allow-list.
data-residency-violation dataResidency Fires when data would be processed in a region not on the allow-list.
model-not-allowed model Fires when the agent uses an LLM model not on the approved list.
tool-not-allowed tool Fires when the agent invokes a tool/function not on the approved list.
pii-present piiPresent Fires when the action is flagged as involving personal data (PII).
rate-limit-exceeded callCount Fires when the rolling call count exceeds a configured maximum.
hol-trust-below-review holTrustScore Routes to human review when the counterparty's MetaMynd Trust Index (HCS-28) score is below a soft review line. Guidance, not a hard block — author it with an ESCALATE decision. The score is resolved server-side; no counterparty score → the atom does not fire.
evidence-requirement evidenceTypes Fires when the action is not backed by every REQUIRED evidence type the agent attests to in evidenceTypes (missing evidence — including none supplied). A REQUIRE control (SAFR §24): author it with ESCALATE or BLOCK so an under-evidenced action is stopped or reviewed.
evidence-confidence-below evidenceConfidence Fires when the attested evidence confidence is below a required minimum — or absent (SAFR §24). A min of 0 / unset is no requirement. Author with ESCALATE to route low-confidence actions to review.
context-value-in — Fires when a field of the request context (e.g. op, or params.op) has one of the listed values — key a rule on WHAT the agent is doing, not on the risk level it declares about itself. Author with ESCALATE ("deleting records needs approval") or BLOCK. Case, width, whitespace, hyphens/underscores, invisible characters and common Cyrillic/Greek look-alike letters are ignored when comparing. A deny-list cannot foresee every spelling: when the safe values are known, prefer context-value-not-in. When it fires, the verdict and the escalation carry a context-value risk signal naming the field and value.
context-value-not-in — Fires when a field of the request context has a value that is NOT on the owner's allow-list ("anything other than read or list needs approval"). The sound form of an operation rule: a spelling or look-alike the owner did not foresee fires instead of passing. Same comparison rules, missing and actions options as context-value-in, and the same context-value risk signal when it fires.

Appendix D — Future directions

Not implemented at 1.0. Recorded so implementers know the intended direction, and kept out of the normative text so nothing in the body is aspirational.

  • Content-addressed rule packs. Each published rule-pack version addressed by content hash, so a bundle can name an exact immutable pack.
  • HCS-10 channels. An on-chain, auditable channel binding for peers that require message-level durability rather than TLS request/response.
  • WebSocket transport. A duplex binding for streaming interactions.
  • HCS-2 registry discovery. Trustless discovery of DIDs and service endpoints via a mirror node, with the issuer offline.
  • Signed bundle staleness enforcement at the edge, beyond the risk-tiered rules of §6.2.

Changes from 0.4

  • Order of checks (§8.5) rewritten from 9 steps to 16, transcribed from the implementation. Containment, operating modes, rate limits, circuit breakers, OBSERVE folding and the reassessment floor were all absent.
  • Reason codes: 26 → 59, now generated from the registry rather than maintained by hand.
  • New: resource scope (§5.3), delegation and the narrowing invariant (§5.4), agent posture (§7), secondary controls (§9), effects and reconciliation (§12), decision records and the governance envelope (§14), privacy-preserving verification consolidated (§15).
  • §8.3 gains the three signing details (key encoding, number stringification, field identity) that previously cost every non-JavaScript implementer a day each.
  • Platform governance surfaces — supervisory access, sandbox programmes, incident routing, the trust graph — removed from scope into MetaMynd Platform Governance. They are not required for interoperability.
  • [PROPOSED] clauses moved to Appendix D; the body is now implemented-only.