arizuko › concepts › route tokens
A route token is a 32-byte random string that maps to one JID. POST a body to a URL carrying that token and the body lands as an inbound message at the mapped JID. The JID prefix encodes routing intent at the data layer — web: for a browser chat, hook: for a webhook from GitHub, Linear, or anywhere else that knows how to POST JSON.
All tokens live in the route_tokens table. Four columns:
token_hash sha256 of the raw token; lookup key
jid web:<folder>[/<suffix>] | hook:<folder>/<source>[/<suffix>]
owner_folder the folder that minted the row; bounds revocation
created_at when it was minted
Each URL is bound to its JID prefix kind; mechanics shared:
| URL | Token JID | Methods | What happens |
|---|---|---|---|
/chat/<token>/ | web: only | GET, POST | GET serves the chat widget. POST appends a message; SSE streams the reply for browsers, plain response for non-browser callers. |
/chat/<token>/mcp | web: only | GET, POST | Per-token MCP surface (send_message, get_round, get_round_status). |
/hook/<token> | hook: only | GET, POST | Same widget on GET, same POST behavior, same SSE as /chat/<token>/. Different URL, different JID prefix kind. |
The two URLs share their handler internals — same widget, same POST path, same SSE — but the binding is enforced at token lookup: a hook: token presented at /chat/<token>/ returns 404, and a web: token at /hook/<token> returns 404. Each issuance verb picks its URL: issue_chat_link returns /chat/<token>/, issue_webhook returns /hook/<token>. One URL per kind keeps each surface single-purpose.
web:<folder> means “anonymous visitor chat at this folder.” web:acme is acme's public chat URL. Add a suffix to split one folder across pages: web:acme/support and web:acme/sales are different conversations against the same agent, distinguishable in routing rules.
hook:<folder>/<source> means “webhook from source targeting folder.” hook:acme/eng/github is GitHub events for the acme/eng folder. hook:acme/eng/linear/comments partitions Linear into a comments stream separate from hook:acme/eng/linear/issues. The <source> segment is what the agent sees in the inbound's sender field.
JID prefixes split routing — a hook: message and a web: message can fire different rules even though one table issued both tokens. The URL the request arrived on matches the JID prefix one-to-one.
Two sources mint tokens, one writer inserts the row.
issue_chat_link or issue_webhook on its group's MCP socket. The agent gets the URL back once and hands it to a human to paste.arizuko token issue <folder> chat mints a chat token for a folder. The same command with hook <source> mints a webhook. The REST equivalent is POST /v1/route_tokens/chat or /v1/route_tokens/hook, OAuth-gated through proxyd. dashd surfaces a button on the folder page.Both paths hit the same insertRouteToken writer. Same row shape, same audit trail.
The raw token is returned exactly once at issue time. Only the sha256 is stored. Losing the token means reissuing — there is no recovery.
Folders do not get a chat token automatically when they are created. A folder gets a token when someone calls issue_chat_link for it. This keeps unused folders from accumulating live URLs.
Resolved by the unified grants tier. Lower tier = broader mint reach.
| Tier | Can mint for |
|---|---|
| 0 | any folder |
| 1 | self and any descendant |
| 2 | self only |
| 3+ | no mint |
Tier 1 at acme can mint hook:acme/eng/github on behalf of the acme/eng child. In that case the row's owner_folder stays at acme — the issuer, not the JID target. Revocation follows owner_folder, so acme retains control of the tokens it minted.
One DELETE on the row. The next request to the URL returns 401 immediately — no grace period, no cache to wait out. An agent in folder A cannot delete a token whose owner_folder = B; the ACL gate at revoke_route_token requires admin on owner_folder.
Whoever holds the URL can POST to it. There is no second factor, no signing on the body, no per-caller identity at this surface. webd does not verify X-Hub-Signature or any other upstream HMAC — that belongs to a skill on the agent side, not the platform. Rotation is by reissue: mint a new token, swap the URL at the source, delete the old row.
webd applies a per-token rate limit in memory. The ceiling is chosen by JID prefix — web: sits at a lower bucket sized for humans, hook: at a higher bucket for machine bursts. Body size is capped at 1 MiB.
Previously called slink; renamed to route tokens for clarity once webhooks joined the same primitive.
web:<folder> and hook:<folder>/<source> reach an agent.specs/5/W-webhook-routes.md — the route_tokens spec.