# Clawmates per-tenant runtime (ZeroClaw) — Phase 1 spike Goal of Phase 1: stand up **one** Clawmates tenant runtime container (a slim ZeroClaw daemon) on **gw-04**, drive it from a small Clawmates-side client (provision an agent → run a turn → stream events), and prove one **§15 gate** by routing an outbound action through a stub Clawmates MCP tool. See `docs/agent-engine-architecture.md` for the full design. ## Confirmed runtime contract (from the live MindHealth/ClawHealth images in prod) - Image entrypoint: `zeroclaw`, default cmd: `daemon` - Gateway port: `42617` (`ZEROCLAW_GATEWAY_PORT`) - Per-tenant data volume mounted at `/zeroclaw-data` (`HOME=/zeroclaw-data`, `ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace`) - Config overrides via env: `ZEROCLAW_=value` (double-underscore = nesting) ## Build the slim image (off-box, push to the private registry) ZeroClaw's own multi-stage Dockerfile builds the daemon; `ZEROCLAW_CARGO_FEATURES` selects channels. We drop the heavy defaults (lark/whatsapp) — Clawmates exposes outbound capabilities as gated MCP tools, not native channels. ```bash # from a build host (this Mac, or Gitea CI) with the zeroclaw checkout: docker build \ -t web-01:5000/clawmates-runtime:dev \ --build-arg ZEROCLAW_CARGO_FEATURES="channel-webhook" \ ~/projects/zeroclaw docker push web-01:5000/clawmates-runtime:dev # registry is Tailscale-only, insecure HTTP ``` > Build is heavy (zeroclaw is ~160k LOC Rust). Prefer building on a beefy box or CI, not gw-04; > gw-04 only *runs* the image. ## Run the dev stack on gw-04 ```bash scp deploy/clawmates-runtime/compose.dev.yml gw-04:/home/redclaw/clawmates/ ssh gw-04 'cd ~/clawmates && docker compose -f compose.dev.yml up -d' ``` The runtime gateway is bound to `127.0.0.1:42617` on gw-04 (not public). Reach it for the spike over the SSH/Tailscale tunnel; production fronts it with Traefik on its own router, isolated from gr33t/cbh. ## §15 model (unchanged moat) The tenant agent is provisioned with **no native outbound tools and no real channel/secret config** (`tools.allow` locked). Every sensitive capability (email/slack/pay/delete/…) is a **Clawmates MCP tool**; the agent's only egress is that gated MCP door, where `cm-safety` + `cm-secrets` apply approvals, taint, single-use grants, broker execution, and journaling. ## Confirmed drive recipe (Phase 1 ✅ — proven on tank) A turn ran end-to-end (`scout` answered over `/ws/chat`). The working flow: 1. **Configure** a tenant daemon. Two gotchas the spike uncovered: - **Provider-map entries must be set via the prop/env path, NOT a TOML sub-table.** `[providers.models.groq.default]\nmodel="…"` in `config.toml` parses to an *empty* entry (`groq.default = {}`, model ``). Set them via env instead: `ZEROCLAW_providers__models__groq__default__model=…`, `ZEROCLAW_providers__models__groq__default__api_key=…` (double-underscore = path; key from Infisical). - The agent references the provider by **`type.alias`**: `[agents.scout] model_provider = "groq.default"`. - Onboarding gate is a flag — set `[onboard_state] quickstart_completed = true` (else `/ws/chat` returns `NEEDS_ONBOARDING`). `onboard_state` + `agents.*` DO load fine from `config.toml`. 2. **Pair** for a bearer token: read the one-time code from the daemon's startup log (`X-Pairing-Code: NNNNNN`), then `POST /pair` with header `X-Pairing-Code: ` → `{token}`. 3. **Run a turn** over WebSocket `/ws/chat?agent=&token=` (Node's global WebSocket can't set headers, so the token goes in the query — gateway accepts header > subprotocol > query). Send `{"type":"message","content":"…"}`; receive `session_start` → `chunk*` → `tool_call`/`tool_result` → `approval_request` (the §15 hook) → `done`. Drive client: `tools/runtime-spike/drive.mjs` (Node ≥22). Example: ```bash PAIR_CODE= node tools/runtime-spike/drive.mjs http://:42617 scout "hi" ``` ## Driving topologies from Clawmates (`POST /api/topologies/run`) `cm-orchestrator` owns the topology graph; for each turn it drives one ZeroClaw role-agent in this container over the recipe above, via `ZeroClawDriveExecutor` (`crates/cm-api/src/topology_exec.rs`). The server reads these env knobs (unset = the endpoint 500s; the rest of cm-api is unaffected): - `ZEROCLAW_GATEWAY_URL` — e.g. `http://127.0.0.1:42617` - `ZEROCLAW_PAIRING_CODE` — the one-time code from the daemon startup log - `ZEROCLAW_AGENT_MAP` — optional `role=alias,role=alias` (e.g. `analyst=researcher`) - `ZEROCLAW_DEFAULT_AGENT` — fallback alias for unmapped roles (default `scout`) `agent.config.example.toml` declares the multi-agent role-cast (coordinator/researcher/writer/worker + `scout`), all tool-free. Manual E2E: bring up the stack, grab the pair code, sanity-drive one alias with `drive.mjs`, then `POST /api/topologies/run {task, graph}` (build a graph via `/api/topologies/build`) and confirm the `RunRecord` has real per-step outputs + per-turn tokens. ## Next - [ ] **Demonstrate a §15 gate**: give `scout` a tool under the Supervised profile and confirm the `approval_request` round-trip over the socket (drive client already handles it). - [ ] Replace zeroclaw's native approval with the **Clawmates MCP door** (`cm-safety`/`cm-secrets`). - [ ] Map WS events → `run_events` journal; `/api/cost` → billing. - [ ] `docker save` the image → `docker load` on **gw-04** and run `compose.dev.yml` there.