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

281 lines
15 KiB
Markdown

# 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)
```bash
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 work**`agent.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 & §15**`agent.tool.call` `{agentId,tool,target,doorRequired}` · `door.request`
`{doorId,agentId,action,target,summary}` · `door.resolve` `{doorId,decision,by}`.
**Collaboration & world**`agent.message` `{fromAgentId,toAgentId,text,ts}` · `world.touch`
`{agentId,nodeId,kind}` (the Gource retarget) · `node.activity` `{nodeId,label,kind,heat}` ·
`topology.update` `{formation,nodes[],edges[]}`.
**System**`telemetry` `{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`:
```ini
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):
```ini
# /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.
---
## 8. Build order (recommended)
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.*