Files
clawmates/infrastructurehandoff/IMPLEMENTATION.md
T
Omar SobhandClaude Opus 4.8 fb59378aa2
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Fleet P2b: run agent sandboxes on connected nodes (RemoteDriver + placement)
Agents can now provision their sandbox on a connected fleet node instead of the
gateway host. Local stays the strict default, so existing agents are byte-for-
byte unaffected until explicitly placed elsewhere.

Security parity: the daemon links the REAL cm-sandbox DockerDriver and runs the
typed container ops (sb_provision/sb_exec/sb_destroy/sb_health/sb_list) through
it — identical hardening (cap-drop ALL, seccomp, no-net, read-only, non-root) to
local sandboxes. cm-sandbox spec types are now Serialize/Deserialize so the spec
crosses the channel.

- cm-api: RemoteDriver (impl SandboxDriver over the node channel) + HubDriverProvider
  (impl cm_runtime::NodeDriverProvider, hands out a driver only for connected
  nodes via a sync online set) + NodeHub.call/is_connected. AppState.with_node_hub
  so the hub is shared with the placement provider.
- cm-runtime SandboxManager: driver_for(node_id) routes by the recorded
  agent_containers.node_id (local default = existing driver, identical path);
  placement_node() reads the workspace setting and falls back to local if the
  node is offline; exec/release route accordingly. NodeDriverProvider trait.
- DB: 0020_workspace_placement + repo (for_agent/get/set/clear).
- main.rs: build the NodeHub first; inject HubDriverProvider into the agent
  manager + share the hub with AppState.
- API+UI: GET/PUT /api/fleet/placement + a "Run agents on: Local / <node>"
  selector in the Fleet overview.

Note: a node must be able to pull the agent image (the daemon docker-pulls it);
interactive PTY for agent containers on remote nodes is not wired (Terminal app
stays local) — the in-dashboard node shell already covers host access.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 12:48:50 -07:00

15 KiB

Clawmates Frontend — Implementation & Wiring Handoff

Audience: the build agent (or engineer) integrating these screens into the Clawmates platform (the Rust workspace + Next.js app) and feeding them live from the durable runner.

What this package is: every Clawmates screen as a self-contained HTML file, plus a dependency-free real-time bridge and client adapter that make them update live. Read this file once, top to bottom, before wiring anything. Everything runs offline in demo mode with synthetic data, so you can see each screen breathe before touching the backend.


0. Package layout

handoff/
├── IMPLEMENTATION.md         ← this file
├── README.md                 ← 60-second start
├── install.sh                ← idempotent installer (Node check, static server, .env)
├── .env.example              ← copy → .env for live mode
├── screens/                  ← the UI. Open any .dc.html directly in a browser — no build step.
│   ├── Clawmates World.dc.html
│   ├── Clawmates Observe.dc.html
│   ├── Clawmates Dashboard.dc.html
│   ├── Clawmates Infrastructure.dc.html
│   ├── Clawmates Auth.dc.html
│   ├── Clawmates Landing.dc.html
│   └── support.js            ← the .dc runtime (MUST sit next to the .dc.html files)
└── realtime/
    ├── server.mjs            ← demo + live SSE bridge (Node ≥20, zero deps)
    ├── clawmates-live.js     ← browser client adapter → window.ClawmatesLive
    └── events.schema.json    ← the Clawmates Event Taxonomy (the contract)

What a .dc.html file is

Each screen is a Design Component: a single HTML file whose markup + a small logic class are rendered by the bundled support.js runtime. There is no build step and no npm install to view them — open the file in a browser, or serve the screens/ folder statically. support.js must sit beside the .dc.html files (it does, in screens/).

To edit a screen's behavior you change two regions inside the file: the <x-dc>…</x-dc> template (markup, inline-styled) and the class Component extends DCLogic { … } block (state + a renderVals() that returns the values the template binds to). All live data flows into a screen by calling this.setState(...) from a ClawmatesLive.on(...) handler — see §3.


