MAQPNADocs

Gateway data-plane API

Every endpoint agents call on the MAQPNA gateway - MCP, A2A, model calls, egress proxy and probes - with headers, bodies, status codes, reason values and curl examples.

This page is the reference for the HTTP API that agents and their SDKs call on the MAQPNA gateway: Model Context Protocol (MCP) tool calls on /mcp, agent-to-agent (A2A) calls on /a2a, model calls on /llm, the browser egress proxy, and the health, readiness, version and metrics endpoints. The admin API (/v1/*) has its own page: Admin API. Session tokens come from the identity broker: Identity broker API.

Use this page when you write an agent without an SDK, debug what an SDK sends, or build a client in a language MAQPNA has no SDK for. For a task-first introduction, read Connect to the gateway and Tool calls over MCP first.

Source: cmd/maqpna-gateway (route table in server.go and the register*Routes functions).

Endpoints at a glance#

Method Path Auth Purpose
POST /mcp/{server} Session token, scope tools:{server} Governed MCP JSON-RPC (single message or batch)
GET /mcp/{server} Session token, scope tools:{server} MCP server-to-client SSE stream (streamable HTTP)
DELETE /mcp/{server} Session token, scope tools:{server} End the MCP session (Mcp-Session-Id)
GET /.well-known/oauth-protected-resource/mcp/{server} None RFC 9728 metadata for external MCP clients (only when identity.trustedIssuers is set)
POST /a2a/{ns}/{agent} Session token, scope agents:{agent} A2A v1.0 JSON-RPC binding
any /a2a/{ns}/{agent}/{rest...} Session token, scope agents:{agent} A2A v1.0 HTTP+JSON binding
GET /a2a/{ns}/{agent}/.well-known/agent-card.json Session token (none with a2a.publicCards) Agent card
GET /a2a/jwks.json None Public keys that sign served agent cards
POST /llm/{route}/v1/chat/completions Session token, scope models:{route} OpenAI Chat Completions
POST /llm/{route}/v1/completions same OpenAI Completions
POST /llm/{route}/v1/embeddings same OpenAI Embeddings
GET /llm/{route}/v1/models same OpenAI model list
POST /llm/{route}/v1/responses same OpenAI Responses API
POST /llm/{route}/anthropic/v1/messages same (Authorization or x-api-key) Anthropic Messages
POST /llm/{route}/anthropic/v1/messages/count_tokens same Anthropic token counting
CONNECT, absolute-form any host Proxy-Authorization, scope tools:maqpna-egress Browser egress proxy (only with egress.enabled)
POST /v1/sessions/self/result Session token Report the session's result (see Admin API)
GET /healthz None Liveness
GET /readyz None Readiness
GET /version None (hidden on the public listener with adminListen) Build information
GET /metrics None (hidden on the public listener with adminListen) Prometheus metrics

The session-scoped endpoints that sandbox tools use (/v1/sessions/{id}/exec, /files, /evidence, /tools:authorize, /infra:*, /liveview) accept a session token too. They are listed with the rest of /v1/sessions/* in the Admin API.

Auth model#

Every data-plane call carries a session token: a short-lived, Ed25519-signed token that the identity broker issued for one session. The gateway verifies it against the broker's public keys (JWKS), checks the scope for the target, and forwards the call with its own credential for the MCP server or model endpoint. The agent's Authorization header is never forwarded.

sequenceDiagram
    autonumber
    participant OP as Operator
    participant IB as Identity broker
    participant AG as Agent (sandbox)
    participant GW as Gateway
    participant UP as MCP server / model
    OP->>IB: POST /v1/token (namespace, agent, session, user, scopes)
    IB-->>OP: session token (Ed25519 JWS)
    OP->>AG: token file (MAQPNA_TOKEN_FILE)
    GW->>IB: GET /.well-known/jwks.json (every 300 s, and on an unknown kid)
    AG->>GW: POST /mcp/github<br/>Authorization: Bearer <session token>
    GW->>GW: verify signature, aud, exp, revocation,<br/>scope tools:github, DLP, policy, budget
    GW->>UP: tools/call with the gateway's credential<br/>+ X-Maqpna-Namespace, -Agent, -Session, -User
    UP-->>GW: result
    GW->>GW: response DLP, taint, audit record
    GW-->>AG: JSON-RPC result

Token presentation#

Header When
Authorization: Bearer <session token> Default for every endpoint
Authorization: DPoP <session token> and DPoP: <proof> The token is DPoP-bound (claim cnf.jkt). See DPoP
x-api-key: <session token> Only on /llm/{route}/anthropic/v1/*, when Authorization is absent (Anthropic SDKs send the key there)
Proxy-Authorization: Bearer <session token> (or Basic, token as password) Only on the egress proxy

The gateway also accepts access tokens from external identity providers listed in identity.trustedIssuers (external MCP clients). A missing token on /mcp returns 401 with a WWW-Authenticate header that points at the RFC 9728 metadata document.

Scopes#

Scope Grants
tools:<server> /mcp/<server>, and the built-in servers maqpna-web, memory, maqpna-exec, maqpna-client, maqpna-infra, maqpna-egress, maqpna-k8s
models:<route> /llm/<route>/...
agents:<name> / agents:<ns>/<name> A2A calls to an agent in the caller's namespace / in <ns>. tools:* never grants it

A granted scope may be a glob (tools:*, models:coder-*). See Identity and delegation.

Session-token claims the gateway reads#

Claim Use
sub SPIFFE-style ID spiffe://<trust-domain>/ns/<ns>/agent/<agent>/session/<session>
maqpna_ns, maqpna_agent, maqpna_session, maqpna_tier The session identity; policy, budgets and audit use them
act The user the agent acts for ({"sub":"user:[email protected]"}), nested for A2A delegation
scope Space-separated scopes
aud, iss, exp, nbf Verified; default audience maqpna-gateway, leeway 30 s
cnf.jkt DPoP binding

The full claim table is on the Identity broker API page.

Common error envelope#

Every error the gateway generates carries the same fields, wherever the protocol puts them. Clients match on the reason token (the text before the first :), never on message or detail.

Field Type Meaning
domain string Always maqpna.com for gateway errors. Errors from a remote A2A agent keep their own domain
reason string <token>[:<qualifier>], for example budget_exceeded:agent/day, dlp:github_token, missing_scope:tools:github
detail string Free text when the raw reason was not a token, for example a rule's own reason text
policy, rule string The deciding policy and rule, when one decided (dlp and the profile for DLP; budget and <scope>-<window> for budgets)
approvalId, expiresAt string On approval_required (code -32002)
connectURL string On consent_required: where the user connects the provider

Where the envelope lives:

Surface Shape
MCP and A2A JSON-RPC error.data of the JSON-RPC error
A2A HTTP+JSON google.rpc.Status with one ErrorInfo; reason is the upper-case token (BUDGET_EXCEEDED), the rest is in metadata
/llm (OpenAI wire) {"error": {"message", "type", "code", "reason", "domain"}}
/llm/{route}/anthropic {"type": "error", "error": {"type", "message"}}; the MAQPNA type and code are appended to message
Egress proxy Plain text maqpna egress: <reason> with header X-Maqpna-Egress: denied

A denied MCP call:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32001,
    "message": "denied by policy default-agent-policy rule destructive-needs-human: approval_denied by [email protected] (apr-3f9c)",
    "data": {
      "domain": "maqpna.com",
      "reason": "approval_denied",
      "detail": "approval_denied by [email protected] (apr-3f9c)",
      "policy": "default-agent-policy",
      "rule": "destructive-needs-human"
    }
  }
}

The values are illustrative; the field names, the code and the message pattern (denied by policy <policy> rule <rule>: <reason>) come from the gateway. The complete list of reason tokens, their groups and how a client should react to each is in Error envelope and reason codes and Error and reason codes.

JSON-RPC codes#

Code Default reason Meaning
-32001 policy_denied Denied: policy, kill switch, DLP, budget, tool pin, denied or expired approval
-32002 approval_required Held for approval (async mode, see below)
-32003 unauthorized Missing or invalid token, missing scope, unknown server
-32020 header_mismatch Mcp-Method, Mcp-Name or MCP-Protocol-Version disagrees with the body
-32021 missing_client_capability The request needs a client capability the client did not declare
-32022 unsupported_protocol_version The gateway or the MCP server does not govern that revision; data.supported lists alternatives
-32700, -32600, -32601, -32602, -32603 parse_error, invalid_request, method_not_found, invalid_params, internal_error Standard JSON-RPC

MCP: /mcp/{server}#

The gateway speaks MCP over streamable HTTP. {server} is the name of an MCP server visible to the caller's namespace (an MCPServer resource rendered into the gateway's registry), or a built-in server (maqpna-web, memory, maqpna-k8s). Supported revisions: 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28.

POST /mcp/{server}#

Send one JSON-RPC message or a batch (a JSON array).

Request headers

Header Required Meaning
Authorization Yes Bearer <session token> (or DPoP)
Content-Type Yes application/json
Accept Recommended application/json, text/event-stream (the MCP server may answer with an SSE stream)
Mcp-Session-Id After initialize Passed through to the MCP server
MCP-Protocol-Version 2025-06-18 and later Must match the revision in use
Mcp-Method, Mcp-Name 2026-07-28 Must match the body's method and tool or resource name; a mismatch or a missing required header is 400 with -32020
X-Maqpna-Async No 1: do not wait for a held approval; answer -32002 with an approvalId at once
X-Maqpna-Approval-Id No Retry a held call with the ID of an approved approval
traceparent No W3C trace context; the gateway continues the trace

The X-Maqpna-* request headers are removed before the call is forwarded.

Methods and how the gateway handles them

Method Gateway behaviour
initialize Forwarded; the result's capabilities are narrowed to what the gateway proxies (tools, resources, prompts, completions, logging; tasks is removed)
tools/list Forwarded; the result is filtered per caller: tools the policy would always deny and drifted or unpinned tools (pin onChange: hide) are removed. Every tool definition is hashed for tool pinning
tools/call Fully governed: kill switch, scope, request DLP, injection guards, tool pin, policy, budgets, approvals, response DLP, taint, audit record
resources/read, prompts/get, completion/complete Forwarded; client payloads pass request DLP; the content they return taints the session when the server is untrusted
server/discover (2026-07-28) Forwarded; supportedVersions and capabilities narrowed to what the gateway governs
ping, notifications, responses to server requests (sampling, elicitation, roots) Forwarded; they still need the tools:{server} scope; client answers pass request DLP
tasks/*, tools/call with params.task Refused: -32601 / -32602. Task-augmented execution is not proxied

Request IDs must be a string or an integer. Other IDs (null, fractions, booleans, objects) get -32600. In a batch, request IDs must be unique.

Example: call a tool

curl -sS "$MAQPNA_GATEWAY_URL/mcp/echo" \
  -H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hello"}}}'

An allowed call returns the MCP server's result unchanged, except for DLP redactions:

{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"hello"}]}}

Approvals. When a rule says require_approval, the gateway holds the call:

  • Synchronous (default). The HTTP request stays open until an approver decides or the approval expires (approvalTimeoutSeconds, default 300). Approved: the call runs once and you get its result. Denied or expired: -32001 with reason approval_denied or approval_expired.
  • Asynchronous (X-Maqpna-Async: 1). The gateway answers at once:
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32002,
    "message": "approval required (destructive); retry with header X-Maqpna-Approval-Id once approved",
    "data": {
      "domain": "maqpna.com",
      "reason": "approval_required",
      "detail": "destructive",
      "approvalId": "apr-3f9c",
      "expiresAt": "2026-10-18T14:10:00Z",
      "policy": "default-agent-policy",
      "rule": "destructive-needs-human"
    }
  }
}

Retry the same call (same tool, same arguments) with X-Maqpna-Approval-Id: apr-3f9c. While the approval is pending you get -32002 again; once approved the call runs and the approval is consumed (single use). An approval ID for another call gets -32001 with reason approval_invalid.

  • MCP 2026-07-28 clients get an InputRequiredResult instead of -32002, whose requestState (maqpna-approval:<id>) the client echoes on retry.

A session can hold at most 20 pending approvals; the next held call is denied with approval_quota_exceeded. An identical pending call reuses its existing approval.

Tool pinning. The gateway hashes every tool definition it sees in tools/list and compares it with the pins of the caller's policies (ToolPolicy.spec.toolPins) or of the MCPServer:

Pin mode Effect
off Nothing recorded
learn First-seen hashes recorded and audited; nothing blocked
enforce + onChange: hide A changed or unpinned tool is removed from tools/list; tools/call is denied with -32001, reason tool_definition_changed or tool_unpinned
enforce + onChange: approve A call to a changed tool requires approval
enforce + onChange: alert Allowed and audited

A tools/call for a tool the gateway has not seen listed triggers one upstream tools/list (at most every pinCacheSeconds, default 60 s). See MCP servers and tool pinning.

Status codes

HTTP JSON-RPC When
200 result or in-band error Every governed decision (allowed, denied -32001, held -32002), including missing scope on a request
400 -32700, -32600, -32020, -32022 Invalid JSON, invalid batch, bad request ID, duplicate member names, header mismatch, unsupported revision
401 -32003 Missing or invalid token (WWW-Authenticate set)
403 — / -32001 A notification or response that was denied (no ID to answer), or a revoked caller on the stream endpoints
404 -32003 Unknown MCP server (unknown_server, ambiguous_server, cross_tenant_server in data.reason)
413 -32600 Body larger than maxBodyBytes (default 4 MiB)
502, 503 -32603 MCP server unreachable (upstream_unavailable), audit ledger unavailable in auditFailurePolicy: closed (audit_unavailable)

GET /mcp/{server} and DELETE /mcp/{server}#

GET opens the server-to-client SSE stream of the streamable HTTP transport; DELETE ends the MCP session named by Mcp-Session-Id. Both need the session token and the tools:{server} scope, are checked against the kill switch, and are proxied with the gateway's credential. Server-initiated messages on the stream pass DLP.

curl -N "$MAQPNA_GATEWAY_URL/mcp/echo" \
  -H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
  -H "Accept: text/event-stream" \
  -H "Mcp-Session-Id: <session id from initialize>"
HTTP When
200 Stream open / session ended (the MCP server's answer)
401 Missing or invalid token
403 Token lacks tools:{server}, or the caller is revoked
404 Unknown MCP server

Built-in server memory#

POST /mcp/memory serves governed long-term memory with the tools memory_put {key, value, tags?}, memory_get {key}, memory_search {query, limit?} and memory_delete {key}. It needs scope tools:memory and a memory store bound to the caller's agent. Values are never written to the audit ledger; the record holds the store and the value's SHA-256. Without a store you get memory_backend_unavailable.

GET /.well-known/oauth-protected-resource/mcp/{server}#

RFC 9728 protected-resource metadata for external MCP clients that authenticate with your identity provider. It is served only when identity.trustedIssuers is configured and an issuer allows the server; otherwise 404.

{
  "resource": "https://gateway.example.eu/mcp/github",
  "authorization_servers": ["https://sso.example.eu/realms/acme"],
  "scopes_supported": ["tools:github"],
  "bearer_methods_supported": ["header"],
  "resource_name": "MAQPNA governed MCP server github"
}

A2A: /a2a/{ns}/{agent}#

Agents call other agents through the gateway. {ns}/{agent} is a hosted agent with spec.a2a.expose or an A2APeer (a remote agent). The gateway governs A2A v1.0 and accepts A2A 0.3 method names, which it governs as their v1.0 equivalents. The upstream always receives JSON-RPC; the HTTP+JSON binding is translated.

Every call needs scope agents:<agent> (agents:<ns>/<agent> across namespaces), must come from a caller in the target's allowedCallers, and is governed like a tool call: policy with server a2a/<agent> (a2a/<ns>/<agent> across namespaces) and tool SendMessage, skill:<id> or the method name; approvals, DLP, taint, budgets, the kill switch and the audit ledger apply. Both hops are audited.

Request headers

Header Meaning
Authorization Bearer <session token>
Content-Type application/json (or application/a2a+json)
A2A-Version 1.0; absent or 0.3.x selects the 0.3 protocol
A2A-Extensions Passed through
X-Maqpna-Async, X-Maqpna-Approval-Id As for MCP

POST /a2a/{ns}/{agent} (JSON-RPC binding)#

Method (v1.0) 0.3 name
SendMessage message/send
SendStreamingMessage (SSE) message/stream
GetTask tasks/get
ListTasks tasks/list
CancelTask tasks/cancel
SubscribeToTask (SSE) tasks/resubscribe
CreateTaskPushNotificationConfig tasks/pushNotificationConfig/set
GetTaskPushNotificationConfig tasks/pushNotificationConfig/get
ListTaskPushNotificationConfigs tasks/pushNotificationConfig/list
DeleteTaskPushNotificationConfig tasks/pushNotificationConfig/delete
GetExtendedAgentCard agent/getAuthenticatedExtendedCard
curl -sS "$MAQPNA_GATEWAY_URL/a2a/team-a/reviewer" \
  -H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"role":"ROLE_USER","messageId":"m-1","parts":[{"text":"Review PR 4821"}]}}}'

The answer is the callee's JSON-RPC response. Errors use the envelope in error.data.

/a2a/{ns}/{agent}/{rest...} (HTTP+JSON binding)#

HTTP Path below the agent Equivalent method
POST message:send SendMessage
POST message:stream SendStreamingMessage
GET extendedAgentCard GetExtendedAgentCard
GET tasks ListTasks (query parameters become params; historyLength, pageSize are numbers)
GET tasks/{id} GetTask
POST tasks/{id}:cancel CancelTask
POST, GET tasks/{id}:subscribe SubscribeToTask
POST tasks/{id}/pushNotificationConfigs CreateTaskPushNotificationConfig
GET tasks/{id}/pushNotificationConfigs ListTaskPushNotificationConfigs
GET tasks/{id}/pushNotificationConfigs/{configId} GetTaskPushNotificationConfig
DELETE tasks/{id}/pushNotificationConfigs/{configId} DeleteTaskPushNotificationConfig

A unary answer is the result object with 200; streams are SSE streams of the bare response objects. Errors are google.rpc.Status:

{"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"}}]}}

A pending approval on this binding is 409 with status ABORTED.

Agent card#

GET /a2a/{ns}/{agent}/.well-known/agent-card.json returns the card of a hosted agent, or a gateway-issued proxy card of a verified remote peer. It needs a session token with the agents: scope for that agent, unless a2a.publicCards is true. The card lists two interfaces, both at <publicURL>/a2a/{ns}/{agent}: JSONRPC and HTTP+JSON, protocol version 1.0, and a Bearer security scheme. With a2a.cardSigningKey set, the card is signed (JWS over the RFC 8785 canonical form) and the keys are published at GET /a2a/jwks.json (Cache-Control: public, max-age=300; 404 when signing is off).

curl -sS "$MAQPNA_GATEWAY_URL/a2a/team-a/reviewer/.well-known/agent-card.json" \
  -H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")"
HTTP When
200 Card
401, 403 Missing token, or no agents: scope for the agent
404 Unknown agent
502 Remote peer card unavailable or not verified

Push notifications#

A push-notification config names a webhook the callee posts task updates to, bypassing the gateway. The gateway therefore refuses every webhook URL (in CreateTaskPushNotificationConfig and in a send's configuration) that is not https to a host matching a2a.pushNotificationHosts. With no hosts configured, push notifications are refused. The reason is push_url_not_allowed.

A2A reasons#

unknown_agent, caller_not_allowed, unknown_skill, unknown_method, delegation_depth (act chain longer than a2a.maxDelegationDepth, default 5), delegation_loop, callee_revoked, peer_card_unverified, push_url_not_allowed. See Agent-to-agent calls.

Model calls: /llm/{route}#

{route} is a model route rendered from the agent's spec.model (or a static models entry). Every model call needs scope models:<route>; the route may also restrict namespaces and agents. The gateway meters tokens and cost, runs DLP and injection guards on prompts and completions, enforces budgets and tokensPerMinute, and fails over to the route's fallbacks. With modelPolicy: policy it also evaluates your policies (server llm/<route>, tool = the operation, for example chat.completions); a require_approval result is a denial, because model calls cannot wait for approvals.

Wire formats#

The governance pipeline always runs on the client's wire format. Just before the upstream call, the endpoint's provider adapter turns the request into the provider's API, and the response back:

Client wire format Paths
OpenAI /llm/{route}/v1/chat/completions, /completions, /embeddings, /models (GET)
OpenAI Responses /llm/{route}/v1/responses
Anthropic Messages /llm/{route}/anthropic/v1/messages, /messages/count_tokens
Provider (route endpoint) Modes Upstream auth (default)
openai (OpenAI-compatible, default) — apiKeyFile as Bearer
azure-openai v1 (default), deployments Entra bearer token, or apiKeyFile as api-key
bedrock converse (default), openai SigV4, or a Bedrock API key as Bearer
vertex native (default), openai Google OAuth bearer token
gemini — apiKeyFile as x-goog-api-key
anthropic direct (default), vertex, bedrock x-api-key, Google bearer, SigV4
oci-genai — apiKeyFile as Bearer
llamay OpenAI-compatible per-session context binding; see Llamay models

Point an SDK at the route: OpenAI SDKs use base URL $MAQPNA_GATEWAY_URL/llm/<route>/v1; Anthropic SDKs use ANTHROPIC_BASE_URL=$MAQPNA_GATEWAY_URL/llm/<route>/anthropic. The session token is the API key.

curl -sS "$MAQPNA_GATEWAY_URL/llm/team-a.coder/v1/chat/completions" \
  -H "Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Say hello"}]}'
import os
from openai import OpenAI

client = OpenAI(
    base_url=os.environ["MAQPNA_GATEWAY_URL"] + "/llm/team-a.coder/v1",
    api_key=open(os.environ["MAQPNA_TOKEN_FILE"]).read().strip(),
)
r = client.chat.completions.create(model="gpt-4o-mini", messages=[{"role": "user", "content": "Say hello"}])
print(r.choices[0].message.content)

Anthropic wire format:

curl -sS "$MAQPNA_GATEWAY_URL/llm/claude/anthropic/v1/messages" \
  -H "x-api-key: $(cat "$MAQPNA_TOKEN_FILE")" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"model":"claude-sonnet-4-5","max_tokens":256,"messages":[{"role":"user","content":"Say hello"}]}'

The response is the provider's answer in the client's wire format. For streaming chat completions the gateway may add stream_options.include_usage to meter the stream; it then drops the usage-only final chunk if the client did not ask for it.

Provider-hosted tools. A request that asks the provider to call MCP servers itself (Anthropic mcp_servers / mcp_toolset, OpenAI Responses mcp tools) is denied with provider_hosted_mcp. A provider-hosted server tool (web search or fetch, code execution, file search, computer use, image generation, Gemini grounding, Bedrock system tools) is denied with provider_hosted_tool:<name> unless the route's providerTools lists it.

Errors#

{"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"}}
HTTP type Typical code / reason
400 invalid_request_error invalid_body, invalid_json, unsupported_by_provider
401 maqpna_unauthorized missing_token, invalid_token, token_expired
402 maqpna_budget_exceeded budget_exceeded
403 maqpna_unauthorized missing_scope, namespace_not_allowed, agent_not_allowed
403 maqpna_policy_denied policy_denied, provider_hosted_mcp, provider_hosted_tool, model_not_allowed_for_data_class
403 maqpna_approval_required approval_required (approval_required_unsupported_for_models)
403 maqpna_revoked revoked
403 maqpna_dlp_blocked dlp:<detector>
403 maqpna_egress_denied egress_denied
404 maqpna_not_found route_not_found
429 maqpna_rate_limited tokens_per_minute (reason rate_limited); honour Retry-After
502 maqpna_upstream_error upstream_error, upstream_unavailable
503 maqpna_audit_unavailable audit_unavailable

code keeps its pre-v0.1.0 value for OpenAI-SDK compatibility; reason is the published token. See Model calls and LLM call governance.

Egress proxy#

With egress.enabled, the gateway's data listener is also an HTTP forward proxy for browser-profile sandboxes. CONNECT requests (HTTPS tunnels) and absolute-form requests (plain HTTP) are governed here instead of being routed to the API. The browser sidecar's relay adds the session token.

Each request is:

  1. Authenticated: Proxy-Authorization: Bearer <session token> (or Basic with the token as password), scope tools:maqpna-egress, and the kill switch.
  2. Checked against the URL rules and the domain policy (the agent's spec.browser.allowedDomains).
  3. Evaluated by your policies with server maqpna-egress and tool connect or http (arguments host, port, method, url). require_approval is enforced as a denial.
  4. Dialled through the guarded transport (SSRF block list, rebinding-safe DNS, residency check) and audited.
curl -sS -x "$MAQPNA_GATEWAY_URL" \
  --proxy-header "Proxy-Authorization: Bearer $(cat "$MAQPNA_TOKEN_FILE")" \
  https://docs.example.eu/
HTTP When
200 Tunnel established / response relayed
400 Bad target
403 Denied (X-Maqpna-Egress: denied; body maqpna egress: <reason>), including revoked and missing_scope:tools:maqpna-egress
407 Missing or invalid token (Proxy-Authenticate: Bearer realm="maqpna-egress")

DPoP-bound tokens cannot be used at the proxy (dpop_proxy): a proxy request cannot carry a proof.

Health, readiness, version and metrics#

GET /healthz#

Liveness. Always 200 while the process serves.

curl -sS "$MAQPNA_GATEWAY_URL/healthz"
{"status":"ok"}

GET /readyz#

Readiness. 200 with {"status":"ready"} when policies are loaded, the gateway has identity verification keys, and the revocation list, state backend and audit ledger are healthy. Otherwise 503 with the list of problems. A draining replica answers 503 with shutting down.

{"status":"not ready","problems":["policies not loaded","no identity verification keys"]}

Other checks (audit ledger, governance stores) add their own problem texts. The common ones:

Problem text Cause
policies not loaded policyFile missing or invalid at start
no identity verification keys JWKS not reachable and no identity.publicKeyFiles
revocation list unavailable (failing closed): <error> Revocation list unreadable
state backend unreachable: <error> PostgreSQL state backend down
shutting down The replica is draining

GET /version#

Build information. Hidden (404) on the public listener when adminListen is set.

{"component":"gateway","version":"2.1.0","commit":"<commit>","buildDate":"<date>","goVersion":"<go version>","platform":"linux/amd64"}

The fields are component, version, commit, buildDate, goVersion, platform and tags; the values above are placeholders.

GET /metrics#

Prometheus text format (text/plain; version=0.0.4). Hidden (404) on the public listener when adminListen is set; then scrape the admin listener. The metric families are listed in Metrics.

curl -sS "$MAQPNA_GATEWAY_URL/metrics" | grep maqpna_gateway_requests_total

Request limits#

Limit Default Setting
Request body 4 MiB maxBodyBytes
Time to send the body 60 s bodyReadTimeoutSeconds
Upstream timeout 60 s upstreamTimeoutSeconds
Approval wait 300 s approvalTimeoutSeconds
Pending approvals per session 20 fixed

Every response carries X-Content-Type-Options: nosniff. Browser origins are allowed only when listed in the gateway's CORS settings; preflight allows GET, POST, DELETE, OPTIONS with Authorization, Content-Type.

Next steps#