Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Web Dashboard

The web dashboard is Intendant’s default frontend. It is a single-page app served by the controller’s built-in HTTP/WebSocket gateway, running entirely in the browser with WASM-powered state management (the presence-web crate, mobile-responsive). Since the design-overhaul flip the default look is the v2 chrome (Iris accent: left navigation rail, oversight bar, ⌘K command palette, bottom composer); Activity uses its newer v3 presentation inside that permanent html.ui-v2 namespace. Dark is the default theme; a light theme ships alongside it (Settings → Appearance, or the ⌘K theme toggle — browser-scoped, persisted per browser). The previous Catppuccin Mocha generation and its ?ui=v1 escape hatch were deleted after the soak period. The SPA is served as one self-contained file, static/app.html — a generated artifact: build.rs assembles it from the ordered fragments in static/app/ (manifest.txt fixes the order) via crates/app-html-assembler, and CI rejects any drift between fragments and artifact. Edit the fragments, never the artifact; see static/app/README.md. For iteration, INTENDANT_APP_HTML_PATH=<file> makes the gateway re-read a disk copy of app.html on every request instead of serving the embedded one (see Configuration) — edit a fragment, run the assembler, refresh.

On by default

There is no opt-in: the gateway starts automatically unless you pass --no-web, --mcp, or --json (those modes own stdio / are headless by contract). The --web flag simply forces it on and optionally sets the port.

./target/release/intendant "task"          # dashboard comes up; URL is printed
./target/release/intendant --web            # explicitly enable
./target/release/intendant --web 9000       # explicit port
./target/release/intendant --no-web "task"  # disable; headless single round

The server binds port 8765 by default, auto-incrementing through 8785 if it is busy; the chosen port is printed at startup. With the default mTLS transport, open https://<host>:<port>/ in a browser after running intendant access setup and enrolling that browser/device. Use --bind 127.0.0.1 when starting plaintext local/debug dashboards with --no-tls.

Correction vs. older docs: --web is the default and no longer “implies --mcp”. Earlier docs described --web as opt-in and tied to MCP mode — neither is true now.

Secure Browser Contexts and LAN Access

The dashboard shell, Activity log, Sessions, Settings, and basic display viewing can run over ordinary HTTP. Some browser capabilities are different: browsers expose them only to a secure context.

Use a secure dashboard context when you need:

  • Station WebGPU rendering (navigator.gpu) — otherwise Station falls back to its canvas-2D WASM renderer.
  • Microphone and camera (navigator.mediaDevices, getUserMedia) for live voice, browser-side audio/video capture, or camera recording.
  • Screen/window capture from the browser (getDisplayMedia) when a browser is the capture source.
  • Privileged browser APIs such as the async clipboard in stricter browsers.

Practical rules:

  • https:// with a trusted certificate is the normal secure context for remote browsers.
  • http://localhost and http://127.0.0.1 are treated as secure by most desktop browsers, but not by every embedding. In particular, the macOS WKWebView wrapper uses the custom intendant:// scheme because http://localhost there does not expose media devices.
  • http://<host-ip> is not a secure context. Use default native mTLS or --tls with a trusted certificate for public/authority-free bytes. The packaged macOS app’s local bridge also supplies a secure context for local development, but no Developer ID-signed/notarized release exists for this alpha, so its current artifact is not a distribution anchor. A generic Caddy/nginx HTTPS reverse proxy supplies encryption and a secure context, not client authentication. If it forwards to daemon loopback, the proxy is itself a root trust boundary and must enforce approved client identity while protecting its upstream from other callers. The macOS app wrapper starts its bundled backend with mTLS by default and fails closed with setup guidance when access certs are missing.
  • Clicking through a self-signed certificate warning is not a reliable substitute for installing/trusting the certificate; browsers may still withhold secure APIs.

Headless daemon posture

The controller always runs headless and tees its stdout/stderr to daemon.log in the session directory (so the dashboard’s “Download session report” can include controller output). With no task argument the agent starts idle and waits for tasks submitted from the dashboard; with a task argument it runs the task as the foreground session under the same gateway, then falls through to the idle daemon loop.

Tabs

The v2 chrome groups thirteen destinations in the left navigation rail — Activity, Sessions, Agenda, and Memory (Work), Live display and Station (Watch), Terminal and Files (Machine), Usage (Insight), Access and Vault (Trust), Settings and Debug (System). The oversight bar on top carries the phase pill, stop control, context meter, transport state, and the ⌘K command palette. Activity’s own header carries its five-view switch and Timeline-only session/layout controls; the composer — the global task input — docks at the bottom and reaches the daemon from any destination. New events arriving while you are elsewhere raise a badge on the rail item.

On content-heavy destinations the composer can collapse to a compact pill and expand in place; each tab remembers its own state. Its target switcher can move the prompt to another live session without leaving the current destination, and the conversation peek mirrors the target transcript’s latest 12 entries read-only, updates live, and links back to the full Activity view.

The target switcher also lists connected peers (RC-C1): picking a peer row delegates the composed text as a task on that daemon; picking one of its sessions sends a session-scoped follow-up — both through this daemon’s peer grant over the federation link, never a browser lane. One global operating target backs the per-pane host pickers (Shell, Files, Transfers, Sessions, Stats): changing any one moves them all (the Shell’s cloud exec hosts stay pane-local — they are exec venues, not daemons). Peer session cards and Station peer nodes carry host-scoped action pills (resume, message, rename, interrupt) routed through api_peer_session_control and derived from the peer-advertised grant, and media affordances (live display, file transfer, the browser↔peer control tunnel) render from the connected link’s transport class — a relay-only peer gets an honest “no live media” note plus the federation-fold session list instead of dead pills. Peer transcripts (RC-C2): clicking a peer session card (or a Station peer node’s transcript action) opens the session-detail overlay reading the transcript from the peer — over the browser↔peer tunnel when the link supports it, else the daemon-mediated api_peer_session_detail federation fetch that works on relay-only links — under a provenance banner naming the peer, the link class, and the snapshot’s fetch time, with Refresh and an “Open on ” hand-off; frames and recordings stay on the peer and render as absent, never as dead thumbnails. Peer approvals fold into the global rail: the bottom approval panel, its queue, and the attention center key every item by (host, id) — approval ids are per-daemon counters that collide across daemons — with a provenance line naming the governing daemon and the delegation lane; deciding routes through api_peer_approval under this daemon’s peer grant (Approve all maps to accept_for_session), the peer’s own profile gate authorizes it, and a grant known to lack approval.resolve is said up front instead of failing after the click. On a peer-link reconnect the fold clears and repopulates from the peer’s bootstrap re-announce — older peers degrade to the session rows’ needs_approval mark. Agenda, Memory, Vault/custody, Access/IAM, and Settings never route to a peer.

The section headings below keep the internal pane names (Video is the Live display destination, Stats is Usage) — ids, routes, and deep links are unchanged by the redesign.

One design language, bounded DOM

Every tab speaks the design language introduced with the Access redesign: glass cards with a title + one-line explainer, chips for status (a chip always carries its label — never color alone), warm-scale badges for authority, folds for power-user surfaces, and real empty states. The shared component classes are ui-* (grouped-selector aliases of the Access acc-* rules — one source of truth); chart and heatmap colors come from the validated --viz-* ramp tokens in :root. Don’t improvise new button/chip styles or viz colors — reuse these.

Two invariants keep a long-lived dashboard responsive:

  • Hidden panes don’t render. High-frequency events (transport status ticks, update_usage, session refreshes, transfer progress) route through renderOrDefer(tab, key, fn): visible panes render immediately; hidden panes remember only the latest render thunk per key and run it once on the next pane entry (switchTab flush / visibilitychange). Data merging still happens eagerly — only DOM work is deferred.
  • Client state is bounded. The live log DOM is capped (10k entries, pruned with scroll compensation), session-window histories cap at 5000 items (older entries stay reachable via remote paging; replay dedup survives the trim), finished file transfers prune beyond 100, and per-display metric cards retire with their display.

The log stream manages scroll position manually (overflow-anchor: none): when you scroll up to read, appends and prunes never move your view — new entries count on a bottom-center jump-to-live pill instead. QA harnesses can probe pane/render state and drive the real log append pipeline via window.__intendantPaneDiag (the app script is module-scoped, so probes cannot reach bindings by name).

QA probes (window.qa)

The SPA is one <script type="module">, so harnesses evaluating in the page’s main world cannot reach module bindings by name. Fragments that have QA-relevant state export it on the shared readback namespace, declared next to the state it reads:

window.qa = Object.assign(window.qa || {}, {
  sessionsHydration: () => ({ seen, unresolved, inFlight }),
});

Each entry is cheap and JSON-serializable; unless noted, probes are side-effect-free snapshot reads. Current probes: qa.sessionsHydration() (sessions-tab relationship-hydration termination, 40-session-launch.js), qa.sessionsFuel() (new-session credential preflight, 55b-session-launch.js), qa.newSessionAgentPrefs() (last-used launch-option prefill state, same fragment), qa.focusSurface() (Activity Focus target/promotion state), qa.arrangeMenu() (the Arrange menu’s bulk-sweep rows, counts, and sub-agent auto-minimize state), qa.closableLens (the closable-at-a-glance lens: claim() drives the pure positive-only classifier with an explicit matrix, state() snapshots chip count + lens engagement, set(on) — a drive, not a read — toggles the lens), and qa.station() — a pointer to window.stationProbe, which predates the namespace and keeps its legacy name (the validator’s --station-* probes and smoke skills depend on it). window.__intendantPaneDiag above is the other legacy surface.

scripts/validate-dashboard.cjs reads probes back without a bespoke sink via the repeatable --probe-json EXPR flag (optional label=EXPR form): after the checks pass — and on failure, before exit, so you see the state that failed — it evaluates each expression in the page and prints one probe <label> = <compact JSON> line to stdout (thrown errors print as {"error":...}; ~4KB cap per probe):

node scripts/validate-dashboard.cjs --url "http://127.0.0.1:<port>/#sessions" \
  --wait-for-function "(() => typeof window.qa === 'object')()" \
  --probe-json "fuel=window.qa.sessionsFuel()" \
  --probe-json "window.qa.sessionsHydration()"

Activity

The default tab and the dashboard’s canonical DOM control surface. Activity v3 re-presents the established behavior in a calmer card/timeline language while retaining the accessibility floor, low-GPU path, stable element ids, deep links, and automation hooks. Station is the rendered-canvas alternative and immersive roadmap; both dispatch through the same control plane.

Five visible views (the first retains the internal log id for compatibility):

  • Timeline — a scrollable event stream of everything in the system, grouped by centered turn separators. User prose, worker/command activity, and system metadata have distinct reading tiers; the Options popover carries Normal/Verbose/Debug density plus the host filter. Event sources remain classified as:

    • system — session lifecycle, approvals, context management
    • worker — model responses, reasoning summaries, task completion
    • agent — command execution output (stdout/stderr, exit codes)
    • live — voice transcripts, presence lifecycle, tool requests
    • server — presence model internals (thinking, tool calls)

    The Activity header carries two layouts, persisted per browser. Focus (the default) promotes the selected/prompt-target session’s existing, hydrated session window as the timeline; with no target it falls back to the combined all-session stream. On wide viewports a vitals rail follows the foreground session (working tree, context budget, prompt cache, rate limits, changes). Grid shows the per-session window grid with relationship wires and the concurrent stream below. The same header holds the session switcher and context-sensitive Minimize done control.

    Timeline also carries the approval card. Approve clears the pending command once; Approve all like this sets that approval category’s rule to auto (the shipped per-category machinery, scoped and revocable in Settings) and then approves; Switch to Full autonomy is the old native approve-all and is labeled for its broader, daemon-wide scope. Explicit native deny rules and hard human/live-audio gates still remain. Skip and Deny complete the set (y / a / s / n). A follow-up text input sends a message after a round completes.

    The question rail is the approval card’s sibling for requests of input, never permission (ask_user, ctl ask): up to four structured questions on one panel, each with options and pick bounds, optional free text, a per-question follow-up input, and rendered preview cards (show-then-ask: sandboxed HTML prototypes, images, text snippets) that accept anchored notes. A blocking ask shows its expiry as a live countdown the user can hold open; the panel can be tucked away and restored from a chip. Agenda-backed (parked) asks ride the same panel with no countdown: they never expire, dismissing one (skip/deny) clears the rails but leaves the item open on the Agenda — where its card wears a “dismissed · still open” chip and an Open question panel button brings it back — and answers land on the item (and are delivered into the asking session while it still lives).

    Pending requests also escalate beyond the open tab (the attention center, static/app/57-attention-notifications.js): every pending approval/question across sessions counts into a (N) document-title prefix and favicon badge (on by default; toggle under Settings → Appearance → Notifications), and — strictly opt-in from the same card, which is the only place notification permission is ever requested — a browser Notification fires when a request arrives while the tab is hidden; clicking it focuses the tab and the owning session. The badge clears as requests resolve and drops on stream disconnect (the reconnect bootstrap rebuilds what still stands). For closed tabs entirely, the daemon nudges the Connect rendezvous and opted-in browsers get a Web Push (see self-hosted-rendezvous.md — Notifications; payloads never carry work content). The same opt-in channel may announce only that a hosted lease request exists; its key, preset, lifetime, label, and request id remain daemon-local.

  • Context — the agent’s current working context (what it is operating on).

  • Managed — operator console for managed-Codex context maintenance (see below).

  • Changes — file changes / diffs, in two labeled realms: the session’s own tracked edits (rewind baseline / external diff log) and Uncommitted in working tree — the target session’s checkout’s git-dirty set (the same status parse, and the same effective checkout, the vitals dirty chip counts, activity locus included; capped at 200 rows with an honest truncation note). The empty state only claims “no changes” when both realms are empty, and affirms a clean working tree when that is known. Has its own badge when new changes land.

  • Control — direct controls for steering the run.

Session-window headers carry a vitals chip (the operator-statusline port, backend-neutral): a git segment for the session’s working tree (⎇ branch ●dirty +ahead/−behind ✓|⚠ merge-parity ⇡unpushed, fetch-free, conflict-tinted when a merge with the primary would conflict); a prompt-cache segment — ⚡NN% hit share of the latest request (green ≥90, yellow ≥50) plus a live ⏳m:ss TTL countdown where the provider’s cache TTL is known (Anthropic 5m/1h; hidden for OpenAI whose TTL is undocumented), dimming to once cold; and a rate-limit gauge — ▮NN% 5h for the most-used window (Claude Code subscription 5h/7d, Codex primary/secondary, native Anthropic per-minute headers), dim below 70%, yellow from 70%, red from 90% with the reset countdown appended; the tooltip lists every window. Session-facts chips (the vitals config section, wire-first from each backend’s own echoes with the launch config as the honestly-marked fallback) show the model + configured reasoning effort (⬡ claude-fable-5 · max) and the permission mode in plain words (🛡 Acts without asking) — bypass/ungated modes get a quiet warning tint (cosmetic, never the health dot), the raw backend vocabulary (bypassPermissions, workspace-write · on-request, the autonomy level) is one tap away in the explainer, and the vitals pane always lists both rows, degrading to “not reported” instead of hiding. When a cache countdown enters its final minute the dashboard raises one toast per idle period (plus a browser notification if permission was already granted and the tab is hidden) — early enough that a follow-up still reuses the warm cache, never after the fact. Sections appear as producers fill them; the chip hides in narrow windows. Station’s agent focus panel shows the same vitals as git / cache / limits rows.