1. The screens

Screen Purpose Primary live inputs (see §3 taxonomy)
World The "Large World" map in three formations: Hierarchy (org▸company▸team▸claw tree), Flat (peer mesh), Live (Gource-style convergence on a <canvas>). topology.update, world.touch, node.activity, agent.status
Observe Linked observation deck — Agent close-up (live reasoning + the agent's live computer screen) and System mission-control (both claws, comms, routines, telemetry). agent.task.update, agent.reasoning.delta, agent.computer.frame, agent.message, telemetry, routine.update, door.request
Dashboard Tiered admin: org→company→team→claw with topology morph + the per-claw "anatomy" (brain compartments) and the agent's computer. agent.status, topology.update, routine.update
Infrastructure Fleet management. Local & Tailscale first (host health cards, tailnet device list, 3-step Connect-a-host wizard), then Cloud (AWS/GCP/Apple silicon/Hetzner/DigitalOcean connect + provision). host.health, host.status, tailscale.devices, cloud.runner (see §5)
Auth Clerk-style sign-in / sign-up with Google / Apple / GitHub OAuth + email. n/a (wire to Clerk — see §6)
Landing Marketing page (deploy ladder, 12 topologies, §15 safety, self-host). static

Canonical entry point for the product app: World, Observe, Dashboard, and Infrastructure are the in-product surfaces. Auth gates them; Landing is public.

1a. Per-screen integration seams (where synthetic becomes live)

Each screen ships with a built-in synthetic animation so it looks alive offline. To make it real, register handlers in the logic class' componentDidMount() and feed setState. The exact seams:

  • World → _startLive() (the canvas engine). It holds targets[] (project/service/event nodes) and agents[] whose .target is currently chosen on a random timer. Replace the random retarget with world.touch: on each event, set the matching agent's .target to the index of nodeId and force a beam; the node-heat/glow already keys off "hot". Rebuild targets[] from topology.update. Hierarchy/Flat node lists also come from topology.update.
  • Observe → Agent mode. Bind the WORKING-ON-NOW card to agent.task.update (replace), the REASONING STREAM to agent.reasoning.delta (append), the live computer screen to agent.computer.frame (replace). The door toast binds to door.request / door.resolve.
  • Observe → System mode & Dashboard. Telemetry pills ← telemetry; comms list ← agent.message; routines/loops ← routine.update; status dots ← agent.status.
  • Infrastructure. Host health cards ← host.health + host.status; the Tailscale device list ← tailscale.devices; cloud runners ← cloud.runner. The Connect-a-host and Connect-a-provider wizards are driven by the daemon-pairing and provisioning APIs in §5.

Every seam is a few lines: ClawmatesLive.on('world.touch', e => engine.touch(e.agentId, e.nodeId)). No markup changes — you are feeding values into components that already exist.


2. Run it (60 seconds, no backend)

chmod +x install.sh && ./install.sh
# Terminal 1 — synthetic event bridge
(cd realtime && CLAWMATES_MODE=demo node server.mjs)
# Terminal 2 — serve the screens
caddy file-server --root ./screens --listen :8080      # or: npx serve ./screens -l 8080

Open http://localhost:8080/Clawmates%20World.dc.html?live=http://localhost:8420/live, switch to the Live formation, and the agents converge on events streamed from the bridge. Without ?live=, each screen runs its built-in synthetic animation (safe fallback). The ?live= param (or window.CLAWMATES_LIVE_URL) is read by clawmates-live.js.


3. Real-time architecture & the Event Taxonomy

The runner emits many internal events; the bridge (realtime/server.mjs) normalizes them into a small, stable taxonomy and fans them out as one SSE feed. The browser only ever sees these. Full JSON Schema: realtime/events.schema.json. Human summary:

Agent workagent.status {agentId,status,role} · agent.task.update {agentId,taskId,title,elapsedMs,steps[]} · agent.reasoning.delta {agentId,text} (append) · agent.computer.frame {agentId,app,url?,lines[]}.

Tools, doors & §15agent.tool.call {agentId,tool,target,doorRequired} · door.request {doorId,agentId,action,target,summary} · door.resolve {doorId,decision,by}.

Collaboration & worldagent.message {fromAgentId,toAgentId,text,ts} · world.touch {agentId,nodeId,kind} (the Gource retarget) · node.activity {nodeId,label,kind,heat} · topology.update {formation,nodes[],edges[]}.

Systemtelemetry {tokensPerMin,costPerHr,loops,doorsPending} · routine.update {routineId,name,kind,owner,schedule?,progress?,state}.

Infrastructure (new — see §5)host.status {hostId,status} · host.health {hostId,cpu,ram,memPressure,disk,load,containers} · tailscale.devices {tailnet,devices[]} · cloud.runner {provider,id,region,size,status,cpu,ram,agents,costPerHr}.

The integration seam in the bridge

normalize(evt, payload) in server.mjs translates one raw runner event → zero+ taxonomy events. The upstream event names there (turn.started, tool.invoked, runner.telemetry, …) are placeholders — reconcile them with the actual cm-api SSE shapes. Start by mapping just world.touch, agent.status, and telemetry; that alone brings World and the top bars alive. Add the rest incrementally. Unknown events are ignored, so the bridge is forward-compatible.

The client adapter (clawmates-live.js)

Include it in a screen (or inject via the Next proxy). It opens an EventSource, dispatches each event to ClawmatesLive.on(type, fn) handlers and a window CustomEvent('clawmates:<type>'), and replays the last value per stateful key so a late-loading screen paints current state immediately (mirrors the runner's checkpoint-resume). If no live URL is set, it is inert.

Security invariant — carry it through. The bridge is read-only on the runner stream. It must never hold or forward secret-broker credentials, and it must expose no door-approval endpoint to the browser. Door approvals stay on the authenticated cm-api path; the bridge only renders that a door is pending so the UI can badge it.


4. Wiring to cm-api [INTEGRATES WITH cm-api]

Live mode points the bridge at the runner's SSE gateway and exchanges a session for a bearer token, exactly like the Next.js /api proxy. .env:

CLAWMATES_MODE=live
CM_API_SSE_URL=http://127.0.0.1:8080/v1/runs/stream   # cm-api streaming gateway
CM_API_TOKEN=sk-...                                    # read-only, scoped to run events
LIVE_PORT=8420
LIVE_ALLOWED_ORIGINS=http://localhost:8080             # the screens' origin (CORS)

Deployment shapes:

  • A) Standalone: serve screens/ with Caddy; run the bridge as its own systemd service beside the product. Good for an ops/observability surface.
  • B) In-app (tightest): mount each screen in the Next.js app via <iframe src="/viz/world?live=/api/live"> and proxy /api/live to the bridge so it inherits the app's session→bearer swap and CORS.

