Connect to the gateway
How an agent gets its session identity and token, the environment variables and endpoints it is given, how long a token lives and how it is refreshed.
Your agent never holds an API key for a tool or a model. It holds one thing: a session token, a short-lived signed identity for this one session. It sends that token as a bearer token to the MAQPNA gateway, and the gateway does everything else. This page explains where the token comes from, which variables tell your code where the gateway is, and what to do when the token expires.
How the token reaches your code#
sequenceDiagram
autonumber
participant OP as Operator
participant IB as Identity broker
participant K as Secret SESSION-maqpna-token
participant AG as Your agent (sandbox)
participant GW as Gateway
OP->>IB: POST /v1/token (namespace, agent, session, user, scopes, tier, ttl)
IB-->>OP: signed EdDSA token, SPIFFE ID, expiry
OP->>K: write token
K-->>AG: mounted at /var/run/maqpna/token (read-only)
Note over OP: env MAQPNA_GATEWAY_URL, MAQPNA_TOKEN_FILE,<br/>MAQPNA_TOOL_NAME_URL, ...
AG->>AG: SDK reads the file on every request
AG->>GW: POST /mcp/github tools/list<br/>Authorization Bearer session token
GW->>GW: verify signature with the broker's public keys (JWKS),<br/>audience maqpna-gateway, expiry, revocations
GW-->>AG: tools this session may use
Note over OP,K: at 80% of the lifetime the operator<br/>mints a new token into the same Secret
- You (or a controller) create an
AgentSessionfor yourAgent. - The operator checks that the requested scopes are a subset of the agent's, then asks the identity broker for a token.
- The token is written to the Secret
<session>-maqpna-tokenand mounted read-only into the sandbox at/var/run/maqpna/token. On a tier-2 (confidential VM) session it is written to an in-memory volume after attestation instead, and never stored in the cluster. - The SDK reads the file on every request, so a refreshed token is picked up without a restart.
Under maqpna dev run there is no operator and no file: the CLI asks the local broker for a token and passes it in MAQPNA_TOKEN.
What is in the token#
Decode a token (no signature check) with maqpna token inspect:
maqpna dev run --session dev-tok --ttl 20m --scope tools:echo -- sh -c 'maqpna token inspect "$MAQPNA_TOKEN"'
{
"claims": {
"iss": "maqpna-identity",
"sub": "spiffe://maqpna.local/ns/dev/agent/coder/session/dev-tok",
"aud": "maqpna-gateway",
"iat": 1792359480,
"nbf": 1792359480,
"exp": 1792360680,
"jti": "e0b1a9a07eb330c49d75a22dbccd29e3",
"act": {
"sub": "user:you@localhost"
},
"scope": "tools:echo",
"maqpna_tier": "dev",
"maqpna_session": "dev-tok",
"maqpna_ns": "dev",
"maqpna_agent": "coder"
},
"expiresAt": "2026-10-18T21:58:00Z",
"kid": "o15cBqYfnnnmTdFXvY5NdtVU5ZQlEHNrh6V3WKNuGDk"
}
| Claim | Meaning | Used for |
|---|---|---|
sub |
The session's workload identity (SPIFFE ID): trust domain, namespace, agent, session. | Policy, audit, kill switch |
act.sub |
The user the agent acts for. | Policy principals, approvals (an approver cannot approve their own calls) |
scope |
tools:<server>, models:<route>, agents:<name>, space-separated. |
The gateway refuses a server, route or agent the token has no scope for (missing_scope). |
exp |
Expiry. | token_expired after it |
jti |
Token ID. | Revoking one leaked token with maqpna kill --jti |
Check a signature against the broker's public keys with maqpna token verify TOKEN --jwks http://127.0.0.1:8081/.well-known/jwks.json (exit status 3 when invalid).
Environment variables#
The operator sets these in every sandbox (internal/controller/podspec.go); maqpna dev run sets the local equivalents.
| Variable | In a cluster | Under maqpna dev run |
|---|---|---|
MAQPNA_GATEWAY_URL |
The gateway's in-cluster URL | http://127.0.0.1:8080 |
MAQPNA_TOKEN_FILE |
/var/run/maqpna/token |
not set |
MAQPNA_TOKEN |
not set | the session token |
MAQPNA_SESSION, MAQPNA_AGENT |
session and agent names | dev-<random> (or --session) and coder (or --agent) |
MAQPNA_TOOL_<NAME>_URL |
{gateway}/mcp/<name> for each spec.tools entry |
one per MCP server of the local stack |
MAQPNA_MODEL_ENDPOINT, MAQPNA_MODEL_ROUTE |
{gateway}/llm/<ns>.<agent>/v1 and the route, when spec.model.endpoint is set |
{gateway}/llm/stub/v1 with --stub-llm (route variable not set) |
MAQPNA_MODEL_NAME, MAQPNA_MODEL_PROVIDER |
from spec.model.name and .provider |
stub and openai with --stub-llm |
MAQPNA_ANTHROPIC_BASE_URL |
{gateway}/llm/<route>/anthropic, when spec.model.adapter is anthropic or bedrock |
not set |
OPENAI_BASE_URL, OPENAI_API_KEY |
not set (set them from the variables above) | the model route and the session token, with --stub-llm |
MAQPNA_TASK, MAQPNA_TASK_FILE |
the session's task (inline when at most 4 KiB; file at /var/run/maqpna/input/task) |
not set |
MAQPNA_WORKSPACE, MAQPNA_PARENT_SESSION |
workspace mount; parent of a forked session | not set |
<NAME> is the server name in upper case, with every character that is not a letter or digit replaced by _: server jira-cloud is MAQPNA_TOOL_JIRA_CLOUD_URL. Variables you set in Agent.spec.env cannot override the MAQPNA_* ones, and they cannot reference the session's token Secret.
Endpoints your agent calls#
All on MAQPNA_GATEWAY_URL, all with Authorization: Bearer <session token>:
| Endpoint | For | Page |
|---|---|---|
POST /mcp/{server} |
MCP JSON-RPC: initialize, tools/list, tools/call |
Call tools over MCP |
POST /llm/{route}/v1/chat/completions, /completions, /embeddings, /responses; GET /llm/{route}/v1/models |
OpenAI-compatible model calls | Call models |
POST /llm/{route}/anthropic/v1/messages |
Native Anthropic Messages (token also accepted as x-api-key) |
Call models |
POST /a2a/{namespace}/{agent} |
Agent-to-agent (A2A) JSON-RPC | Call another agent |
POST /v1/sessions/self/result |
Report how the run ended | Handle decisions |
Connect from code#
from maqpna import MaqpnaClient
client = MaqpnaClient.from_env() # MAQPNA_GATEWAY_URL, MAQPNA_TOKEN_FILE or MAQPNA_TOKEN, ...
print(client.session, client.available_tool_servers())
token = client.token() # the current token, re-read from the file
import { MaqpnaClient } from "@kosa8/maqpna";
const client = MaqpnaClient.fromEnv();
console.log(client.availableToolServers());
const token = await client.token();
client, err := maqpna.NewFromEnv(ctx)
if err != nil {
return err
}
fmt.Println(client.Session(), client.AvailableToolServers())
token, err := client.Token(ctx)
TOKEN="${MAQPNA_TOKEN:-$(cat "${MAQPNA_TOKEN_FILE:-/var/run/maqpna/token}")}"
curl -sS "$MAQPNA_TOOL_ECHO_URL" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Read the file again before each request. Do not cache the token for the life of the process.
Without MAQPNA_GATEWAY_URL, from_env() / fromEnv() / NewFromEnv fail with ConfigError. They never fall back to a public endpoint.
Token lifetime and refresh#
| Where | Default lifetime | Refresh |
|---|---|---|
| Cluster | At most 15 minutes (operator flag --token-ttl), and never past the session's own TTL |
The operator mints a new token at 80% of the lifetime into the same Secret; the kubelet updates the mounted file. |
| Warm-pool sandbox | Handed over by the broker when the sandbox is claimed (MAQPNA_BOOTSTRAP_URL) |
The SDK refreshes at 80% of the lifetime. |
maqpna dev run |
1 hour (--ttl) |
None: start a new maqpna dev run. |
The SDKs re-read the token file on every request, and after a 401 they re-read it and retry once. If you write your own client, do the same. An expired token gets JSON-RPC -32003 with token_expired; a missing one gets HTTP 401 with missing_token.
Mint a token by hand#
For a quick test against a shared installation, mint a token with the broker's admin token, or read a running session's token from its Secret:
# local stack: MAQPNA_BROKER_URL and MAQPNA_BROKER_TOKEN come from maqpna dev env
eval "$(maqpna dev env)"
maqpna token mint --broker "$MAQPNA_BROKER_URL" --namespace dev --agent coder --session s1 --scope tools:echo --ttl 15m
# cluster: an existing session's token
kubectl get secret fix-4821-maqpna-token -n team-a -o jsonpath='{.data.token}' | base64 -d > tok
maqpna call --gateway https://gateway.example.eu --token-file tok --server github --list
What can go wrong#
| Symptom | Reason | Fix |
|---|---|---|
ConfigError: MAQPNA_GATEWAY_URL |
The agent runs outside a sandbox or maqpna dev run. |
Run it as maqpna dev run -- CMD. |
-32003 token_expired |
The token lived past exp. |
Locally, raise --ttl; in a cluster, re-read the file instead of caching the token. |
-32003 missing_scope:tools:github |
The token has no scope for that server. | Add the server to Agent.spec.tools, or --scope tools:github locally. |
HTTP 404 unknown_server |
No MCP server of that name is visible to this namespace. | maqpna mcp list -n NS. |
token_audience_mismatch |
The token was minted for another audience. | Mint for maqpna-gateway. |