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).

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.


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.


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; a counterparty signing as itself (8.7.6) that the owner's registry would let claim this hold; or — only where an anonymous claim of it would be granted (an owner who registered no claimant, on testnet) — anyone, since there an anonymous settlement grants nothing an anonymous claim does not. 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.


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.

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 and the authorizationId it presented when it had them, and 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_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 Lost the atomic cap-reservation race. EXPOSURE_THRESHOLD_EXCEEDED
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
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. 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. 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 deviates from the agent's own recorded spend history (graph-backed baseline, not a preset cap). 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

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

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.

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.