cm-orchestrator owns the topology graph; each turn now drives a real ZeroClaw
role-agent in a container via the proven gateway drive recipe, instead of a
tool-free cm-llm call.
- crates/cm-api/src/topology_exec.rs: ZeroClawDriveExecutor impl TurnExecutor —
pair (POST /pair + X-Pairing-Code, token cached) -> ws /ws/chat?agent=<alias>
-> send {type:message,content} -> drain chunk/done/approval_request/error.
approval_request is recorded as a BLOCKED GatedAction, never auto-approved (§15).
Role->alias via ZEROCLAW_AGENT_MAP, fallback ZEROCLAW_DEFAULT_AGENT (scout).
- POST /api/topologies/run {task,graph} -> execute() -> RunRecord, persisted
best-effort to the existing topology_runs table (no migration). compare stays
tool-free. from_env() is read in-handler so cm-api still boots unset.
- deploy/clawmates-runtime: example config now declares a tool-free multi-agent
role-cast; README documents the ZEROCLAW_* knobs + run endpoint.
tokio-tungstenite 0.26 (already in lock) + dev axum `ws` for the hermetic test.
3 lib tests green, clippy clean, SQLX_OFFLINE build clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
5.4 KiB
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_<dotted__path>=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.
# 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
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:
- 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="…"inconfig.tomlparses to an empty entry (groq.default = {}, model<unset>). 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/chatreturnsNEEDS_ONBOARDING).onboard_state+agents.*DO load fine fromconfig.toml.
- Provider-map entries must be set via the prop/env path, NOT a TOML sub-table.
- Pair for a bearer token: read the one-time code from the daemon's startup log
(
X-Pairing-Code: NNNNNN), thenPOST /pairwith headerX-Pairing-Code: <code>→{token}. - Run a turn over WebSocket
/ws/chat?agent=<alias>&token=<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":"…"}; receivesession_start→chunk*→tool_call/tool_result→approval_request(the §15 hook) →done.
Drive client: tools/runtime-spike/drive.mjs (Node ≥22). Example:
PAIR_CODE=<code> node tools/runtime-spike/drive.mjs http://<host>: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:42617ZEROCLAW_PAIRING_CODE— the one-time code from the daemon startup logZEROCLAW_AGENT_MAP— optionalrole=alias,role=alias(e.g.analyst=researcher)ZEROCLAW_DEFAULT_AGENT— fallback alias for unmapped roles (defaultscout)
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 withdrive.mjs, thenPOST /api/topologies/run {task, graph}(build a graph via/api/topologies/build) and confirm theRunRecordhas real per-step outputs + per-turn tokens.
Next
- Demonstrate a §15 gate: give
scouta tool under the Supervised profile and confirm theapproval_requestround-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_eventsjournal;/api/cost→ billing. docker savethe image →docker loadon gw-04 and runcompose.dev.ymlthere.