Claude c0732607b9 dispatcher: pinned human-key approvals, worker launcher, Director, durable board, NATS bridge
Approvals: the server loads a pinned Ed25519 human.pub (es-core format)
at startup and fails closed (503) without one. Tickets carry a
server-computed subjectHash (sha256 of the task diff) and a unique
timestamp; signatures cover es-core's canonical {runId, stage,
subjectHash, timestamp} payload (stages APPROVE/REJECT) and are verified
against the pinned key only. Client public keys and passphrases are
ignored; tickets are re-bound to the current diff at sign time.

Workers: a launcher turns scheduler decisions into `opencode serve`
processes in per-task worktrees with an isolated OPENCODE_DB, drives the
OpenCode v2 session/prompt/event API behind a small WorkerApi interface,
relays tool/status events to the bus, commits results, and kills workers
on shutdown. A long-lived Director session replies in chat; cards are
created only from a zod-validated ```tasks block (/ship kept as the
explicit fallback). Board state persists in bun:sqlite (WAL). NATS
JetStream/KV mirroring is real ([email protected].3) behind NATS_URL.

Tests cover forged keys, client-supplied keys, mismatched subject hashes,
replayed tickets/signatures, changed diffs, es-core key compatibility,
the launcher and Director against a fake opencode server, persistence,
and JetStream against nats-server.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012AuVk4nEvBXaTjs4G6wdW4
2026-10-16 19:32:14 +00:00

Agency Control Plane — Fleet Dispatcher Daemon + High-Fidelity WebUI

The Fleet Control Plane schedules expert missions across isolated jj / Git worktrees, gates human approvals behind single-use Ed25519 tickets, and renders the whole fleet in one dark-mode WebUI with five locked-in UX pillars.

Layout

  • dispatcher/ — Fleet Dispatcher Daemon: pure DAG scheduler with path-overlap arbitration (model.ts, wildcard.ts, scheduler.ts), 3-tier Bifrost failover (limits.ts), hash-chained audit ledger (audit.ts), crash-safe worktree coordinator (worktrees.ts), transactional landing (landing.ts), ring-buffered SSE / NATS event bus (event-bus.ts), single-use approval tickets + Ed25519 helpers (approval-tickets.ts), and the loopback Bun HTTP + SSE control API (server.ts, index.ts).
  • web/ — Unified High-Fidelity WebUI (@heretek-ai/agency-web):
    1. KanbanBoard.tsx — 5-column worktree Kanban (Backlog, Ready, In Progress, Waiting Approval / QA, Done) with branch badges (fleet/task-101), owned-path chips, and model-tier indicators.
    2. DirectorChat.tsx — persistent dockable Director sidebar with /ship <spec>, mission charter preview, and 1-click DAG injection.
    3. WorkerDrillDown.tsx — live SSE tool-call timeline, stage gates, token/cost telemetry, DAG dependency graph, and Docker Compose profile badge (core / appsec / android / game / research).
    4. SplitDiffViewer.tsx — side-by-side / unified diff viewer with file tree, +/- counters, and highlighted hunks.
    5. ApprovalDrawer.tsx — exact subjectHash, diff summary, QA verdicts (qa-functional + qa-adversarial), and the 1-click / passphrase Ed25519 approval gate.
  • scripts/accept/ — per-wave acceptance scripts (wave-1.sh, wave-2.sh, wave-3.sh); docs/reports/ — wave reports and drift-guard verdicts; docs/evidence/ — QA red-proof sabotage logs.

Quickstart

bun install
bun run check     # typecheck + lint, zero warnings
bun test          # full suite: dispatcher + web
bash scripts/accept/wave-3.sh   # wave 3 gate (install, check, tests, I1 probe)
make accept       # all three waves

Serve the dispatcher API (loopback only) via startServer in dispatcher/src/server.ts, then point the WebUI at it (web/index.html loads web/src/app.tsx, which talks to the same /api/* routes).

Control API (dispatcher/src/server.ts, Bun HTTP + SSE, 127.0.0.1 only)

Method & Path Purpose
GET /api/board Kanban columns, tasks, worktree statuses, active model tier (tier-1-opencode / tier-2-commercial / tier-3-local), DAG edges.
POST /api/director/message Append steering message; decompose new task cards onto the board.
GET /api/tasks/:id/drilldown Worker session metadata, stage gates, live tool-log buffer, git diff hunks.
POST /api/approvals/:taskId/ticket Mint single-use 60s-TTL preview ticket bound to { taskId, subjectHash }.
POST /api/approvals/:taskId/sign Verify ticket + Ed25519 signature/passphrase; waitingHuman → ready/done; audit-chained.
GET /api/events SSE stream (ring-buffered, replayable sequence IDs; ?snapshot=1 for finite replay).

The WebUI (web/src/app.tsx) seeds its store from GET /api/board, subscribes to /api/events via EventSource, loads per-card drill-downs from /api/tasks/:id/drilldown, and mints/signs approvals through the ticket endpoints.

Security Invariant I1 (binding)

The human approval secret entered in the approval dialog is held only in a lexical function-closure variable for the duration of the fetch("/api/approvals/:id/sign") call, is overwritten and nulled in a finally block immediately after, and is never written to fleet-store, localStorage, sessionStorage, or DOM data attributes.

  • Enforcement: web/src/security/approval-client.ts (approveWithPassphrase / approveOneClick / assertNoPassphraseLeak); the drawer (ApprovalDrawer.tsx) reads the secret from an uncontrolled password input at submit time and clears the input afterwards — no React state ever holds it.
  • Proof: web/test/approval-invariant.test.ts (static source probe + runtime wire/sink assertions) and the I1 section of scripts/accept/wave-3.sh, which fails the wave on any violation.
S
Description
No description provided
Readme
603 KiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 94.8%
Shell 5%
Makefile 0.1%
HTML 0.1%