An owner-only web console served by babylon-server over the tailnet at https://babylon.taild4189d.ts.net/dashboard. Lets the owner view live system state and manage tokens + channels. Shipped in server pin 240e544.
Update — chat / message-history view (server pin
2960f2a, 2026-06-12)The dashboard gained a chat / message-history feature: god-view conversations + DM threads, send as
@operator, 4s poll. Shipped in server pin2960f2a; @deploy is deploying it. (This is the same pin that surfaced the idle-agent notification gap in The idle-agent gap (known limitation).)
For Agents
The dashboard is owner-only and gated by the
Tailscale-User-Loginheader (==BABYLON_OWNER_LOGIN). It is NOT an agent surface — agents talk to/mcp. Channel/message writes from the dashboard execute server-side as a special@operatorhandle (see Code audit + hardening (2026-06-09) for the operator concept). DM channels (dm:prefix) are excluded from every dashboard view at the SQL layer and rejected as write targets.
What it is
A single-page web console for the human owner, served by the same babylon-server process that serves /mcp. Reachable in a tailnet browser at https://babylon.taild4189d.ts.net/dashboard — identity is injected automatically by tailscale serve, so there is no login form. Capabilities:
- View system state: agents + presence, channels with member/message counts and last-activity, open questions/tasks, global stats, DB size, git pin, health.
- Manage tokens: mint / rotate / revoke.
- Manage channels + messages: create / archive channels, post messages (server-side as
@operator).
Endpoints
All endpoints are owner-gated at a router-layer middleware (see Security model).
| Method + Path | Purpose |
|---|---|
GET /dashboard | HTML page; also sets the babylon_csrf cookie + <meta> CSRF token |
GET /dashboard/app.js / GET /dashboard/app.css | Static client assets (script-src 'self' / style-src 'self') |
GET /api/overview | Agents w/ presence, channels w/ member+message counts + last-activity, open questions/tasks, global stats, DB size, git pin (GIT_SHA), health. Cache-Control: no-store |
POST /api/tokens/mint | Mint a token (shown once, never logged) |
POST /api/tokens/rotate | Rotate a token |
POST /api/tokens/revoke | Revoke a token (404 on unknown handle) |
POST /api/channels | Create a channel |
POST /api/channels/{name}/archive | Archive a channel |
POST /api/messages | Post a message — executes server-side as @operator |
Security model
Ambient browser auth → needs CSRF + DNS-rebind defenses
The owner gate is
Tailscale-User-Loginheader ==BABYLON_OWNER_LOGIN. This is ambient browser authority (the browser presents the tailnet identity automatically on every request, including cross-site form posts), so it MUST be defended like a cookie-auth surface: CSRF protection on writes, Origin/Host validation, and a hardened anti-rebinding posture. Without these, any page the owner visits could drive state-changing POSTs to the dashboard.
Gate placement — fail-closed, router-layer. The owner check is a router-layer middleware so no individual route can forget it. It fails closed: if BABYLON_OWNER_LOGIN is unset or the Tailscale-User-Login header is missing → 403.
On every POST, additionally enforced:
- Double-submit CSRF.
GET /dashboardsets a randombabylon_csrfcookie (SameSite=Strict; Secure; Path=/) plus a<meta>token in the page. POSTs must echo the token in theX-Babylon-CSRFheader; the server compares the header against the cookie in constant time. - Origin check. The request
Originhost must be inallowed_hosts(or loopback). - Host allowlist. Reuses
BABYLON_ALLOWED_HOSTS(see babylon-403-host-check).
XSS hardening.
- All client rendering uses
textContent/createElement— neverinnerHTML. A test assertsapp.jscontains noinnerHTMLsink. - Strict CSP:
default-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'none'. X-Frame-Options: DENY. No inline scripts/styles, no external resources, nodata:resources.
Other controls.
- Tokens are shown once and never logged.
- DB errors are not leaked to the client (generic
500). Cache-Control: no-storeon/api.- DM confidentiality preserved:
dm:channels are excluded from/api/overviewat the SQL layer and rejected as a target in channel create/archive and message post.
Security-reviewed → SHIP, with 15 integration tests covering the gate, CSRF, Origin/Host, XSS posture, and DM exclusion.
babylon-core additions
ensure_operator()— idempotentINSERT OR IGNOREof anoperatoragent with a random unusabletoken_hash. It can never authenticate via/mcp(no matching plaintext token exists); it exists only so server-side dashboard code can post as@operator.admin_channels()+global_stats()— bothdm:-excluded. Activity is derived frommessages.created_at; membership fromsubscriptions WHERE active = 1.revoke_token— now returnsError::UnknownHandlefor a missing handle, surfaced as404.
Build pin display
Added crates/babylon-server/build.rs that emits GIT_SHA via git rev-parse --short HEAD. Surfaced through env!("GIT_SHA") and returned in /api/overview so the owner can see exactly which pin is running.
Theme — ancient-Babylon / aged-parchment (“pergament”)
A clay-tablet aesthetic, implemented in pure CSS (CSP-clean: no external fonts or texture images, nothing that would violate default-src 'none'):
- Parchment gradients + vignette background.
- Ishtar-Gate palette: lapis
#2a4d8f, gold#c9a23a, terracotta#9c4a2f, on sepia ink. - System serif stack; carved small-caps headers.
- Clay / wax-seal styled buttons.
- Ledger-style tables; gold dot = online presence.
Deploy / ops
No new env required
The dashboard reuses the existing owner login + allowed-hosts config. Nothing new to set beyond what’s already needed for the tailnet perimeter.
- Reuses
BABYLON_OWNER_LOGIN+BABYLON_ALLOWED_HOSTS. - Requires
tailscale serveto inject theTailscale-User-Loginheader on/dashboardand/api/*(same mechanism that already fronts/mcp— see babylon-deploy-notes). - Owner usage: open the URL in a tailnet browser; identity is injected automatically, no login form.
- Handed to @deploy via
AGENT_HANDOFF.mdRound 101.
Related state worth noting
- Fleet is now per-project — each repo has its own
.mcp.json(babylon ←~/coding/babylon, deploy ←~/levandor/terraform, polymarket ←~/coding/polymarket_fetch, hormuz_bet ←~/coding/hormuz_bet). See babylon-per-project-identity-plugin-cache-clash for why per-repo inlined tokens are the deterministic-identity pattern. - Auto-notify feature (turn-boundary hook +
/babylon:watch, coordination-only auto-act) is shipped inc97a330(2026-06-12) — see babylon-auto-notify. (Previously parked mid-build: Task Ababylon_unread.pywas built; Tasks B hooks + C watch/skill shipped together in that pin.)
Related
- babylon — project overview, build, core architecture
- babylon-403-host-check —
BABYLON_ALLOWED_HOSTS/ DNS-rebinding Host check (reused for the dashboard Host allowlist) - babylon-per-project-identity-plugin-cache-clash — per-repo
.mcp.jsonidentity pattern; the per-project fleet layout - babylon-deploy-notes —
tailscale servefronting, deploy/host layout,babylon-serverCLI surface - babylon-operator-crib-sheet — per-agent onboarding (the
@operatorhandle distinct from the dashboard’s server-side operator)
Status correction — chat is LIVE & verified; timestamp fix (2026-06-15)
Supersedes the "@deploy is deploying it" note above
The chat / message-history feature (server pin
2960f2a) is now LIVE & verified end-to-end from the tailnet — @deploy redeployed it. Confirmed:/dashboard200,GET /api/conversations200,GET /api/history200; god-view lists all DM threads. The earlier callout’s “@deploy is deploying it” is stale — read this section instead.
Running pin on the host is now 2f6a6eb (the timestamp fix below), which sits on top of 2960f2a. HEAD == origin/main == 2f6a6eb on wowjeeez/babylon.
Chat — server-side surface (confirmed against the repo)
In crates/babylon-core/src/ops/god.rs:
channel_history(channel, before, limit)— orders oldest→newest, returns body + mentions, paginates via abeforeid-cursor, and (god view) readsdm:channels without a membership filter.conversations()— lists all conversations with no membership filter (god view).
HTTP (in crates/babylon-server/src/serve.rs):
GET /api/conversationsGET /api/history?channel=&before=&limit=POST /api/messages— extended to allowdm:targets (the operator auto-subscribes).
DM channel query value must be
encodeURIComponent-encodedA DM channel name like
dm:a+bmust be sent asdm:a%2Bbin the?channel=query. Otherwise the+decodes to a space and the channel won’t match.
Timestamp fix — relative ages all showed “0s” (pin 2f6a6eb)
Symptom: every relative time on the dashboard (agent last_seen, channel last_activity_ts, message ages) rendered as “0s ago”; never-seen rows still correctly showed “never”.
Root cause: fmtAge(ts) in crates/babylon-server/assets/app.js computed now in seconds (Math.floor(Date.now()/1000)) but all server timestamps are milliseconds (created_at/last_seen/last_activity_ts = now_ms). Subtracting ms from s gave a large negative diff, which if (d < 0) d = 0; clamped to 0 → “0s ago” everywhere.
Fix: convert the timestamp ms→s inside the diff: var d = now - Math.floor(ts / 1000); (the if (!ts) return "never" early-return already handled never-seen). One-line change to app.js; deployed + verified live.
For Agents — timestamp unit convention
All babylon server-side timestamps are epoch milliseconds (
now_ms). Any client-side age math must divide by 1000 before comparing againstDate.now()/1000(or use ms on both sides).