Wire contract
Every message that travels between the parties, in order, with the fields that carry meaning.
enforcement pointagent
Refused, with the address of the authority that could grant it and a handle on the attempt. Over a transport with no status line, the same fields ride a JSON-RPC error.
The remediation object decodes to RFC 9396 authorization_details plus the two additions that make a third-party decision possible: authorization_server and ticket.
The messages of the four-beat grant, in the order they travel. Conceptual background is in The four beats; this page is the field-by-field contract.
The repository's own copy is docs/PROTOCOL.md.
Beat 1 — Challenge
The enforcement point registers the attempt with the owner's authority, then refuses. Over HTTP:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: UMA realm="alice-vault",
error="insufficient_authorization",
as_uri="https://alice-as.uma.lab",
ticket="<ticket>",
resource_metadata="https://gateway.uma.lab/.well-known/oauth-protected-resource/mcp",
scope="trades:execute",
authorization_remediation="<base64url JSON>"| Parameter | Source | Meaning |
|---|---|---|
realm |
UMA 2.0 | Protection realm |
as_uri |
UMA 2.0 | The owner's authorization server |
ticket |
UMA 2.0 | The negotiation handle |
scope |
RFC 6750 §3 | Scopes the call needed |
resource_metadata |
RFC 9728 §5.1 | The resource's metadata URL, which must equal the one RFC 9728 §3 forms from the resource called; the client reads that document to corroborate as_uri |
error, authorization_remediation |
draft-ietf-oauth-rar-metadata-remediation |
Structured remediation |
authorization_remediation decodes to:
{
"authorization_details": [{
"type": "https://u4a.ai/spec/core/1.0#tool-call",
"locations": ["https://gateway.uma.lab"],
"identifier": "alice-vault/execute_trade",
"actions": ["execute_trade"],
"datatypes": ["trades:execute"]
}],
"authorization_reference": "s256:6cR6qTmCj6s0S95MxCfdfwfXJ8myLtBg-PiL8v93H0g",
"authorization_server": "https://alice-as.uma.lab",
"ticket": "<ticket>"
}authorization_details and authorization_reference are that draft unchanged.
authorization_server and ticket are the two additions, and they are what let
a party who is not the caller decide.
Non-HTTP encoding. Where there is no status line, the same JSON rides a
JSON-RPC error (code -32001). The challenge is specified as parameters; each
binding says how they travel. See MCP binding.
Beat 2 — Attempt
POST /token
grant_type = urn:ietf:params:oauth:grant-type:uma-ticket
ticket = <ticket>Answered with 403 need_info, a rotated ticket, and the owner's terms:
{
"error": "need_info",
"ticket": "<rotated>",
"required_claims": [{
"claim_type": "https://u4a.ai/spec/terms/1.0#myterms-agreement",
"claim_token_format": ["https://u4a.ai/spec/terms/1.0#myterms-agreement-v1+jws"],
"friendly_name": "Alice's terms: Holdings summary",
"terms_template": {
"template_id": "alice/advisor-tier1/v2",
"terms_uri": "https://alice-as.uma.lab/terms/alice/advisor-tier1/v2",
"proffered_by": "https://alice-as.uma.lab",
"purpose": "Suitability review for advisory onboarding",
"scope": ["positions:read"],
"expires_in": 172800,
"prohibited": ["retention-after-review", "marketing", "model-training"],
"resource_id": "alice-vault/get_positions",
"family": "<negotiation-family-id>",
"nonce": "<nonce>"
}
}]
}An AAuth mission reference (approver + s256) may be offered as an additional
acceptable claim_token_format.
Beat 3 — Commit
POST /token
grant_type = urn:ietf:params:oauth:grant-type:uma-ticket
ticket = <rotated>
claim_token = <base64url(agreement JWS)>
claim_token_format = https://u4a.ai/spec/terms/1.0#myterms-agreement-v1+jwsThe agreement is the template echoed and signed by the agent's key:
{
"iss": "agent:<keyid>",
"aud": "https://alice-as.uma.lab",
"iat": 1751900000,
"template_id": "alice/advisor-tier1/v2",
"terms_uri": "https://alice-as.uma.lab/terms/alice/advisor-tier1/v2",
"purpose": "Suitability review for advisory onboarding",
"scope": ["positions:read"],
"expires_in": 172800,
"prohibited": ["retention-after-review", "marketing", "model-training"],
"family": "<negotiation-family-id>",
"nonce": "<nonce>",
"reason": "Suitability review before Thursday's client meeting.",
"mission": { "approver": "https://ps.example", "s256": "<content-hash>" }
}Everything above the blank line is the owner's template, repeated. The last two are the only claims the requesting side authors, both optional:
reason— free text, capped atUMA_AS_MAX_REASONbytes. Recorded and shown to the owner; never parsed, scored, or compared to her purpose.mission— a reference to a mandate the requesting side is acting under: who approved it, and the content hash of what was approved. An AAuth mission is cited by its person server and itsmission_s256. Recorded, never dereferenced: the authorization server has nothing to fetch, so it is a claim rather than an attestation.
Neither can widen anything. Her policy may read only their absence
(request.reason_absent, request.mission_absent), and both conditions are
unstorable under then: auto. On per-operation tiers the agreement also
carries operation, which is the one requester-authored claim that is
enforced — see the two intents.
The header may also carry client_id, a URL describing who operates the agent,
and signature_agent, the operator's key directory. Both are optional and
neither widens anything: the authorization server resolves them itself, and what
it learns can only make a request stricter. See
agent assurance.
The JWS protected header carries either jwk (pseudonymous bare key) or an
agent_token (an aa-agent+jwt whose cnf.jwk is the signing key, verified
against the issuer's published keys). The same key signs the agreement and later
proves possession of the grant.
Verification. Signature against the header key; echo matches the proffered
template on nonce, family, template id, terms URI and purpose; prohibited not
weakened; expires_in not extended; an operation present if the tier is
per-operation; reason within the byte cap; mission, if present, an object
with an https approver and a content hash. Then policy, then one of:
| Outcome | Response |
|---|---|
| Known connection, non-ask-me tier | Grant (beat 4) |
| New agent, any tier | 403 request_submitted, kind=connection |
| Ask-me tier | 403 request_submitted, kind=operation |
| Weakened echo, bad signature, policy failure | request_denied |
A requesting side that will not accept the terms may end the negotiation with
decline=true; the refusal is recorded in the owner's ledger.
Beat 4 — Grant
{
"access_token": "<RPT: RFC 9068 at+jwt, cnf-bound>",
"token_type": "PoP",
"expires_in": 3600,
"receipt": "<myterms-receipt+jws>"
}The RPT:
{
"iss": "https://alice-as.uma.lab",
"sub": "<agent id or pseudonymous handle>",
"aud": "https://gateway.uma.lab",
"jti": "rpt_<id>",
"exp": 1751910000,
"cnf": { "jwk": { "…agent signing key…": "" } },
"permissions": [
{ "resource_id": "alice-vault/get_positions",
"resource_scopes": ["positions:read"], "exp": 1752072800 }
],
"contract": "s256:<agreement-hash>"
}Ask-me grants additionally carry:
"single_use": true,
"operation": { "tool": "execute_trade",
"params_s256": "<hash of the exact approved order>" }The receipt is a JWS counter-signed by the owner's authority that embeds the
complete agent-signed agreement alongside the terms URI, agreement hash, agent
key thumbprint and negotiation family. Both sides hold identical, dually-signed
copies.
The authorized call
The agent retries with an RFC 9421 signature over @method @authority @path
authorization, the RPT in an Authorization: PoP … header.
Every signature in the set is Ed25519: ed25519 for an RFC 9421 request,
Ed25519 or EdDSA for a JWS, as the authorization server's metadata says in
signing_alg_values_supported and http_message_signature_alg_values_supported.
The algorithm is taken from the key, never from the header. A signed request
carries created and is refused outside a sixty-second window either side of
the verifier's clock, or past its expires. A signed owner request that changes
anything is accepted once; a second presentation of the same signature is
refused. A client that needs an operation done at most once asks for a
single-use grant.
A client's client_id and signature_agent travel in the agreement's protected
header. The operator is the origin of the client_id; a key directory at any
other origin attests to nothing.
The enforcement point checks in this order, and the order is normative:
POST /introspect— non-consuming. Is the token live, does the connection still stand?- The tool maps to a
resource_idpresent inpermissions, with the scopes it needs, inside the permission's ownexp/nbf. If not, a challenge. The organization's ceiling (organization_envelope_exceeded) and a joint mandate (joint_mandate_unsatisfied) are checked here. - The request signature verifies against the RPT's
cnfkey (invalid_token, 401), and the operation has not been re-declared worse than the grant'sconsequence(consequence_changed). - For single-use grants, an exact
operation.params_s256match. A call with noargumentshas the parameters{}. POST /consume— atomic, and only now.
Consuming earlier lets an unsigned replay destroy an approval the owner
personally gave, and a refusal at any earlier step leaves the grant unspent. A
caller that loses the consume race is told consumed: false and must deny.
Every refusal value except temporarily_unavailable is terminal for the token
presented. A client that receives one it does not recognise treats it as
terminal and does not send the same call again with the same token.
Introspection reasons
An inactive answer carries a reason, so a re-negotiable failure is distinguishable from a settled one:
| Reason | Enforcement point's response |
|---|---|
connection_revoked |
403 access_revoked — terminal, do not re-challenge |
organization_revoked |
403 access_revoked — terminal: her organization ended the agent's reach to resources it claims |
already_consumed |
Fresh challenge |
revoked |
Fresh challenge — the grant ended with a connection she revoked, and the agent has since been admitted again |
expired |
Fresh challenge |
unknown_token |
Fresh challenge. Also the answer for a grant over another owner's resources, whatever its state |
invalid_signature |
Fresh challenge |
| anything else | 403 access_revoked — a reason the enforcement point does not recognise is treated as terminal |
/consume answers {"consumed": true} to the caller that spent the grant.
Otherwise consumed is false and error says why: already_consumed,
not_single_use, or the reason introspection would give now. A caller that
could not reach /consume has learned nothing about the grant, and answers
503, not already_consumed.
Ticket lifecycle
Every presentation consumes the ticket and, if the negotiation continues, issues a fresh one. The family id, assigned when the permission is registered, is stable across rotations and is the correlation id for logging, audit and owner decisions.
Connections
A connection is the standing relationship between an owner and a specific agent, keyed by a handle whose shape follows the agent's identity level:
| Identity level | Handle | Why |
|---|---|---|
| Pseudonymous | RFC 7638 JWK thumbprint (jkt:…) |
The key is the identity, so it must persist for the relationship to persist |
| Identified | Issuer-qualified subject of the verified aa-agent+jwt |
Session keys rotate; a thumbprint-keyed connection would forget the agent every session |
While no active connection exists, first contact pends regardless of tier. Once active, non-ask-me tiers auto-grant for that agent; ask-me tiers still pend per operation. Revoking a connection marks every live grant bound to that handle consumed in the same operation.