arizuko › reference › Route tokens
route tokens
Developer reference for the route_tokens primitive: schema, mint surface (MCP + REST), URL handlers at /chat/<token>/ and /hook/<token> — either URL accepts any valid token; the JID kind is metadata, mechanics shared. SSE events, rate limits. Spec: specs/5/W-webhook-routes.md.
Schema
CREATE TABLE route_tokens (
token_hash BLOB PRIMARY KEY,
jid TEXT NOT NULL,
owner_folder TEXT NOT NULL,
created_at TEXT NOT NULL,
context TEXT
);
CREATE INDEX route_tokens_jid ON route_tokens(jid);
- token_hash
- sha256 of the raw token. The raw token is returned exactly once at issuance.
- jid
- Destination.
web:<folder>[/<suffix>]for chat tokens,hook:<folder>/<source>[/<suffix>]for webhooks.<folder>is the destination folder, distinct fromowner_folder(the issuer). - owner_folder
- Folder of the issuing principal. Bounds revocation:
revoke_route_tokenrequires admin on this folder, not on the JID folder. - created_at
- RFC3339 timestamp.
- context
- Optional. Free-text instructions from the issuer on how to process this link's inbound. Snapshotted onto each arriving message and rendered to the agent as
<link-context>. NULL means no instructions — the pre-context behavior.
Token format
- 32 random bytes, base64url-encoded (~43 chars, 256 bits of entropy).
- Stored as sha256; raw value returned exactly once.
- No expiry. Revocation = delete the row.
Mint surface
One internal writer (insertRouteToken) reached through two faces, per specs/5/5-uniform-mcp-rest.md.
| Action | MCP tool | REST endpoint |
|---|---|---|
| Issue chat link | issue_chat_link(jid_suffix?, context?) | POST /v1/route_tokens/chat |
| Issue webhook | issue_webhook(source_label, jid_suffix?, context?) | POST /v1/route_tokens/hook |
| List | list_route_tokens() | GET /v1/route_tokens |
| Revoke | revoke_route_token(jid) | DELETE /v1/route_tokens/{jid} |
REST request body
Each POST body carries the same params as the matching MCP tool:
POST /v1/route_tokens/chat
{ "jid_suffix": "support", // optional
"context": "bug reports; triage, don't chat" } // optional
POST /v1/route_tokens/hook
{ "source_label": "github", "jid_suffix": "main", // jid_suffix optional
"context": "push events; summarize daily" } // optional
owner_folder is bound from session context (the agent's folder or the caller's grants) and is never a parameter. Tier scope (lower = wider reach):
| Tier | Mint scope |
|---|---|
| 0 | Any folder. |
| 1 | Self and descendants. Issued on behalf of a descendant: owner_folder stays at the issuer. |
| 2 | Self only. |
| 3+ | No mint. |
Mint response
{
"token": "Yp3v...Q2",
"url": "https://<host>/hook/Yp3v...Q2",
"jid": "hook:acme/eng/github"
}
Token returned once. The <folder> segment of the JID is the destination folder (here acme/eng); for cross-folder mints the row's owner_folder may differ. url is built from WEB_HOST plus the issuance verb's canonical URL — /chat/<token>/ for issue_chat_link, /hook/<token> for issue_webhook. Either URL accepts any valid token (below).
Link context
The optional context minted onto a token is the issuer's instructions on how to process the data received through that link — per link, not per folder or per sender. webd snapshots it onto every message arriving through the token (messages.link_context), and routd renders it into the handling agent's prompt as a sibling of <topic>:
<link-context>
Bug reports from the acme website. Triage and file; don't chat back.
</link-context>
- Set only at mint time, by any mint face (MCP, REST, dashd, CLI). Immutable per token — to change the contract, mint a new link and revoke the old.
- Snapshot semantics: a message is processed under the context that was live when it arrived; revoking or re-minting never re-interprets stored messages.
- Several tokens on one JID may carry different contexts; each message carries the context of the token it arrived through, and the newest trigger message wins in a mixed batch.
- No context (NULL) — no tag, behavior identical to a pre-context token.
/chat/<token>/ and /hook/<token> — any valid token at either URL
The two URLs share handler internals — same lookup, same append path. Lookup does not filter on the token's JID kind: a hook: token works at /chat/<token>/ and a web: token works at /hook/<token>. Kind is metadata — it drives sender attribution and default output style — not a URL access gate. Each issuance verb still returns its canonical URL: issue_chat_link → /chat/<token>/, issue_webhook → /hook/<token>.
| Method | Path | Purpose |
|---|---|---|
| GET | /chat/{token}/ | Chat widget (HTML). |
| POST | /chat/{token}/ | Append body as one inbound at the row's JID. Returns turn_id (browsers open SSE). |
| GET | /chat/{token}/{turn_id}/sse | Live SSE for the round. |
| GET, POST | /chat/{token}/mcp | Per-token MCP surface: send_message, get_round, get_round_status. |
| POST | /hook/{token} | Append body verbatim as one inbound at the row's JID. Fire-and-forget; returns 204. |
The token row's JID prefix drives sender attribution and default output style. Body shape, snapshot/status URLs, SSE events, and reconnect via Last-Event-Id are the same round-handle protocol used by every web reply — see components/webd.
POST body:
{"content": "...", "topic": "..."} # JSON
content=...&topic=... # form
<any opaque bytes> # webhook payloads: body verbatim
Default JSON response (browser path):
{
"user": { "id": "msg_abc", "content": "...", "created_at": "..." },
"turn_id": "msg_abc",
"status": "pending"
}
For hook: tokens, the inbound message webd writes:
{
"jid": "hook:<folder>/<source>[/<suffix>]",
"sender": "<source>",
"content": "<raw POST body, verbatim>"
}
sender is the <source> segment of the JID (the value the agent passed as source_label at mint time). Request headers are dropped at ingest — only the body is forwarded, so upstream HMAC headers (e.g. X-Hub-Signature-256) can't be verified downstream; the URL is the only secret. Body is opaque to webd — whatever the source POSTed, verbatim, up to 1 MiB.
Rate limits
| JID prefix | Bucket scope | Default ceiling |
|---|---|---|
web: | per token, in-memory | human-typing rate |
hook: | per token, in-memory | machine-burst rate |
Excess returns 429. Body cap is 1 MiB, env-configurable.
CLI
arizuko token <instance> issue chat <folder> # mints web:<folder>
arizuko token <instance> issue chat <folder> support # mints web:<folder>/support
arizuko token <instance> issue webhook <folder> github # mints hook:<folder>/github
arizuko token <instance> issue webhook <folder> linear issues # mints hook:<folder>/linear/issues
arizuko token <instance> issue chat <folder> --context "bug reports; triage, don't chat"
arizuko token <instance> list <folder>
arizuko token <instance> revoke <jid> [<owner_folder>]
The CLI is the FS-mounted host-admin path: it writes the same route_tokens row directly to routd.db (cmd/arizuko/token.go) and prints the raw token once. --context (-c) attaches per-link instructions, same as the context mint param.
Related
- concepts/route tokens — primitive + bearer model.
- components/route tokens — how it fits in the daemon graph.
- how-to: chat link, how-to: webhooks.
- reference/jid —
web:andhook:JID shapes. - reference/env § webd — rate-limit and body-cap env vars.
- Spec:
specs/5/W-webhook-routes.md.