v0.8.5 moved upstream's own container builders to rust:1.98-slim (#9527) and kept 1.96 only as the declared SOURCE floor - what the crates promise, not what upstream actually builds with. We were pinned at 1.96 and had never compiled this code on it; the local check ran on 1.97. Track upstream instead of trusting the floor, staying on the bookworm variant so the binary's glibc still matches the debian:bookworm-slim runtime stage. CARGO_BUILD_JOBS defaults to 6 because the whole fleet is offline and gw-04 is now both the only reachable x86_64 host and the box serving production, so a build must not take every core from the services running beside it. Built and deployed: clawmates-runtime:v085 reports zeroclaw 0.8.5, health 200 with every component ok including the new relay, pairing survived the recreate, and the claude_cli/kimi_cli slots still resolve alongside upstream's grok_cli. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
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.