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]>
281 lines
15 KiB
Markdown
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.*
|