systemd unit for the bridge (read-only, hardened):

# /etc/systemd/system/clawmates-live.service
[Unit]
Description=Clawmates Live Visualization Bridge
After=network.target
[Service]
WorkingDirectory=/opt/clawmates/handoff/realtime
EnvironmentFile=/opt/clawmates/handoff/.env
ExecStart=/usr/bin/node server.mjs
Restart=always
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

5. Infrastructure wiring (Local & Tailscale first, then Cloud)

The Infrastructure screen is the most backend-coupled. It maps directly onto the daemon + tailnet + cloud-provisioning model.

5a. Local hosts — the clawmates-node daemon

The Connect a host wizard (Install → Connect → Verify) is the UI for the daemon-pairing flow:

  1. Install. cm-api mints a one-time token and returns the install command shown in the wizard: clawmates-node --server https://clawmates.work --token <token> (Docker should be present on the node so it can run agents). The curl … /install.sh | bash one-liner needs the binary hosted; for now cargo build --release -p clawmates-node once, then run the line.
  2. Connect. The daemon dials home over an outbound WSS (NAT-friendly, no inbound port). When cm-api sees the socket, emit host.status {hostId,status:"connected"} + the host specs; the wizard flips to Connected.
  3. Verify. cm-api runs the built-in check (uname + docker version) on the node; stream the result so the wizard shows the verification and the host is added to the fleet.

