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. Work not yet built is listed in Appendix D — Future directions. If it is in the body, it exists.
This specification states what an implementer needs in order to interoperate. How a particular gate stores state, schedules work or recovers from faults is out of scope.
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
- Terminology
- Roles and entities
- Architecture
- Identity
- Authority: mandates, resource scope, delegation
- Policy artifacts
- Agent posture: containment, operating mode, reassessment
- The authorize protocol
- Secondary controls: rate limits and circuit breakers
- Escalation resolution
- Payment execution binding (x402)
- Effects and reconciliation
- Evidence and anchoring
- Decision records and the governance envelope
- Privacy-preserving verification
- Discovery and channels
- Security considerations
- Extensibility and versioning
- Conformance
- 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.
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. Every mandate active under the old DID is reissued under the new one with
the same terms and cumulative spend; a delegated mandate is reissued delegated and re-validated
against its delegator, or else revoked (5.4.11). The old DID no longer verifies: the gate refuses a
request signed as it IDENTITY_ROTATED, and GET /did/{did} answers 410 Gone with
didDocumentMetadata.deactivated: true (the HTTP binding of W3C DID Resolution). The old DID's
Authority Manifest carries retired: { reasonCode: "IDENTITY_ROTATED" }. Neither public read names
the replacement DID; the IDENTITY_ROTATED refusal carries it as replacementDid, outside the
digested verdict, and only to a request validly signed with the replaced key.
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:
- Testnet and
did:keyagents — 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 platform's
limits for its plan. Beyond them the gate refuses with
PRINCIPAL_UNVERIFIED. A testnet ordid:keyagent is never refused with it.
The mainnet check 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, and a cached public
projection of it (a README badge) SHOULD reflect a containment within a minute.
4.4.3. Verified does not mean identity-checked. The registry's principal block reports
not only verified but the basis of the verification — the weakest across the principals
behind the agent's active mandates:
basis |
Meaning |
|---|---|
provider |
An automated third-party provider verified the principal (KYC/KYB). |
manual_review |
A person on the platform's compliance team reviewed and decided. |
self_approved |
The platform approved the principal automatically (sandbox and beta self-serve). No identity check ran. Such a principal cannot create a mainnet agent. |
unrecorded |
Verified, but the record does not say by whom. Never upgraded to a stronger basis. |
A counterparty that needs a real identity check MUST require provider or
manual_review, not merely verified: true. The block also carries identityChecked, which is
true only when verified is true and the basis is provider or manual_review. A
self-approved principal reads verified: true, basis: "self_approved", identityChecked: false.
5. Authority
5.1 Mandates
5.1.1. A mandate is an ODRL policy carried in a verifiable credential. It names a target action and carries permissions and prohibitions, each with constraints.
5.1.2. Evaluation order within a mandate is: validity window, then prohibitions, then a matching permission whose constraints all hold. A prohibition that fires outranks any permission.
5.2 Constraints
A constraint is { leftOperand, operator, rightOperand }. Operators are a fixed set — new
scenarios add operands, not operators:
eq · neq · lt · lteq · gt · gteq · isAnyOf · isNoneOf · isPartOf ·
before · after
5.2.1. An unknown operator MUST fail closed.
5.2.2. Well-known operands map to stable reason codes so a denial names its cause:
mm:payAmount and mm:cumulativeSpend → SPEND_LIMIT_EXCEEDED; mm:merchant →
MERCHANT_NOT_ALLOWED; mm:route → ROUTE_NOT_ALLOWED; mm:counterparty →
COUNTERPARTY_NOT_ALLOWED; mm:jurisdiction → JURISDICTION_REQUIRED when the request has no
effective jurisdiction, JURISDICTION_NOT_ALLOWED when it has one outside the list (§5.2.3). Any
other operand denies with CONSTRAINT_FAILED:<operand>.
5.2.2a. A cumulative cap is per currency. Its limit is a bare number, so the committed spend it is checked against MUST count only holds in the request's currency (compared case-insensitively); summing two currencies has no meaning without a rate, and none is applied. A hold with no recorded currency counts against every currency, so the check fails closed. This changes nothing for a single-currency mandate; for one that allows several currencies (or any), the cap applies to each separately.
5.2.3. Allowed jurisdictions. A mandate MAY restrict the jurisdictions an action may be
performed in. Issuance (POST /policy/mandate) takes an optional allowedJurisdictions: a list
of ISO 3166-1 alpha-2 codes, trimmed, upper-cased, de-duplicated and sorted; the group code EU
expands to the 27 member states. An empty or malformed list is refused (400 MALFORMED_REQUEST); omitting it means no restriction. The term is stored as an ordinary
constraint on the permission:
{ "leftOperand": "mm:jurisdiction", "operator": "isAnyOf", "rightOperand": ["DE", "FR", "SG"] }
- It is judged on the request's effective jurisdiction (§8.3.12) — the country the owner
registered for the payee, else the signed top-level
jurisdiction— never on an itinerary value. With no effective jurisdiction the request is blockedJURISDICTION_REQUIRED; outside the list,JURISDICTION_NOT_ALLOWED. GET /policy/mandate/{ref}/statusreturns it asallowedJurisdictions(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". Nor may it drop or widen its parent's argument policy (§8.3.14):
each of the child's allow paths MUST sit at or below one of the parent's, and each of the
parent's sensitive paths MUST appear in the child with the same mode or redact.
- Allowed jurisdictions (§5.2.3). A child that states no
allowedJurisdictionsinherits 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 (EUexpanded) and it MUST be a subset of the parent's (isAnyOfnarrowing, 5.4.2) — otherwise the delegation is refusedCONSTRAINT_WIDENED(operandmm: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 isDELEGATION_NARROWING_FAILED.
5.4.6. The chain MUST terminate at a bounded depth (reference implementation: 3).
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. This holds for a reissue too: every mandate delegated from
the superseded mandate is revoked, and the delegator re-delegates if it still wants to.
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.
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. 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. On failure
(MANDATE_REVOKED, DELEGATION_CHAIN_BROKEN or DELEGATION_NARROWING_FAILED) nothing is issued: an
owner's change is refused and the delegated mandate stays as it was, while an identity rotation
revokes it (8.7.15). A reissue is never a way out of a chain.
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.2.8. Policy commitment. The policy-update op a compile anchors on the agent's topic
SHOULD carry policyRoot and policyParts: a salted commitment to the compiled bundle's parts,
so the owner can show an auditor one part of the policy issued to the agent, with proof it was in
that policy, and nothing else. The parts are each mandate (id = action:hash), each SOP (id),
each pinned standard (id = key@version), and one subject part naming the agent, the version
label, the owner principal and the sorted revoked actions; parts sharing an id are numbered #2,
#3… in content order, skipping any number another part's id already bears. Each is a leaf
sha256( "MAGP-POLICY-PART-v1" ‖ 0x00 ‖ kind ‖ 0x00 ‖ id ‖ 0x00 ‖ canonical(content) ‖ 0x00 ‖ salt )
where canonical is JSON with object keys sorted and undefined members dropped, and salt is the
lower-case hex of an HMAC-SHA256 under the issuer's secret over the bundle hash, the kind and the
id. Leaves are ordered by (kind, id) and policyRoot is "sha256:" followed by their Merkle root,
built as the evidence tree is (13.2): a lone last node is paired with itself. policyParts is the
leaf count. The salt is per part and per bundle, so a disclosed part reveals nothing about the
others, and a part cannot be confirmed by guessing its content: a mandate from a template differs
from its neighbours only in a cap or a counterparty, and an unsalted hash of it could be found by
trying the few candidates. An issuer with no secret to salt with MUST NOT anchor a commitment.
The salts are derived, not stored, so an issuer that replaces its secret can no longer open the
commitments anchored under the old one. policyParts is public: it reveals how many parts the
policy has, never which.
The owner obtains an opening from GET /agent-identity/{ref}/policy-disclosure (bundleHash
selects a compiled bundle, else the active one; kind and id select the part, and without them
the response names the parts there are to open). The opening is { did, bundleHash, policyRoot, policyParts, disclosure: { kind, id, content, salt, proof } }. A verifier recomputes the leaf,
walks the proof to policyRoot, refuses a proof whose length does not fit a tree of policyParts
leaves or whose positions name a leaf beyond it (the duplicated last node is not a further part),
and finds a policy-update op for the same DID carrying that root, bundle hash and count. The op's
consensus timestamp is when that policy took effect, and a decision is placed against it by bundle
hash. magp-evidence verifyPolicyDisclosure does all of this with the issuer offline.
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). A molecule that could not be judged escalates even if
another of its atoms would have kept it from firing.
6.3.5. Policies declare the trust they need. A molecule MAY declare
requireProvenance: { field: level }. For each named field the molecule needs a well-formed
value whose provenance is at least level; otherwise it decides at least escalate
(CONTEXT_UNVERIFIABLE). An unknown level is read as the strictest (attested), and a
document declaring one is rejected at authoring, so a typo can only make a rule refuse more.
A molecule that declares nothing behaves exactly as before, except as 6.3.4 requires.
6.3.6. Counterparty derivation. A guard SHOULD let the service supply
trustedContext — context the service derived from the real call, never from the agent —
which is applied over the agent's claim and labelled gateway_derived. A trustedContext
the service configured but that yields nothing usable — not an object, or a riskLevel that is
not a level (a lookup that missed and returned nothing) — means its deriver is broken, and the
request MUST be refused; it MUST NOT fall back to the agent's word. The operating-mode
gate (§7.2) judges the effective risk, so an agent in supervised mode cannot skip the
high-risk escalation by claiming low.
6.3.7. What this does and does not establish. It closes omission, garbling and
understating where a trusted source exists. It does not make an agent's claim true: with no
owner riskTier, no gateway derivation and no requireProvenance, riskLevel is still the
agent's word — only hiding or garbling it is now refused. The other agent-supplied fields
(tool, model, consent, …) keep their documented absent-field behaviour
(Appendix C); a rule that must not depend on the agent's word about them declares
requireProvenance. jurisdiction is no longer one of them: the gate enforces only the
effective jurisdiction of §8.3.12 (registered payee country, else the signed field), and an
itinerary jurisdiction is ignored.
6.3.8. Owner-keyed operations (non-financial risk). A financial action has risk the issuer
derives from facts the agent cannot fake (the amount against the cap, a first payment to an
unvetted merchant). A non-financial action has no amount, so unless the owner configures
otherwise, whether it escalates depends on the riskLevel the agent declares — an agent that
labels op: "delete-all" as low is not reviewed. The owner closes this by keying a rule on
what is being done: the context-value-in atom fires when a context field (a dot path such
as op or params.op) has one of the owner's values, and context-value-not-in fires when it
has a value not on an allow-list (Appendix C). Both are pure, so the gate and every guard
bundle reach the same verdict. Values are compared after Unicode NFKC, removal of invisible
characters, lower-casing, folding of common Cyrillic/Greek look-alikes of Latin letters, and
removal of whitespace, hyphens and underscores; a value that is an object or a nested list is
unjudgeable and MUST fire; a list fires if any element would; a field that is absent
fires only when the atom is configured missing: "fire". A deny-list cannot foresee every
spelling, so an allow-list is the stronger form when the safe operations are known. When a
refusing or escalating molecule fires on such an atom, the gate SHOULD report it with the
decision as a risk signal { signal: "context-value", level: "high", detail } (e.g.
op=delete-all is owner-marked for review), alongside the financial signals, so the review
shows the risk was derived from the request rather than declared by the agent. The field is
only as good as the executor's binding of it: it must be the field the executor acts on, and
the approved context is bound at claim (8.7.20).
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>"
}
merchantandresourceare top-level signed fields (§8.3.1), each optional and each the empty string in the signed message when absent.resourcenames 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 unsigneditinerary: 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 theitineraryis ignored.itinerarycarries request context — the atom inputs beyond the signed core. It is unsigned: every value in it,riskLevelincluded, is an agent's own claim and is treated as one (§6.3).amountandcurrencymay be omitted together for a non-financial action; a mandate that carries a spend constraint still requires a real amount.nonceis single-use;issuedAtbounds freshness.- An optional
traceobject 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.
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 ) )
canonicalis 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 Python250.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
issuedAtof the authorize request it accompanies, so a digest cannot be lifted from one authorization onto another. payloadDigestandpayloadSignatureare 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
payloadSignatureunder 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 notsha256:and 64 lower-case hex, or a signature that does not verify — withPAYLOAD_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,nullfor an unbound one), outside the digested decision content. The binding is not covered by the §8.3.1 signature, so a hop could strip it. An agent that sent a digest MUST treat a permit or escalation whosepayloadDigestis 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
carries no digest until the agent binds the payload of the action that will run (§8.3.11).
The review's status reports
payloadBound(a boolean, 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 — withPAYLOAD_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 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
payloadDigestissha256:and 64 lower-case hex; and thatpayloadSignatureverifies under the keyagentDidresolves to — and only THEN say anything about the hold: that it was issued toagentDidand (when its mandate names an action) toaction, so an unauthenticated caller cannot probe a hold's owner or action by authorization id. It MUST then check thatissuedAtis fresh and consumenonceonce, 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/authorizeand 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 only while the hold is live, unclaimed and carrying no digest, atomically 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 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, an unresolvable key, or a signature that does not verify |
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 —
jurisdictionpresent → 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 failsSIGNATURE_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 signedjurisdiction, upper-cased; else none. It is the ONLY value the gate enforces: the mandate's allowed-jurisdictions term (anmm:jurisdictionisAnyOfconstraint) and thejurisdiction-not-allowedatom are both judged on it. An itineraryjurisdiction/mm:jurisdictionis 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
jurisdictionthat differs from it — or entries for one payee that name more than one country — is blockedJURISDICTION_MISMATCH, a hard block, whether or not the mandate restricts jurisdictions. The decision record'sjurisdictioncontrol names the source (signed|registered), and the rule context labels the valueagent_signedorauthoritativeaccordingly. - When the mandate restricts jurisdictions for the action, or an enforced (non-
observe) SOP or Standard molecule judgesjurisdiction-not-allowed, a request with no effective jurisdiction is blockedJURISDICTION_REQUIRED; one outside the mandate's list is blockedJURISDICTION_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; 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 refusedAPPROVAL_INPUT_UNAVAILABLE. docs/protocol/authorize-v2-vectors.jsonpublishes 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
envelopeSignatureaftersignature(§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 refusedCONTEXT_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). A relay can also strip it, so a counterparty that relies on the
context SHOULD require it, refusing an unsigned context with
CONTEXT_SIGNATURE_REQUIRED. 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.jsonpublishes known-answer vectors under the §8.3.8 test key: the request, the canonical envelope, its hash and the signature.
8.3.14. Argument policy (optional). The signed fields and the rules do not see the arguments a
tool runs with, so a call within its cap can still carry a field nothing judged. A mandate's
permission MAY carry arguments: { allow, sensitive? }, issued with the mandate and carried in
the signed bundle:
allowlists the JSON Pointer (RFC 6901) paths the arguments may carry; a listed path permits everything below it, and a*segment matches any one key or array index. The root is not a path. Every value in the arguments MUST sit at or below a listed path, or the call is refusedARGUMENT_NOT_ALLOWED, naming the offendingpath. Keys are compared exactly: a renamed or re-cased key is refused.sensitivelists{ path, mode }. Before the call leaves the verifier, a value at aredactpath is replaced with"[REDACTED]", and a value at anhmacpath withhmac-sha256:followed by the lower-case hex HMAC-SHA256, under the executor's own key, of the value's canonical JSON (object keys sorted). Where both apply,redactwins. A verifier with no key MUST refuse a call that needs one (SENSITIVE_ARGUMENT_UNPROTECTED) rather than send the value in the clear. The hash is over the value alone, so under one key an equal value hashes equally in every field and tool: that is what lets a tool match values it cannot read. An owner who does not want values linked usesredact.- It is enforced by the executor that holds the arguments (the MCP guard; the HTTP gateway's JSON
body; the A2A guard, where a task's arguments are its one data part), not at authorize, on a
verdict that could run the call and before any claim (§8.7.11), so a refused call spends no
hold. Arguments it cannot check — not a single JSON object, deeper than 32 or larger than 10,000
values, not given to it, or a malformed policy — are refused
ARGUMENTS_UNCHECKABLE. Every policy covering the action applies. - An agent that runs its own tool in-process holds the tool to it too. The gate never sees the
arguments, so it names the policy instead: a permit carries
argumentPolicies(the action's policies,[]for none: always stated, so an agent can tell "none" from an issuer that predates the field), and so does an approved escalation's status (§9a), so a resume is held to the same policy before its one resume is taken (§9a.6). This is cooperative, as the in-process wrapper is; the boundary remains an executor in another process. - The owner may set, change or remove a deployed agent's policy for one action; like any change to the mandate's terms it reissues that mandate (§8.7.15). A delegated mandate may only narrow its delegator's policy.
- The payload digest (§8.3.9) is taken over the arguments the agent sent, which is what it signed; the tool receives the sanitized copy.
- An executor that predates this field ignores it. An owner relying on it SHOULD require
executors that state they enforce it (
checksArguments).
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 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.
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 (§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.
- 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 missingriskLevel(§6.4.3) — MUST NOT end evaluation. The escalate is carried; the remaining rule stages and the mandate evaluation (13) still run, and anyblock,suspendorquarantineamong 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. A molecule's ownblockstill 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
jurisdictionthrough the v2 message (§8.3.12), a malformed value beingMALFORMED_REQUEST— the gate resolves the request's effective jurisdiction, in this order: (a) the country the mandate's owner registered for the signedmerchantin the payee directory, when it has one — authoritative; (b) else the signedjurisdiction, upper-cased; (c) else none. The itinerary'sjurisdiction/mm:jurisdictionis 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 judgesjurisdiction-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.)
- 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
callCounta rule reads, circuit breakers (10), committed spend, counterparty trust, the Standard and SOP molecules (11, 12) and the mandate (13) with the signed operands,resourceincluded. 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). - The floors of step 15 only ever lift a permit to escalate, never downgrade a stricter verdict. The spend-anomaly and runtime-assurance floors are opt-in.
- 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 (§9).
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
A permitting verdict for a value-bearing action creates a hold: a reservation against the
mandate's cap, identified by the authorizationId. This section specifies the hold as the agent and
the executing counterparty see it.
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) the effect may already have
happened, so the reservation MUST remain against the cap until it is settled or the claimer
releases it. It MUST NOT lapse with the hold window, and only the claimer may lower or release it.
8.7.4. Settling a hold. POST .../capture { amountCharged, bookingRef?, settlementTxHash?, claimToken? }
settles a hold; POST .../void { reason?, claimToken? } releases one. The authorization id is not a
credential (the agent hands it to every Service it asks to execute), so:
- Settling a claimed hold below its authorized amount, or voiding it, MUST come from its
claimer: its signature (8.7.6) or, for an anonymous claim, the claim token that claim returned
(
data.claimToken). Settling at the full authorized amount needs no token. - Settling or voiding an unclaimed hold MUST come from the hold's own agent, with
agentProof: { agentDid, nonce, issuedAt, signature }— its signature overMAGP-SETTLE-v1|verb|agentDid|authorizationId|nonce|issuedAt|...fields(verbcaptureorvoid; fields as in 8.7.6; each escaped as in 8.3.1), fresh and single-use like an authorize request — or from a counterparty signing as itself (8.7.6) that the owner's registry (8.7.10) would let claim it. An anonymous settlement of an unclaimed hold MUST be refused.
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 MUST be
refused AUTHORIZATION_ALREADY_CLAIMED (HTTP 409) and carry no claim token.
EFFECT_TRANSITION_CONTENDED means the claim did not land and the hold is still claimable; the
claimant SHOULD retry it once. The hold window is enforced by the claim itself.
8.7.6. Counterparty identity. A claim token is a bearer secret: it proves "I made the claim", not
who the claimer is. A counterparty that holds a key SHOULD instead sign its 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, and the fields are those of the call:
| action | fields |
|---|---|
claim |
the Idempotency-Key (8.7.7) when one is sent, then payload=<digest> (8.7.11) when a digest is stated |
capture |
amount, bookingRef, settlementTxHash |
unknown, void |
reason |
refund |
amount (the empty string for a full refund), reason |
reconcile |
none |
Each field is escaped for \ 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.
- The verifier MUST check the signature against the key the DID commits to, reject a stale or
future
issuedAt, and consume the nonce once. An identity that fails any check MUST be refused with aCOUNTERPARTY_*code; it MUST NOT fall back to anonymous handling. - The identity that signs the claim is the claimer. Every later call that lowers or releases the hold
MUST be signed by the same identity (
COUNTERPARTY_MISMATCH, HTTP 403, otherwise). No claim token is issued to an authenticated claimer. - An implementation MAY require an authenticated claimer for every claim
(
COUNTERPARTY_AUTH_REQUIRED).
Refund. POST .../refund { amount?, reason?, claimToken? } records a reversal of a captured hold:
no money moves, but the cumulative cap counts less. It MUST be accepted only from the hold's
claimer (its refund signature, or its claim token), the mandate's owner, or a platform operator
(COUNTERPARTY_MISMATCH otherwise).
8.7.7. Idempotent claim. A claimant whose claim response was lost MAY retry with the same
Idempotency-Key header: 32–128 characters of [A-Za-z0-9._:~-] with at least 128 bits of
randomness, chosen per claim call. A malformed key, or one that contains the authorization id, is
refused IDEMPOTENCY_KEY_INVALID (400). A retry by the same identity while the effect is still
dispatching is answered 200 with the original grant and replayed: true; once the effect has
moved on it is an ordinary AUTHORIZATION_ALREADY_CLAIMED. A gateway SHOULD send the
authorization id to the upstream as its Idempotency-Key, and MUST NOT forward one the agent
supplied.
8.7.8. Outcome lookup. GET /policy/mandate/authorize/{id}/effect (public, keyed by the
authorization id) answers what became of an authorization:
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 |
retrySafe is the only safe basis for issuing a fresh authorization for the same intent. To retry
a not_started hold, void it first and read not_executed. An unknown or in_flight outcome
MUST NOT be retried blindly. The response also carries claimed, claimedAt, currency,
authorizedAmount, settledAmount and settlementEvidence (8.7.9); never a claim token or the
claimer's identity.
Capture and void are not idempotent the way a claim is. A capture of a hold that is no longer held
answers 409 NOT_HELD; a void of one answers 200 { voided: false, reasonCode: "NOT_HELD" }. A
caller that retries either after a timeout or a 5xx MUST read the outcome first: the first call
has usually landed.
A refused capture, void or refund is a 4xx whose message is the bare reason code and whose
data is { captured | voided | refunded: false, authorizationId, reasonCode, detail }. A client
MUST branch on reasonCode, never on detail. The same code has the same HTTP status on every
call. The codes are listed in Appendix A.
8.7.9. Authorized and settled amounts. The authorized amount is recorded once and never
overwritten; the settled amount is what the cumulative cap then counts, and MUST NOT exceed it
(AMOUNT_EXCEEDS_AUTHORIZED). Every settlement states what it rests on as settlementEvidence:
unattested, unclaimed_lowered, counterparty_attested, independently_confirmed,
operator_resolved or reconciled_at_authorized. Where the deployment observes the remote system, a
claimer's lowered settlement or release MUST be supported by that observation, and is refused
otherwise (SETTLEMENT_NOT_CONFIRMED, SETTLEMENT_AMOUNT_MISMATCH, SETTLEMENT_NOT_FOUND); the
claimer can still settle in full or mark the effect unknown.
8.7.10. Trusted counterparties. An owner MAY register the counterparty DIDs that may claim
their holds, each optionally limited to named merchants. An owner with any registered counterparty —
and every hold of a mainnet agent — accepts only a claim signed by an active, registered identity
(COUNTERPARTY_AUTH_REQUIRED, COUNTERPARTY_NOT_REGISTERED, COUNTERPARTY_NOT_ALLOWED_FOR_MERCHANT,
HTTP 403). A refused claim leaves the hold claimable.
An entry's purpose is claim or report (a Service registered only to report what it did, 16.4). A
report entry is never a claimant, except for an approval claim (8.7.18) of a zero-amount hold
of an agent it was registered for.
A registration carries the service's own acceptance: the service signs
MAGP-COUNTERPARTY-ACCEPT-v1|<registrant principal DID>|<service DID>|<purpose>|<nonce>|<expiresAt>
(fields escaped as in 8.3) over a challenge the owner obtained, with the key its DID embeds. A service
MUST sign only a well-formed acceptance naming its own DID — never free text (16.2).
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, in the
x-magp-payload-digest header and, for an authenticated claim, as the signed field payload=<digest>.
The issuer compares it with the digest it stored at authorize, before recording the claim:
| claim | hold | result |
|---|---|---|
| digest equal | bound | granted; the grant reports payloadDigest |
| digest different | bound | PAYLOAD_DIGEST_MISMATCH (403) |
| no digest | bound | PAYLOAD_DIGEST_REQUIRED (403) |
| digest | unbound | PAYLOAD_NOT_BOUND_AT_AUTHORIZE (403) |
| — | unbound, mandate requires binding | PAYLOAD_BINDING_REQUIRED (409) |
| malformed digest | — | PAYLOAD_DIGEST_INVALID (400) |
Every refusal leaves the hold unclaimed. The comparison is against the digest the issuer stored, never only against the signed request the executor was handed.
The executor SHOULD also compare the digest of what it will execute with the signed request before
it claims. It MUST refuse, rather than skip the check for, a payload it cannot digest
(PAYLOAD_NOT_CANONICALIZABLE), and MUST NOT forward a URL query string, path parameter or
fragment the signature does not cover (QUERY_NOT_BOUND) unless its operator listed that key as
unbound. A JSON body that two readers could read differently — a repeated key, a number a double
cannot hold exactly, invalid UTF-8, a byte-order mark — MUST be refused, not digested.
8.7.12. Who may push a claimed hold along. Marking an outcome unknown
(POST .../effect/unknown) and asking for reconciliation (POST .../effect/reconcile) are accepted
only from the claimer, the mandate's owner, or an operator (COUNTERPARTY_MISMATCH,
RECONCILE_NOT_PERMITTED).
8.7.13. Owner-forced reconciliation. An owner MAY force a claimed hold into reconciliation
(POST .../effect/force-reconcile). The claimer then loses every power over it, including capture
and void (HOLD_UNDER_RECONCILIATION, 409), and the hold settles only on what reconciliation
establishes.
8.7.14. Payee directory. An owner MAY register the accounts each merchant is paid into. A
lowered settlement for a merchant with a registered account MUST name one of them as payTo
(PAYEE_NOT_REGISTERED, PAYEE_NOT_VERIFIED).
8.7.15. Revocation and holds. Revoking a mandate — directly, through its delegation chain
(5.4.9), by reissue or by decommissioning the agent — releases every hold under it that no
counterparty has claimed, and leaves every claimed hold to settle by the claimed-hold rules. A claim
or capture of an unclaimed hold under an inactive mandate, or under a delegated mandate with an
inactive ancestor, is refused MANDATE_REVOKED or DELEGATION_CHAIN_BROKEN (409).
8.7.16. Rules changed while a hold waits. A change to the agent's SOPs or bound Standards applies
to holds that are not yet claimed. Before an unclaimed hold is claimed or captured, the gate MUST
evaluate the current rules over the context the hold was granted on. If they no longer permit it, the
claim is refused AUTHORIZATION_RULES_CHANGED (409) and the hold voided; if they cannot be evaluated,
it is refused CONTROL_UNAVAILABLE (409) and the hold left unclaimed. A hold minted by a person's
approval is not refused for an escalate. A claimed hold is never re-judged.
8.7.17. Containment and holds. Containment (7.1) stops what the agent was already allowed to do.
A claim or capture of an unclaimed hold of a contained agent is refused with the containment code —
AGENT_SUSPENDED, AGENT_QUARANTINED or AGENT_DECOMMISSIONED (409) — and the hold voided, whoever
offers to execute it. Reinstating the agent does not revive the hold.
8.7.18. Executing an approved escalation. An executor that re-evaluates the agent's policy
reaches escalate again for a request a person has already approved. When its own verdict is
escalate and the request carries an authorizationId, the executor MAY claim that hold with
requireHumanApproval: true. The issuer grants such a claim only for a hold minted by a person's
approval, and otherwise refuses ESCALATION_NOT_APPROVED (409). Every grant carries
approvedByHuman; the executor MUST permit only on approvedByHuman: true, and only over an
integrity-protected channel to the issuer. Only an escalate is lifted this way; a block,
suspend or quarantine is never overridden. Every other claim check still applies. A modified
approval carries no payload digest, so it is refused PAYLOAD_BINDING_REQUIRED until the agent binds
the payload of what will run (8.3.11).
8.7.19. Claim expectations. A claim SHOULD state what the executor is about to execute as
expect: { agentDid, action, amount, currency, merchant }. The issuer MUST compare these with the
hold before granting and refuse a difference — AUTHORIZATION_AGENT_MISMATCH,
AUTHORIZATION_ACTION_MISMATCH, AUTHORIZATION_AMOUNT_MISMATCH, AUTHORIZATION_CURRENCY_MISMATCH
or AUTHORIZATION_MERCHANT_MISMATCH (403) — leaving the hold unclaimed. A malformed expect is
refused CLAIM_EXPECTATION_INVALID (400); an unknown key is ignored. An executor that still finds a
mismatch after a grant MUST release the claim before refusing.
8.7.20. Approved-context binding. A hold a person approved is for the request that person
reviewed, including its context. The approved context's digest is sha256: + hex SHA-256 of the
RFC 8785 canonical JSON of { "MAGP-APPROVED-CONTEXT-v1": context } (an absent context is {}; for a
MODIFY that changed the itinerary, the reviewer's itinerary), pinned by
docs/protocol/resume-binding-vectors.json
(contextVectors). A claim MUST state the digest of the context it is about to execute as
expect.contextDigest. For a person-approved hold the issuer refuses a difference
AUTHORIZATION_CONTEXT_MISMATCH (403) and a missing digest AUTHORIZATION_CONTEXT_REQUIRED (403).
The grant names the digest it compared (contextDigest); the executor MUST release the claim and
refuse when it differs from its own. An executor that runs a body other than the itinerary SHOULD
require payload binding (8.3.9).
8.7.21. In-process resume claim. An SDK that runs an approved escalation's tool in its own process
takes the approval's one resume at the issuer before it runs:
POST /policy/escalations/{escalationId}/resume-claim with
{ agentDid, nonce, issuedAt, requestDigest, contextDigest, signature }. requestDigest is the
resume binding digest (MAGP-RESUME-BIND-v1) and contextDigest the approved-context digest
(8.7.20), both computed from the SDK's own arguments. The agent signs
MAGP-RESUME-CLAIM-v2|escalationId|authorizationId|agentDid|requestDigest|contextDigest|nonce|issuedAt
(each field escaped as in 8.3), pinned by
docs/protocol/resume-binding-vectors.json
(claimVectors). The issuer refuses a request difference ESCALATION_REQUEST_MISMATCH (403) and a
context difference AUTHORIZATION_CONTEXT_MISMATCH (403). Exactly one valid claim is granted; others
are refused AUTHORIZATION_IN_USE (409) while the resume may still be running, and
AUTHORIZATION_VOIDED or AUTHORIZATION_ALREADY_USED (409) once its hold is voided or settled. An
issuer that requires v2 says so on the escalation's status (resumeClaimVersion: 2); an SDK MUST
sign v2 when it is present.
9. Secondary controls
Controls that protect the system rather than express authority. Both are optional. A deployment
chooses whether they fail open or closed when they cannot run; failing closed, a rate counter or
breaker lookup that errors blocks with CONTROL_UNAVAILABLE, and the spend-anomaly and assurance
floors escalate with it. A request MUST NOT be able to cause that error itself: a scope value
that cannot be looked up is refused MALFORMED_REQUEST.
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, and the digests of 8.7.20; it names no principal and no budget. With an
authorization it also reports executed, executedAt and outcome (as
GET /policy/mandate/authorize/{id}/effect reports it). status stays approved after the action
ran.
10.2a. GET /policy/mandate/{ref}/status is public as a registry read (4.4.2), and minimized: an
unauthenticated caller gets policyId, agentDid, status, principalVerified,
principalTierReason, allowedJurisdictions, vcHash, mandateStoreRef, anchorRef and
expiresAt. The principal's DID and the budget (cap, spent, remaining, byCurrency) are
returned only to the mandate's owner. The response says which it is (view: "public" | "owner").
10.3. Approval requires M of N distinct approvers, configurable per agent, defaulting to
- 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_REVOKEDotherwise,DELEGATION_CHAIN_BROKENfor a chain that cannot be walked, 5.4.10), and still the agent's current mandate for the action (MANDATE_SUPERSEDEDif 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) and circuit breakers (§9.2).
- Committed spend and counterparty trust, applied as server-derived fields (§6.3), so the review's stored itinerary cannot supply them.
- The signed operands,
resourceincluded; the unsigned itinerary never stands in for them. - The jurisdiction (§8.3.12): the stored signed value, resolved again against the payee registry as of the decision. A reviewer cannot change it, and a MODIFY cannot move the action to another jurisdiction.
An input that cannot be reconstructed refuses the approval with APPROVAL_INPUT_UNAVAILABLE; the
review stays pending. A MODIFY receives the same inputs; its block closes the review as
modified.
10.4.2. Revoking a mandate closes its pending reviews. Every review still pending under a
revoked mandate becomes denied, with the reason MANDATE_REVOKED — or MANDATE_SUPERSEDED when
the mandate was reissued — and an evidence event is recorded. The status route reports that 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.
10.5. An unresolved escalation expires — ESCALATION_EXPIRED. The hold an approval mints carries
its own window 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) answersintegrityValid(the signature checks out),payloadMatch,outcomeStatus, andsettlementEvidence(below) for any relying party, with no authentication and no state about who is asking.receiptVersion. The receipt's field set is versioned, and a verifier reconstructs the exact message each version signed, never adding a later version's fields. Version 1 signsexecutionReceiptId,actionPassportId,authorizationId,agentDid,authorizedPayloadDigest,executedPayloadDigest,payloadMatch,outcomeStatusandissuedAt; version 2 addssettlementEvidence(8.7.9).
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. The blind path proves the agent's committed jurisdiction
against it and does not consult the payee registry. 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 refusedGATEWAY_OWNER_MISMATCHbefore 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.
A Service that holds no one owner's credentials — a public tool, or one that resolves each caller's
own credential — MAY admit every governed agent, but only by saying so explicitly ('any'),
never by omission, and it then takes no owner binding.
16.4. Reporting what a Service did. A Service that executes without claiming an authorization,
or refuses a request itself, SHOULD report each governed request it answers to
POST /policy/gateway-reports:
{ servedAgentDid, outcome: "executed" | "refused", reasonCode, httpStatus, request, reportId, occurrences }.
requestis the agent's signed request as received (8.3). The authorization it named ispresentedAuthorizationId;authorizationIdis set only when the Service claimed that authorization for this request.reportId(8–64 characters of[A-Za-z0-9_-]) is the Service's own id for the report, kept across retries;occurrencesis how many identical refusals the report stands for (always 1 forexecuted,REPORT_OCCURRENCES_INVALIDotherwise).- The report is signed as the Service (MAGP-SERVICE-v1, action
report, the request's nonce in the authorization-id slot, fieldsservedAgentDid | callerAgentDid | action | outcome | reasonCode | httpStatus | reportId | occurrences). - The issuer records it only when the signature verifies and the served agent's owner registered the
signing DID as a counterparty of either purpose (8.7.10), and files it under that owner. An
executedreport MUST carry a request whose agent signature verifies (REPORT_CALLER_UNVERIFIED). A resent report is answeredALREADY_RECORDED; the issuer MAY limit reports per Service (REPORT_RATE_LIMITED, 429).
Reports are visibility only: they never change a decision, a hold or a cap.
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:
- resolves agent keys from the DID (§4.3.1);
- rebuilds and verifies the canonical message exactly (§8.3);
- applies the order of checks in §8.5, stopping at the first failure;
- treats
allowandobserveas the only permitting dispositions (§8.6.3); - never treats
escalateas permission (§8.6.4); - fails closed as §17.1 requires;
- enforces the narrowing invariant on any delegated mandate it accepts (§5.4);
- emits reason codes from Appendix A, or documents extensions;
- 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 /agent-identity/{ref}/policy-disclosure |
owner | Open one part of an anchored policy commitment (6.2.8) |
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 (a missing or invalid field, e.g. a negative amount) before any policy evaluation ran. Returned in the same verdict shape as every other decision. | — |
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 limits of its principal's plan (§4.4.1). Testnet and did:key agents are never refused with it; verification is reported for a counterparty's own risk policy. | IDENTITY_UNVERIFIED |
AGENT_NOT_FOUND |
block | Unknown agent DID. | IDENTITY_UNVERIFIED |
AGENT_KEY_UNVERIFIED |
block | BYOK key not yet proven. | IDENTITY_UNVERIFIED |
IDENTITY_ROTATED |
block | The request is signed as a DID its agent no longer uses: the owner rotated the agent's key (Rotate key), which issued a new DID and moved the agent's mandates to it. The old DID and key stop working at once. | IDENTITY_REVOKED |
IDENTITY_KEY_MISMATCH |
block | Registered public key does not match the key embedded in the agent's own self-certifying DID. | IDENTITY_UNVERIFIED |
MANDATE_REVOKED |
block | The mandate has been revoked, or a mandate it was delegated from is inactive (§5.4.10). Refuses the action at authorize (§8.5 step 1; a guard reads revokedActions, §6.2.6), a pending review (§10.4.2), an approval, and a claim or capture of an unclaimed hold (§8.7.15). |
IDENTITY_REVOKED |
DELEGATION_CHAIN_BROKEN |
block | A delegated mandate whose chain cannot be walked to its root: a parent is missing, the chain cycles, or it is deeper than the bound (§5.4.10). Fails closed at authorize, at an approval, and on a claim or capture of an unclaimed hold. | IDENTITY_REVOKED |
DELEGATION_NARROWING_FAILED |
block | A reissue of a delegated mandate whose changed terms would not narrow its delegator's current terms (§5.4.2–§5.4.5, §5.4.11). An owner's change is refused (409, nothing changes); an identity rotation revokes the delegated mandate instead. | 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 verifies against an earlier canonical message shape (§8.3.7), not the current one: a valid key on an outdated client. Still blocked, but named so the client is upgraded rather than the key rotated. | 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 | A payload binding (§8.3.9, §8.3.11) is present but not valid: a digest without a signature or the reverse, a malformed digest, or a signature that does not verify under the agent's key. A digest nobody signed binds nothing. | ENVELOPE_INTEGRITY_FAILURE |
PAYLOAD_BINDING_REQUIRED |
block | The mandate's owner requires this action's payload to be bound (requirePayloadBinding, §8.3.9) and none is: at authorize, the request carries no binding; at a claim, the hold carries no digest yet. Send the payload so the agent signs its digest, or bind the hold (§8.3.11) before claiming. |
ENVELOPE_INTEGRITY_FAILURE |
QUERY_NOT_BOUND |
block | Executor-side (§8.7.11): a governed request carries a URL query, path parameter or fragment that the agent's signature and payload binding do not cover. Refused before anything is claimed. | 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 has no effective jurisdiction (§8.3.12) while one is required: the mandate restricts jurisdictions for this action, or an enforced rule judges jurisdiction-not-allowed. An itinerary jurisdiction 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, or the owner's entries for that payee name more than one country. Judged at authorize and again at an approval. A hard block: it outranks an escalate. | COUNTERPARTY_NOT_ALLOWED |
Exposure / counterparty (SAFR §22/§23)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
SPEND_LIMIT_EXCEEDED |
block | Mandate budget constraint failed. | EXPOSURE_THRESHOLD_EXCEEDED |
CAP_EXCEEDED |
block | The mandate's spend cap, enforced atomically as the hold is written (§8.5 step 16): committed spend plus this amount is over the cap. Usually a concurrent authorize took the rest of the budget. Not retryable until spend is released. | EXPOSURE_THRESHOLD_EXCEEDED |
CURRENCY_NOT_ALLOWED |
block | The request's currency is not one a cap is denominated in (a mandate spend constraint, or an SOP or rule-pack cap scoped to a currency), so the cap cannot judge it and refuses. Request in the cap's currency, or have the owner add it. | MANDATE_SCOPE_VIOLATION |
AMOUNT_NOT_DETERMINABLE |
block | The gate could not establish how much value the action moves, so no cap could be applied to it (amount-unknown atom). | EVIDENCE_MISSING |
MERCHANT_NOT_ALLOWED |
block | Merchant outside mandate. | COUNTERPARTY_NOT_ALLOWED |
COUNTERPARTY_NOT_ALLOWED |
block | Counterparty outside mandate. | COUNTERPARTY_NOT_ALLOWED |
ROUTE_NOT_ALLOWED |
block | Route outside mandate. | ACTION_NOT_PERMITTED |
RATE_LIMIT_EXCEEDED |
block / escalate / observe | A time-windowed rate limit fired (rate-limit-exceeded atom). | RATE_LIMIT_EXCEEDED |
CONTROL_UNAVAILABLE |
block / escalate | A secondary control could not run and the deployment is configured to fail closed on them: the rate window or circuit-breaker lookup blocks; the spend-anomaly check or assurance lookup escalates to a person (§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 capture of an unclaimed hold refused because the agent's current SOP or Standard rules no longer permit it (§8.7.16). The hold is voided and nothing executes; the agent asks again under the new rules. | POLICY_VIOLATION |
ESCALATION_NOT_APPROVED |
escalate | A claim that asked to execute only a hold a person approved (§8.7.18) — the executor's own policy judged the request as needing review — presented a hold that no approval minted. The hold is left unclaimed and unchanged; the request still needs a person, so the agent escalates and waits for the decision. | HUMAN_REVIEW_REQUIRED |
AUTHORIZATION_CONTEXT_MISMATCH |
block | A claim or an in-process resume claim of a hold a person approved stated a context other than the one that person approved (§8.7.20, §8.7.21). Refused; the hold is left for the approved request. Resume with the arguments the escalation was raised with. | MANDATE_SCOPE_VIOLATION |
AUTHORIZATION_CONTEXT_REQUIRED |
block | A claim of a hold a person approved did not state the context it is about to execute (§8.7.20), or an in-process resume claim was signed with a message that binds no context (§8.7.21). Refused, the hold untouched. The detail names the executor or SDK version to upgrade to. |
ENVELOPE_INVALID |
AUTHORIZATION_CONTEXT_UNAVAILABLE |
block | A claim of a hold a PERSON approved, whose approving review could not be found to read the approved context from (§9a.5). Refused rather than claimed unchecked; the hold is left as it was. | POLICY_VIOLATION |
ESCALATION_REQUEST_MISMATCH |
block | The arguments given to resume an approved escalation do not reproduce the request the owner approved (§8.7.21): its amount, currency, merchant, resource, payload or context. Nothing runs; resume with the arguments the escalation was raised with. | MANDATE_SCOPE_VIOLATION |
CONTEXT_TOO_LARGE |
block | An agent with bound SOP or Standard rules sent a rule context (its itinerary and envelope metadata) too large to record whole with the hold (§8.7.16). Refused rather than trimmed: a trimmed context could drop a field a rule reads and let the hold slip the claim-time re-check. Send a smaller context. | ENVELOPE_INVALID |
SANDBOX_CALLER_SHARE_EXCEEDED |
block | The shared public sandbox only: this caller (told apart by a keyed hash of its network) already holds its share of the sandbox's rolling spend budget, so one visitor cannot keep the budget drained for everyone else. The budget heals as the window rolls past; a real tenant's mandate never returns this. | POLICY_VIOLATION |
Evidence-quality controls (SAFR §24). Author-facing codes for the evidence atoms.
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
EVIDENCE_INSUFFICIENT |
block / escalate | A required evidence type was not attested (evidence-requirement atom). | EVIDENCE_MISSING |
EVIDENCE_CONFIDENCE_LOW |
block / escalate | Attested evidence confidence below the required minimum (evidence-confidence-below atom). | EVIDENCE_QUALITY_LOW |
Containment / circuit breaker (SAFR §21)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
AGENT_SUSPENDED |
suspend | Agent contained (suspended) by a prior firing rule — reinstate to act again. | CIRCUIT_BREAKER_ACTIVE |
AGENT_QUARANTINED |
quarantine | Agent contained (quarantined) by a prior firing rule — reinstate to act again. | CIRCUIT_BREAKER_ACTIVE |
AGENT_DECOMMISSIONED |
decommission | Agent decommissioned (retired) by its owner — final: it is never reinstated; a new agent takes over its work. The owner's reason, if they gave one, is the record's note. | CIRCUIT_BREAKER_ACTIVE |
CIRCUIT_BREAKER_OPEN |
block | A scoped circuit breaker (agent/tenant/tool/counterparty) is open — tripped manually or by failure rate. | CIRCUIT_BREAKER_ACTIVE |
Human review (SAFR §12)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
MODE_READ_ONLY |
block | READ_ONLY operating mode denies a value-bearing action. (The owner's act of pinning that mode is recorded under the same code, as MODE_AUTONOMOUS / MODE_SUPERVISED / MODE_RESTRICTED are.) | ACTION_NOT_PERMITTED |
MODE_REVIEW |
escalate | Operating-mode floor routed to human review. | HUMAN_REVIEW_REQUIRED |
MODE_RESTRICTED_REVIEW |
escalate | RESTRICTED mode: every value-bearing action needs a human. | HUMAN_REVIEW_REQUIRED |
MODE_SUPERVISED_REVIEW |
escalate | SUPERVISED mode: high-consequence action needs a human. | HUMAN_REVIEW_REQUIRED |
CONTEXT_UNVERIFIABLE |
escalate | A rule needed a context field (e.g. riskLevel) and could not get a well-formed value from a source it trusts — missing, unrecognised, or less trusted than the rule requires (spec §6.3). | HUMAN_REVIEW_REQUIRED |
REASSESSMENT_PENDING |
escalate | A material authority change awaits owner review. | HUMAN_REVIEW_REQUIRED |
SPEND_PATTERN_ANOMALY |
escalate | Amount is far above the agent's own recent payments for this action (a learned baseline, not a preset cap). Amounts the mandate's per-transaction cap and budget already bound closely are not re-checked. | HUMAN_REVIEW_REQUIRED |
AGENT_NOT_CERTIFIED |
escalate | The agent's latest deployment-assurance certificate is 'blocked' (a required assurance gate failed or was never assessed). | HUMAN_REVIEW_REQUIRED |
ASSURANCE_STALE |
escalate | The agent's latest deployment-assurance certificate has passed its validUntil — the assessed facts are no longer current. | HUMAN_REVIEW_REQUIRED |
ESCALATION_PENDING |
escalate | Held awaiting approver resolution (§9a). | HUMAN_REVIEW_REQUIRED |
ESCALATION_EXPIRED |
block | Escalation TTL elapsed (§9a.4). | HUMAN_REVIEW_TIMEOUT |
ESCALATION_CONTEXT_ALTERED |
block | The pending escalation's stored request context no longer matches its row-integrity commitment made at escalate time (Tier 2 context-claim binding, SAFR §5) — reservation refused. | ENVELOPE_INTEGRITY_FAILURE |
MANDATE_SUPERSEDED |
block | Approval refused: the mandate the review was raised under is no longer the agent's current mandate for the action (a newer one has been issued) — the agent asks again (§10.4.1). Also the reason a reissue closes the reviews pending under the mandate it replaced (§10.4.2). | MANDATE_MISSING |
APPROVAL_INPUT_UNAVAILABLE |
block | Approval refused: an input the gate judges the request on could not be read or reconstructed at approval time (rate counter, breaker store, or the signed resource or signed jurisdiction of a review stored before it was kept) — refused, never skipped; the review stays pending (§10.4.1). | EVIDENCE_MISSING |
ESCALATION_APPROVED |
allow | Approver upgraded the held request (§9a). | — |
ESCALATION_DENIED |
block | Approver denied the held request (§9a). | — |
Decision token / capability binding (SAFR §20)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
AMOUNT_MISMATCH |
block | x402 amount exceeds the authorized amount (§7a). | DECISION_TOKEN_INVALID |
COMMITMENT_MISMATCH |
block | Executed transaction does not match the authorized commitment. | DECISION_TOKEN_INVALID |
SESSION_MISMATCH |
block | Capability bound to a different agent↔MCP session. | DECISION_TOKEN_INVALID |
CAPABILITY_EXPIRED |
block | Capability past its expiry (§7.7). | DECISION_TOKEN_INVALID |
CAPABILITY_SIGNATURE_INVALID |
block | Capability signature invalid or wrong issuer key. | DECISION_TOKEN_INVALID |
CAPABILITY_OK |
allow | Capability signature + transaction/session binding verified. | — |
CAPABILITY_AGENT_MISMATCH |
block | The capability was issued to a different agent DID than the one presenting it — or carries no agent at all (a v1 capability) while the verifier requires one (§15.3). | DECISION_TOKEN_INVALID |
CAPABILITY_SCOPE_MISMATCH |
block | The capability names a different mandate, action or currency than the verifier expects, or names none (a v1 capability) where one is required (§15.3). | DECISION_TOKEN_INVALID |
Evidence quality (SAFR §24)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
BBS_KYC_LEVEL_TOO_LOW |
block | Disclosed KYC level below the required floor. | EVIDENCE_QUALITY_LOW |
MetaMynd-specific extensions beyond the SAFR floor (no §30 alias)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
HOLD_VOIDED |
void | Reservation expired or explicitly voided (§7a.4). | — |
SETTLEMENT_FAILED |
block | Facilitator could not verify/settle (§7a). | — |
AGENT_NOT_ADMITTED |
block | A Service-side guard refused a validly signed request from an agent it does not admit (allowedAgents, §16.3) — before fetching its policy or claiming anything. | COUNTERPARTY_NOT_ALLOWED |
CREDENTIAL_PROFILE_NOT_PERMITTED |
block | The agent is admitted to the Service but not to this credential profile (a route, tool or skill that admits fewer agents, §16.3) — refused before its policy is fetched or anything claimed. | COUNTERPARTY_NOT_ALLOWED |
GATEWAY_OWNER_MISMATCH |
block | The agent's signed bundle names an owner other than the principal that owns this Service's credentials (gatewayOwnerPrincipal, §16.3), or names none — refused before any claim. | COUNTERPARTY_NOT_ALLOWED |
GATE_UNREACHABLE |
block | Guard fail-closed on transport error: the issuer could not be reached (a network failure, or a 5xx). A Service-side guard returns it when the policy bundle fetch fails that way; a 4xx answer is not an outage. | — |
ZK_POLICY_OK |
allow | All zero-knowledge predicates verified blind. | — |
ZK_AMOUNT_PROOF_INVALID |
block | Bulletproofs range proof (amount ≤ cap) failed. | EXPOSURE_THRESHOLD_EXCEEDED |
ZK_JURISDICTION_PROOF_INVALID |
block | Set-membership proof (jurisdiction ∈ allow-list) failed. | REGULATORY_PROHIBITION |
ZK_CALLER_POLICY_REJECTED |
block | A private-authorize request carried its own policy (cap, allowedJurisdictions). The terms are read from the agent's active mandate; a caller-chosen policy is refused (HTTP 400) rather than ignored, so an old client fails loudly (§15.1). |
ENVELOPE_INVALID |
ZK_TRANSACTION_COMMITMENT_REQUIRED |
block | A private-authorize request carried no transactionCommitment (HTTP 400). Every allow returns a capability bound to the transaction it authorizes, so a request with no transaction to bind to is refused rather than answered with an unbound verdict (§15.1). |
ENVELOPE_INVALID |
ZK_CAP_MISMATCH |
block | The range proof was built against a cap other than the agent's mandate cap, so it proves nothing about this mandate. Rebuild it against the cap in the agent's policy bundle (§15.1). | ENVELOPE_INVALID |
ZK_AMOUNT_PROOF_REQUIRED |
block | The mandate caps this action but the private-authorize request carries no amount commitment + range proof (§15.1). | EVIDENCE_MISSING |
ZK_JURISDICTION_PROOF_REQUIRED |
block | The mandate restricts jurisdictions for this action but the private-authorize request carries no jurisdiction commitment + membership proof (§15.1). | EVIDENCE_MISSING |
ZK_MANDATE_TERMS_UNSUPPORTED |
block | The mandate has no term the zero-knowledge path can prove for this action (no spend cap, no jurisdiction allow-list), or has one in a form it cannot prove (a non-upper-bound cap, a jurisdiction exclusion). Use /policy/mandate/authorize (§15.1). | MANDATE_SCOPE_VIOLATION |
ZK_CURRENCY_NOT_ALLOWED |
block | The private-authorize request names a currency outside the mandate cap's currencies, or names none where the cap allows several (§15.1). | MANDATE_SCOPE_VIOLATION |
ZK_HUMAN_REVIEW_UNAVAILABLE |
block | The agent's operating mode (SUPERVISED/RESTRICTED) routes value-bearing actions to a human, and the zero-knowledge path can neither see the amount nor hold anything for a reviewer — so it refuses. Use /policy/mandate/authorize (§15.1). | HUMAN_REVIEW_REQUIRED |
BBS_PRESENTATION_OK |
allow | Issuer-signed attributes verified via BBS selective disclosure. | — |
BBS_PROOF_INVALID |
block | BBS proof invalid, wrong issuer key, or nonce mismatch (replay). | ENVELOPE_INTEGRITY_FAILURE |
POLICY_BUNDLE_OK |
allow | Policy bundle signature + freshness verified. | — |
POLICY_BUNDLE_STALE |
block | Bundle older than maxStaleness — fails closed for any action under a pinned key, else for a value-bearing one. | POLICY_VIOLATION |
POLICY_BUNDLE_UNSIGNED |
block | Bundle carries no signature — fails closed for any action under a pinned key, else for a value-bearing one. | POLICY_VIOLATION |
POLICY_BUNDLE_SIGNATURE_INVALID |
block | Bundle signature invalid or tampered (any tier). | ENVELOPE_INTEGRITY_FAILURE |
POLICY_BUNDLE_UNVERIFIED |
block | No pinned policy key and the bundle came over plain http — nothing authenticates it, so a value-bearing action fails closed. | ENVELOPE_INTEGRITY_FAILURE |
Action Passport (MetaMynd Governed Execution scope, Module E/F)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
PASSPORT_EXPIRED |
block | The Action Passport bound to this authorization has passed its expiry — capture refused. | MANDATE_EXPIRED |
PASSPORT_ALREADY_CONSUMED |
block | The Action Passport was already consumed by a prior capture — single-use, replay refused. | DECISION_TOKEN_INVALID |
PASSPORT_REVOKED |
block | The Action Passport was revoked (its hold was voided) before capture. | DECISION_TOKEN_INVALID |
Platform rule packs
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
SOP_SPEND_CAP |
block | A starter SOP or rule pack's per-transaction cap: the amount is over the cap in the cap's currency. | EXPOSURE_THRESHOLD_EXCEEDED |
SOP_BUDGET_CAP |
block | A rule pack's total budget: committed spend plus this amount is over the budget. | EXPOSURE_THRESHOLD_EXCEEDED |
RISK_REVIEW |
escalate | A starter SOP or rule pack's risk review: the effective risk level is at or above the pack's threshold. | HUMAN_REVIEW_REQUIRED |
PII_REVIEW |
escalate | A rule pack sends a request that says it carries personal data to a human. | HUMAN_REVIEW_REQUIRED |
CONTENT_REVIEW |
escalate | A rule pack's starter term list matched the request text (a STARTER list, meant to be replaced with the owner's own). | HUMAN_REVIEW_REQUIRED |
JURISDICTION_BLOCKED |
block | A rule pack's jurisdiction list does not include the request's jurisdiction. | MANDATE_SCOPE_VIOLATION |
DATA_RESIDENCY_REVIEW |
escalate | A rule pack's data-residency list does not include the request's data region. | HUMAN_REVIEW_REQUIRED |
EVIDENCE_INCOMPLETE |
escalate | A rule pack requires evidence types the request did not carry. | EVIDENCE_MISSING |
EVIDENCE_CONFIDENCE_REVIEW |
escalate | The request's evidence confidence is below a rule pack's minimum. | HUMAN_REVIEW_REQUIRED |
COUNTERPARTY_TRUST_REVIEW |
escalate | The counterparty's trust score is below a rule pack's review threshold. | HUMAN_REVIEW_REQUIRED |
RATE_LIMIT_REVIEW |
escalate | The agent's call count in its rate-limit window passed a rule pack's early-review mark (before the hard RATE_LIMIT_EXCEEDED ceiling). | HUMAN_REVIEW_REQUIRED |
Automated containment (MetaMynd Governed Execution scope, Module H)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
REPEATED_DENIALS_CONTAINED |
quarantine | Automated containment: this agent crossed the repeated-denial threshold within the rolling window set by the deployment and was quarantined. | CIRCUIT_BREAKER_ACTIVE |
MANUAL_CONTAINMENT |
suspend / quarantine | The agent was contained (suspended or quarantined) by its owner or an operator from the dashboard or API. The owner's reason, if they gave one, is the record's note. Reinstate it to act again. | CIRCUIT_BREAKER_ACTIVE |
REINSTATED_AFTER_REGRESSION_CHECK |
allow | The agent's owner reinstated it after its current policy passed every Scenario Bank set that applies to it (the sets checked are on the record); it may act again. | — |
Claim, resume and settlement (an executor's or SDK's refusal)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
AUTHORIZATION_REQUIRED |
block | The executor requires an authorization (an authorizationId from an allow or an approval) and the request carried none. | DECISION_TOKEN_INVALID |
AUTHORIZATION_NOT_FOUND |
block | No hold exists for the authorizationId presented. | DECISION_TOKEN_INVALID |
AUTHORIZATION_EXPIRED |
block | The hold was not claimed within its time-to-live; ask again. | MANDATE_EXPIRED |
AUTHORIZATION_VOIDED |
block | The hold was released before this claim or resume — by its agent, its owner, a revoke, a containment or a rule change. Nothing can run under it, and its owner must approve the request again. | DECISION_TOKEN_INVALID |
AUTHORIZATION_ALREADY_CLAIMED |
block | Another executor (or an earlier attempt) already claimed this single-use hold. | DECISION_TOKEN_INVALID |
AUTHORIZATION_ALREADY_USED |
block | A resume of an approval that is already finished with: its hold was settled, claimed by a gateway, or expired, so the tool is not run again (§8.7.21). | DECISION_TOKEN_INVALID |
AUTHORIZATION_IN_USE |
block | The one resume of this approval is already taken and may still be running — by a concurrent resume in this process or another one (§9a.6); its authorization has not settled yet. Exactly one runs. | DECISION_TOKEN_INVALID |
AUTHORIZATION_AGENT_MISMATCH |
block | The hold belongs to another agent than the one whose request presents it (§8.7.19). | IDENTITY_UNVERIFIED |
AUTHORIZATION_ACTION_MISMATCH |
block | The hold authorizes another action than the request's (§8.7.19). | MANDATE_SCOPE_VIOLATION |
AUTHORIZATION_AMOUNT_MISMATCH |
block | The hold authorizes another amount than the request's (§8.7.19). | MANDATE_SCOPE_VIOLATION |
AUTHORIZATION_CURRENCY_MISMATCH |
block | The hold authorizes another currency than the request's (§8.7.19). | MANDATE_SCOPE_VIOLATION |
AUTHORIZATION_MERCHANT_MISMATCH |
block | The hold authorizes another merchant than the request's (§8.7.19). | COUNTERPARTY_NOT_ALLOWED |
AUTHORIZATION_CLAIM_UNREACHABLE |
block | The executor could not reach the issuer to claim the hold, so it did not execute (fail closed). | — |
COUNTERPARTY_AUTH_REQUIRED |
block | A claim or settlement of a hold was not signed by the agent or by a counterparty the owner registered: an authorizationId alone settles nothing. | IDENTITY_UNVERIFIED |
COUNTERPARTY_MISMATCH |
block | Only the counterparty that claimed the hold may settle it lower, void it or mark it unknown. | COUNTERPARTY_NOT_ALLOWED |
PAYLOAD_DIGEST_INVALID |
block | The payload digest presented is not a well-formed sha256 digest. | ENVELOPE_INVALID |
PAYLOAD_DIGEST_MISMATCH |
block | What the executor is about to run is not the payload the agent signed (payload binding, §8.3.9). | ENVELOPE_INTEGRITY_FAILURE |
PAYLOAD_NOT_BOUND |
block | The executor requires the agent to bind its payload (§8.3.9) and the request bound none. | ENVELOPE_INVALID |
PAYLOAD_NOT_CANONICALIZABLE |
block | The payload cannot be canonicalised (a value JSON cannot carry, too deep or too large), so it cannot be bound or compared. | ENVELOPE_INVALID |
ARGUMENT_NOT_ALLOWED |
block | The tool call carries an argument outside the paths its mandate's argument policy allows (Permission.arguments.allow); the refusal names the JSON Pointer path. Raised by the guard that executes the call, before any hold is claimed. Remove the field, or have the owner allow it. |
MANDATE_SCOPE_VIOLATION |
ARGUMENTS_UNCHECKABLE |
block | The mandate sets an argument policy for this tool and the guard cannot check the call against it: the arguments are not a single JSON object (or are too deep or too large), the guard was not given them, or the policy itself is malformed. Refused rather than run unchecked. | ENVELOPE_INVALID |
SENSITIVE_ARGUMENT_UNPROTECTED |
block | The mandate's argument policy says a value must leave keyed-hashed (sensitive: hmac) and the guard has no key to hash it with (argumentKey, AGENTSAFE_ARGUMENT_KEY), so the call is refused rather than sent in the clear. Configure the key on the Service. |
— |
PAYLOAD_ALREADY_BOUND |
block | A late payload binding (§8.3.11) was refused: the hold is already bound to another payload. | ENVELOPE_INTEGRITY_FAILURE |
PAYLOAD_BINDING_UNSUPPORTED |
block | Raised by the SDK: its key provider cannot sign a payload binding, so the request is not sent unbound. | — |
PAYLOAD_BINDING_NOT_CONFIRMED |
block | Raised by the SDK: the issuer answered a late binding without confirming the digest (it predates §8.3.11), so the hold is treated as unbound. | — |
HOLD_UNDER_RECONCILIATION |
block | The hold is being reconciled (its outcome is unknown) and cannot be claimed or settled until that resolves. | — |
EFFECT_TRANSITION_CONTENDED |
block | An overlapping attempt on the same hold is in progress; retry the same request (not a refusal of it). | — |
AMOUNT_CHARGED_REQUIRED |
block | A settlement must state the amount actually charged. | — |
REFUND_AMOUNT_INVALID |
block | A refund must be a positive amount no greater than what was captured. | — |
SETTLEMENT_REUSED |
block | A settlement proof (x402 payment) was already used for another authorization. | DECISION_TOKEN_INVALID |
ESCALATION_MODIFIED_APPROVED |
allow | A reviewer MODIFIED the escalated action and the modified action passed the gate: a hold was minted on the reviewer's values. | — |
Fail-closed executor and guard errors
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
GUARD_ERROR |
block | The executor guard failed while verifying the request (not an issuer outage); nothing executed. | — |
LOCAL_EVAL_ERROR |
block | The agent's guard failed while evaluating its cached rules locally; nothing ran. | — |
ISSUER_UNREACHABLE |
block | The executor could not reach the issuer it needs (bundle, claim or status), so it did not execute. | — |
ISSUER_API_REQUIRED |
block | The executor is configured to claim or settle but was given no issuer API to do it with. | — |
NO_ISSUER |
block | No issuer is configured for an operation that needs one. | — |
BUNDLE_SUBJECT_MISMATCH |
block | The policy bundle fetched is for another agent than the request's. | ENVELOPE_INTEGRITY_FAILURE |
MAGP_ENVELOPE_MISSING |
block | An A2A task arrived without the MAGP extension envelope (the signed request). | ENVELOPE_INVALID |
CAPABILITY_REQUIRED |
block | The tool requires a capability credential the request did not present. | DECISION_TOKEN_INVALID |
CAPABILITY_CHECK_ERROR |
block | The capability credential could not be checked, so the call was refused. | — |
SERVICE_IDENTITY_REQUIRED |
block | The executor has no service identity (serviceDid / key) for an operation that must be signed. | — |
SERVICE_SIGNING_FAILED |
block | The executor could not sign with its service key. | — |
NO_SERVED_AGENT |
block | The executor was asked to act for an agent it does not serve. | IDENTITY_UNVERIFIED |
NO_FACILITATOR |
block | An x402 payment needs a facilitator and none is configured. | — |
REPORT_OUTCOME_INVALID |
block | An executor's outcome report was malformed and was not sent. | — |
REPORT_UNREACHABLE |
block | An executor's outcome report could not be delivered (it is kept and re-sent). | — |
Rules MetaMynd writes for you: the Controlled Testing plans and the SOP compiler's drafts
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
SANDBOX_PER_TXN_CAP |
block | Controlled Testing baseline plan: an amount over its per-transaction cap. | EXPOSURE_THRESHOLD_EXCEEDED |
SANDBOX_CUMULATIVE_CAP |
block | Controlled Testing baseline plan: committed spend over its cumulative cap. | EXPOSURE_THRESHOLD_EXCEEDED |
SANDBOX_RISK_REVIEW |
escalate | Controlled Testing baseline plan: a high-risk action sent for review. | HUMAN_REVIEW_REQUIRED |
SANDBOX_JURISDICTION |
block | Controlled Testing baseline plan: a jurisdiction outside the plan's list. | MANDATE_SCOPE_VIOLATION |
SANDBOX_TOOL_NOT_ALLOWED |
block | Controlled Testing baseline plan: a tool outside the plan's allow-list. | ACTION_NOT_PERMITTED |
SANDBOX_PII_DISCLOSURE |
block | Controlled Testing baseline plan: a request that discloses personal data. | POLICY_VIOLATION |
SANDBOX_EVIDENCE_CONFIDENCE |
escalate | Controlled Testing baseline plan: evidence confidence below the plan's minimum. | EVIDENCE_QUALITY_LOW |
SANDBOX_RATE_EXCURSION |
observe | Controlled Testing baseline plan: call rate above the plan's watch mark (flagged, not refused). | — |
PROBE_CAP |
block | Governance Respect Probes: the over-cap probe was refused, as the probe expects (the agent must then stop). | EXPOSURE_THRESHOLD_EXCEEDED |
PROBE_ESCALATE |
escalate | Governance Respect Probes: the high-risk probe was escalated, as the probe expects (the agent must then wait). | HUMAN_REVIEW_REQUIRED |
AMOUNT_ABOVE_LIMIT |
block | A rule the SOP compiler drafted from "no payment may exceed X": the amount is over X. | EXPOSURE_THRESHOLD_EXCEEDED |
AMOUNT_NEEDS_APPROVAL |
escalate | A rule the SOP compiler drafted from "payments over X need approval": the amount is over X. | HUMAN_REVIEW_REQUIRED |
BUDGET_EXCEEDED |
block | A rule the SOP compiler drafted from a total budget: committed spend plus this amount is over it. | EXPOSURE_THRESHOLD_EXCEEDED |
BUDGET_NEEDS_APPROVAL |
escalate | A rule the SOP compiler drafted from a total budget that needs approval beyond it. | HUMAN_REVIEW_REQUIRED |
NO_FINANCIAL_AUTHORITY |
block | A rule drafted from "the agent has no financial authority": any request with a positive amount is refused (one with no amount is not). | ACTION_NOT_PERMITTED |
CONSENT_MISSING |
block | A rule the SOP compiler drafted from a consent requirement: the request says consent was not given. | POLICY_VIOLATION |
PII_PRESENT |
block | A rule the SOP compiler drafted from "personal data must not be shared": the request carries personal data. | POLICY_VIOLATION |
PII_NEEDS_REVIEW |
escalate | A rule the SOP compiler drafted that sends a request carrying personal data to a human. | HUMAN_REVIEW_REQUIRED |
PROHIBITED_TERM |
block | A rule the SOP compiler drafted from a list of banned words: the request text contains one. | POLICY_VIOLATION |
PROHIBITED_CONTENT |
block | A rule the SOP compiler drafted from a prohibited-content requirement: the request text matched. | POLICY_VIOLATION |
PROHIBITED_CONTENT_NEEDS_REVIEW |
escalate | A rule the SOP compiler drafted that sends prohibited or abusive content to a human. | HUMAN_REVIEW_REQUIRED |
RISK_TOO_HIGH |
block | A rule the SOP compiler drafted that refuses actions at or above a risk level. | POLICY_VIOLATION |
RISK_NEEDS_REVIEW |
escalate | A rule the SOP compiler drafted that sends actions at or above a risk level to a human. | HUMAN_REVIEW_REQUIRED |
OPERATION_NEEDS_APPROVAL |
escalate | An owner-keyed operation rule: the request context names an operation (e.g. op=delete-all) the owner sends to a human, whatever risk level the agent declared. | HUMAN_REVIEW_REQUIRED |
OPERATION_NOT_ALLOWED |
block | An owner-keyed operation rule: the request context names an operation the owner does not allow, or omits a field the rule requires. | ACTION_NOT_PERMITTED |
SCENARIO_PER_TXN_CEILING |
block | A Scenario Bank set's per-transaction ceiling: the amount is over it. Remedy: lower the amount, or change the owner's cap the set checks. | EXPOSURE_THRESHOLD_EXCEEDED |
SCENARIO_CUMULATIVE_CEILING |
block | A Scenario Bank set's cumulative ceiling: committed spend plus this amount is over it. | EXPOSURE_THRESHOLD_EXCEEDED |
SCENARIO_RATE_CEILING |
block | A Scenario Bank set's rate ceiling: the agent's calls in the window are over it. | RATE_LIMIT_EXCEEDED |
SCENARIO_CONSENT_MISSING |
block | A Scenario Bank set: the request says consent was not given for the data it uses. | POLICY_VIOLATION |
SCENARIO_SOURCE_NOT_APPROVED |
block | A Scenario Bank set: the request's data source is not on the approved list. | POLICY_VIOLATION |
SCENARIO_DATA_RESIDENCY |
block | A Scenario Bank set: the request's data region is outside the allowed regions. | REGULATORY_PROHIBITION |
SCENARIO_TOOL_NOT_ALLOWED |
block | A Scenario Bank set: the tool is not on the set's allow-list. | ACTION_NOT_PERMITTED |
SCENARIO_MODEL_NOT_ALLOWED |
block | A Scenario Bank set: the model is not on the set's allow-list. | ACTION_NOT_PERMITTED |
SCENARIO_HIGH_RISK_REVIEW |
escalate | A Scenario Bank set: a high-risk action is sent to a human. | HUMAN_REVIEW_REQUIRED |
MCP tool-call policies (the built-in policy catalog)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
APPROVED |
allow | An MCP tool-call policy governs this action and none of its rules refused it: the call may run. | — |
NO_POLICY |
allow | No active MCP tool-call policy governs this action (the verdict's standardRef is UNGOVERNED): the call is allowed ungoverned. Remedy: attach a policy if the tool should be governed. | — |
DATA_SOURCE_NOT_APPROVED |
block | A catalog policy: the tool call's data source is not on the policy's approved list. | POLICY_VIOLATION |
RISK_THRESHOLD |
escalate | A catalog policy: a high-risk tool call is sent to a human. | HUMAN_REVIEW_REQUIRED |
PROHIBITED_CLAIM |
block | A catalog policy: the text makes a prohibited financial claim (e.g. "guaranteed returns", "risk-free"). | REGULATORY_PROHIBITION |
SENSITIVE_ADVICE |
escalate | A catalog policy: the text reads as medical or legal advice, so a human reviews it. | HUMAN_REVIEW_REQUIRED |
HTTP gateway refusals (agentsafe-http-gateway)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
MISSING_GOVERNANCE |
block | HTTP 401: a governed route was called without a signed MAGP request (by default the x-magp-request header). Remedy: sign the call with the agent's guard (buildSignedRequest) and send it in that header, or in whatever the gateway's extractGovernance reads. |
ENVELOPE_INVALID |
GATEWAY_ACTION_MISMATCH |
block | HTTP 403: the signed request names a different action than the one this gateway route serves. Nothing is claimed and no nonce is consumed. Sign the call with the route's action, or call the route that serves the action you signed. | ACTION_NOT_PERMITTED |
PAYLOAD_UNBINDABLE |
block | HTTP 403: the JSON body carries none of the governed fields (amount, currency, merchant) at its top level — nested, renamed, differently cased or an array — so the gateway cannot show the signature covers it. Remedy: put the governed fields at the body's top level, or give the route its own bind. |
ENVELOPE_INVALID |
BIND_ERROR |
block | HTTP 502: the route's payload binder threw reading the body, so the gateway cannot show the signature covers it (fail closed). Remedy: fix the route's bind function or the body it is given. |
— |
GOVERNANCE_ERROR |
block | HTTP 502: verifying the request threw inside the gateway — the guard failed, or the route's trustedContext deriver threw or returned nothing usable (fail closed). The error says which. Remedy: fix the deriver or the guard's configuration; it is not a policy verdict. |
— |
CREDENTIAL_UNAVAILABLE |
block | HTTP 502: the gateway's credential vault released no credential for this route, so the call was not forwarded and the hold it claimed was released. Remedy: configure the credential, or mark a route that needs none credential: false. |
— |
UPSTREAM_UNREACHABLE |
block | HTTP 502: the permitted call could not be forwarded because no connection to the upstream was made, so nothing executed. The claimed hold was released and the body says executed: false. A retry is safe once the upstream is reachable. |
— |
ACCESS_PRINCIPAL_UNAVAILABLE |
block | HTTP 502: the route checks what its upstream returns against the tenant and user the request is served for (route.access), and the operator's resolver named neither, so the call was not forwarded and the claimed hold was released (executed: false). Remedy: fix the resolver, or the fixed principal the route names. |
— |
ACCESS_CONTEXT_MISMATCH |
block | HTTP 502: the upstream ran, but a tenant or user field in its answer (path) names someone other than the principal the request is served for, so the whole answer was withheld (executed: true). The value is never returned. Investigate the upstream query or filter that returned another principal's record. |
MANDATE_SCOPE_VIOLATION |
ACCESS_CONTEXT_UNCHECKABLE |
block | HTTP 502: the upstream ran, but its answer could not be checked against the request's principal: not strict JSON, too many fields, or a tenant or user field the route names is missing or not a string or number (path, detail). It was withheld rather than released unchecked (executed: true). Remedy: correct route.access to the upstream's response shape. |
— |
OUTPUT_FORBIDDEN_WORD |
block | HTTP 403: what the call would send (its body after any argument rewrite, its query string, its path, or a header the route names) contains one of the operator's forbidden words (route.output, or the MCP guard's output), so nothing was sent and the claimed hold was released (executed: false). path says where (? for the query string, @path for the path, @headers/<name> for a header); the word is never returned. |
POLICY_VIOLATION |
OUTPUT_UNCHECKABLE |
block | HTTP 403: what the call would send could not be checked for forbidden words (a body that is not UTF-8, JSON that is not strict, a form or query string that does not decode, or a value that is not plain data), so it was refused rather than sent unchecked (executed: false). detail says why. |
— |
GATEWAY_ERROR |
block | HTTP 502: the gateway's HTTP server caught an unexpected error handling the call (fail closed). Check the gateway's log; the request may be retried. | — |
BLOCKED |
block | HTTP 403: the guard refused the call without naming a code (an older or custom guard). Treated as a block (§13.3). | — |
EXECUTED |
allow | An outcome report's code: the gateway forwarded the permitted call and the upstream answered (the report's httpStatus says how). | — |
UPSTREAM_ERROR |
allow | An outcome report's code, and the reason a claimed hold is marked unknown: the permitted call was forwarded and the upstream failed without an HTTP answer, so whether it took effect is unknown until reconciled. | — |
RESPONSE_LOST |
allow | The reason recorded when a claimed hold's effect is marked unknown (effect/unknown), as RESPONSE_LOST or RESPONSE_LOST:<executor's reason>: the call was forwarded and its outcome could not be established. The hold stays committed until it is reconciled (§8.7.12). |
— |
SETTLEMENT_THREW |
block | The gateway's settlement call to the issuer (capture, void or mark-unknown) threw. It is retried as transient; after the last attempt it is logged and the claimed hold stays committed to the cap until it is reconciled. | — |
Guard and SDK fail-closed refusals
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
CONTEXT_SIGNATURE_REQUIRED |
block | A service-side guard run with requireContextSignature received a request whose context carries no envelope signature (or had it stripped). Remedy: have the agent sign its context (the SDKs do by default; do not pass signContext: false). |
ENVELOPE_INVALID |
CONTEXT_SIGNING_UNSUPPORTED |
block | Raised by the SDK: its key provider (or the agentsafe-signer daemon) cannot sign the request's context, so the request is not sent unsigned. Remedy: upgrade the signer, or pass signContext: false. | — |
JURISDICTION_SIGNING_UNSUPPORTED |
block | Raised by the SDK: the agentsafe-signer daemon predates signed jurisdictions (signer 0.18.0), so the request is not sent. Remedy: upgrade the signer, or omit jurisdiction. |
— |
SIGNER_UNREACHABLE |
block | Raised by the SDK: the agentsafe-signer daemon holding the agent's key could not be reached or failed, so nothing was signed or sent (not a gate outage). Remedy: start the daemon or fix its socket path. | — |
CAPABILITY_INVALID |
block | The MCP guard's capability verifier rejected the capability credential the call presented, without naming a more specific code. | DECISION_TOKEN_INVALID |
NOT_APPLIED |
block | A service-side guard's settlement call (capture, void, refund or mark-unknown) was refused by the issuer without a reasonCode; nothing changed on the hold. The HTTP status and the issuer's message ride along. | — |
REFUSED |
block | Raised by the Python client: a settlement or late payload-binding call was refused and the issuer's answer carried no reasonCode or message. | — |
UNKNOWN |
block | Raised by the Python client: the gate's answer carried no reasonCode (an older or non-MAGP endpoint). Read per §13.3: a non-allow decision without a known code is a block. | — |
Claim payload and x402 settlement binding
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
PAYLOAD_DIGEST_REQUIRED |
block | A claim of a hold whose authorization bound a payload digest presented none: an executor cannot skip the binding by omission (§8.3.9). Nothing is claimed. Remedy: the executor states the digest of what it is about to run (guard payloadDigest). | ENVELOPE_INVALID |
PAYLOAD_NOT_BOUND_AT_AUTHORIZE |
block | A claim presented a payload digest for a hold whose authorization bound none: the executor believes the payload is bound and it is not (§8.3.9). Nothing is claimed. Remedy: the agent binds its payload when it asks (or binds it late, §8.3.11). | ENVELOPE_INVALID |
MISSING_BINDING |
block | x402: the payment requirements carry no MAGP authorization (extra.magpAuthorizationId), so the payment cannot be tied to a hold (§7a.2). |
DECISION_TOKEN_INVALID |
AUTHORIZATION_MISMATCH |
block | x402: the payment is bound to another authorization than the one being settled (§7a.2). | DECISION_TOKEN_INVALID |
AMOUNT_INVALID |
block | An amount that is not a usable number: an x402 paid amount that is not a non-negative integer of minor units (§7a.2), a capture whose amountCharged is negative or not a number, or a refund that is not positive. | — |
BINDING_OK |
allow | x402: the payment is bound to this authorization and pays no more than it authorized (§7a.2). | — |
Capture, release and refund of a hold
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
AMOUNT_EXCEEDS_AUTHORIZED |
block | A capture whose amountCharged is over the authorized hold. Nothing is captured; an overspend goes through reconciliation, not capture. | EXPOSURE_THRESHOLD_EXCEEDED |
AGENT_SETTLE_SIGNATURE_INVALID |
block | The agent's signature on a settlement of its own unclaimed hold does not verify over MAGP-SETTLE-v1 for this call. | ENVELOPE_INTEGRITY_FAILURE |
PAYEE_NOT_REGISTERED |
block | A settlement below the hold named an account paid that is not one the owner registered for this merchant (payee directory), so the lower amount is not accepted. | COUNTERPARTY_NOT_ALLOWED |
PAYEE_NOT_VERIFIED |
block | The payee directory could not be read to confirm the account paid. Remedy: settle at the full authorized amount, or retry. | — |
SETTLEMENT_NOT_CONFIRMED |
block | The remote system could not confirm a lower settled amount, or (on a release after a claim) that nothing was settled. Remedy: settle at the authorized amount, mark the effect unknown for reconciliation, or retry once it can confirm. | — |
SETTLEMENT_AMOUNT_MISMATCH |
block | The remote system reports a different settled amount than the settlement states. | — |
SETTLEMENT_NOT_FOUND |
block | The remote system reports no settlement for this authorization, so a lower settled amount cannot be confirmed. | — |
EFFECT_NOT_CAPTURABLE |
block | The hold's effect is in a state that cannot be captured (unknown, reconciling, ...): reconcile it first. | — |
EFFECT_NOT_VOIDABLE |
block | The hold's effect is in a state that cannot be released (unknown, reconciling, ...): reconcile it first. | — |
INVALID_EFFECT_TRANSITION |
block | The requested move of the hold's effect (e.g. to reconciling or compensating) is not allowed from its current state; read its state and act on that. | — |
HOLD_STATE_CHANGED |
block | The hold was claimed, settled or voided by another caller while this capture or refund ran; nothing was applied. Read its current state and retry. | — |
NOT_CAPTURED |
block | Only a captured (settled) authorization can be refunded; this one is not. | — |
ALREADY_REFUNDED |
block | Nothing remains to refund: the capture was already refunded in full. | — |
AMOUNT_EXCEEDS_REFUNDABLE |
block | The refund is more than what remains to refund of the capture. | — |
CLAIM_EXPECTATION_INVALID |
block | HTTP 400: a claim's expectation (the action, amount, currency or merchant the executor expects the hold to authorize) is malformed. | ENVELOPE_INVALID |
IDEMPOTENCY_KEY_INVALID |
block | HTTP 400: the Idempotency-Key on a hold operation is malformed. | — |
MANDATE_BLOCK |
block | An approved escalation, re-judged against the mandate and rules as they stand at reservation, no longer passes and no more specific code was produced. Nothing is reserved; ask again. | POLICY_VIOLATION |
MODIFIED_APPROVED |
allow | A reviewer-modified action, re-judged against the rules as they stand, passed every check (the hold is then minted as ESCALATION_MODIFIED_APPROVED). | — |
COMPENSATION |
allow | The separately authorized $0 effect opened to compensate (reverse) an earlier one; its booking reference names the effect it compensates. | — |
Counterparty authentication (a claim, settlement or outcome report signed by an executor)
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
COUNTERPARTY_AUTH_MALFORMED |
block | The executor's counterparty signature headers are incomplete or malformed (nonce, issuedAt or signature missing or out of range). | ENVELOPE_INVALID |
COUNTERPARTY_DID_UNSUPPORTED |
block | The executor's service DID is not one whose key the issuer can resolve (use did:key). | IDENTITY_UNVERIFIED |
COUNTERPARTY_REQUEST_EXPIRED |
block | The executor's signed request is outside the freshness window (too old, or issued too far in the future). Check the executor's clock. | ENVELOPE_INVALID |
COUNTERPARTY_SIGNATURE_INVALID |
block | The executor's counterparty signature does not verify against its service DID. | ENVELOPE_INTEGRITY_FAILURE |
COUNTERPARTY_REPLAY_DETECTED |
block | The executor's signed request reuses a nonce already seen: a replay is refused. | ENVELOPE_INTEGRITY_FAILURE |
COUNTERPARTY_NOT_REGISTERED |
block | The executor's service DID is not an active counterparty the agent's owner registered (or not a trusted entry where a proof of control is required). | COUNTERPARTY_NOT_ALLOWED |
COUNTERPARTY_NOT_ALLOWED_FOR_MERCHANT |
block | The registered counterparty is scoped to other merchants than this hold's. | COUNTERPARTY_NOT_ALLOWED |
COUNTERPARTY_PROOF_REQUIRED |
block | Registering the counterparty needs a proof of control of its DID (a signed challenge) and none was given. | IDENTITY_UNVERIFIED |
COUNTERPARTY_PROOF_INVALID |
block | The proof of control of the counterparty DID is malformed or does not verify. | IDENTITY_UNVERIFIED |
COUNTERPARTY_PROOF_EXPIRED |
block | The challenge the proof of control signs has expired; fetch a new one. | IDENTITY_UNVERIFIED |
REPORT_CALLER_UNVERIFIED |
block | HTTP 400: an 'executed' outcome report must carry a caller request that verifies; this one does not. | ENVELOPE_INTEGRITY_FAILURE |
REPORT_OCCURRENCES_INVALID |
block | HTTP 400: an 'executed' outcome report counts exactly one occurrence (only refusals are aggregated). | ENVELOPE_INVALID |
REPORT_RATE_LIMITED |
block | HTTP 429: the executor sent outcome reports faster than the issuer accepts them; retry later (the guard keeps and re-sends them). | RATE_LIMIT_EXCEEDED |
Effect reconciliation and compensation
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
NOT_RECONCILABLE |
block | HTTP 409: the effect is neither unknown nor reconciling, so there is nothing to reconcile; its current effectState is returned. | — |
NOT_RECONCILING |
block | An operator resolution needs the effect to be reconciling; it is not. | — |
NOT_COMPENSABLE |
block | Compensation can begin only for an effect in compensation_required; this one is not. | — |
NOT_COMPENSATING |
block | Compensation can be completed only for an effect that is compensating; this one is not. | — |
RECONCILE_COOLDOWN |
block | HTTP 429: this effect was probed moments ago; retry after the time in detail. |
RATE_LIMIT_EXCEEDED |
RECONCILE_NOT_PERMITTED |
block | HTTP 403: the caller is not one that may resolve this hold (its claimer, or a signed-in user who may manage it). | — |
NEEDS_HUMAN |
escalate | Reconciliation could not decide what happened (no reconciler for this effect, or the remote was inconclusive): the effect stays reconciling for an operator to resolve. | HUMAN_REVIEW_REQUIRED |
RECONCILED_SUCCEEDED |
allow | Reconciliation found the effect happened: the hold is captured at the amount the remote system (or the operator) says was settled. | — |
RECONCILED_FAILED |
block | Reconciliation found the effect did not happen: the hold is released and the effect is failed_final. | — |
COMPENSATION_REQUIRED |
escalate | The effect happened but must be reversed (it violates a postcondition or policy): it waits for a compensating action. | HUMAN_REVIEW_REQUIRED |
SETTLED_OVER_AUTHORIZED |
escalate | Reconciliation found more settled than was authorized: the cap counts the real amount and the effect waits for compensation. | EXPOSURE_THRESHOLD_EXCEEDED |
COMPENSATION_STARTED |
escalate | A separately authorized compensating effect was opened for this one, which is now compensating. | — |
COMPENSATED |
allow | The compensating action succeeded: the original effect is compensated. | — |
COMPENSATION_FAILED |
block | The compensating action failed: the original effect is compensation_failed and needs a person. | — |
EFFECT_RECONCILE_NO_HANDLER |
escalate | Evidence row: no reconciler can probe this effect, so it waits for an operator (the answer is NEEDS_HUMAN). | HUMAN_REVIEW_REQUIRED |
EFFECT_RECONCILE_UNRESOLVED |
escalate | Evidence row: the reconciler could not decide what happened, so it waits for an operator (the answer is NEEDS_HUMAN). | HUMAN_REVIEW_REQUIRED |
EFFECT_RECONCILED_SUCCEEDED |
allow | Evidence row of RECONCILED_SUCCEEDED. | — |
EFFECT_RECONCILED_FAILED |
block | Evidence row of RECONCILED_FAILED. | — |
EFFECT_COMPENSATION_REQUIRED |
escalate | Evidence row of COMPENSATION_REQUIRED. | HUMAN_REVIEW_REQUIRED |
EFFECT_SETTLED_OVER_AUTHORIZED |
escalate | Evidence row of SETTLED_OVER_AUTHORIZED. | EXPOSURE_THRESHOLD_EXCEEDED |
Owner actions on the evidence trail
| Code | Decision | Meaning | SAFR §30 alias |
|---|---|---|---|
MODE_AUTONOMOUS |
allow | The owner pinned the agent's operating mode to autonomous. | — |
MODE_SUPERVISED |
allow | The owner pinned the agent's operating mode to supervised. | — |
MODE_RESTRICTED |
allow | The owner pinned the agent's operating mode to restricted. | — |
MODE_PIN_CLEARED |
allow | The owner cleared the agent's operating-mode pin; the derived mode applies again. | — |
REASSESSMENT_REQUIRED |
escalate | The owner recorded a change to the agent that needs its assurance reassessed before it is trusted as before. | HUMAN_REVIEW_REQUIRED |
REASSESSED |
allow | The owner cleared a pending reassessment of the agent. | — |
REGRESSION_CHECK_PASSED |
allow | The agent version's current policy passed every Scenario Bank set that applies to it (the sets checked are on the record). | — |
TRANSCRIPT |
allow | An agent-chat session transcript was anchored as evidence (hashed on-chain, encrypted off-chain). | — |
Appendix B — Canonical examples
Canonical message
did:hedera:testnet:zEEZ…|flight-purchase|150|USD|skyward-air|9f2c…|2026-07-10T05:12:00Z
A narrowing delegation — parent caps at 500 across two merchants; the child caps at 50 against one of them, and repeats the parent's cumulative constraint because omitting it would widen:
{ "target": "flight-purchase",
"permission": [{ "target": "flight-purchase", "constraint": [
{ "leftOperand": "mm:payAmount", "operator": "lteq", "rightOperand": 50 },
{ "leftOperand": "mm:cumulativeSpend", "operator": "lteq", "rightOperand": 500 },
{ "leftOperand": "mm:merchant", "operator": "isAnyOf", "rightOperand": ["skyward-air"] }
]}]}
A refused widening — the same child with mm:payAmount raised to 5000 is rejected at
issuance with CONSTRAINT_WIDENED; omitting mm:payAmount entirely is rejected with
CONSTRAINT_DROPPED.
Appendix C — Atom catalog
Atoms are pure predicates over request context. Generated from the reference
implementation; GET /standards/atoms returns the live catalog and is authoritative.
New governance requirements are added as new atoms, which does not require a protocol
version bump (§18.1).
| Predicate | Required context | Fires when |
|---|---|---|
amount-over |
amount |
Fires when a single action amount exceeds a configured limit (per-transaction cap). |
amount-unknown |
amount |
Fires when the action carries no usable amount, or a NEGATIVE one — the gate cannot trust either for capping. A deny-by-default control for value-moving actions: author it with BLOCK ahead of a spend cap, otherwise an amount that is missing, unparseable, or negative passes the cap untested (amount-over only ever fires above the limit, so a negative amount clears every positive cap). A genuine $0 amount does NOT fire this — only attach it to actions that must always carry a real, non-negative amount. |
cumulative-over |
amount, cumulativeSpend |
Fires when cumulative spend (already-spent + this transaction) exceeds a configured total budget. |
risk-at-or-above |
riskLevel |
Fires when the assessed risk level is at or above the configured threshold. |
data-source-not-approved |
dataSourceId |
Fires when the action uses a data source not on the approved list. |
consent-missing |
consent |
Fires when explicit consent is absent for the action. |
text-matches |
prompt, output |
Fires when the prompt or output contains any of the configured terms. |
jurisdiction-not-allowed |
jurisdiction |
Fires when the action's jurisdiction is not on the allow-list. |
data-residency-violation |
dataResidency |
Fires when data would be processed in a region not on the allow-list. |
model-not-allowed |
model |
Fires when the agent uses an LLM model not on the approved list. |
tool-not-allowed |
tool |
Fires when the agent invokes a tool/function not on the approved list. |
pii-present |
piiPresent |
Fires when the action is flagged as involving personal data (PII). |
rate-limit-exceeded |
callCount |
Fires when the rolling call count exceeds a configured maximum. |
hol-trust-below-review |
holTrustScore |
Routes to human review when the counterparty's MetaMynd Trust Index (HCS-28) score is below a soft review line. Guidance, not a hard block — author it with an ESCALATE decision. The score is resolved server-side; no counterparty score → the atom does not fire. |
evidence-requirement |
evidenceTypes |
Fires when the action is not backed by every REQUIRED evidence type the agent attests to in evidenceTypes (missing evidence — including none supplied). A REQUIRE control (SAFR §24): author it with ESCALATE or BLOCK so an under-evidenced action is stopped or reviewed. |
evidence-confidence-below |
evidenceConfidence |
Fires when the attested evidence confidence is below a required minimum — or absent (SAFR §24). A min of 0 / unset is no requirement. Author with ESCALATE to route low-confidence actions to review. |
context-value-in |
— | Fires when a field of the request context (e.g. op, or params.op) has one of the listed values — key a rule on WHAT the agent is doing, not on the risk level it declares about itself. Author with ESCALATE ("deleting records needs approval") or BLOCK. Case, width, whitespace, hyphens/underscores, invisible characters and common Cyrillic/Greek look-alike letters are ignored when comparing. A deny-list cannot foresee every spelling: when the safe values are known, prefer context-value-not-in. When it fires, the verdict and the escalation carry a context-value risk signal naming the field and value. |
context-value-not-in |
— | Fires when a field of the request context has a value that is NOT on the owner's allow-list ("anything other than read or list needs approval"). The sound form of an operation rule: a spelling or look-alike the owner did not foresee fires instead of passing. Same comparison rules, missing and actions options as context-value-in, and the same context-value risk signal when it fires. |
Appendix D — Future directions
Not implemented at 1.0. Recorded so implementers know the intended direction, and kept out of the normative text so nothing in the body is aspirational.
- Content-addressed rule packs. Each published rule-pack version addressed by content hash, so a bundle can name an exact immutable pack.
- HCS-10 channels. An on-chain, auditable channel binding for peers that require message-level durability rather than TLS request/response.
- WebSocket transport. A duplex binding for streaming interactions.
- HCS-2 registry discovery. Trustless discovery of DIDs and service endpoints via a mirror node, with the issuer offline.
- Signed bundle staleness enforcement at the edge, beyond the risk-tiered rules of §6.2.
Changes from 0.4
- The order of checks (§8.5) has 16 steps, adding containment, operating modes, rate limits, circuit breakers, OBSERVE folding and the reassessment floor.
- Reason codes are generated from the registry (Appendix A).
- 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 (§15).
- Platform governance surfaces moved to MetaMynd Platform Governance.
1.0 editorial revision (2026-10-20). Implementation detail that an interoperating party does not need was removed. No requirement on the wire, the order of checks or the reason codes changed.
