MAQPNADocs

Error envelope and reason codes#

When the gateway refuses or cannot complete a call, it says why in a machine-readable way. Every error it generates carries the same envelope fields, wherever the protocol puts them (cmd/maqpna-gateway/envelope.go):

Field Meaning
domain Always maqpna.com for gateway errors. Agent-to-agent (A2A) protocol errors from a remote agent keep a2a-protocol.org
reason <token>[:<qualifier>]. The token is from the published list below; the qualifier adds machine-readable context: budget_exceeded:agent/day, dlp:github_token, missing_scope:tools:github
detail Free text when the raw reason was not a token, for example a rule's own reason text (destructive) or approval_denied by bob@acme.eu
policy, rule The deciding policy and rule, when a policy decided; otherwise the source: policy revocation/<id> for the kill switch, policy dlp with the profile as rule, policy budget with rule <scope>-<window>, rule pin/<server>/<tool> for tool pinning

The reason list is append-only: no token is removed or renamed within v1alpha1. A test (TestReasonEnumLiterals) fails the build when a reason literal in the gateway is not in the list.

Where the envelope lives#

Surface Shape
Model Context Protocol (MCP), POST /mcp/{server} JSON-RPC error.data = {domain, reason, detail?, policy?, rule?, …}, plus call-specific keys such as approvalId, expiresAt, connectURL
A2A JSON-RPC binding Same as MCP
A2A HTTP+JSON binding google.rpc.Status: {"error": {code, status, message, details: [ErrorInfo]}}; ErrorInfo.reason is the upper-case token (BUDGET_EXCEEDED), domain is maqpna.com, the rest in metadata (including jsonrpcCode)
Model calls, /llm/{route}/v1/* OpenAI style {"error": {message, type, code, reason, domain}}. type is the error class (maqpna_policy_denied, …), code keeps its pre-v0.1.0 value for SDK compatibility, reason is the published token
Admin API, /v1/* {"error": "<message>"}. Adopting the envelope there is Planned for v1beta1 of the admin API

How a client should read an error#

flowchart TD
    E[Error from the gateway] --> D{"domain == maqpna.com?"}
    D -- no --> P["Protocol or upstream error:<br/>handle per MCP or A2A spec"]
    D -- yes --> T["token = reason up to the first colon"]
    T --> G{token group}
    G -- "approval_required" --> W["Wait or retry with<br/>X-Maqpna-Approval-Id"]
    G -- "auth: unauthorized, missing_token,<br/>token_expired, ..." --> R["Refresh the session token<br/>(re-read MAQPNA_TOKEN_FILE)"]
    G -- "rate_limited" --> B["Back off: Retry-After"]
    G -- "policy, approvals, routing" --> S["Stop and report:<br/>a retry will get the same answer"]
    G -- "availability:<br/>upstream_unavailable, audit_unavailable" --> X["Retry with backoff"]
  1. Check domain. Only maqpna.com errors use this list; A2A reuses JSON-RPC codes such as -32001 (TaskNotFound) with its own meaning.
  2. Take the token: the text before the first :. Never match on message or detail.
  3. Decide by group: refresh credentials for authentication errors, back off for rate_limited (honour Retry-After), retry later for availability errors, and stop for policy decisions.

JSON-RPC codes and HTTP status#

Code Default reason Meaning HTTP (MCP)
-32001 policy_denied Denied by policy, kill switch, DLP, budget, pin, or a denied or expired approval 200 (in-band); 403 for notifications and streams
-32002 approval_required Approval pending (async mode) 200 (409 ABORTED on A2A HTTP+JSON)
-32003 unauthorized Missing or invalid token, missing scope, unknown server 401, 403 or 404
-32020 header_mismatch MCP 2026-07-28 headers disagree with the body 400
-32021 missing_client_capability A required client capability was not declared 400
-32022 unsupported_protocol_version Revision not governed; data.supported lists alternatives 400
-32700, -32600, -32601, -32602, -32603 parse_error, invalid_request, method_not_found, invalid_params, internal_error Standard JSON-RPC 400, 413 (body too large), 502 (upstream), 503 (audit unavailable)

Model-route error types: maqpna_unauthorized (401/403), maqpna_policy_denied (403), maqpna_approval_required (403), maqpna_revoked (403), maqpna_dlp_blocked (403), maqpna_egress_denied (403), maqpna_budget_exceeded (402), maqpna_rate_limited (429), maqpna_not_found (404), maqpna_upstream_error (502), maqpna_audit_unavailable (503) and invalid_request_error (400).

The reason list#

Group Tokens
Authentication and authorization unauthorized, missing_token, invalid_token, token_expired, token_signature_invalid, token_audience_mismatch, missing_scope, insufficient_scope, header_mismatch, principal_unverified, svid_invalid, svid_required, svid_trust_domain, svid_mismatch, tenant_mismatch
Policy policy_denied, no_applicable_policy, default_action, rate_limited, revoked, revocation_list_unavailable, budget_exceeded, dlp, guard, guard_unavailable, tool_definition_changed, tool_unpinned, tool_definition_unverified, cedar_unavailable, cedar_error, rego_unavailable, external_policy_error, egress_denied, model_not_allowed_for_data_class
Approvals approval_required, approval_denied, approval_expired, approval_invalid, approval_aborted, approval_required_not_supported_for_egress, approval_required_unsupported_for_models, ciba_rejected, ciba_unavailable, user_confirmation_unavailable
Routing and resources unknown_server, ambiguous_server, cross_tenant_server, route_not_found, route_namespace_not_allowed, route_agent_not_allowed, namespace_not_allowed, agent_not_allowed, consent_required, per_user_unavailable, memory_backend_unavailable
A2A unknown_agent, caller_not_allowed, unknown_skill, unknown_method, delegation_depth, delegation_loop, callee_revoked, peer_card_unverified
Availability audit_unavailable, upstream_unavailable, upstream_credential_unavailable, upstream_error, egress_dial_failed, egress_upstream_failed
Protocol parse_error, invalid_request, invalid_params, method_not_found, internal_error, invalid_body, invalid_json, unsupported_protocol_version, missing_client_capability

Aliases accepted on input and mapped to the published token: expired → token_expired, bad_signature → token_signature_invalid, audience → token_audience_mismatch, guard_blocked → guard, tokens_per_minute → rate_limited. Audit records use the same vocabulary, plus annotations of allowed calls such as intent, model_override and upstream_status.

What you see#

A budget denial on an MCP call:

{"jsonrpc":"2.0","id":12,"error":{"code":-32001,"message":"denied by policy budget rule agent-day: budget_exceeded:agent/day","data":{"domain":"maqpna.com","reason":"budget_exceeded:agent/day","policy":"budget","rule":"agent-day"}}}

The same class of error on a model call (HTTP 402):

{"error":{"message":"agent budget (day) exhausted for team-a/coder: spent 10.000412 of 10.000000 USD","type":"maqpna_budget_exceeded","code":"budget_exceeded","reason":"budget_exceeded","domain":"maqpna.com"}}

The same on the A2A HTTP+JSON binding (HTTP 403):

{"error":{"code":403,"status":"PERMISSION_DENIED","message":"denied by policy budget rule agent-day: budget_exceeded:agent/day","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"BUDGET_EXCEEDED","domain":"maqpna.com","metadata":{"jsonrpcCode":"-32001","policy":"budget","reason":"budget_exceeded:agent/day","rule":"agent-day"}}]}}

The amounts are illustrative; the field names, codes, tokens and message patterns come from deniedError, budgets.go and a2a_rest.go. In the ledger the rule of this denial is written budget/agent-day. The CLI follows the same pattern for its own errors (errors.golden):

$ maqpna approvals list
✗ --gateway is required
  Pass --gateway URL, set MAQPNA_GATEWAY_URL, or save it in a context once:
  → maqpna context set NAME --gateway URL --use