Thereafter stream host.health (CPU / RAM / mem pressure / disk / load / containers) on an interval → the live host cards. If the host is on Tailscale, include its 100.x IP so the card can show the copyable ssh <tailscale-ip> target.

5b. Tailscale network — fleet metrics

The Tailscale network panel maps to: Infra → Fleet → Tailscale network → Connect. The user pastes their tailnet (e.g. your-org.ts.net) + a Tailscale API key; cm-api polls the Tailscale API and emits tailscale.devices {tailnet, devices:[{name,ip,os,status,seen}]} → the live device list (online / last-seen / IP / OS). Note the daemon can also join the tailnet for the operator (tailscale up --ssh, or pass --tailscale-authkey <key> to have the daemon run it) — --ssh is what gives keyless, ACL-gated SSH (ensure the tailnet ACL permits SSH via an ssh rule / tag).

5c. Cloud providers — provision runners

The Cloud screen connects AWS / GCP / Apple silicon / Hetzner / DigitalOcean. The connect wizard maps to:

  1. Credentials → stored in the secret broker over its private socket; they never enter agent code or any sandbox. (Same invariant as §3.)
  2. Region + instance size → the provision request.
  3. Provision → cm-api launches a VM from an image with clawmates-node baked in; on boot it dials home over WSS (no inbound port) and the §15 sandbox profile is applied. Emit cloud.runner updates → the live cloud-runner cards (CPU/RAM, agent count, hourly cost). Deprovision terminates the VM.

Ordering: ship Local & Tailscale first (own hardware, zero inbound exposure); Cloud is the next rung and reuses the same daemon + WSS + §15 model — it's the same fleet, just provisioned for you.


6. Auth (Clerk)

The Auth screen is a themed Clerk surface (Google / Apple / GitHub OAuth + email, sign-in ↔ sign-up toggle). To make it real, replace the static markup with Clerk's <SignIn> / <SignUp> components and pass an appearance theme that reuses the screen's tokens (near-black #08080a, coral #ff6f61, the rounded inputs and primary button). cm-api auth is local by default or clerk at runtime — match whichever the deployment uses. The bridge/screens never see auth secrets.


7. Verification checklist

  1. node realtime/server.mjs (demo) prints Live bridge on http://localhost:8420/live · mode=demo.
  2. curl -N http://localhost:8420/live streams event: … lines that don't stop.
  3. World ?live=…Live shows particles retargeting in sync with the world.touch lines in the curl output (not the smooth random demo motion).
  4. Kill the bridge mid-stream → the screen shows reconnect, then resumes (EventSource auto-reconnect + adapter replay).
  5. [live] Point at cm-api, trigger a run, and confirm telemetry numbers in the top bar move and a real door.request raises the toast in Observe.
  6. [infra] Run the daemon install line on a test node → host.status connected → health card appears → ssh <tailscale-ip> target resolves.

  1. Bridge normalize() → reconcile upstream event names with cm-api; map world.touch, agent.status, telemetry first.
  2. World Live engine ← world.touch + topology.update. (Biggest payoff, smallest change.)
  3. Observe Agent ← agent.task.update / agent.reasoning.delta / agent.computer.frame; door toast.
  4. Infrastructure ← daemon pairing (§5a), tailnet metrics (§5b), then cloud provisioning (§5c).
  5. Auth ← Clerk components with the theme tokens.
  6. Promote the bridge from read-only mirror to a typed gateway with backpressure + per-tenant scoping; add a time-scrubber to replay the runner's checkpoint log (Gource-style playback).

The .dc.html files in screens/ are the source of truth for the UI. This document plus realtime/ is everything needed to serve them and make them breathe with live agent activity. Keep support.js next to the screens.