arizuko › components › dashd
dashd
What it is
In plain terms, dashd is the control panel for whoever runs the instance. Open it in a browser to see groups, messages, and tasks, and to edit a few of the agent’s files.
dashd is the operator dashboard: an HTMX server that renders pages over the owner daemons’ databases (routd’s, onbod’s and runed’s) and a fixed set of markdown files. Reads are the default; the only write paths are PUT and DELETE on that markdown list under each group’s folder.
Routes live at /dash/*. The portal at /dash/ links to status, tasks, activity, groups, memory, and profile pages, each rendered server-side as one HTML page plus HTMX partials for live sub-views.
Why it exists
Without dashd, operators inspect an arizuko instance through each daemon's /v1, journalctl -u arizuko_<inst>, and the arizuko CLI. That works for an engineer, but it asks a lot of anyone else, and it never gives a one-screen answer to “what is this instance doing right now”.
dashd owns no tables of its own. It reads from tables other daemons own — groups, routes, scheduled_tasks, messages, sessions, channels, auth_users, written by routd, authd, timed, and onbod — and renders them. Which of those reads go over the owner daemon’s /v1/* API and which stay direct is settled in specs/5/16-mcp-rest-unification.md.
How it fits
operator browser
|
v /dash/...
|
proxyd requireAuth: JWT or refresh-token cookie
| stamp X-User-Sub, X-User-Groups, HMAC sig
v
dashd render HTML / HTMX partial
| read: routd's + onbod's tables
| write: PUT|DELETE /dash/memory/{folder}/{rel}
v
filesystem: <groups>/<folder>/MEMORY.md, diary/*.md, ...
dashd reads identity from headers set by proxyd’s requireAuth, and it proves the request really came through proxyd: every /dash/* handler — read views included — runs through guard, which calls auth.ProxydTransit. That checks a service:proxyd ES256 bearer against authd’s keyset. The bearer proves transit and nothing else; the end-user identity is the X-User-Sub and X-User-Groups headers, and dashd trusts those only once the transit proof holds. A request that skips proxyd carries no valid bearer, so it never reaches a handler. Only /health, /openapi.json, and the htmx asset stay public. Write verbs go further: every admin mutation runs requireAdmin, which calls auth.Authorize with action admin against the caller’s groups, plus a same-origin CSRF guard.
Two write paths today. The memory editor accepts MEMORY.md, .claude/CLAUDE.md, and flat *.md under diary/, facts/, users/, and episodes/. Reads are capped at 1 MiB, and the path joiner blocks any symlink that escapes the group folder. Admin writes — routes editor, groups CRUD, per-user secrets — sit behind the admin check above.
Standalone usage
dashd needs a port and a way to reach routd's tables — the store endpoint the instance is served from, or that owner's file. Point AUTHD_URL at authd and dashd verifies proxyd’s transit bearer against its keyset on every /dash/* request — a request that arrives without one gets redirected to login. Leave authd unwired for local dev and the guard passes through, which is why you still front dashd with proxyd on the public internet: proxyd terminates TLS, runs the login flow, and stamps the X-User-* headers dashd trusts.
export DATA_DIR=/srv/data/arizuko_demo
export DASH_PORT=:8080
export INSTANCE_NAME=demo
./dashd
# browse http://localhost:8080/dash/
On a file instance DATA_DIR resolves each owner at $DATA_DIR/store/<owner>/ — dashd reads routd's and onbod's — and the groups tree at $DATA_DIR/groups/; on a served instance the same reads go to the endpoint and no file is opened. Set DB_PATH to override routd's file independently. Pointing it at a retired database is refused rather than served stale. INSTANCE_NAME appears in the portal header.
Health: GET /health returns 200 when the database is reachable. With authd wired, exposing dashd directly still rejects any request that carries no proxyd transit bearer — but proxyd owns TLS and the login flow, so front it with proxyd anyway.
The page set
Read views (each one HTML the browser renders directly):
/dash/— portal index linking the other pages./dash/status/— groups, sessions, channels at a glance./dash/activity/— the last 50 messages across all groups./dash/tasks/— scheduled tasks fromscheduled_tasks./dash/groups/— group folders with their channel routes./dash/memory/— the editable markdown view per group./dash/packages/— installed packages frominstalled_packages(name, source, revision, owned assets). Read-only; install / upgrade / remove is thearizuko packagesCLI./dash/profile/— the calling user’sauth_usersrow.
HTMX partials (tasks/x/list, activity/x/recent) feed live sub-views.
Admin pages (admin auth + CSRF):
/dash/routes/— routes editor: list, create, edit, delete rules. This is routd’s message-routing table — which folder an inbound message lands in. For the operator it also lists the unrouted chats: every chat that has messaged the instance and reaches no group, with a one-click claim that writes the rule. routd holds those chats — stores them, fires no turn — because a folder confers files, secrets and grants, and anyone can message a bot, so no group is a safe default./dash/proxyd/— the reverse-proxy route table: which backend an inbound URL lands on. Lists the live rules and takes an add or delete form. Operator-only. dashd owns no SQL here — every read and write is an HTTP call to proxyd’s/v1/proxyd_routes, so proxyd stays the single writer and logs the one audit row against the operator dashd forwarded./dash/groups/new+/dash/groups/{folder}/settings— group create / settings / delete./dash/me/secrets— per-user capability token set, list, delete./dash/me/env— per-user model API keys (ANTHROPIC_API_KEYand the like), kept separate from capability tokens./dash/me/connections— surrogate OAuth: “Connect GitHub” writes an access + refresh token into the samesecretsrow a pastedGITHUB_TOKENwould, without the user minting a PAT by hand./dash/audit/— the audit trail browser: newest first, 50 a page, filtered by category, actor or folder. Federated across routd, runed and authd — each daemon serves its ownaudit_logatGET /v1/auditand dashd merges the three, with a Source column saying which answered. A source that fails gets a banner naming it, because an empty section would claim nothing happened there./dash/usage/— per-folder model spend and turn counts. The spend comes from the model gateway through routd’s/v1/cost/summary; an instance with no gateway has no meter, and the page says so rather than showing a zero./dash/proactive/— which groups may speak first, their quiet hours, any brokenproactive:block, and when each chat last interjected. Read-only:mode:stays operator-edited in the group’sCLAUDE.mdso the file remains the single source./dash/engagement/— every conversation the agent is staying in without being addressed, with the group and the time left, plus a disengage button per row for a chat it should stay out of. Operator-only. Like/dash/proxyd/, dashd owns no SQL here: both faces are HTTP calls to routd’s/v1/engagement, so routd stays the single writer, applies the folder check, and writes the audit row inside the mutation’s own transaction./dash/services/,/dash/routd/,/dash/runed/— the daemon cockpits: health tiles, errored chats with a retry, live runs with a kill./dash/authd/— the identity cockpit. Which signing key is in use, what a rotation retired and when the passes it signed stop being accepted; then one row per login — who, what they can reach, when it started, how often it has renewed — each with a sign out button. Operator-only, and like/dash/proxyd/it owns no SQL: authd is the sole signer, so dashd asks it over HTTP rather than reading its tables, which is also what puts the revoke’s audit row in the mutation’s own batch. No key material or token value is rendered, and there is no fleet-wide logout button — that means retiring the active key, which stays out-of-band, and the page says so./dash/channels/whatsapp/pair— self-service WhatsApp re-pair page. Requires the**operator super-grant. Calls whapd/v1/pair/start; the pairing code is returned once, expires in 60s, rate-limited to 5/hr. Every start writes an audit row inmessages.
The migration to /v1/*
Most pages still read routd's tables directly. The plan is to swap each read for a call to the sibling daemon that owns the table. /dash/proxyd/ already works this way and is the shape the rest are heading for:
/dash/groups/→routd/v1/groups+routd/v1/routes./dash/tasks/→timed/v1/tasks(with a form POST for create)./dash/activity/→routd/v1/messages?limit=50&order=desc./dash/profile/→onbod/v1/users/{sub}./dash/proxyd/→proxyd/v1/proxyd_routes— done.
After the migration dashd holds an operator session token (issued by proxyd at OAuth login), forwards it as Authorization: Bearer ... on every /v1/* call, and stops opening the owner databases at all. The audit page already works this way — it fans out to routd, runed and authd over HTTP rather than reading their files.
What dashd does not do
dashd does not send messages, schedule tasks, mint tokens, or admit users — those belong to routd, timed, proxyd, and onbod. Writes are scoped to the surfaces listed above: memory editor and the admin pages. Everything else is a view.
Go deeper
- reference/env — dashd — full env var table.
- components/proxyd — the auth gate that sits in front.
specs/5/16-mcp-rest-unification.md— one resource declaration, two faces: MCP and REST.specs/4/Q-dash-memory.md— memory view and edit surface.