arizuko › components › routd
routd
What it is
In plain terms, routd is the switchboard. A message comes in from any channel, routd decides which group folder it belongs to, checks the sender is allowed, runs the turn, and sends the reply back out.
routd is the routing plane: the conversation state machine. It is the sole appender of messages — no other daemon writes to the message log. It resolves the route table (topic, sticky, and reply rules) to a target folder, runs the orchestration loop that claims each turn and dispatches it to runed, and delivers the agent’s replies back to the channel adapter that the message came from.
routd owns routd.db and migrates it itself. It opens no sibling daemon’s database — cross-daemon data (authd identity, runed session logs) arrives over HTTP, never by reaching into another file.
Why it exists
An inbound message is just text plus a sender. Three things have to happen before the agent ever sees it, and all three are routd’s job.
First, routing: a Telegram DM, a Slack thread, and a web chat all have to land in the right group folder. The route table holds the rules — a topic-kind rule, a sticky thread that stays glued to one folder, a reply that follows its parent. Without routd resolving those rules, every message is an orphan with no folder to run in.
Second, authorization: routd is the grant authority. It owns the acl and acl_membership tables and runs the per-action authz gate (auth.AuthorizeWith) on every folder-scoped request — MCP and REST alike. The question it answers is not “who are you” (that is authd’s job) but “may you do this to these params.” A handler that resolves a jid or folder binds it to the caller’s folder here, so a request can never reach across folders.
Third, the MCP host: the agent talks to arizuko over an MCP socket, and routd hosts that socket in-process (ServeTurnMCP). It derives the folder’s grant rules, wires the socket to its own DB and the channel deliverer, and forwards the agent’s conversation tools (reply, send, like, …) back through /v1/turns/{turn_id}/*. runed only mounts the ipc dir and sets Input.ExternalMCP=true; it runs no MCP server of its own.
Put differently: routd is where routing, authorization, and the MCP host all live, because all three need the same thing — the message log and the folder a turn belongs to. Splitting them across daemons would mean three copies of that state drifting apart.
How it fits
channel adapter (teled / slakd / webd / …)
| POST /v1/messages (CHANNEL_SECRET signed)
v
routd resolve routes -> target folder
| authz gate (auth.AuthorizeWith on acl / acl_membership)
| append message (sole appender -> routd.db)
|
+--POST /v1/runs--> runed (spawns the container)
| |
| agent MCP socket | ServeTurnMCP in-process; runed mounts ipc dir
| <-reply/send/like-> | /v1/turns/{turn_id}/*
v
channel deliverer (chanreg) -> back out to the adapter
Inputs: signed inbound from channel adapters (POST /v1/messages, CHANNEL_SECRET); the agent’s conversation-tool calls over the in-process MCP socket; due-task claims from timed. Outputs: dispatch calls to runed (POST /v1/runs); delivered replies through the chanreg deliverer to whichever adapter owns the JID prefix.
Hard deps: runed reachable at $RUNED_URL to execute turns; authd’s JWKS to verify tokens offline (routd verifies, never signs). Channel adapters register their egress URL and owned JID prefixes via POST /v1/channels/register.
Routing and re-arming
On startup routd re-arms scheduled tasks left in firing by an earlier crash, so a restart mid-turn doesn’t strand a cron task. The route table and its read/write surface (/v1/routes, /v1/web_routes, /v1/route_tokens/*) are how an operator points a channel, a thread, or a public /chat/<token>/ URL at a folder.
Standalone usage
routd runs as a daemon against its own routd.db, but it is useless on its own — it has nothing to route until a channel adapter registers, and nothing to run until runed answers. Run it pointing at a reachable runed and authd.
cd /srv/data/myinstance
export DATA_DIR=/srv/data/myinstance
export LISTEN_ADDR=:8080
export RUNED_URL=http://runed:8080
export AUTHD_URL=http://authd:8080
export AUTHD_SERVICE_KEY=$(grep ^AUTHD_SERVICE_KEY .env | cut -d= -f2)
export CHANNEL_SECRET=$(grep ^CHANNEL_SECRET .env | cut -d= -f2)
export WEB_HOST=localhost:8080
./routd
With AUTHD_URL unset the token verifier runs open — local-dev only, never production. GET /health returns 200 once the process is up; the red flag is turns stuck unclaimed in the loop, or runed unreachable on dispatch.
What routd does not do
routd does not spawn containers (runed does), does not sign tokens (authd does), and does not host HTML or SSE (webd does). It routes, authorizes, appends, and delivers — the conversation plane, nothing else.
Go deeper
- concepts/routing — how an event resolves to a folder, topic and sticky rules.
- concepts/grants — the authorization model routd enforces.
- reference/schema — acl — the
acl/acl_membershiptables routd owns, plus the rest ofroutd.db. - reference/env — routd — routd’s config (and the split wiring vars).
routd/README.md— full HTTP surface, tables owned, file map.specs/5/E— the routing plane split.