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]>
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 holdstargets[](project/service/event nodes) andagents[]whose.targetis currently chosen on a random timer. Replace the random retarget withworld.touch: on each event, set the matching agent's.targetto the index ofnodeIdand force a beam; the node-heat/glow already keys off "hot". Rebuildtargets[]fromtopology.update. Hierarchy/Flat node lists also come fromtopology.update. - Observe → Agent mode. Bind the WORKING-ON-NOW card to
agent.task.update(replace), the REASONING STREAM toagent.reasoning.delta(append), the live computer screen toagent.computer.frame(replace). The door toast binds todoor.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 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:
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/liveto 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:
- 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). Thecurl … /install.sh | bashone-liner needs the binary hosted; for nowcargo build --release -p clawmates-nodeonce, then run the line. - 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. - 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:
- Credentials → stored in the secret broker over its private socket; they never enter agent code or any sandbox. (Same invariant as §3.)
- Region + instance size → the provision request.
- Provision → cm-api launches a VM from an image with
clawmates-nodebaked in; on boot it dials home over WSS (no inbound port) and the §15 sandbox profile is applied. Emitcloud.runnerupdates → 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
node realtime/server.mjs(demo) printsLive bridge on http://localhost:8420/live · mode=demo.curl -N http://localhost:8420/livestreamsevent: …lines that don't stop.- World
?live=…→ Live shows particles retargeting in sync with theworld.touchlines in the curl output (not the smooth random demo motion). - Kill the bridge mid-stream → the screen shows reconnect, then resumes (EventSource auto-reconnect + adapter replay).
- [live] Point at cm-api, trigger a run, and confirm
telemetrynumbers in the top bar move and a realdoor.requestraises the toast in Observe. - [infra] Run the daemon install line on a test node →
host.statusconnected → health card appears →ssh <tailscale-ip>target resolves.
8. Build order (recommended)
- Bridge
normalize()→ reconcile upstream event names with cm-api; mapworld.touch,agent.status,telemetryfirst. - World Live engine ←
world.touch+topology.update. (Biggest payoff, smallest change.) - Observe Agent ←
agent.task.update/agent.reasoning.delta/agent.computer.frame; door toast. - Infrastructure ← daemon pairing (§5a), tailnet metrics (§5b), then cloud provisioning (§5c).
- Auth ← Clerk components with the theme tokens.
- 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.