The operational envelope extends with grid-envelope chips, served precomputed on the session-catalog row (agenda + boot blocks, session_catalog/grid_envelope.rs — one daemon payload, no SPA-side joins). Agenda-fired sessions show their source item ( + the item id’s first 12 chars; hover reveals the full title, tap offers “Open the agenda card” and copy), the occurrence (▶ started / ✓ completed / ✕ failed… — the occurrence journal’s reverse fold, lineage-following: a resume re-key’s successors share the block, and the occurrence object is the extensible slot Track AO’s attestation will land in), and the sealed inputs (⛉ N sealed; the explainer lists each digest-bound binding ref with its short digest via the one shared formatter, and per-ref actions copy the full sha256). Every window also states its boot era↻ this boot vs ↻ pre-boot, from HS1’s presence substrate joined with live wrapper registry membership — and a session that predates the current daemon boot with no live wrapper renders unmistakably as a ghost (👻 warn chip + dashed desaturated frame): the safe-to-close state, served as a precomputed bit. A backend resumed across a restart is two wrapper rows aliasing one card — the dead pre-restart twin and the live resume-attached twin; claims on the fold rank live wrapper > current era > lineage tip > the rest (resolveSessionWindowBootMeta), so the dead lineage never re-labels a live card as a ghost and two dead generations resolve deterministically to the chain’s newest incarnation. The live set is alias-closed daemon-side (a post-identity entry is keyed by its backend id with the wrapper log-dir id aliased; both count), so readopt-created successors stop false-ghosting. A dead window also carries terminal honesty: the row serves its dir-local end facts (terminal — summary outcome + ended_at verbatim, or the freshest error for crash-frozen dirs), the window states them plainly (“Ended … : outcome” / “Died mid-turn — last activity …”) instead of freezing on a mid-execution look, the status pill reads Ended/Died rather than a stale active phase, and a ghost whose lineage continued shows a clickable “continued as …” pointer derived from the shared resume-lineage resolver (boot.continued_as; the successor’s state is joined client-side from its own card). Liveness itself stays event/boot-derived — hydration never flips ended (the #637 stop-hide law), and the note is presentation of served facts. Fired sessions keep their derived names (the source item’s title, or workflow - node for workflow nodes); an owner rename always wins, and the Track AW refinement — stamped definitions deriving definition name - node id — lands later at the same single derivation seam (agenda/reminders.rs, derive_spawn_session_name) without touching these chips.

The safe-to-stop derivation (Track AO) rides the same occurrence block: stop serves the ruled conjunction from the durable journal — kills_live_run (the lineage tip of a started-without-terminal occurrence: a live firing), owed_work (a superseded member of one, regardless of process state), settled (every terminal) — and the × affordance, the stop confirms, and the occurrence chip render machine-scoped consequence copy from it (a linkless session claims “safe” only as idle ∧ no linkage; a busy linkless card claims nothing). The closable-at-a-glance lens composes those positive claims for the whole grid: a toolbar chip beside Arrange counts the cards whose close the machine already rules safe — settled and quiet, idle with no agenda linkage, or finished; negative served claims veto, and a running window never counts, even settled — hides at zero (never “0 closable”), and, engaged, dims every other card so the closable set reads at a glance. The classifier (sessionWindowClosableClaim in 41-session-window-actions.js) is strictly a composition of already-ruled claims, never a second derivation, and the count plus each card’s class derive in the Arrange menu’s one walk so they can never disagree; an emptied count disengages the lens rather than stranding a fully-dimmed grid.

When a backend announces background commands (currently Claude Code’s wire-reported task registry), the activity explainer inside that vitals panel adds a Background tasks list. A task with an announced output file gets a read-only output viewer: it requests the last 64 KiB by task id, refreshes about every 2 s while the task is running, and stops polling at a terminal state. The server resolves ids through its registry and caps any requested tail at 256 KiB; the browser never supplies an arbitrary file path.

Managed (Activity → Managed)

The manual counterpart to the model-driven managed-context tools. A session picker lists Codex-like sessions — live windows plus historical sessions from the session store — sorted prompt-target first, then managed-mode, live, and most recently updated (labels show name, short id, source, and via <id> when the Codex thread is reached through an Intendant wrapper session). Use target snaps back to the current prompt target.

For a live session the pane calls the per-session MCP get_status and renders a density card: managed/vanilla mode, pressure status, effective and hard-limit token usage with a colored pressure bar, the soft rewind-at threshold, and whether rewind-only gating is active. When the verified dashboard-control tunnel is connected, these dashboard-originated MCP tools/call requests use api_mcp_tool_call; otherwise they fall back to /mcp?session_id=.... Historical sessions show historical status — records and anchors stay readable, but live actions are disabled. Alerts flag non-Codex selections, sessions without managed mode, an insufficient last rewind, and a configured Codex command that doesn’t look like the patched managed build.

  • Rewind — manual rewind_context dispatch with an exact item anchor (call_id or response item id) plus anchor side (before/after), a required reason and carry-forward primer, and optional preserve / discard / artifacts / next-steps lists (one entry per line). Inspect anchor runs inspect_rewind_anchor to show a small window around the candidate before committing.
  • Recent anchors — harvested from the live activity log and the /api/managed-context/anchors history, each with a one-click Use that fills the anchor field (switching the picker to the anchor’s session if needed).
  • Records / Backout — the session’s rewind records from /api/managed-context/records; clicking one shows its JSON and fills the backout form, which runs rewind_backout in inspect, restore, fork, or backout mode with an optional fork name.
  • Lineage and fission — the ledger card. Lineage groups come from the live get_status payload; fission groups come from GET /api/managed-context/fission, the merged ledger + extension view that works for historical sessions too (live-status fission_ledger groups are only a fallback when the endpoint has nothing yet). A fission group row shows the group id, its anchoring tool (fission_spawn, or fission_spawn:head when the spawn fell back to the catalog head), the spawn anchor item id, the canonical session (-- when unclaimed), and — for severed groups — a detached chip carrying the detach time and reason. Each branch row carries a status chip colored by the ledger’s canonical status vocabulary (running / blocked / completed / failed / detached / cancelled; legacy raw values fold the same way the ledger normalizes them), a canonical chip on the claimed branch, an imported chip once the branch result was imported, a changed-file count, the branch charter (objective, write scope, worktree path), and its latest summary. (For the fission model itself — charters, worktrees, detach-on-rewind — see External-Agent Orchestration.)
  • Per-branch fission actionsWait / Import / Cancel / Detach run fission_control against the selected session. Wait uses a 60 s window, and a still_running result is surfaced as an info toast, not an error; import, cancel, and detach ask for confirmation first, with cancel and detach styled as destructive. Claim calls claim_fission_canonical, passing the group’s current canonical id as the compare-and-swap guard when one exists.
  • Spawn fission branches — the spawn form above the ledger list: one to four branch rows (objective required; optional comma-separated write scope and display name; Add branch adds rows, each row has a remove control, and the last row is always kept) plus a tri-state worktree select — default omits use_worktree so write-scoped branches in a git project get isolated worktrees, while on/off force it either way — submitted as a single fission_spawn call for the selected session.
  • Copy status JSON copies the raw status payload.

Rewind, backout, inspect, and fission spawn stay disabled unless the selected session is live and effectively managed. The pane refreshes when the Managed subtab is opened and re-schedules itself (only while the subtab is active) after each pane action, thread-action result, session start, and usage update.

Agenda

A durable daemon ledger for intent that must outlive the current model context: tasks, notes, questions, and deferred follow-ups. The tab can add items, filter Open / Blocked / Questions / Done / Retired / All, answer questions, complete, reopen, or retire entries, and tune due-reminder delivery (including quiet hours and per-item urgency). Bodies and answers are rendered as quoted data, never executed as instructions. The same cache feeds a compact Activity card and refreshes over the live agenda_changed lane.

Questions is a kind view: the open questions plus the answered archive. An open rich (ask-backed) question leads with its Open question panel button — options and previews live on the question rail — with the inline input as the explicit plain-text path; a dismissed-but-open one wears a quiet “dismissed · still open” chip and is not re-announced automatically (the button is the way back). An answered ask-backed item renders the full structured breakdown — per-question “Header: answer” lines, the picked option labels, follow-ups, and preview-anchored notes — not just the joined text. See the Agenda chapter for the park-vs-block doctrine and delivery semantics; Start now’s confirm sheet and the origin-conversation follow-up affordances are covered there too.

Decision cards. The detail panel renders an open question as a decision, not a wall of prose. Structured options (parked with ctl agenda ask --option, or ctl ask --park) render as one-click choice pills in the ask-rail vocabulary, with the (Recommended) label convention highlighted; a prose body’s explicit Recommendation: / Recommended: / Disposition: sentences surface as a highlighted strip whose Use as answer button prefills the answer box (the owner still sends). Bodies honor their own paragraph breaks (white-space: pre-wrap — no markdown engine). Attached file refs open in-dashboard: clicking one fetches GET /api/agenda/items/{item_id}/refs/content — ref-scoped to exactly the locators attached to the item, never a generic file server — and the reader sheet shows the bytes with honest provenance: the sealed snapshot from the sealed-refs store when the ref’s attach pin has one (corruption refuses by name rather than silently serving live bytes), otherwise the live file with its drift verdict against the attach digest (unchanged / changed / unpinned). Text renders escaped, images inline, anything else offers a download; content stays quoted data on every path.

Agenda also carries proposed scheduled-session effects. Agents may propose a goal and fire time, but no work runs until an owner surface approves the exact manifest digest shown in the tab; revising a proposal invalidates the prior approval. The eventual session outcome is written back onto the item.

Memory

The Memory explorer searches the daemon’s bounded claim plane and proposes typed claims (observation, decision, episode, procedure, or preference) with an explicit sensitivity. Results show derived status, labels, provenance, and claim details; candidates are normally excluded by the service but the review-oriented dashboard opts into them by default and offers a visible toggle. Statements are quoted data, never instructions, and memory_changed refreshes the filtered server-side query.

Every response reports the effective storage mode. On the current primary-OS daemon (macOS), the normal durable plane survives daemon restarts under ~/.intendant/memory-plane; other platforms, the INTENDANT_MEMORY_EPHEMERAL=1 kill switch, or a failed durable bootstrap use an honestly labeled ephemeral plane. Memory does not yet sync claims across machines.

Stats

Token-consumption and cost tracking:

  • A KPI tile row up top: today / this-week / all-time cost, lifetime tokens, active days (skeleton tiles while session stats stream in)
  • Per-model breakdown for the main and presence models (prompt, completion, cache-read, and cache-write token counts), with a token-pressure meter per card
  • Cost estimates from a built-in pricing table (OpenAI, Anthropic, Gemini), including the distinct GPT-5.6 Sol/Terra/Luna input, cache-write, cached-read, and output rates
  • Token activity: a daily skyline and a GitHub-style year heatmap on the validated single-hue --viz-* ramp, filterable by agent and period
  • All-sessions cumulative usage and disk usage
  • Display-transport metrics (frame rate, encode latency, bandwidth per display)

Terminal

An embedded xterm.js terminal hosting an interactive Shell session on the daemon (or a selected peer). Session monitoring and control live in the Activity/Station tabs, not here.

Live display

The Computer Use workspace combines a selected WebRTC stage with a live rail for displays, input authority, browser-observed display activity, peer launchers, and the user’s own screen (see Display Pipeline):

  • View mode (default) — watch the agent’s display in real time
  • Take Control — forward mouse and keyboard events to the agent’s display
  • Release — relinquish control, with an optional note
  • Selected display stage — switch among active local displays without recreating their capture sessions or video elements. A shared-view request selects its target once unless that would interrupt active human input, annotation, callout, or full-screen work; later manual selection is respected.
  • Input authority — the toolbar and rail project the same server truth: you, another viewer, available, or connecting. Hiding an interactive or pending display first flushes held keys and mouse buttons, then releases it before input listeners are removed. Editable annotation/callout work blocks navigation instead of being discarded.
  • Display activity — reports real connection, authority, visibility, streaming, recording, annotation, callout, and shared-view transitions, plus the daemon’s per-action cu_action lane: every executed CU action renders as a two-line row (friendly sentence + raw call like left_click(612, 233)) and drives the stage overlays — agent cursor with verb pill, click ripple, keypress chips, screenshot flash. The feed shows only what the daemon actually reported (failed actions never render, and the lane is ephemeral: no session log, no replay, no peer forwarding).
  • Approval card — when a pending approval’s session is the reported driver of the selected display, an amber card in the rail proxies the approval panel’s own Approve/Deny; without real display→session attribution it stays hidden.
  • Peer displays — open on Station, whose viewer understands federated display targets, rather than masquerading as local stages.
  • Responsive controls — below the desktop breakpoint the rail becomes a keyboard-accessible Displays & input drawer; primary controls remain in the stage toolbar.
  • Recording replay — browse and play back recorded sessions with timeline seeking and speed control (1x / 2x / 4x). Live recording controls show a pending command but change REC/activity state only on daemon confirmation. Starting a recording is owner/root-only for the alpha and succeeds only for an active agent-visible display. Private views are never recordable: their pixels must not enter the ordinary session artifact tree, where recording and filesystem APIs follow their existing broader grants.

The live rail’s Your screen card keeps the three screen-on-the-wire concepts separate (see Computer Use):

  • View this machine — a private remote view: an owner/root dashboard may watch and control this machine’s display. The agent cannot see it — the session is agent_visible = false and every agent-facing display lookup skips it. The tile wears a Private view chip and the live rail row a PRIVATE tag.
  • Share with agent — the classic DisplayControl grant: the screen becomes visible to the agent for computer-use tasks until revoked. The tile wears an Agent can see this chip. Sharing while a private view is active upgrades it in place; the reverse never happens implicitly.
  • The tile’s Stream button is the third, unrelated control: frames to the live presence (voice) model only.

Both modes revoke from the same card (Stop viewing / Revoke access), and GET /api/displays annotates entries with capture_active + agent_visible so pickers and chips can render live state.

The private-view ceiling is identical on the legacy /ws transport and the verified dashboard-control DataChannel: only an owner/root dashboard may create a private user-session display, enumerate it through the live registry, receive its media, or send it input. Generic display.view and display.input permissions enumerate and control only agent-visible displays. The browser bootstrap filters explicit private ready/grant records so replay cannot recreate a private display tile for a scoped client. Display lifecycle failure/teardown records do not carry the original visibility bit, however, so live fanout and historical session/event logs may disclose audit metadata such as a display ID, capture failure, or revocation. The private-view ceiling protects pixels and control; it is not an existence-metadata secrecy promise and does not rewrite owner audit history. Revocation remains available to an otherwise authorized scoped caller as a de-escalation path.

Displays appear automatically when the agent’s first command triggers Xvfb auto-launch, or when access to the user’s real session display is granted. WebRTC negotiation (SDP offer/answer + ICE candidates) is multiplexed over the existing dashboard WebSocket. When the verified dashboard-control DataChannel is connected, local display input authority requests and keyboard/mouse input can use that daemon-scoped control tunnel; video media still flows through the per-display WebRTC session.

  • New virtual display (empty state + display picker) — create a virtual display keylessly: the daemon launches an Xvfb through the same machinery agent sessions use, registers a capture session, and every connected dashboard gets a streaming tile (create_virtual_display, gated as display.input, on both the /ws and dashboard-control transports). This is how a newly authorized headless box — no display server, no API key — gets a working display; agents can then target it for computer use. The created display is daemon-owned: it never touches the “Your display” opt-in, and it is destroyed (Xvfb killed) when any dashboard closes its tile; after a hard daemon kill it is reclaimed like any orphaned agent Xvfb on the next allocation. Xvfb is Linux-only; on macOS/Windows the button reports a clear error and “Your display” streams the real desktop instead.

Station

An immersive WASM-rendered control center for the same operational surfaces as the rest of the dashboard — Activity, Context, Managed context, Changes, Sessions, Peers/displays, and Control. The station-web crate draws the whole scene into a single canvas: WebGPU when the browser exposes it (a secure context is required — see Secure Browser Contexts above), with a canvas-2D WASM fallback used automatically when WebGPU is unavailable or forced with ?station_gpu=canvas. One persistent requestAnimationFrame callback is armed while Station is active and has work to animate. Explicit state changes and the 150 ms interaction window paint at display rate; ambient presentation is capped at about 30 fps, with full breathing-HUD raster work throttled to about 10 fps and live video thumbnails refreshed by small drawImage calls. With motion set to zero, a static WebGPU scene re-presents persistent buffers without rebuilding or uploading them (required as a surface keepalive), while the Canvas-2D fallback can park completely when idle.

There is no DOM dock: the rendered scene is the UI. An invisible hotspot overlay mirrors the scene’s interactive elements so they stay reachable from the keyboard. Station actions dispatch through the same control plane as the classic tabs, so anything triggered from Station behaves exactly like its canonical dashboard equivalent. View settings shape the scene: layout (orbital / constellation), mood (calm / cockpit), and fov, motion, ar, and density tuning.

Station is the rendered-canvas counterpart to Activity, not a separate authority surface. Today the scene is a 3D constellation backdrop with the working UI painted as screen-space HUD panels; the destination is action panes living in the scene, and eventually WebXR spatial computing. Activity remains the default DOM control surface while Station carries that immersive roadmap. The dedicated Station chapter carries the architecture, an honest current-state inventory, and the roadmap.

Sessions

A browser of past and current sessions. Four subtabs:

Listing is fast by construction: per-session summaries (tokens, costs, day buckets, disk sizes) are persisted under ~/.intendant/cache/session_index/ keyed by exact file fingerprints (length/mtime/ctime/dev/inode), so a daemon restart re-parses only sessions that actually changed instead of every log in every store. The daemon warms the list at startup, responses are served stale-while-revalidate (an expired cache answers instantly and refreshes in the background), and the Stats tab fetches a slim view=usage variant (~a tenth of the full-row payload) for its whole-corpus fold. Summaries are list-sized by design — day buckets and fingerprint digests, never per-request usage history or per-file stat lists — so the resident cache scales with session count, not transcript length, and the startup sweep prunes entries whose sessions have been deleted. The index directory is safe to delete; it rebuilds on the next scan.

  • Recent — recent sessions as calm three-line cards (title + status chip + source badge; task snippet; compact meta) — the long tail (ids, absolute dates, token breakdown, disk) lives in the meta tooltip and the detail overlay. A stat-tile strip aggregates the filtered set. The list retrieves the complete history: the stream’s quick phase paints the newest ~600 immediately, then the full corpus replaces it, so the tiles agree with the Stats tab. Lists render 300 cards and grow with an explicit Show more control; the first load shows skeleton rows while the list streams. “Changed” timestamps and the newest-first order follow transcript activity (session.jsonl mtime), not daemon bookkeeping writes into the log dir. Child sub-agent sessions are hidden by default; enable Show subagents to include them. Fork and side sessions stay visible with lineage chips that point back to their parent session. Opening a card raises the detail overlay, whose transcript doubles as the lineage explorer: eligible rows grow a hover ⑂ fork affordance (claude-code message anchors, codex turn boundaries — “the child keeps everything above and redoes from here”), abandoned claude-code branches render dimmed with a forkable branch tip chip, and the collapsed Fork points panel lists the complete catalog (native rounds, codex item anchors, anchors beyond the loaded pages). When a peer daemon is connected, a host strip appears above the toolbar: pick a peer chip to browse that daemon’s sessions in place (read-only cards; clicking one hands off to the peer’s own dashboard, where the peer’s own auth applies).
  • Deep Search — search across session history.
  • Worktrees — the git worktree inventory (same card + Show-more treatment): per-checkout size, merge/dirty state, and safety verdicts; aggregate tiles including free disk on the tightest worktree-hosting volume (amber under 10% free, rose under 5%) and reclaimable build output; and related-session chips — the sessions observed inside each checkout, supervised and raw Codex/Claude/Kimi/Pi alike. Clicking a chip focuses the live session window when one exists, otherwise it lands on Recent with the ID prefilled. Checkouts with a CACHEDIR.TAG-marked Cargo target/ offer Clean target/ — delete the build directory to reclaim its bytes without removing the worktree (sources and git state untouched; a warning notes active sessions first).
  • New Session — start a fresh session from the dashboard. Internal-agent launches get an Execution control — Auto (the task-size heuristic decides), Orchestrate (delegates to supervised sub-agents), or Direct (single agent); an explicit choice beats the global Direct header toggle, and the control disables when an external backend is selected. Internal launches also preflight the daemon’s fueled state: with no API key or vault lease, an inline banner explains why the internal agent can’t start (external agents sign in with their own accounts and still work) and deep-links to Settings → API Keys — which applies immediately, no restart. External Codex sessions can choose the binary path, a model, a compatible reasoning effort, sandbox and approval policies, managed_context mode (vanilla or managed), context replay mode, and Fast service tier for that session. The model picker derives its normal choices and per-model reasoning levels from the signed-in Codex account’s models_cache.json; hidden entries stay out, and API-key auth also filters subscription-only models exactly as Codex does. A conservative offline catalog and a Custom-id escape cover pre-fetch, staged, or private models. Blank inherits the global/Codex default; when a selected model cannot inherit the global effort, the picker explicitly selects that model’s advertised default. Model and effort pins persist across attach/resume. The external-agent options sit in a fold that opens when an external backend is selected. Claude Code sessions get per-launch dropdowns for the model (version-safe aliases — fable, opus, sonnet, haiku — that the CLI resolves to the latest release, with a Custom-id escape for full model names), the permission mode, and the reasoning effort (lowmax). The backend pick, binary path, model ids, and effort choices from the last launch submitted in this browser prefill the next visit. Kimi sessions get model (K2.7 Coding, K2.7 Coding Highspeed, K3, inherit, or a custom id), thinking (off through max), permission (manual/auto/yolo), exact active tools (unset/inherit is distinct from an intentionally empty set), and explicit plan/swarm toggles. The Pi controls expose its binary, free-form model pattern or provider/model id, thinking level, and exact active tools (inherit, deliberately none, or a name list). Model and thinking can apply live after launch; tools are launch-time because Pi’s public RPC has no active-tool mutation command. The binary/model/effort/thinking convenience values use per-browser localStorage; a remembered value that is no longer a valid option — or a backend the daemon now reports missing — falls back to the inherit default. The authority- and cost-shaped fields — sandbox, approval policy, permission mode, managed context, context replay, Fast tier, and Kimi plan/swarm — deliberately re-seed from the global Settings defaults on every visit so a one-off escalation or execution-shape change never becomes a sticky default.

Settings → Providers & models exposes the daemon’s global Codex, Claude, Kimi, and Pi defaults independently of whichever backend is currently selected. With an attached project, Save writes that project’s intendant.toml; a projectless daemon writes <state-root>/intendant.toml (normally ~/.intendant/intendant.toml) while /api/project-root remains null. These defaults seed new sessions, but an existing session’s persisted launch config continues to control its next attach/resume. Pi’s Settings fields persist directly to [agent.pi] rather than creating a daemon-wide Pi runtime-config mirror. Reattached Pi sessions merge the current TOML base with their persisted overlay (the overlay wins), while supported live changes use RPC.

Internal sessions’ window menus additionally expose Delegate… — spawn a supervised sub-agent (task, optional name, role, backend, worktree isolation) under that session on the model’s behalf. The parent is notified with a follow-up and collects the result with its wait_sub_agents tool; see Native Multi-Agent Orchestration.

External-agent session cards and Activity windows also expose Launch config for per-session binary and managed-context settings. Use Save to update the next attach/resume, or Save & restart to apply the new binary/mode immediately to that external backend. These settings are stored with the Intendant wrapper session and, for canonical backend session IDs, in an external-session overlay. They are used on the next attach/resume so a daemon restart or page refresh does not fall back to the current global Settings pane. Claude Code sessions get four pinnable rows: model (the same version-safe aliases as New Session, plus a Custom-id escape), permission mode, allowed tools (comma-separated rules; all pins the explicitly-unrestricted empty list so a session can escape a restrictive global list), and reasoning effort. Every field saves as a pin or the explicit inherit clear — and “default” is a real permission mode that pins, unlike the other fields’ clear sentinels. On a live session, Save additionally applies the model and permission pins immediately (native set_model / set_permission_mode control requests); tools and effort take effect at the next launch or via Save & restart.

Kimi sessions get seven pinnable rows: binary, model, thinking, permission, plan mode, swarm mode, and exact active tools. The tools row distinguishes Inherit profile, None, and an exact name list; an empty list really disables all optional tools. Save applies every profile field live through Kimi’s native session APIs; Save & restart is needed only to replace the binary/process. Dashboard Control and Station expose the same live model, thinking, permission, plan, swarm, and active-tool controls. Kimi’s action menu derives from its advertised actions — including compact, head fork, side, undo, archive/restore, goals and native budgets, review, normal/highspeed model switching, model/tool catalogs, exact tool changes, context clear, and background-task control — rather than a backend-name heuristic.

Pi sessions get four pinnable rows: binary, model, thinking, and exact active tools. Save applies model and thinking live and persists the resulting pins; binary/tools apply on the next attach or through Save & restart. The action menu derives compact, fork, side, rename, model, and thinking from the advertised Pi capability list. It does not show goals, native sub-agents, review, rollback, plan/todo, or MCP controls that upstream Pi does not have.

The separate Restart with saved config action is a power-user shortcut for reapplying settings that were already persisted elsewhere. The Managed activity view exposes rewind anchors, saved records, restore, and fork/backout actions. With the patched managed Codex binary, fork/backout starts a new Codex thread while inheriting the saved rollout’s lineage prompt-cache key; there is no separate cache-reset opt-in in the dashboard. Editable user-message entries still perform an in-place rollback when the message is active in the current thread. Superseded user-message entries in a managed Codex session show the same edit control as a historical branch action: submitting the text creates a child thread from the newest saved pre-rewind rollout containing the clicked message, rolls that child back to the selected turn, and sends the replacement there. The edit chip labels this as branching so the active compacted session is not mistaken for the target of the mutation.

Files

Edit, browse, download, and upload files on the local daemon or a configured peer target. The tab is split into two sub-tabs: Editor (the default) and Transfers (the download/transfer-history/upload tooling). The target summary uses the same access abstraction as Terminal: local/mTLS and peer dashboard-control routes are shown as targets with their available capabilities rather than as transport internals. Hosted Connect directory records are not Files or Terminal targets by themselves: they carry no authority and appear as discovery/remembered metadata. After navigation and trusted approval, a daemon-origin Operate lease may expose the exact hosted Files/Terminal routes; View and Tasks never do.

The daemon-side durable state behind this surface — staged uploads (uploads/<session-id>/ blob + sidecar) and transfer job metadata (transfers/jobs, plus daemon-materialized transfers/artifacts) — lives under <project>/.intendant/ on project-rooted daemons. A projectless daemon (the macOS app daemon, or any intendant launched from a directory with no project marker) serves the same endpoints from a daemon-global fallback store at ~/.intendant/global-store/ with an identical layout; a project root, when present, always wins. The global store is pruned on daemon startup: upload session dirs, job files, and materialized artifacts idle for more than 14 days are removed (project stores are never pruned).

The Editor sub-tab is a full-bleed workbench: a slim toolbar (target picker + one-line route summary + new file/folder), a lazy directory tree rail on the left (rooted at the project root locally, ~ on peers; hidden-file toggle; hover-revealed per-row rename and delete — rename edits the name inline, delete is a two-click arm-then-confirm that escalates to an explicit “Delete all?” for non-empty folders), and a multi-tab CodeMirror editor filling the rest (vendored bundle, static/codemirror-bundle.js, lazy-loaded on first use; syntax highlighting by filename, dirty markers, hover-reveal close, a Reload-or-Overwrite conflict banner, Cmd/Ctrl-S, and a Cmd/Ctrl-F find bar with smart-case matching, live counts, and Enter / Shift-Enter stepping). Renaming a file whose buffer is open retargets the tab in place (same undo history); deleting one closes a clean tab and flips a dirty one to the missing-file banner so unsaved work survives. One accent answers “whose disk is this?”: the active editor tab’s underline, the tree selection, and the statusbar host chip render blue while editing this daemon and mauve on a peer (--files-accent). Reads and writes ride the same fs API family as everything else and are therefore IAM-scoped end to end:

  • Local targets use GET /api/fs/stat|list|read and POST /api/fs/write|rename|delete (all classified FilesystemWritewrite_roots for mutation, and gated by authorize_http_filesystem_access exactly like mkdir — a rename authorizes both legs, since removing the source and creating the destination are each writes).
  • Peer targets ride the peer’s dashboard-control tunnel: api_fs_stat/list requests, api_fs_read byte streams, api_fs_write upload frames, and api_fs_rename/api_fs_delete requests. Enforcement happens on the receiving daemon against its own peer profile (file-operator vs file-reader) and per-peer filesystem roots; the browser only picks where a request is sent, never whether it is allowed.
  • Saves are conflict-checked: full reads return the content’s sha256 (X-Content-Sha256 header on HTTP, sha256 in the byte-stream result), the editor sends it back as expected_sha256, and a mismatch returns 409 {code:"conflict", current_sha256}, which the UI surfaces as a Reload-or-Overwrite banner instead of clobbering. New files save with create_new; force is the explicit overwrite escape hatch. Writes land atomically (same-directory tempfile, fsync, permission-preserving rename).
  • Guardrails: binary or non-UTF-8 and >2 MB files are refused with a pointer to the Downloads flow; per-request write payloads cap at the shared 100 MB upload limit; UTF-8 files keep their dominant line-ending style on save. A rename never replaces an existing destination (409 code:"exists") and refuses cross-filesystem moves; deletes remove symlinks as links (never following) and require an explicit recursive for non-empty directories (409 code:"not_empty").

Access

Unified administration for how dashboards and daemons reach each other:

  • Access is available as both the in-dashboard Access tab and a first-class /access page on daemon origins. The page opens the same Access surface without task/session chrome so it can act as a local fleet-admin home.
  • The surface is organized around the product mental model — who is acting, how the request travels, and the target daemon decides — in six panes:
  • Overview answers “who am I here and what can I reach”: actionable attention banners first (failed routes, a hosted Connect account the daemon refuses, draft grants, unreadable IAM state), then an identity hero (your principal, the route you arrived on, your role and what it allows), a daemon card with fleet/people/peer counts, a fleet-at-a-glance strip, and a three-step explainer of the access model.
  • Daemons lists targets, not raw transports: each row shows a clean display name (never a bare IP or key when a better name exists), a route chip, your role badge there, capability chips, and Stats/Files/Shell/Display actions for local, mTLS, or peer-operable targets. Connect-only directory records never become Files/Terminal targets and expose no such actions. The pane also hosts the per-peer messaging/task/approval quick controls and capability routing, both in collapsed poweruser folds.
  • People & Devices owns the user/client domain. It shows your identity on this daemon, a guided grant flow (who → identity details → role → active or draft) with a role-card picker and advanced identity metadata folded away, and every user/client principal with its grants. Grants can be activated, drafted, or revoked inline; draft and revoked records stay visible for review.
  • Peers owns the daemon-to-daemon domain: inbound/outbound summaries, the Link-daemons wizard (Request Peer Access, Join Invite, Grant Peer Invite, Manual Add), inbound peer access requests, approved/revoked inbound peer identities, and every peer-profile grant in both directions.
  • Diagnostics owns route health, including hosted Connect directory/link status, local/mTLS, trusted local WebRTC control, event delivery, byte streams, uploads, and self-tests. The oversight bar’s access dot links here, and its prefix names the actual route (mTLS, Connect, WebRTC, or local) so “Ready” is never ambiguous.
  • Advanced is the poweruser den: the role catalog and the exact policy×permission matrix, every grant with policy/transport/reason detail, the raw model inspector (principals, grants, policies, permissions, IAM roles, audit events, transports), the local IAM state card with raw JSON links, unresolved architecture notes, and the public-sharing posture (nothing is shared unless explicitly granted).
  • Old deep links keep working: #access/policies, #access/audit, and #access/public resolve to Advanced, #access/invitations to Peers, and #access/targets to Daemons.

Access uses one vocabulary across trusted daemon-served loopback/direct-mTLS dashboards and peer federation. The packaged macOS app contains a local mTLS bridge, but no signed/notarized distribution exists for this alpha. Hosted Connect itself contributes directory/navigation metadata only. A daemon may separately expose the optional hosted-control lane at its fleet origin:

  • A target is a daemon the dashboard can operate. A Connect-linked directory record is navigation metadata, not authority; it becomes operable through a separately trusted direct route or an approved hosted lease.
  • A principal is the actor being trusted: the current local session, an mTLS browser certificate, or a peer daemon. Browser identity-key records and future organization groups also exist in the model, but keys are not an active general alpha login credential. An exact hosted_lease is a separate short-lived principal bound to a tab key. connect_account remains in the IAM vocabulary for compatibility, but a hosted account assertion is route metadata and does not authenticate to the daemon.
  • A grant connects one principal to one target with a role and status. A loopback browser or a verified owner-mTLS browser is root-compatible with the local daemon. A hosted-origin key may exist in IAM and may be assigned a scoped grant for use after trusted re-enrollment, but presentation through Connect cannot exercise that grant. A hosted lease is written only by the dedicated trusted-confirmation transaction and expires automatically. A peer route has a daemon peer-profile grant. An approved inbound peer identity appears as a peer-daemon principal with a peer-profile grant to this daemon; revoked identities remain visible as revoked grants for audit clarity. Local IAM grants loaded from iam.json are enforced when active and bound to an authentication mechanism the transport actually supplies. In this alpha that browser mechanism is mTLS, not a browser identity key. A human_user record can carry account/provider and organization metadata, but Connect account metadata alone is not an authenticating binding. Draft and revoked records remain visible for review.
  • A policy defines the shape of authority behind a grant. root and peer-profile are enforced today, but they live in different domains: root is user/client authority and peer-profile is daemon-to-daemon authority. Local user/client bindings can also use enforced scoped roles: scoped-human (access model inspection only), observer, session-reader, terminal, files-read, files-write, peer-user, and operator. Hosted leases use separate compiled View, Tasks, and Operate presets; persisted role rows do not define their permissions. Directory-scoped file access, public shares, organization groups, and external identity policy are design targets, not hidden enforcement.
  • A permission is the operation gate the daemon enforces. Access administration now separates access.inspect from access.manage, and peer topology separates peer.inspect, peer.manage, and peer.use. peer.use is the delegation gate: acting through a connected peer — opening a tunnel (dashboard-control, file-transfer, or display signaling) or sending it a message, task, or approval decision, directly or via coordinator routing — presents this daemon’s peer credentials, and the receiving peer authorizes each action against its own grants for this daemon — so relaying is never inferred from local capabilities, it is granted by name (operator and peer-user carry it; peer.manage implies it for compatibility). Owner/root dashboard sessions have all of these. Existing peer profiles are mapped conservatively: peer-root can inspect access and inspect/manage/use peer topology, but access.manage remains reserved for trusted root user/client sessions. Display permissions carry the same private-view ceiling: display.view and display.input reach only agent_visible sessions; private user-session displays additionally require an owner/root dashboard.
  • A transport is only how an authorized route is carried: browser mTLS, loopback HTTP, daemon-origin HTTPS/WSS through the reachability relay, trusted daemon-origin WebRTC control, or daemon-to-daemon peer mTLS. Hosted Connect is a directory/link and ciphertext-relay surface, not the authority.

The browser may also maintain a local fleet registry for navigation: daemon ids, labels, remembered URLs, and the route/auth summary last seen from a daemon. This registry is client-side metadata, not an authorization source. If a remembered target is no longer configured on the current daemon, Access shows it as a browser-local record with operation buttons disabled. The daemon still owns IAM enforcement for every request.

The hosted /connect directory maintains account-scoped fleet navigation metadata through GET /api/fleet/targets, POST /api/fleet/targets/sync, and POST /api/fleet/targets/{target_id}/forget. The hosted service stores only navigation metadata: target ids, labels, route labels, URLs, capability hints, and last-seen timestamps. It does not store browser mTLS private keys, daemon IAM grants, peer secrets, dashboard session grants, or passkey private material. Linked Connect daemons are merged into that directory list as live connect_daemon records and override stale remembered labels. Records the browser pushes are signed with its identity key and re-verified after every round trip; target rows and the fleet strip badge each synced record as verified (this browser), signed (another device), unverified, or a hosted claim. The signer key is carried inside the record and is not yet anchored to an owner/device trust set. The current browser detects same-key alteration, but a malicious store can substitute a fresh, internally valid self-signed record on another device. Connect-served code can also wield the hosted origin’s browser key while loaded. These signatures are metadata integrity/attribution hints, not daemon authentication. The former hosted /app?connect=1&daemon_id=... dashboard route is retired and always redirects to /connect. A linked card exposes Request control only when the daemon registration carries a valid signed hosted-capability hint and the daemon has a relay-mode fleet-DNS route. The action is an ordinary navigation to the daemon’s fleet-origin public doorbell; it carries no Connect assertion or authority. Direct mTLS dashboards on daemon origins remain local-first; cross-origin sync to intendant.dev is a separate explicit-consent design problem, not something the current cookie model does silently.

The important security-domain split is:

  • User/client daemon access means a human-operated trusted dashboard can control a daemon. Browser/native mTLS certificates are the shipped remote credential; browser identity keys are record-only in this alpha. Unbound browser mTLS owner sessions remain root-compatible only when the browser is loopback or presents the verified owner client certificate. Hosted Connect passkeys authenticate only the directory account; Connect browser signaling is disabled at the service and legacy Connect control events are dropped by the daemon. A local generic grant does not turn that page into a control session. The optional fleet-origin lane instead authenticates an exact, approved hosted_lease with fresh tab-key proofs. Future coworker/team access belongs on trusted user/client surfaces, not in peer federation.
  • Peer access means one daemon can call capabilities on another daemon. That uses daemon-to-daemon mTLS identities and peer profiles such as peer-operator or peer-root. Peer access does not imply that the human’s browser can open the remote daemon directly, and browser access does not imply that two daemons can federate.

The model is backend-backed. GET /api/access/overview and the dashboard-control api_access_overview method return schema version 1 with scope, targets, principals, grants, policies, permissions, transports, supported_principal_kinds, iam, and explicit unresolved architecture notes. Root dashboard sessions and peer daemon sessions now pass through the shared IAM operation evaluator, while preserving the existing root and peer-profile semantics. The overview still exposes one product model over the current transport/auth paths rather than replacing mTLS, browser-key checks, or peer profiles.

The local IAM foundation lives beside the native access cert store as iam.json and is also available at GET /api/access/iam/state or dashboard-control api_access_iam_state. Its schema contains principals, roles, grants, audit_events, and daemon-local hosted_control state. The daemon exposes this state for inspection, merges managed principals/grants into the unified overview, and enforces active scoped user/client grants when a request can be bound to a stable local principal. Today the shipped browser binding is a browser/native mTLS client-certificate fingerprint. Browser identity-key records are staged but not consumed by direct /ws or dashboard-control offers; human_user records can carry optional account provider, verified-provider, handle, and organization metadata without making that metadata an authenticator. Requests remain root-compatible without a stored binding only when they are loopback or present the verified owner mTLS certificate. Certless remote --tls supplies HTTPS and a secure context, not daemon authority; remote protected HTTP, WebSocket, and dashboard-control routes require mTLS. --allow-public-plaintext never opts a remote caller into ambient root. Hosted Connect has no root fallback and its browser offer/ICE/close path remains refused before mutation. The daemon drops those events from old/self-hosted services before inspecting a browser key or grant. Separately, fleet-origin code can create a signed public doorbell when hosted_control_enabled is true; only local/direct-mTLS owner surfaces can approve it, and the resulting lease is evaluated under a dedicated ceiling and immutable floor. Active generic grants are evaluated by role on trusted routes, while draft or revoked records deny instead of silently becoming root again. The iam.enforcement object reports root_session_grants: true, peer_profile_grants: true, user_client_grants: true, and principal_binding: root_peer_and_local_user_client. Root sessions can create or update local user/client grants through the People & Devices pane, POST /api/access/iam/user-client-grants, or dashboard-control api_access_iam_upsert_user_client_grant. Existing grants can be activated, drafted, revoked, or role-changed with POST /api/access/iam/grants/update or dashboard-control api_access_iam_update_grant.

The Access pane’s Hosted control card is manage-gated and hidden from authority-free fleet pages. It shows the daemon’s current View, Tasks, or Operate ceiling; pending daemon-signed requests; active leases; and the hosted-eligible session set. Trusted local and direct-mTLS owner sessions can reduce and decide a pending request, revoke a lease, change the ceiling, or mark an existing session eligible through /api/access/hosted-control/*. Approval can only reduce the requested preset/lifetime. An integrated daemon requires the explicit hardening acknowledgement before Operate. Lowering the ceiling revokes leases above it; raising it leaves existing leases unchanged. The card cannot edit the compiled floor, which excludes IAM/access, credentials/vault unseal, organization-root operations, approval resolution, peer administration, settings/API-key management, and its own ceiling from every hosted preset.

An active grant whose only credential is a browser key is refused when that key records a hosted or rendezvous-controlled fleet origin. Renaming the principal human_user does not change that composition check. A human record that also carries a valid browser mTLS certificate remains grantable; its key and account fields are metadata beside the certificate authenticator. Older IAM files can still contain active hosted/fleet pure-key grants, but the access overview projects them as status: inactive_binding, enforced: false, and authority: none, and the lifecycle API refuses to activate them.

The grant flow’s Apply to step fans one grant out across the fleet: the page calls each selected daemon’s POST /api/access/iam/user-client-grants directly and reports per-daemon results; every target authorizes independently. Cross-origin use of the fleet Access APIs (/api/access/overview, /api/access/iam/state, /api/access/iam/user-client-grants, /api/access/iam/grants/update, /api/access/enrollment-requests[/decide]) is origin-gated per daemon: only the daemon’s own origin, the macOS app scheme, its outbound peer routes, and its approved inbound peer identities may drive them, and responses are never wildcard-readable. Requests from any other page are refused outright, so a browser-installed mTLS certificate cannot be steered cross-site. Reach the Access page by an origin the fleet advertises (the target rows already link that way) for cross-daemon administration to work.

The same posture now applies daemon-wide: authority-bearing responses carry no Access-Control-Allow-Origin by default (same-origin only), and every request for a non-authority-free route bearing a foreign Origin is refused before IAM or transport-authority resolution. Cross-/same-site Fetch Metadata closes the top-level-navigation and subresource cases where browsers omit Origin. /config is not public: it requires presence.read, echoes only the daemon’s own or a fleet-allowlisted Origin, and returns Cache-Control: no-store because ICE configuration can contain TURN credentials. Only authority-free shell/static bytes, the agent card, /connect/bootstrap, /connect/status, the hosted bootstrap/request/poll doorbell, and public signed-document doorbells remain public; those requests always use an anonymous role:none context even on loopback or when a client certificate is present. A successful hosted poll returns a lease document but does not make the public request itself an authority-bearing session. Authority-bearing direct signaling under /connect/dashboard/* and the legacy /ws event lane accept browser requests only from the daemon’s own origin or the local packaged-app scheme, before mTLS/local transport facts become a grant. For cleartext browser traffic, “own origin” additionally requires localhost or a literal loopback address; merely matching attacker-controlled Origin and Host values is not enough, which closes the DNS-rebinding route into local root authority. Non-loopback browser access uses HTTPS/mTLS. Native and daemon clients that do not send a browser Origin remain transport-authenticated as before. The macOS app is unaffected: its custom-scheme pages are proxied natively, and the intendant:// scheme is treated as the daemon’s own origin.

Fleet/relay HTTPS requests take a separate path when hosted control is enabled: each protected request carries a tab-key proof bound to its lease, daemon, origin, method, raw path/query, nonce, and timestamp. /ws consumes a seconds-lived one-use ticket minted through such a proof. Neither mechanism changes relay provenance into trusted-local authority.

Device enrollment has implemented queue/state/UI plumbing, but it is staged in this alpha: the production queue intentionally has no writer, direct /ws and dashboard-control offers do not present a browser identity-key proof, and raw hosted/fleet browser-key traffic is refused before enrollment. The GET response reports status: staged and writer_available: false; ordinary alpha traffic therefore cannot create a request or a usable key-login enrollment. GET /api/access/enrollment-requests / api_access_enrollment_requests list the queue (access.inspect), and POST /api/access/enrollment-requests/decide / api_access_enrollment_decide (access.manage) approve with a role or deny. Approval reuses the normal user-client grant upsert with the queued key’s public key and route origin attached, so the ordinary active-binding validation and audit apply for test-seeded/legacy records. People & Devices can render such records, but its queue is empty in production and remote alpha enrollment uses mTLS certificates.

The same IAM evaluator now protects the direct dashboard HTTP routes that expose Access, target discovery, settings, filesystem reads/writes, sessions, worktrees, displays, diagnostics, and managed-context data. Static bootstrap assets, /config, /.well-known/agent-card.json, local Connect status, direct dashboard signaling, and the WebSocket bootstrap stay outside this generic HTTP IAM gate because they either do not mutate daemon state or have their own same/app-origin plus transport/authentication binding.

GET /api/dashboard/targets and api_dashboard_targets remain the compatibility target model used by older UI paths: target id/host id, display label, access domain (user_client or peer), route (current_dashboard or peer_route), effective role (root or peer_profile), connection state, and capability hints. The browser may refine the local route label to Intendant Connect, Browser mTLS, or Local/debug because only the browser knows how the current page was reached, but it should not invent principal/grant/policy vocabulary.

GET /api/dashboard/tabs / api_dashboard_tabs (access.inspect) is the tab-presence surface: every live dashboard connection on either event lane (the /ws WebSocket or a dashboard-control tunnel session), each entry carrying its lane, grant provenance (local / client / peer), grant label, remote host, user agent, connect time, and whether it currently holds the voice presence or display input authority (with display ids). Browser tabs group by a client-declared per-tab id: the SPA mints a random id in sessionStorage and sends it as ?tab= on the /ws URL and tab_id in control-tunnel offers. The id is sanitized, display-only, and grants nothing; server-internal connection ids never appear in the payload (the same rule the input-authority wire follows). The Access Overview renders this as the Open dashboards card — “N tabs connected · this tab · holds voice / display input” — refreshed on pane entry and every 15 s while the pane is visible; peer-daemon control connections are counted separately from tabs. A Connect-origin tab cannot create an entry: the service refuses browser signaling and the daemon drops legacy Connect control events before the registry. A fleet-origin tab admitted with a one-use hosted WebSocket ticket does appear, labeled by its hosted lease rather than a Connect account.

Vault

The v2 dashboard gives credential custody its own top-level home. It reuses the existing Credential vault, Agent accounts, and Custody trail sections (the standalone /access page keeps them under Access → Advanced): create or unlock the client-encrypted sealed vault with a passkey or recovery phrase, manage provider credentials and external-agent sign-ins, fuel daemon sessions with time-boxed leases, and inspect lease/egress custody events. Secrets are decrypted only inside the verified vault worker while unlocked; full-credential leases may temporarily materialize a provider’s native auth files, as described in Credential Custody.

The shipped dashboard stores the sealed blob on the daemon through its trusted control channel. Connect retains an account-vault storage API, but the default Connect page has no vault client and there is no bridge from a Connect-origin account vault to a trusted daemon session in this alpha.

Debug

Observer display and browser workspaces (daemon diagnostics and raw event streams live under Access → Diagnostics). The observer display is a headless debug screen the agent can draw on, recordable from here. The browser-workspace panel does manual smoke testing of local CDP-backed browser workspaces and their leases. CDP workspaces prefer managed Chromium/Chrome-for-Testing executables; on macOS system Chrome/Chromium apps require choosing system_cdp or setting INTENDANT_BROWSER_WORKSPACE_ALLOW_SYSTEM_BROWSER=1. Run intendant setup browsers to install or repair the managed browser cache.

Settings

The configuration panel for the current session: API keys, external-agent backend settings, computer-use/provider options, presence, transcription, recording, live audio, and the runtime sandbox. Peer/network administration moved to Access. Old #settings/network deep links are redirected to #access/overview.

Settings → Security holds the runtime write sandbox card (“Runtime sandbox” — the confinement of the native agent’s shell and file tools to granted paths, [sandbox] in intendant.toml): a live on/off toggle, the effective write-grant set, and an extra-grants editor (one path per line, absolute or project-relative). Saves ride the regular /api/settings flow and apply to new commands immediately — no restart; when --sandbox/--no-sandbox pinned the state for this daemon run, the toggle is disabled and saves only persist intent for the next start. This is a different layer from the Codex/Claude Code sandbox settings — external agents bring their own.

Late-join and session replay

The gateway is built so a browser that connects late sees the full picture immediately. On WebSocket connect the server sends a sequence of bootstrap messages:

  1. state_snapshot — the full AgentStateSnapshot plus this connection’s id, the /config payload, and the session_id
  2. Cached usage_update — latest token usage
  3. Cached status — latest autonomy / session id / task
  4. Cached display_ready — latest display info for WebRTC sessions
  5. browser_workspace_snapshot — active browser workspaces and lease state
  6. log_replay — historical session events parsed from session.jsonl

So refreshing the page, or opening a second browser mid-run, replays the session rather than starting from blank.

Live voice (optional)

The dashboard supports optional low-latency voice via Gemini Live or OpenAI Realtime. Voice is entirely optional — the dashboard is fully usable without it.

When activated:

  • The browser connects directly to the model’s realtime API for voice I/O.
  • The WASM layer (presence-web) handles mic capture, resampling, and WebSocket streaming.
  • The live model receives agent events and narrates progress, and can call presence tools (submit_task, approve_action, check_status, …) which are routed over the dashboard WebSocket to the server.
  • Server-side text presence is automatically paused (the two are mutually exclusive).

Voice setup

  1. Choose the live provider in Settings.
  2. For Gemini, enter the long-lived API key in the browser. An unlocked client vault is preferred; per-origin browser localStorage is the legacy fallback and migration source. The browser sends that key directly to Gemini Live, not to the daemon.
  3. For OpenAI, configure or lease the credential to the daemon. The browser requests a short-lived Realtime client secret through api_voice_session (or POST /session when the control tunnel is down) and never receives the daemon’s long-lived OPENAI_API_KEY.
  4. Click the microphone button. After either credential flow, realtime audio connects directly from the browser to the selected provider.

Active vs. passive browsers

Only one browser can be the active voice controller at a time:

  • The first browser to connect voice becomes active.
  • Additional browsers are passive observers — they receive events but do not pause server-side presence.
  • A passive browser can request active status, which force-disconnects the previous active browser. Handover carries the last checkpoint summary and conversation context.

Session continuity across reconnects

The presence session protocol survives refreshes and dropped connections:

  1. On connect the server sends a presence_welcome with current state, missed events, and conversation context.
  2. The browser sends periodic presence_checkpoint messages summarizing the conversation.
  3. On reconnect the server replays events since the last checkpoint.

This keeps the voice model from losing context. The protocol and mutual exclusion are detailed in Presence Layer.

Server-side transcription

Independently of browser-side voice, the server can transcribe microphone audio via the Whisper API when [transcription] is enabled (or --transcription is passed):

[transcription]
enabled = true
provider = "openai"
model = "whisper-1"
language = "en"

The browser streams PCM16 audio; the server buffers it in ~3s chunks (buffer_secs, RMS-filtered to skip silence) and sends each chunk to the transcription endpoint. Transcripts are broadcast as user_transcript events and written to the session log. See Configuration.

Secure Browser Contexts

  • Microphone/camera require a secure context. Plain http://<host-ip> is not a secure context in the browser, so getUserMedia is blocked there. Reach the dashboard over one of:
    • http://localhost (e.g. an SSH tunnel: ssh -L 8765:localhost:8765 host),
    • HTTPS/mTLS via the default dashboard transport, --tls, or [server.tls] (see below), or
    • the macOS app bundle, which serves the page over a custom intendant:// scheme specifically to restore the secure context (see Getting Started). The bundle starts its daemon with native mTLS by default so remote browsers get a safe context over https:// and must present an enrolled client identity.
  • Voice credentials: Gemini’s long-lived key is held in the browser (preferably in the unlocked client vault; localStorage is the legacy fallback). OpenAI’s long-lived credential stays with the daemon or a lease; the browser receives only a short-lived Realtime client secret.

HTTPS / TLS

./target/release/intendant                       # default: mTLS, requires access certs
./target/release/intendant --tls                 # TLS-only; installed server cert/key when present, else self-signed
./target/release/intendant --no-tls --bind 127.0.0.1 # explicit local plaintext/debug escape
./target/release/intendant --tls-cert c.pem --tls-key k.pem   # bring your own

By default, the gateway serves HTTPS/WSS with browser client certificates required. --tls (or [server.tls] enabled = true) makes the gateway serve HTTPS/WSS without requiring client certificates. With no explicit cert/key override, TLS-only uses installed access server certs when present and falls back to an auto self-signed certificate. Plain HTTP via --no-tls is intended for local/programmatic debugging; wildcard plaintext refuses startup when the host has a public interface unless --allow-public-plaintext is passed. TLS-only mode loads no client CA. It gives remote browsers HTTPS and a secure context, but certless remote requests to protected HTTP, WebSocket, and dashboard-control routes are still denied unless another authenticated peer/hosted-lease identity applies; use the default mTLS mode for browser client certificate authentication. The gateway demuxes per connection: a first byte of 0x16 (a TLS ClientHello) is wrapped in the rustls acceptor, while raw WebRTC ICE-TCP/UDP media is left untouched. The TLS stack is pure Rust (rustls + rcgen) and works on every platform including Windows — no nginx, no OpenSSL. See the [server.tls] keys under Configuration → [server].

For explicit mutual-TLS with client certificates (only enrolled devices can connect), use native --mtls / [server.mtls]; this is also the default when no transport flag is supplied. Use intendant access setup to generate the per-user access CA/server/client certs and run strict enrollment. See Getting Started → Dashboard access over TLS and Peer Federation. For the daemon posture and remote control surface, see Control Plane & Daemon.

WebRTC Dashboard Control Tunnel

Intendant also has an experimental daemon-scoped WebRTC DataChannel transport for trusted dashboard control traffic. It is not authentication: the browser bootstraps from the daemon’s normal dashboard origin, then uses that daemon’s local /connect/dashboard/* signaling endpoints. Loopback callers may use the local-root posture; remote callers must present mTLS. Certless --tls provides only a secure context, and --allow-public-plaintext grants no authority. The existing WebSocket lane remains as a compatibility fallback.

The handshake is bound to the daemon identity:

  • the browser creates a intendant-dashboard-control DataChannel and sends an SDP offer to /connect/dashboard/offer;
  • the daemon answers with SDP plus a signed binding over the offer hash, answer hash, session id, timestamp, and daemon Ed25519 public key;
  • the browser verifies that binding with WebCrypto before using the channel.
  • the retired Connect-rendezvous prototype additionally compared the directory’s daemon key; that path is unreachable in the default product and remains only in negative mixed-version tests.

The tunnel can be the primary event lane — dashboard events and intents flow through it instead of the legacy /ws — for an authenticated daemon-origin dashboard, a locally built loopback packaged macOS app, and the capability fallback. There is no remote signed-native authentication bridge in this release. WebKit browsers (Safari) share that WebSocket limitation — against an mTLS daemon the /ws hangs in CONNECTING forever while fetch/XHR keeps working — so a plain browser tab whose /ws has not opened within a few seconds of an attempt promotes the tunnel automatically; the event-lane reconnect machinery then owns start, api_dashboard_bootstrap hydration, retry, backoff, and status. The promotion is derived, never stored: it clears the moment a /ws proves able to open (dual delivery is absorbed by event dedup), and intent sends that find no open lane surface an actionable error and kick the fallback instead of dropping silently.

When enabled with localStorage.intendant_dashboard_transport = "webrtc-control" (or window.intendantDashboardControl.enable()), dashboard JSON reads prefer the DataChannel and fall back to HTTP through the browser-side DashboardTransport boundary. Current tunneled reads include the local Agent Card identity, sessions, session detail, lazy command-output loads for the active session, active-session timeline history, active-session changes/diffs, lazy exact context-snapshot loads, filesystem picker stat/list/mkdir operations, deep session search, settings, API-key status, server-side voice-session token minting, project root, display enumeration, recording metadata, worktree inventory, staged-upload descriptors, scoped recording asset byte streams, archived session frame image byte streams, bounded session-report zip downloads, and peer state. Managed-context history reads for records, anchors, and fission groups also use the tunnel. Current tunneled mutations include active-session rollback/redo/prune, session-data deletion, staged upload deletion, settings save, API-key save, peer add/remove, peer access-request pairing, peer message/task/approval actions, peer-display WebRTC signaling, eligible-peer lookup, visual-freshness diagnostics NDJSON appends, worktree scan/remove, dashboard managed-context MCP tool calls, coordinator routing, and dashboard session-control and dashboard-action controls. Annotation attach/save/send and clip creation use a dedicated dashboard media/editor protocol over the same verified channel: annotation image bytes travel as upload_* frames committed by api_media_annotation_attach or api_media_annotation_submit; clips use api_media_clip_start, ordered api_media_clip_frame uploads keyed by clip_id, then api_media_clip_end or api_media_clip_cancel. Allowlisted settings-style ControlMsgs, such as autonomy, approval-rule, external-agent, Codex, and verbosity settings, can also dispatch over the DataChannel when it is verified. Display input authority uses dedicated DataChannel RPCs and a display_input frame rather than the generic ControlMsg allowlist. The Shell terminal tab uses dedicated terminal_* frames over the same verified channel. Session lifecycle, steering, approvals, interrupt, resume, stop/restart, rename, and launch-config changes use a separate api_session_control_msg RPC with its own allowlist instead of broadening the generic settings-style api_control_msg. Smaller dashboard action controls use api_dashboard_action_msg; this includes Codex thread actions, display take/release/grant/revoke, the diagnostics visual-marker toggle, recording and debug toggles, and browser workspace create/acquire/close/release. It has its own allowlist and the same no-replay fallback rule as the other mutation RPCs. All three control-message RPCs are multiplexers, not authorization buckets: after admitting the outer method, the daemon authorizes every parsed inner ControlMsg against that action’s operation and any extra owner requirement. The outer method can never launder its coarse permission into another action’s authority. In particular, GrantUserDisplay and ResolveDisplayRequest are owner/root-only, RevokeUserDisplay remains available for de-escalation, and StartRecording plus the pre-encoder diagnostics marker are owner/root-only for the alpha. Setting up the debug screen and starting its recorder are also owner/root-only; stopping a debug recording or tearing its screen down remains available for de-escalation. Recording also requires an active agent-visible display; private views never produce artifacts. The legacy /ws control lane applies the same inner-message authorization, so switching transports does not change authority. Mutation fallbacks are deliberately conservative: if a connected WebRTC RPC fails after it may have reached the daemon, the dashboard surfaces the error instead of repeating the write over HTTP. The visual-freshness sampler follows the same rule for NDJSON appends; it uses the legacy HTTP endpoint only when no verified DataChannel path is available.

The tunnel mirrors HTTP JSON response semantics. Application errors travel as successful transport frames with _httpStatus/_httpOk metadata so existing UI code can render the same error message it would render for an HTTP response. Transport failures, unknown RPC methods, and aborted requests still reject the browser-side promise.

Several paths intentionally stay outside this JSON tunnel:

  • static assets and WASM bundles;
  • native media fallback URLs and transfer paths outside the scoped dashboard byte/upload protocols;
  • general filesystem mutations and durable broad file-transfer queues;
  • generic MCP-over-HTTP for external clients;
  • non-allowlisted ControlMsg mutations;
  • display WebRTC media channels;
  • daemon-to-daemon federation authentication.

Peer mTLS remains a separate trust boundary. The dashboard tunnel authenticates the browser-to-this-daemon control path; it does not grant or replace a daemon’s peer-scoped client certificate for federation.

Connect-Style Local Bootstrap Slice

The daemon also exposes a narrow, experimental Connect-style bootstrap surface for testing the public-origin signaling shape without changing the normal dashboard:

EndpointAuthenticationPurpose
GET /connect/bootstrapPublic, authority-free bytesMinimal HTML bootstrap page for WebRTC dashboard-control transport testing; receiving it grants nothing
GET /connect/statusPublic, authority-free bytesJSON health/capability probe for the bootstrap surface; contains no credential or grant
POST /connect/dashboard/offerloopback local root or remote mTLSBrowser SDP offer -> daemon SDP answer plus signed binding
POST /connect/dashboard/iceloopback local root or remote mTLSBrowser trickle ICE candidate for a control session
POST /connect/dashboard/closeloopback local root or remote mTLSClose a control session

Those paths are deliberately allowlisted one by one; allowlisting is routing, not authority. They do not make /, /config, /ws, /api/*, recordings, or the full dashboard’s protected data without the normal dashboard authentication. The bootstrap page exposes window.intendantConnectDashboard for tests and diagnostics; it verifies the same daemon-signed binding as the full dashboard control experiment, then uses the DataChannel RPC protocol directly. Its small browser-side transport supports plain JSON requests, chunked JSON responses, bounded byte_stream_* downloads, and upload_* frames, so the local bootstrap check can cover both read-style artifacts and media/editor writes without making the full dashboard certless. The bootstrap HTML/status bytes may be fetched remotely without a certificate, but certless signaling and control remain loopback-only. A custom Origin, public plaintext opt-in, or self-signed HTTPS connection from a remote address does not synthesize local root; remote protected paths require mTLS. These endpoints are useful for same-origin dashboard experiments and diagnostics; by themselves they do not solve browser trust for a public page talking to a daemon HTTPS certificate the browser has not already accepted.

Run the focused browser check against a local daemon with:

PLAYWRIGHT_NODE_PATH=/path/to/node_modules \
  node scripts/validate-connect-bootstrap.cjs --origin https://127.0.0.1:8766

The check intentionally uses no client certificate. It must see /config rejected with 401, then prove that /connect/bootstrap can create a verified dashboard-control DataChannel, issue RPC requests, read a bounded byte stream, and commit media/editor uploads over the tunnel.

To test the full dashboard bundle’s local signaling path, run a loopback-only plaintext debug daemon through:

node scripts/validate-dashboard-control-local-signaling.cjs \
  --dashboard-binary ./target/release/intendant \
  --daemon-port 8877

That harness enables window.intendantDashboardControl in the real SPA and asserts that the verified DataChannel reports signalingMode: "local-http".

This slice is a local low-level harness for the dashboard-control tunnel. It does not implement account signup, passkeys, daemon linking, or a durable daemon registry. Its job is to keep the same-origin tunnel protocol easy to exercise while the hosted Connect service owns the account and route-link UX. It is not a claim-authority path; legacy/feature-off hosted acceptance tests assert persistent role:none refusal instead of authorizing this local transport through Connect.

Retired Local Rendezvous Control Emulator

Rejected protocol history; no success path remains. An earlier emulator served the daemon SPA from a public rendezvous origin and brokered a direct WebRTC control channel. That experiment established transport feasibility but failed the code-provenance boundary: the hosted service could replace the JS that exercised authority. Its success-path contract and dashboard inventory have been removed from the current documentation so they cannot be mistaken for product behavior.

scripts/validate-connect-rendezvous.cjs now exists only as an adversarial mixed-version refusal check. It sends a legacy hosted offer and requires the daemon to reject it before registry, IAM, enrollment, or DataChannel mutation. Trusted daemon-origin tests cover successful dashboard-control transport.

Hosted Connect and Hosted-Control Production Alpha

The hosted-service slice is implemented as a separate binary, intendant-connect. It serves a public web origin, handles passkey-only account registration/login, lets a signed-in user link a daemon route with a single-use 12-word claim code, and stores directory/fleet metadata. Browser dashboard signaling is compiled off: authenticated offer/ICE/close calls return 403 before service state mutation. The optional reachability relay moves raw daemon-terminated TLS; it does not host the daemon SPA or mint authority.

In production, run it behind ordinary public TLS for a public origin such as https://connect.intendant.dev:

INTENDANT_CONNECT_TOKEN="$(openssl rand -base64 32)" \
  ./target/release/intendant-connect \
    --listen 127.0.0.1:9876 \
    --origin https://connect.intendant.dev \
    --rp-id intendant.dev \
    --data-file <state-file>

The retired --static-root PATH flag (and INTENDANT_CONNECT_STATIC_ROOT) is accepted and ignored for deployment compatibility. It cannot mount a filesystem tree or re-enable the daemon SPA, WASM, or vault kernel on the hosted origin. Connect serves only explicit compile-time embedded pages/assets.

The --rp-id intendant.dev value means passkeys are scoped to the owned Intendant parent domain while the actual UI can live on connect.intendant.dev. For compatibility, the live production-alpha instance currently keeps its original INTENDANT_CONNECT_RP_ID=connect.intendant.dev; changing that value is a credential migration and existing users must register new passkeys. Browsers also allow http://localhost:<port> as a secure context for local development, so the same binary can be E2E-tested without public TLS.

The hosted service also serves /access as the account/fleet entry point. /connect is the canonical passkey, route-link, daemon-list, label, release, and audit surface. A daemon with the optional lane enabled may publish a separately signed capability hint; when it also has a relay-mode fleet-DNS route, its card shows Request control and navigates to the daemon’s HTTPS fleet origin. The historical /app route always redirects to /connect, including crafted ?connect=1&daemon_id=... queries.

The daemon side still uses the normal [connect] outbound rendezvous client:

[connect]
enabled = true
rendezvous_url = "https://connect.intendant.dev"
daemon_id = "vortex-deb-x11-intendant"
auth_token = "same daemon token configured on intendant-connect"
relay_enabled = true
relay_endpoint = "connect.intendant.dev:443"
hosted_control_enabled = false

The hosted MVP flow is:

  1. The daemon locally generates a short-lived 12-word BIP39 claim code, then registers its daemon_id, persistent identity public key, code hash, fresh timestamp, and identity signature through /api/daemon/register. Connect never receives or returns the plaintext code or URL. When the optional lane is enabled, a separate daemon signature binds that capability bit to the same identity, code hash, and registration timestamp; old services can still verify the unchanged registration proof.
  2. A successful, single-use registration proof rotates a short-lived daemon-session credential. The daemon must present it on /api/daemon/next, answer, error, dry, and claim-proof, including when deployment-wide open registration skips the shared bearer.
  3. The user opens the daemon-printed /connect#claim_code=... URL or enters the code, then signs in or registers with a passkey. The browser scrubs the fragment, normalizes and hashes the code locally, and submits only the digest. Query-string and plaintext claim bodies are rejected. The page states that the code is single-use and is not a password, recovery phrase, API key, private key, or passkey secret.
  4. Connect sends a claim_challenge event to the daemon. The daemon signs that challenge with its daemon identity key, and Connect verifies the signature before recording the account/route link. This changes no daemon IAM state and grants no access.
  5. The Connect card offers fleet-origin navigation only when the capability signature verifies and relay-mode DNS is live. The Connect passkey/account assertion is not sent as daemon authentication and creates no principal.
  6. The fleet-origin page generates a non-extractable tab-ephemeral P-256 key and signs a bounded lease request. A local console, direct-mTLS owner dashboard, or qualifying signed app reviews the daemon-signed request. Approval may only reduce the preset or lifetime and mints an exact expiring hosted_lease.
  7. The tab signs each protected HTTPS request and obtains a one-use WebSocket ticket for the event lane. The daemon intersects the active lease, current ceiling, compiled preset, route/method/frame classification, and concrete action/target wall. Root control and lease management remain on local or direct-mTLS owner surfaces. The retired Connect offer/ICE/close path remains refused at both ends; a generic browser-key grant does not override it.

The state file durably stores users, passkeys, daemon account/route links, hashed claim codes, account-scoped fleet navigation metadata, and a capped audit log. Plain claim codes are never in the service; WebAuthn challenge state and rotating daemon-session credentials are memory-only. The service does not accept browser offers. It exposes a minimal account/fleet UI today: passkey registration/login, claim-code entry, daemon list, daemon labels, route metadata, signed fleet-origin navigation, release link, fleet target listing/forget, and audit events. The visible account identity is the globally unique account name/handle; the internal WebAuthn display-name field is derived from that handle and is not a separate user-facing profile field in the MVP UI.

Hosted Connect does not provide team IAM or authenticate daemon control. The target daemon’s separately enabled lease lane provides the bounded control surface. The local IAM schema already has portable account/provider, verified-provider, handle, and organization fields, but hosted account metadata remains non-authenticating directory data.

There is no hosted dashboard or Settings/Debug control panel in the Connect service. Connect shows directory/link health and a conditional navigation button. The daemon’s fleet origin serves the bounded UI. Its event stream is an explicit projection: sessions/logs, bounded status/usage, agent-visible display state, and required live events; it omits IAM/access, settings, credentials, approval payloads, peer state, browser workspaces, private displays, app-anchor state, and lease-management records.

Production-alpha hardening now includes:

  • cookie-backed user mutations require same-origin requests and a per-session CSRF header;
  • auth, claim, and daemon hot paths have simple in-memory rate limits keyed by reverse-proxy client headers;
  • /healthz is a cheap liveness probe and /readyz verifies that the state directory is usable; the Connect pages/assets are embedded in the binary, so readiness does not depend on a static dashboard root;
  • security-relevant service events are emitted as structured JSON on stderr in addition to the persisted user audit log;
  • releasing a daemon link removes it from the account and clears legacy service-side active-session bookkeeping. Current daemons ignore any legacy hosted close event.

For mixed-version alpha upgrades, restarting only Connect does not terminate a legacy P2P DataChannel that was already established. Upgrade and restart each daemon, close old Connect tabs, and let the IAM migration revoke legacy connect-bootstrap grants. New mixed-version attempts are blocked twice: at the current service and at the current daemon.

The reverse proxy in front of intendant-connect must terminate public TLS for connect.intendant.dev, forward Host, set X-Forwarded-For/X-Real-IP, and strip any inbound copies of those client-IP headers before setting them. Keep the service bound to 127.0.0.1, keep INTENDANT_CONNECT_TOKEN in a secret store, and back up the configured state file; that file is the current account/passkey/route-link database, not a daemon authority store.

The production-alpha operator path is captured in scripts, but live target details are not stored in the public repository. Provide them through a private env file or command-line flags:

cat > ~/.config/intendant/connect-prod-alpha.env <<'EOF'
CONNECT_HOST=<ssh-host>
CONNECT_SSH_USER=<ssh-user>
CONNECT_SSH_KEY=<private-ssh-key-path>
CONNECT_REMOTE_SOURCE=<remote-source-directory>
CONNECT_SERVICE=<systemd-service-name>
CONNECT_REMOTE_READYZ_URL=<local-readiness-url>
CONNECT_REMOTE_STATE=<remote-state-json-path>
CONNECT_PUBLIC_ORIGIN=https://connect.intendant.dev
EOF

CONNECT_OPS_ENV=~/.config/intendant/connect-prod-alpha.env \
  scripts/deploy-connect-prod-alpha.sh
CONNECT_OPS_ENV=~/.config/intendant/connect-prod-alpha.env \
  scripts/connect-state-backup.sh --passphrase-file ~/.config/intendant/connect-backup.passphrase
CONNECT_OPS_ENV=~/.config/intendant/connect-prod-alpha.env \
  scripts/connect-state-restore.sh --yes \
  --passphrase-file ~/.config/intendant/connect-backup.passphrase \
  ~/.local/share/intendant/connect-backups/intendant-connect-state-YYYYMMDDTHHMMSSZ.json.enc

The deploy script syncs the current worktree to the configured remote source directory, builds on the host, restarts the configured systemd service, and checks both the configured local readiness URL and the public connect.intendant.dev readiness URL. Backup and restore default to encrypted state snapshots and require an explicit plaintext flag for diagnostics.

Current alpha limits:

  • one linked account per daemon; the link is route metadata, not ownership; no teams, recovery, or account email flow;
  • an optional shared deployment bearer gates registration (and always guards admin APIs); fresh daemon-key proofs and rotating short-lived daemon-session credentials protect the per-daemon mailbox endpoints even in open mode;
  • rate limits, web sessions, and daemon-session credentials are single-process in-memory state; browser offers are rejected before pending/active session mutation, and plaintext route codes never enter the service;
  • no high-availability storage or database migrations; the state file is a single-node alpha persistence layer;
  • the hosted lane is off by default and dark when off. Connect carries no application-layer dashboard RPC and cannot approve a lease; its relay carries only daemon-terminated TLS ciphertext;
  • View and Tasks expose no Files or Terminal surface. Operate may use the daemon-served terminal, filesystem, and agent-visible display-input routes, but never private displays, IAM/access, credentials, approvals, peers, settings, or its own ceiling;
  • peer daemon-to-daemon mTLS remains separate from Connect account login.

Run the hosted MVP E2E locally with:

cargo build --bin intendant-connect --bin intendant
node scripts/validate-connect-hosted-mvp.cjs

That validator starts intendant-connect, launches a daemon with outbound Connect enabled, uses a browser virtual authenticator for passkey registration, links the daemon route, labels it, verifies that authenticated browser offer/ICE/close calls return 403 without enqueueing, creates a local operator grant as an adversarial regression fixture, and verifies the service still creates zero control sessions. A daemon regression feeds the same events as if from an old/self-hosted service and checks that control, IAM, and enrollment state stay unchanged. The validator then releases the route and verifies the audit events. This remains the feature-off/legacy-path validator. The Rust E2E hosted_lease_is_the_only_relay_control_authority covers the enabled lane through the real relay ingress: signed request, trusted approval, proof-bound HTTP, replay refusal, one-use WebSocket ticket, action denial, ticket reuse refusal, and closure on revocation. Successful Shell, Files, media, display, and byte-stream transport remain covered by the trusted local/direct dashboard-control validators.

Rejected Connect-Served Dashboard Design

Historical decision only; this is not the current hosted-lease contract. The Connect-origin WebRTC prototype was rejected because encrypted transport did not make replaceable Connect-served JavaScript a trusted authority client. Its detailed success-path, session-grant, relay, RPC, media, and rollout specifications were removed: they were not a roadmap and no longer describe callable code.

The surviving engineering result is narrower: daemon-signed WebRTC bindings and the dashboard transport abstraction remain useful on authenticated daemon-origin paths and local packaged-app development builds. No signed/notarized packaged release exists for this alpha. Hosted Connect is an account, route, presence, push, metadata, and ciphertext-relay service. It serves no daemon SPA or privileged dashboard assets; /app and /app.html redirect to /connect; browser offer/ICE/close calls are refused before mutation; and current daemons drop legacy hosted-control events. No browser-key grant or compatibility-ceiling edit can reactivate that design.

The current hosted-control product is a different construction: daemon-served fleet-origin code borrows an exact short-lived lease after trusted confirmation, and a compiled immutable floor bounds every preset. Its contract is specified in Hosted Control; the broader boundary and migration remain in Trust Architecture.

HTTP endpoints

Routing matches the parsed (method, path) — exact routes or their /-nested sub-routes, query string stripped — so the dispatch chain and the per-route IAM/Origin gates always classify a request identically. Grouped by family (sub-routes elided where the family is uniform):

EndpointDescription
GET /The dashboard SPA
GET /configLive-model configuration JSON (provider, model, sample rates, git SHA)
GET /debugDebug JSON (agent state, voice connection, active browser)
POST /sessionMint ephemeral session tokens for Gemini Live / OpenAI Realtime (own/app origin; CredentialsManage IAM permission)
GET /wasm-web/*, GET /wasm-station/*Compiled WASM + JS glue (content-hash cache-busted)
GET /audio-processor.jsAudioWorklet processor for mic capture
GET /vault-kernel.jsThe vault crypto kernel worker; the SPA hash-verifies it against its assembled-in pin before instantiating (see credential custody)
GET /.well-known/agent-card.jsonAgent card (identity + capabilities) for peers and integrations
POST /mcpStreamable-HTTP MCP server (per-tool IAM; see MCP server)
WS / or WS /wsMain WebSocket: events, fallback Shell terminal I/O, presence protocol, WebRTC signaling; direct browser upgrades require the daemon’s independently trusted origin or local app scheme; fleet/relay upgrades require a one-use hosted ticket and receive only the hosted event/action projection
GET /api/sessionsList past sessions (/stream NDJSON variant, /search full-text)
GET /api/session/{id}/*Per-session detail, report, log replay, context snapshots, recordings, frame assets
POST /api/session/{id}/agent-outputFetch persisted agent output by id for a historical/session-scoped transcript (POST-shaped read: the ids ride the body)
DELETE /api/session/{id}[/{target}], POST /api/session/{id}/delete[/{target}]Delete archived session data (native DELETE plus WKWebView fallback)
GET /api/session/current/history, GET /api/session/current/changes[/*]Current-session reads: serialized history and file-change list/detail
POST /api/session/current/{rollback,redo,prune,agent-output}Current-session actions: history rollback/redo/prune mutations, plus the POST-shaped agent-output fetch
GET /api/session/current/uploads[/*], POST /api/session/current/uploads, DELETE /api/session/current/uploads/{id}Task attachment store: list, upload, raw fetch, delete
GET /api/managed-context/{records,anchors,fission}Managed-context state: rewind records, anchors, fission groups
GET /recordings/*, GET /frames/*Current-session recording segments and captured frame assets
GET /api/fs/{stat,list,read}, POST /api/fs/{mkdir,write}Scoped filesystem browsing and editor writes (fs scope enforced per grant)
GET/POST /api/settings, GET /api/api-key-status, GET /api/project-rootSettings and provider-key status
POST /api/api-keysStore provider API keys (credential custody: CredentialsManage, which no peer profile carries)
GET /api/external-agentsExternal-agent backend availability (configured command, installed, auth posture, last used) plus passive zero-quota compatibility status (artifact fingerprint, in-band version, manifest digest, finding counts) — drives the fueling nudge and new-session picker
GET /api/displays, POST /api/diagnostics/visual-freshnessDisplay inventory; visual-freshness probe marker
GET /api/hosted-control/{bootstrap,certificate-ledger}, POST /api/hosted-control/{requests,requests/poll,anchor-decisions,witness-reports}Public hosted doorbell and signed certificate-observation records: dark when disabled; the ledger is also readable over authenticated direct peer routes, request creation/poll proves the tab key, and signed-app inputs verify the enrolled anchor
POST /api/hosted-control/passkey/{register/start,register/finish,start,finish}Exact-own-origin WebAuthn enrollment and assertion ceremonies on a configured custom name; enrollment consumes a direct-owner invitation and assertion success issues only the signed tab’s bounded daemon lease
POST /api/hosted-control/ws-ticketMint a seconds-lived one-use WebSocket ticket from a proved active hosted lease
GET /api/access/{overview,iam/state}, GET /api/dashboard/targetsTrust-architecture snapshots (IAM state, fleet targets)
GET/POST /api/access/hosted-control[/*]Manage-gated hosted policy, pending decisions, active revocation, session eligibility, certificate confirmation, and exact-evidence override
POST /api/access/...Trust mutations: enrollment decide, IAM grant upsert/update, hosted lease management, org trust/revoke, org-grant issue/renew/revoke-member, issuer init/delegate/install, revocation-list apply
GET /api/peers[/*], POST /api/peers[/*], DELETE /api/peersPeer federation: registry reads (GET), pairing + management/signaling (POST), registry removal (DELETE)
POST /api/coordinator/routeMulti-agent coordinator task routing (peer lane)
GET /api/worktrees, POST /api/worktrees/{inspect,scan,remove,clean,merge}Agent worktree inventory and lifecycle (clean = reclaim a checkout’s Cargo target/; merge = session-linked worktree finish card)
GET /connect/{bootstrap,status}, POST /connect/dashboard/{offer,ice,close}Daemon-origin WebRTC control bootstrap: certless only on loopback, remote callers require direct mTLS; fleet SNI and hosted Connect browser APIs cannot open it

Declared API routes

Every /api/*, /session, and /mcp route is declared exactly once in gateway_routes::ROUTES (src/bin/caller/gateway_routes.rs); dispatch, the pre-dispatch IAM classification, and the OPTIONS preflight (CORS posture + allowed-method union) all derive from those declarations, and the table below is rendered from them. A unit test (endpoint_docs_match_chapter) fails when the chapter and the code drift; regenerate with cargo test --bin intendant endpoint_docs_match_chapter -- --nocapture and paste the printed block between the markers. Authorization names the PeerOperation the IAM gate evaluates; public routes carry their authority in the payload itself (signature/shape), and the federation surface derives its operation per method/path from federation_http_operation.

fleet allowlist in the generated CORS column describes which already-trusted dashboard origins may call a daemon’s independently verified direct-mTLS URL. It does not authorize the rendezvous-controlled fleet WebPKI URL. The listener classifies fleet SNI first. It rejects non-public HTTP/MCP, signaling, and WebSocket traffic before route CORS, browser mTLS, or IAM is resolved unless the optional hosted lane validates an exact lease proof/ticket and the route’s hosted classifier admits it. The fleet certificate endpoint itself is not in that classifier and can therefore be requested only from a trusted direct surface. fleet or loopback (the session-list rows the multi-daemon Stats tab reads) is the same allowlist echo plus loopback-host page origins on connections that themselves arrive over loopback — a sibling daemon’s dashboard on another port of the same machine; these rows historically answered with a wildcard Access-Control-Allow-Origin, which is retired — an admitted origin is echoed exactly (with Vary: Origin) and every other response omits the header.

MethodPathAuthorizationCORSBodyDescription
GET/api/fs/statFilesystemReadown originnoneStat a filesystem path (scope-checked)
GET/api/fs/listFilesystemReadown originnoneList a directory (scope-checked)
GET/api/fs/readFilesystemReadown originnoneRead file bytes (scope-checked; supports byte ranges)
POST/api/fs/mkdirFilesystemWriteown originboundedCreate a directory (scope-checked)
POST/api/fs/writeFilesystemWriteown origin≤ 150 MiBWrite file bytes (scope-checked; sha256-guarded overwrite)
POST/api/fs/renameFilesystemWriteown originboundedMove/rename a file or directory (scope-checked)
POST/api/fs/deleteFilesystemWriteown originboundedDelete a file or directory (scope-checked)
GET/api/transfersFilesystemReadown originnoneList transfer jobs, newest first (?id= filters by job id or resume token)
POST/api/transfersFilesystemWriteown originboundedCreate a transfer job (kind download
POST/api/transfers/{id}/chunkFilesystemWriteown originstreamingAppend one raw-body chunk to an upload job (?offset=; ≤ 32 MiB per chunk)
POST/api/transfers/{id}/commitFilesystemWriteown originboundedVerify (size + declared sha256) and atomically rename a finished upload into place
DELETE/api/transfers/{id}FilesystemWriteown originnoneDelete a transfer job (cancels partials; removes managed artifacts)
POST/api/transfers/{id}/deleteFilesystemWriteown originnoneDelete a transfer job (WKWebView POST fallback)
GET/api/transfers/{id}/downloadFilesystemReadown originnoneRead download-job bytes (?offset=&length= or Range → 206; resume metadata echoed as X-Transfer-* headers, X-Content-Sha256 on full reads)
POST/sessionCredentialsManageown originnoneMint an ephemeral Gemini Live / OpenAI Realtime token from a daemon-held provider credential
GET/api/session/current/changes[/…]SessionManageown originnoneList the session’s changed files, or the unified diff for one file (subpath)
GET/api/session/current/historySessionManageown originnoneSerialized rollback History for the current session
POST/api/session/current/rollbackSessionManageown originboundedRoll the current session back to a round (optionally reverting files)
GET/api/codex-cloud/workersStatsReadown originnoneCodex Cloud worker leases from the cached store (?refresh=1 re-syncs via the Codex CLI)
POST/api/codex-cloud/submitTaskown originboundedSubmit a new Codex Cloud task via the daemon host’s Codex CLI and track its worker lease
POST/api/codex-cloud/enrollpublicpublic≤ 8 KiBRedeem a single-use Codex Cloud attach token (public key in, zero-authority cloud-worker certificate out)
GET/api/agendaAgendaReadown originnoneAgenda ledger snapshot: items (oldest first) plus status counts; additive since_seq (delta), shape=summary, q= (search), window=live
GET/api/agenda/items/{item_id}AgendaReadown originnoneOne agenda item, full + decorated, by id or unique prefix (+ its sessions join)
GET/api/agenda/opsAgendaReadown originnoneRaw agenda op-log page (since/item/limit cursor; unknown ops served verbatim)
GET/api/agenda/occurrencesAgendaReadown originnoneRaw occurrence-journal page (since/item/limit cursor; unknown records served verbatim)
POST/api/agenda/opAgendaWriteown origin≤ 16 MiBApply one agenda command (add, ask, answer, patch, transitions, or scheduled-session propose/approve/revoke/withdraw)
GET/api/agenda/definitionsAgendaReadown originnoneAutomation-definition catalog (house + personal, validation state, full text)
GET/api/agenda/sealed/{sha256}AgendaReadown originnoneOne sealed binding-ref snapshot by sha256 pin (read-only, content-addressed)
POST/api/agenda/stampAgendaWriteown originboundedStamp an automation definition (park + propose the instance graph; never approves)
POST/api/daemon/takeoverSettingsown origin≤ 4 KiBRequest drain of this daemon (handover): the scheduler lease frees for a successor; in-flight sessions finish here
GET/api/daemon/handoverStatsReadown originnoneHandover status: lease role, drain state, and co-homed daemons with probed liveness
POST/api/daemon/update-lane/checkSettingsown origin≤ 4 KiBSelf-update lane: run the bounded behind-origin-main / behind-latest-release check now (optional body {“channel”: “releases”
POST/api/daemon/update-lane/produceSettingsown origin≤ 4 KiBSelf-update lane: produce the update artifact on the named channel (dev = source pull+build, releases = verified release download) for the swap chip
POST/api/daemon/update-swapSettingsown origin≤ 4 KiBAsk the attached app supervisor for the one-click update swap (refused when no live supervisor is attached)
POST/api/daemon/update-swap/claimSettingsown origin≤ 4 KiBApp supervisor poll: claim the pending one-click swap request (consuming; expired requests evaporate)
POST/api/daemon/update-swap/resultSettingsown origin≤ 4 KiBApp supervisor report: the outcome of a claimed swap attempt (failures surface on the chip and the notification lane)
GET/api/agenda/blobs/{item_id}/{blob_id}/rawAgendaReadown originnoneFetch one parked-ask preview blob’s raw bytes (attachment; MIME sniffing disabled)
GET/api/agenda/items/{item_id}/refs/driftAgendaReadown originnoneRe-hash one item’s file refs and manifest binding refs against their recorded pins (expand-time drift check)
GET/api/agenda/items/{item_id}/refs/contentAgendaReadown originnoneOne attached file ref’s bytes (?locator=; sealed snapshot when pinned, live with drift verdict otherwise)
GET/api/agenda/items/{item_id}/pr-stateAgendaReadown originnoneLive PR state for one anchor (expand-time render join; cached daemon-side)
POST/api/agenda/reminders/policySettingsown originboundedMerge-patch the agenda reminder policy (quiet hours, urgency, per-item overrides)
GET/api/memory/searchMemoryReadown originnoneBounded Memory claim search (q, limit, candidates); results carry derived status
GET/api/memory/claimMemoryReadown originnoneRead one Memory claim by id prefix (id); status derived at read time
POST/api/memory/proposeMemoryWriteown originboundedPropose one Memory claim (candidate lane; response reports effective durability)
POST/api/memory/judgeMemoryWriteown originboundedJudge one Memory claim (owner curation: accept/dispute/retire/supersede + reason; ring-2 callers get the named actor-not-permitted denial)
POST/api/session/current/redoSessionManageown originboundedRedo the last rolled-back round
POST/api/session/current/pruneSessionManageown originboundedPrune rollback state for the current session
POST/api/session/current/agent-outputSessionManageown originboundedFetch the current session’s persisted agent output by id (POST-shaped read)
POST/api/session/current/uploadsSessionManageown originstreamingUpload a file attachment (raw streamed body; name/destination in query)
GET/api/session/current/uploadsSessionManageown originnoneList uploads for the current session
GET/api/session/current/uploads/{id}/rawSessionManageown originnoneFetch one upload’s raw bytes (attachment; MIME sniffing disabled)
GET/api/session/current/uploads[/…]SessionManageown originnoneUnknown upload subpaths (handler-owned JSON 404)
DELETE/api/session/current/uploads/{upload_id}SessionManageown originnoneDelete one upload (file + sidecar)
DELETE/api/session/{id}SessionManageown originnoneDelete a session’s data
DELETE/api/session/{id}/{target}SessionManageown originnoneDelete one data kind for a session (recordings, frames, …)
DELETE/api/session/{id}/{target}/deleteSessionManageown originnoneDelete one data kind for a session (suffix form)
POST/api/session/{id}/deleteSessionManageown originnoneDelete a session’s data (POST fallback for WKWebView)
POST/api/session/{id}/{target}/deleteSessionManageown originnoneDelete one data kind for a session (POST fallback)
POST/api/session/{id}/agent-outputSessionInspectown originboundedFetch a session’s persisted agent output by id (POST-shaped read)
GET/api/session/{id}/fork-pointsSessionInspectown originnoneUnified fork-point catalog for a session (anchors + eligibility, backend-tagged)
GET/api/session/{id}/background-tasksSessionInspectown originnoneBackground tasks a supervised Claude Code or Kimi session announced (id, description, status, output/cancel availability)
GET/api/session/{id}/background-tasks/{task}/outputSessionInspectown originnoneTail of one background task’s registered file or bounded native output preview (tail_kb query, capped; registry-resolved, never a caller path/server endpoint)
GET/api/session/current[/…]SessionManageown originnoneCurrent-session detail and artifact sub-routes
POST/api/session/current[/…]SessionManageown originnoneCurrent-session detail sub-routes (POST fallback callers)
GET/api/session/{id}/context-snapshotSessionInspectown originnoneReplay one archived context snapshot (file/request_id/request_index/ts selector)
GET/api/session/{id}/reportSessionInspectown originnoneSession report zip (text artifacts; id=current targets the live session)
GET/api/session/{id}/recordingsSessionInspectown originnoneList a session’s recording streams
GET/api/session/{id}/recordings/{stream}/{asset}SessionInspectown originnoneRecording assets: segments listing, playlist.m3u8, or a segment file
GET/api/session/{id}/frames/{filename}SessionInspectown originnoneSession frame image asset
GET/api/session/{id}SessionInspectown originnoneSession detail (paged replay entries; limit/before/source)
GET/api/session[/…]SessionInspectown originnoneSession artifact sub-routes: recordings (+segments/playlist), report zip, frames
POST/api/session[/…]SessionManageown originnoneSession detail sub-routes (POST fallback callers)
GET/api/managed-context/anchorsSessionInspectown originnoneManaged-context anchor catalog
GET/api/managed-context/recordsSessionInspectown originnoneManaged-context record index
GET/api/managed-context/fissionSessionInspectown originnoneManaged-context fission state
POST/api/worktrees/inspectSessionInspectown originboundedInspect one worktree (branch, ahead/behind, dirty state)
POST/api/worktrees/removeSessionManageown originboundedRemove a worktree from the inventory
POST/api/worktrees/cleanSessionManageown originboundedDelete a worktree’s Cargo target/ dir (CACHEDIR.TAG-gated) to reclaim disk, keeping the checkout
POST/api/worktrees/mergeSessionManageown originboundedMerge a session’s linked worktree branch into its base checkout, then remove the checkout
POST/api/worktrees/scanSessionManageown originnoneRescan the worktree inventory (refreshes the cache)
GET/api/worktreesSessionInspectown originnoneCached worktree inventory
GET/api/sessions/streamSessionInspectfleet or loopbacknoneNDJSON stream of the session list
GET/api/sessions/searchSessionInspectown originnoneSearch sessions (q, source, mode, project filters)
GET/api/sessions/message-searchSessionInspectown originnoneMessage-lane search over the shard index (q, source, superseded, subagents, cursor)
GET/api/sessionsSessionInspectfleet or loopbacknoneList sessions (id filter, limit, usage view; fleet/loopback CORS echo for the multi-daemon Stats tab)
GET/api/project-rootSettingsown originnoneProject root path this daemon serves
POST/api/settingsSettingsown originboundedUpdate runtime settings
GET/api/settingsSettingsown originnoneCurrent runtime settings
POST/api/api-keysCredentialsManageown originboundedStore provider API keys in the daemon-config .env
GET/api/api-key-statusSettingsown originnoneWhich provider keys are configured (presence only)
POST/api/integrations/githubCredentialsManageown origin≤ 64 KiBConfigure the GitHub App integration (seal credentials into custody, set the watch list)
GET/api/integrations/github/statusSettingsown originnoneGitHub App integration status (presence + last exchange; never unseals)
DELETE/api/integrations/githubCredentialsManageown originnoneRemove the GitHub App integration credentials from custody
POST/api/integrations/github/manifest-startCredentialsManageown origin≤ 4 KiBBegin the one-click GitHub App Manifest ceremony (mints the single-use state; refuses without a custody backend)
GET/api/integrations/github/callbackpublicown originnoneGitHub App Manifest redirect landing (single-use state is the authorization; renders a return-to-dashboard page)
GET/api/integrations/github/installationsCredentialsManageown originnoneThe App’s installations (App-JWT discovery; works while the install is pending)
GET/api/integrations/github/repositoriesCredentialsManageown originnoneRepositories the installation can see (installation token; feeds the repo picker)
GET/api/local-daemons/tokensCredentialsManageown originnoneLoopback admission tokens for same-home daemon instances (owner handoff)
POST/api/claude-auth/startCredentialsManageown origin≤ 4 KiBStart the Claude sign-in ceremony (claude auth login on a daemon-private PTY)
GET/api/claude-auth/statusCredentialsManageown originnoneClaude sign-in ceremony state (validated sign-in URL; account info on success)
POST/api/claude-auth/codeCredentialsManageown origin≤ 2 KiBSubmit the pasted authorization code to the Claude sign-in ceremony
POST/api/claude-auth/cancelCredentialsManageown originnoneCancel the Claude sign-in ceremony (non-destructive; prior login keeps working)
POST/api/codex-auth/startCredentialsManageown origin≤ 4 KiBStart the Codex sign-in ceremony (codex login --device-auth on a daemon-private PTY)
GET/api/codex-auth/statusCredentialsManageown originnoneCodex sign-in ceremony state (verification URL + one-time code; account info on success)
POST/api/codex-auth/cancelCredentialsManageown originnoneCancel the Codex sign-in ceremony (non-destructive; prior login keeps working)
POST/api/kimi-auth/startCredentialsManageown origin≤ 4 KiBStart the Kimi Code sign-in ceremony (kimi login on a daemon-private PTY)
GET/api/kimi-auth/statusCredentialsManageown originnoneKimi Code sign-in ceremony state (verification URL + one-time code)
POST/api/kimi-auth/cancelCredentialsManageown originnoneCancel the Kimi Code sign-in ceremony (non-destructive; prior login keeps working)
GET/api/external-agentsSessionInspectown originnoneDetected external coding agents (codex, claude, kimi, pi)
POST/api/diagnostics/visual-freshnessDisplayInputown origin≤ 16 MiBVisual-freshness diagnostics transcript sink (NDJSON body)
GET/api/displaysDisplayViewown originnoneEnumerate active displays
any/api/peer-pairing/requests[/…]publicpublicstreamingPeer access-request doorbell: knock (POST, size-capped) or poll one request’s status (GET subpath)
GET/api/hosted-control/bootstrappublicpublicnoneHosted-control doorbell bootstrap (dark unless enabled; no authority)
POST/api/hosted-control/requestspublicpublic≤ 64 KiBSubmit a bounded hosted-control lease request to daemon-local IAM
POST/api/hosted-control/requests/pollpublicpublic≤ 64 KiBPoll one hosted-control request with proof by its browser key
POST/api/hosted-control/anchor-decisionspublicpublicboundedPresent a signed application-anchor decision document
GET/api/hosted-control/certificate-ledgerpublicpublicnoneRead the daemon-signed fleet-certificate ledger (no authority)
POST/api/hosted-control/witness-reportspublicpublic≤ 16 KiBPresent a certificate observation signed by an enrolled application witness
POST/api/hosted-control/passkey/register/startpublicown origin≤ 128 KiBConsume an owner-authorized custom-domain passkey enrollment invitation
POST/api/hosted-control/passkey/register/finishpublicown origin≤ 128 KiBFinish passkey enrollment at the exact configured custom-domain rp_id
POST/api/hosted-control/passkey/startpublicown origin≤ 128 KiBStart an exact-rp_id WebAuthn ceremony for a custom-domain bounded lease
POST/api/hosted-control/passkey/finishpublicown origin≤ 128 KiBFinish custom-domain WebAuthn and issue the requested bounded lease
POST/api/hosted-control/ws-ticketPresenceReadown originnoneMint one short-lived, single-use WebSocket ticket from a proved hosted lease
POST/api/access/org-grantspublicpublic≤ 16 KiBPresent a signed org grant document (verified against locally trusted org keys)
GET/api/access/orgs/{org_handle}/revocationspublicpublicnoneOrg revocation list (ORL) for a trusted org
POST/api/access/orgs/revocations/applypublicpublic≤ 64 KiBApply a signed org revocation list
POST/api/access/org-grants/renewpublicpublic≤ 16 KiBRenew an org grant document (signed payload)
POST/api/access/iam/user-client-grantsAccessManagefleet allowlistboundedUpsert a user-client grant
POST/api/access/iam/grants/updateAccessManagefleet allowlistboundedUpdate an IAM grant
POST/api/access/orgs/trustAccessManagefleet allowlistboundedTrust an org root key on this daemon
POST/api/access/orgs/revokeAccessManagefleet allowlistboundedWithdraw trust in an org root key
POST/api/access/org-grants/issueAccessManageown originboundedIssue an org grant (org root/issuer key on this daemon)
POST/api/access/org-grants/revoke-memberAccessManageown originboundedRevoke an org member (appends to the ORL)
POST/api/access/org-grants/issuers/initAccessManageown originboundedInitialize an org issuer key
POST/api/access/org-grants/issuers/delegateAccessManageown originboundedDelegate to an org issuer
POST/api/access/org-grants/issuers/installAccessManageown originboundedInstall a delegated org issuer key
POST/api/access/enrollment-requests/decideAccessManagefleet allowlistboundedStaged decision API; the default product has no queue writer
GET/api/access/enrollment-requestsAccessInspectfleet allowlistnoneStaged enrollment capability and normally empty queue
GET/api/access/iam/stateAccessInspectfleet allowlistnoneLocal IAM state (roles, grants, bindings)
GET/api/access/overviewAccessInspectfleet allowlistnoneAccess overview for the calling principal
GET/api/access/hosted-controlAccessManageown originnoneHosted-control policy, lease, signed-app anchor, certificate-guard, custom-domain certificate, and passkey state
POST/api/access/hosted-control[/…]AccessManageown originboundedDecide requests, revoke leases, change policy, mark hosted-eligible sessions, act on certificate evidence, or manage custom-domain passkeys
GET/api/access/connect/statusAccessInspectfleet allowlistnoneConnect rendezvous status (discovery-link state and provenance; no claim code)
GET/api/access/connect/claim-codeAccessManagefleet allowlistnoneReveal the current one-time twelve-word claim code (unlinked daemons only)
POST/api/access/connect/configAccessManagefleet allowlistboundedEnable/disable the Connect client (persists to intendant.toml, applies live)
POST/api/access/connect/unclaimAccessManagefleet allowlistboundedUnlink this daemon’s discovery record from its Connect account (daemon-signed)
POST/api/access/tierAccessManagefleet allowlistboundedSet this daemon’s trust tier label (integrated/disposable; null clears)
POST/api/access/fleet-cert/requestAccessManagefleet allowlistboundedRequest a fleet certificate (publish addresses, run the ACME DNS-01 order; async start)
GET/api/dashboard/targetsAccessInspectown originnoneDashboard target list (this daemon + connected peers)
GET/api/dashboard/tabsAccessInspectown originnoneLive dashboard connections (open tabs) with voice/input-authority holders
POST/api/peers/pairing/invitefederation (per method/path)own originboundedIssue a peer-scoped mTLS pairing invite
POST/api/peers/pairing/request-accessfederation (per method/path)own originboundedStart an outgoing access request against a remote daemon’s doorbell
POST/api/peers/pairing/request-access/pollfederation (per method/path)own originboundedPoll an outgoing access request (installs the approved identity)
GET/api/peers/pairing/requestsfederation (per method/path)own originboundedList pending/decided peer access requests
GET/api/peers/pairing/identitiesfederation (per method/path)own originboundedList approved/revoked peer identities
POST/api/peers/pairing/identities/revokefederation (per method/path)own originboundedRevoke a peer identity
POST/api/peers/pairing/requests/{code}/{decision}federation (per method/path)own originboundedDecide a pending access request (approve or deny)
POST/api/peers/pairing/joinfederation (per method/path)own originboundedImport a pairing invite and register the peer
GET/api/peersfederation (per method/path)own originboundedList registered peers (snapshots)
POST/api/peersfederation (per method/path)own originboundedAdd a peer by card URL (optionally persisted)
DELETE/api/peersfederation (per method/path)own originboundedRemove a registered peer
GET/api/peers/eligiblefederation (per method/path)own originboundedList connected peers satisfying every ?capability= filter
POST/api/peers/{peer_id}/messagefederation (per method/path)own originboundedSend a message to a connected peer
POST/api/peers/{peer_id}/taskfederation (per method/path)own originboundedDelegate a task to a connected peer
POST/api/peers/{peer_id}/approvalfederation (per method/path)own originboundedResolve a peer-forwarded approval request
POST/api/peers/{peer_id}/session-controlfederation (per method/path)own originboundedForward a session-lifecycle action to a connected peer’s session
GET/api/peers/{peer_id}/session-detailfederation (per method/path)own originboundedFetch a peer session’s transcript page over the federation HTTP lane (peer-side IAM governs)
POST/api/peers/{peer_id}/webrtcfederation (per method/path)own originboundedRelay display WebRTC signaling to a connected peer
POST/api/peers/{peer_id}/file-transfer-webrtcfederation (per method/path)own originboundedRelay file-transfer WebRTC signaling to a connected peer
POST/api/peers/{peer_id}/dashboard-control-webrtcfederation (per method/path)own originboundedRelay dashboard-control WebRTC signaling to a connected peer
any/api/peers[/…]federation (per method/path)own originboundedPeers sub-router catch-all (handler-owned JSON 404/405 for unknown subpaths and undeclared methods)
POST/api/coordinator/routefederation (per method/path)own originboundedCapability-based task routing through the Coordinator
POST/mcpMCP tokenown origin≤ 16 MiBMCP Streamable HTTP endpoint (JSON-RPC requests + notifications)
GET/mcpMCP tokenown originnoneMCP SSE stream (405: stateless server)
DELETE/mcpMCP tokenown originnoneMCP session delete (405: stateless server)

The four signed-organization rows marked public are courier/verification doors, not daemon-authentication or control doors. The HTTP caller receives no principal, role, or session. A locally trusted org signature authorizes only processing of the bounded document for its named cryptographic subject (or application of signed revocation facts); that subject must authenticate later through a real ingress. Peer subjects use peer mTLS. Human browser-key subjects remain record-only in this alpha: peer offers can verify them for attribution, but no live ingress admits them as its controlling IAM principal.

The full WebSocket message protocol (inbound key/resize/presence/WebRTC frames, outbound term/state/log-replay/tool-response frames) and the gateway’s internal layering are documented in Integrations → Web Gateway. Dashboard session-control actions use the api_session_control_msg dashboard-control RPC; there is no HTTP control-msg route.