arizuko › reference › Route tokens
route tokens
The route_tokens schema, its mint surface in MCP and REST, and the URL handlers at /chat/<token>/ and /hook/<token>. Either URL accepts any valid token, because the JID kind is metadata. SSE events and rate limits are here too.
One row maps a token to a JID
One item per token in routd’s partition of the store (store/route_tokens.go). The coordinate is token_hash, never the JID: one JID can carry several tokens, and a key on the JID would fold them onto one item.
route_tokens key: token_hash = hex(sha256(token))
jid web:<folder>[/<suffix>] or hook:<folder>/<source>[/<suffix>]
owner_folder the issuer's folder
target_folder the folder the token delivers to
created_at RFC3339 timestamp
context optional per-link instructions
kind route, or pair for a pairing link; always written
- token_hash
- sha256 of the raw token. The raw token is returned exactly once at issuance.
- jid
- The chat's name.
web:<folder>[/<suffix>]for chat tokens,hook:<folder>/<source>[/<suffix>]for webhooks. Read it as a label, not as an address — once a suffix is present the folder cannot be recovered from it, which is whytarget_folderexists. - owner_folder
- Folder of the issuing principal. The MCP
revoke_tokenaction needs its grant on the caller's turn folder and deletes only tokens owned by that exact folder. - target_folder
- Where the token delivers. Recorded by the mint, because the JID alone is ambiguous:
web:team/formsreads exactly like a plain token at a folder namedteam/forms, andhook:a/b/creads as foldera/b. Routing and dashboard visibility read this attribute, and nothing parses the folder back out of the JID. A row without it falls back to the JID reading. One JID delivers to one folder:CheckRouteTokenTargetrefuses a mint that binds a JID already delivering elsewhere, and the second mint returns409. - 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>. Empty means no instructions and no tag.
A token is random and stored as a hash
- 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.
- Nothing mints a token on its own. Creating a folder creates no link; a folder gets one when someone asks for it.
Four tools and four endpoints manage tokens
One handler in routd (routd/route_tokens_resource.go) serves both faces and writes through MintRouteTokenInto. The host CLI writes the same row with InsertRouteToken.
| Action | MCP tool | REST endpoint |
|---|---|---|
| Issue chat link | issue_chat_link(target_folder?, jid_suffix?, context?) | POST /v1/route_tokens/chat |
| Issue webhook | issue_webhook(source_label, target_folder?, jid_suffix?, context?) | POST /v1/route_tokens/hook |
| List | list_tokens() | GET /v1/route_tokens |
| Revoke | revoke_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. How far a mint reaches depends on which face you came in through:
| Caller | Mint scope |
|---|---|
| Agent (MCP) | Whatever the caller's mcp:issue_* grant covers, checked against target_folder. Minting a public endpoint is not in role:member, so a plain folder cannot mint at all — it takes /root or a delegated grant. |
| Operator (REST) | target_folder must equal or descend from owner_folder, which is already bound to the caller's own subtree. |
source_label and jid_suffix must each match ^[\w-]+$ (segRe, routd/tokens_http.go), because each becomes one segment of the JID. A slash in either would make a deeper JID that reads as a different folder.
Issued on behalf of a descendant, owner_folder stays at the issuer. Both faces run the same auth.Authorize over the same acl rows — see grants.
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).
A token can carry instructions for the agent
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.
Both URL prefixes accept any valid token
The two URLs share the same lookup and 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 drives sender attribution, not URL access. Each issuance verb 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. |
| POST | /chat/{token}/mcp | The same MCP surface as /mcp — list_groups, send, get_history — entered with the token instead of an account. list_groups returns exactly one group: the one this link is pinned to. |
| 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. Body shape, snapshot/status URLs, SSE events and reconnect via Last-Event-Id use the same round-handle protocol as 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) cannot be verified downstream; the URL is the only secret. Body is opaque to webd — whatever the source POSTed, verbatim, up to 1 MiB.
webd limits each token by rate and body size
| 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. The body limit is a fixed 1 MiB.
The CLI manages tokens on the host
arizuko token -i <instance> issue chat <folder> # mints web:<folder>
arizuko token -i <instance> issue chat <folder> support # mints web:<folder>/support
arizuko token -i <instance> issue webhook <folder> github # mints hook:<folder>/github
arizuko token -i <instance> issue webhook <folder> linear issues # mints hook:<folder>/linear/issues
arizuko token -i <instance> issue chat <folder> --context "bug reports; triage, don't chat"
arizuko token -i <instance> list <folder>
arizuko token -i <instance> revoke <jid> [<owner_folder>]
The CLI is the host-admin path: it writes the same route_tokens row straight into routd's store (cmd/arizuko/token.go) and prints the raw token once. --context (-c) attaches per-link instructions, same as the context mint param.
Five pages give the details
- 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.