chore: commit ZeroClaw per-tenant runtime spike + architecture doc
Saves earlier-phase artifacts that were sitting untracked: - docs/agent-engine-architecture.md — per-tenant containerized ZeroClaw runtime decision doc (clawmates = §15 control plane; zeroclaw = per-tenant runtime). - deploy/clawmates-runtime/ — slim runtime Dockerfile, dev compose, example agent config, README (the proven Phase-1 drive recipe). - tools/runtime-spike/drive.mjs — Node WS drive client for the spike. No secrets (only env-var names / commented placeholders). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2621e97ad2
commit
bd982943b8
@@ -0,0 +1,32 @@
|
|||||||
|
# Slim Clawmates runtime = the zeroclaw daemon (gateway API), API-only.
|
||||||
|
# Simple full-source build (avoids the upstream Dockerfile's stale manifest-
|
||||||
|
# prefetch step). web/dist may be empty → dashboard is omitted, /api/* works.
|
||||||
|
# Requires BuildKit (cache mounts). Build context = the zeroclaw source tree.
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# bookworm-pinned so the binary's glibc matches the bookworm runtime stage
|
||||||
|
FROM rust:1.94-slim-bookworm AS build
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
pkg-config build-essential cmake libssl-dev ca-certificates git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
# Ensure the gateway's include_dir!("../../web/dist") target exists at compile time.
|
||||||
|
RUN mkdir -p web/dist
|
||||||
|
RUN --mount=type=cache,id=cm-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
|
||||||
|
--mount=type=cache,id=cm-cargo-git,target=/usr/local/cargo/git,sharing=locked \
|
||||||
|
--mount=type=cache,id=cm-target-bookworm,target=/app/target,sharing=locked \
|
||||||
|
cargo build --release --locked --bin zeroclaw \
|
||||||
|
&& cp target/release/zeroclaw /usr/local/bin/zeroclaw
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=build /usr/local/bin/zeroclaw /usr/local/bin/zeroclaw
|
||||||
|
ENV HOME=/zeroclaw-data \
|
||||||
|
ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace \
|
||||||
|
ZEROCLAW_GATEWAY_PORT=42617
|
||||||
|
RUN mkdir -p /zeroclaw-data/workspace
|
||||||
|
EXPOSE 42617
|
||||||
|
ENTRYPOINT ["zeroclaw"]
|
||||||
|
CMD ["daemon"]
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
```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 `<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/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: <code>` → `{token}`.
|
||||||
|
3. **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":"…"}`; 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=<code> node tools/runtime-spike/drive.mjs http://<host>:42617 scout "hi"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Phase-1 spike config for a tenant runtime: one provider model + one agent.
|
||||||
|
# Secrets are NOT committed — the API key is injected at runtime via env
|
||||||
|
# ZEROCLAW_providers__models__groq__default__api_key=<key>
|
||||||
|
# (double-underscore = config nesting). Swap groq→anthropic for prod.
|
||||||
|
|
||||||
|
# Mark onboarding complete (headless equivalent of the browser Quickstart) so
|
||||||
|
# the agent will answer; otherwise /ws/chat returns NEEDS_ONBOARDING.
|
||||||
|
[onboard_state]
|
||||||
|
quickstart_completed = true
|
||||||
|
|
||||||
|
[providers.models.groq.default]
|
||||||
|
model = "llama-3.3-70b-versatile"
|
||||||
|
|
||||||
|
# One named agent == one Clawmates "claw". For real §15 we lock tools.allow and
|
||||||
|
# add an MCP client → the Clawmates gated door; for the spike we keep it minimal
|
||||||
|
# and use the Supervised profile so sensitive tools raise an approval_request.
|
||||||
|
[agents.scout]
|
||||||
|
model_provider = "groq.default"
|
||||||
|
risk_profile = "supervised"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Clawmates per-tenant runtime — Phase 1 dev stack (gw-04)
|
||||||
|
# One tenant = one zeroclaw daemon container. Isolated on its own network so it
|
||||||
|
# cannot disturb gr33t / clawbrainhub. Gateway bound to localhost only.
|
||||||
|
#
|
||||||
|
# NOTE: provider key + gateway token are injected from Infisical at deploy time
|
||||||
|
# (do NOT commit secrets). The exact gateway-token env name is TODO (see README).
|
||||||
|
|
||||||
|
name: clawmates-dev
|
||||||
|
|
||||||
|
networks:
|
||||||
|
clawmates-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
tenant-acme-data: {} # per-tenant /zeroclaw-data (config.toml, domain.db, brain.h5, workspace/)
|
||||||
|
|
||||||
|
services:
|
||||||
|
# The user's private multi-agent runtime (one per tenant in production).
|
||||||
|
runtime:
|
||||||
|
image: web-01:5000/clawmates-runtime:dev
|
||||||
|
container_name: clawmates-dev-runtime
|
||||||
|
command: ["daemon"]
|
||||||
|
restart: unless-stopped
|
||||||
|
networks: [clawmates-net]
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:42617:42617" # gateway, localhost-only on gw-04 (tunnel for the spike)
|
||||||
|
volumes:
|
||||||
|
- tenant-acme-data:/zeroclaw-data
|
||||||
|
environment:
|
||||||
|
ZEROCLAW_GATEWAY_PORT: "42617"
|
||||||
|
ZEROCLAW_WORKSPACE: "/zeroclaw-data/workspace"
|
||||||
|
# Provider key (from Infisical at deploy; example shape — confirm exact path):
|
||||||
|
# ZEROCLAW_providers__models__anthropic__default__api_key: "${ANTHROPIC_API_KEY}"
|
||||||
|
# Gateway auth token (env name TODO — see README):
|
||||||
|
# ZEROCLAW_GATEWAY_TOKEN: "${CLAWMATES_DEV_GATEWAY_TOKEN}"
|
||||||
|
# Hardening to add next: read-only rootfs, drop caps, no-new-privileges, mem/cpu limits.
|
||||||
|
|
||||||
|
# TODO (Phase 3): the Clawmates §15 MCP server — the single gated outbound door
|
||||||
|
# (email/slack/pay/delete/…) backed by cm-safety + cm-secrets + the audit journal.
|
||||||
|
# mcp:
|
||||||
|
# image: web-01:5000/clawmates-mcp:dev
|
||||||
|
# networks: [clawmates-net]
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Agent Engine Architecture — per-tenant containerized runtime
|
||||||
|
|
||||||
|
_Status: accepted direction (Phase 0). Supersedes the assumption that Clawmates' own
|
||||||
|
`cm-runtime` is the long-term execution engine. This document is the source of truth for the
|
||||||
|
backend agent-execution strategy; implementation proceeds in the phases at the end._
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Clawmates must let each customer build their own multi-agent "company/team" and run it at
|
||||||
|
scale: many agents, across machines, with full lifecycle management (provision → run →
|
||||||
|
suspend → resume → retire) — without weakening the §15 safety contract (per-tool human
|
||||||
|
approval, taint, secret broker, single audited journal) that is the product's moat and is
|
||||||
|
acceptance-blocking.
|
||||||
|
|
||||||
|
Clawmates' current `cm-runtime` (`crates/cm-runtime/src/runtime.rs`) is a real agent loop, but
|
||||||
|
it is single-host: one server process drives all agents. It does not scale horizontally or
|
||||||
|
isolate tenants at the infrastructure layer.
|
||||||
|
|
||||||
|
## Options considered
|
||||||
|
|
||||||
|
| Engine | Verdict |
|
||||||
|
|---|---|
|
||||||
|
| **clawverse** (Rust, distributed mission orchestrator) | Closest to "distributed agent fleet," but mission/repo-shaped, partly WIP, and re-implements node scheduling/allocation/health that Kubernetes already provides. **Shelved** — revisit only if one tenant must span many nodes/GPUs, or for its mission/self-improvement IP. |
|
||||||
|
| **openclaw** (Node agent OS) | Most complete *single-host* agent OS, but Node (our stack is Rust), single-tenant by design, and we lack confirmed source + license to fork/ship it. **Not the base.** |
|
||||||
|
| **zeroclaw** (`~/projects/zeroclaw`, Rust, MIT/Apache) | Multi-agent-per-daemon runtime; ownable, single-binary, embeddable, ~80 providers, 6-layer sandbox, gateway/RPC drive API. **Chosen as the per-tenant runtime.** |
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Run one containerized ZeroClaw daemon per workspace/tenant.** The unit of scaling becomes the
|
||||||
|
*tenant container*, placed and lifecycle-managed by a standard orchestrator (Kubernetes or Fly
|
||||||
|
Machines) — which replaces a bespoke node/allocation layer. Clawmates remains the **control
|
||||||
|
plane**: tenancy, auth, billing, the audited SSE gateway, and the **§15 safety engine**. The
|
||||||
|
tenant's agents reach the outside world **only** through Clawmates-gated capabilities.
|
||||||
|
|
||||||
|
ZeroClaw **v0.8.0** makes this viable nearly out of the box:
|
||||||
|
- **Multi-agent per daemon** — isolated workspace/memory/model/policy/persona per agent → one
|
||||||
|
container = one tenant's whole team.
|
||||||
|
- **RPC transport** (local socket / remote WSS + token) with **restart-surviving, tmux-style
|
||||||
|
sessions** → clean drive channel and clean scale-to-zero/resume.
|
||||||
|
- **Schema-V3 config CRUD** over the gateway (`/api/config/*`) → declarative provisioning.
|
||||||
|
- **Per-agent tool allowlists + MCP client + private-host allowlists** → the hook to make
|
||||||
|
Clawmates the only outbound door.
|
||||||
|
- **Attribution-aware structured logs** + **per-agent/per-model cost tracking** → feed the
|
||||||
|
audited journal and billing.
|
||||||
|
- **Lean channel bundle as Cargo features** (`crates/zeroclaw-channels/Cargo.toml`:
|
||||||
|
`default-channels = acp-server, email, telegram, webhook`) → tiny, fast-cold-start image.
|
||||||
|
- **MIT/Apache license** → we may fork, containerize, and ship it.
|
||||||
|
|
||||||
|
## Existing infrastructure & what we reuse (this is already proven here)
|
||||||
|
|
||||||
|
The per-tenant ZeroClaw-in-a-container model is **already running in production** in this fleet
|
||||||
|
(ClawBooks, EasyA, gr33t). Clawmates reuses that proven stack rather than introducing K8s/Fly:
|
||||||
|
|
||||||
|
- **Substrate:** Docker + Nginx/Traefik + a per-app **orchestrator** on Hetzner VMs (Tailscale mesh).
|
||||||
|
The orchestrator (`:3500`) provisions/deprovisions a container per user; a webhook bridge (`:3501`)
|
||||||
|
reacts to Clerk `user.created`; Nginx routes `/ws/agent/{userId}` → the container.
|
||||||
|
- **Per-user container:** ZeroClaw binary + a small domain sidecar + plugins/skills.
|
||||||
|
- **Per-user volume:** `/data/users/{id}/` = `config.toml`, `domain.db` (SQLite), `brain.h5`
|
||||||
|
(ZeroClaw's HDF5/EdgeHDF5 memory) , `workspace/`, `channels/`. Backup = **2 portable files**
|
||||||
|
(`domain.db` + `brain.h5`) rsynced offsite — trivial tenant move/restore.
|
||||||
|
- **Image registry:** private registry at `web-01:5000` (Tailscale-only).
|
||||||
|
- **Auth + billing:** Clerk + Stripe (Clawmates already supports Clerk auth mode).
|
||||||
|
- **Bootstrap scripts (reuse):** `redclawsystems/zeroclaw → deploy/clawbooks/bootstrap/`
|
||||||
|
(`scale-up.sh` = Hetzner provision + bootstrap + deploy in ~5 min; `bootstrap-node.sh`;
|
||||||
|
`deploy-to-node.sh`).
|
||||||
|
- **Secrets:** Infisical at `icarus.lan:8443` (`HETZNER_API_TOKEN_RW/RO`, `ANTHROPIC/OPENAI/…`,
|
||||||
|
Clerk, Stripe, Cloudflare, Tailscale). ⚠️ Some are still plaintext in the vault and flagged for
|
||||||
|
rotation — rotate before production.
|
||||||
|
|
||||||
|
### Resources / deploy target (live check)
|
||||||
|
|
||||||
|
The 6-server Hetzner fleet (Helsinki) has ample headroom; **no new server is needed**. Dev/test
|
||||||
|
target: **gw-05** (8c / 15Gi, ~188G free, lightly loaded) — or gw-01 (~234G free). Estimated
|
||||||
|
capacity ~50–100 concurrent per-tenant containers across the fleet without new hardware.
|
||||||
|
|
||||||
|
## Target architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Clawmates control plane (Rust SaaS) — responsibilities UNCHANGED │
|
||||||
|
│ • tenancy, auth, billing, product UI, the single audited SSE gateway │
|
||||||
|
│ • §15 engine: approvals · taint · gated categories · single-use │
|
||||||
|
│ grants · secret broker (cm-safety, cm-secrets, cm-api) │
|
||||||
|
│ • Clawmates MCP server: the ONLY outbound door (email/slack/pay/…) │
|
||||||
|
│ • tenant-runtime manager: provision/suspend/resume one container/tenant│
|
||||||
|
└───────────────┬────────────────────────────────────────────────────────┘
|
||||||
|
│ (a) provision agents via /api/config
|
||||||
|
│ (b) run turns via sessions/prompt (SSE) or RPC
|
||||||
|
│ (c) pull cost + logs
|
||||||
|
│ (d) agent's only egress = our gated MCP tools
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Per-tenant ZeroClaw daemon (slim container, our fork) │
|
||||||
|
│ • N named agents (= N clawmates): per-agent workspace/memory/model │
|
||||||
|
│ • NO native outbound channels/secrets; tools.allow locked │
|
||||||
|
│ • MCP client → Clawmates MCP server for every sensitive capability │
|
||||||
|
└───────────────┬────────────────────────────────────────────────────────┘
|
||||||
|
│ spawns sandboxes for shell/browser
|
||||||
|
▼
|
||||||
|
Per-agent sandboxes — sibling pods / gVisor, brokered via socket-proxy
|
||||||
|
▲
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Substrate: Hetzner VMs (Tailscale) · Docker · Nginx/Traefik · │
|
||||||
|
│ per-app orchestrator (:3500) + Clerk webhook bridge (:3501) │
|
||||||
|
│ — the EXISTING proven ClawBooks/EasyA stack (replaces clawverse). │
|
||||||
|
│ K8s/Fly only if scale later demands it. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Concept mapping
|
||||||
|
|
||||||
|
| Clawmates | ZeroClaw |
|
||||||
|
|---|---|
|
||||||
|
| workspace / tenant | one daemon (one container) |
|
||||||
|
| claw (agent) | one named agent (isolated workspace/memory/model/policy) |
|
||||||
|
| chat session / turn | `POST /api/sessions/new` + `POST /api/session/{id}/prompt` (SSE), or an RPC session |
|
||||||
|
| the claw's "computer" / files | `/api/agents/{alias}/workspace/*` + a sibling sandbox for shell/browser |
|
||||||
|
| routine | a zeroclaw cron job bound to that agent |
|
||||||
|
| credits / billing | `/api/cost` (per-agent / per-model splits, cached-input tokens) |
|
||||||
|
| audit journal (`run_events`) | gateway attribution-aware logs, mirrored into `run_events` |
|
||||||
|
| §15 outbound action | a **Clawmates MCP tool** the agent calls — we gate + broker + journal |
|
||||||
|
|
||||||
|
## §15 — the safety contract stays in Clawmates
|
||||||
|
|
||||||
|
Two independent layers:
|
||||||
|
|
||||||
|
1. **No native edges.** Each tenant agent is provisioned with `tools.allow` excluding native
|
||||||
|
outbound/dangerous tools and with **no real channel or secret config**. It physically cannot
|
||||||
|
email, post, pay, or exfiltrate on its own.
|
||||||
|
2. **One gated door.** Clawmates exposes an **MCP server** of the sensitive capabilities (send
|
||||||
|
email, post to Slack, move money, delete data, change access, grant infra access). When an
|
||||||
|
agent invokes one:
|
||||||
|
- the call lands in Clawmates → classify `Effect` + `TaintSource` (reuse `cm-tools` policy),
|
||||||
|
- if gated → create an `approvals` row, render a preview, **suspend**, await a human decision,
|
||||||
|
- on approve → consume the single-use `execution_grants` row, **broker-execute with the
|
||||||
|
secret** (`cm-secrets`, never exposing the credential), and journal to `run_events`.
|
||||||
|
|
||||||
|
This reuses the existing approval state machine (`cm-safety`), secret broker (`cm-secrets`),
|
||||||
|
and the audited gateway (`cm-api`) verbatim. Defense-in-depth: run agents at ZeroClaw's
|
||||||
|
**Supervised** risk profile so its own per-dispatch allowlist + `ask_operator` back-stop us.
|
||||||
|
|
||||||
|
The §15 properties (taint-by-default, gated categories, single audited channel, single-use
|
||||||
|
grants, broker isolation) are therefore **unchanged** — they move from gating `cm-runtime`'s
|
||||||
|
in-process tools to gating the MCP boundary.
|
||||||
|
|
||||||
|
## Slim per-tenant image
|
||||||
|
|
||||||
|
- Fork/vendor zeroclaw; build with only needed features
|
||||||
|
(`--no-default-features --features channel-webhook` + MCP client), no native chat channels.
|
||||||
|
Push to the private registry at `web-01:5000`.
|
||||||
|
- Gateway bound to an in-pod socket / WSS with a per-tenant token; Nginx routes
|
||||||
|
`/ws/agent/{userId}` → the container (existing convention).
|
||||||
|
- Per-tenant **persistent volume** at `/data/users/{id}/` (`config.toml`, `domain.db`,
|
||||||
|
`brain.h5`, `workspace/`) — the proven 2-file (`domain.db` + `brain.h5`) backup/restore applies.
|
||||||
|
- Provider keys injected at start via env (`ZEROCLAW_*`) from Infisical / the Clawmates broker,
|
||||||
|
not baked into the image.
|
||||||
|
|
||||||
|
## Risks / open questions
|
||||||
|
|
||||||
|
- **Idle economics** — a container per user is costly; the existing orchestrator can **stop idle
|
||||||
|
containers** and start them on next request (per-tenant volume persists; v0.8 restart-surviving
|
||||||
|
sessions make resume clean). Tune idle TTL / wake latency; K8s+KEDA or Fly auto-stop only if we
|
||||||
|
outgrow the orchestrator.
|
||||||
|
- **Nested sandboxing** — tool sandboxes inside a tenant container; prefer **sibling** sandbox
|
||||||
|
pods via the existing `socket-proxy` pattern over privileged DinD; evaluate gVisor/sysbox.
|
||||||
|
- **§15 coverage** — must guarantee *no* un-gated path to the outside; audit zeroclaw's native
|
||||||
|
tool set per build and lock `tools.allow`.
|
||||||
|
- **Fork maintenance** — zeroclaw is v0.8 **beta**, workspace `publish=false`; maintain a thin
|
||||||
|
fork/vendor and track upstream.
|
||||||
|
- **Provisioning contract** — Schema-V3 config CRUD is the provisioning API; pin its behavior.
|
||||||
|
|
||||||
|
## Phased delivery
|
||||||
|
|
||||||
|
- **Phase 0 — this document.** Decision + architecture + §15-via-MCP + risks. ✅
|
||||||
|
- **Phase 1 — Spike (on gw-05).** Build a slim zeroclaw container (push to `web-01:5000`); from a
|
||||||
|
small Clawmates-side client, provision one agent via `/api/config`, run one turn via
|
||||||
|
`sessions/prompt`, stream events back, and prove a §15 gate by routing one outbound action
|
||||||
|
through a stub Clawmates MCP tool. Reuse the ClawBooks `deploy/clawbooks/bootstrap/` scripts as
|
||||||
|
the starting point.
|
||||||
|
- **Phase 2 — Provisioning + lifecycle.** Adapt the existing orchestrator (`:3500`) + Clerk
|
||||||
|
webhook (`:3501`) + Nginx pattern to provision/stop/resume a Clawmates tenant container +
|
||||||
|
per-claw agent on workspace/claw create.
|
||||||
|
- **Phase 3 — §15 MCP server + journal/cost bridge.** Real gated MCP capabilities (`cm-safety` /
|
||||||
|
`cm-secrets`); map zeroclaw attribution logs + `/api/cost` into `run_events` / billing.
|
||||||
|
- **Phase 4 — Idle stop/resume + per-agent sandboxes (sibling containers via socket-proxy).**
|
||||||
|
- **Phase 5 — Cutover** from `cm-runtime` to the containerized runtime behind a feature flag.
|
||||||
|
|
||||||
|
> Reuse source-of-truth: `redclawsystems/zeroclaw → deploy/clawbooks/bootstrap/` and the
|
||||||
|
> ClawBooks/EasyA orchestrator. Rotate the plaintext Hetzner/Cloudflare/Clerk tokens (vault
|
||||||
|
> `Infrastructure Overview.md`) into Infisical before production.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Phase 1: end-to-end on a dev box — one claw answers a turn on a containerized zeroclaw via
|
||||||
|
Clawmates, events journaled, one approval gate fires through the Clawmates MCP door.
|
||||||
|
- Keep Clawmates' E2E + §15 suites (`p0`–`p7`, approval/secret-broker tests) green against the
|
||||||
|
new backend behind the flag before cutover.
|
||||||
|
- Confirm slim-image cold-start and resume-from-sleep preserve sessions.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Clawmates runtime + safety: `crates/cm-runtime/src/runtime.rs`, `crates/cm-safety/`,
|
||||||
|
`crates/cm-secrets/`, `crates/cm-api/`, `crates/cm-tools/`, `migrations/0001_init.sql`,
|
||||||
|
spec `docs/spec.md` §15.
|
||||||
|
- ZeroClaw: `~/projects/zeroclaw` — `crates/zeroclaw-channels/Cargo.toml` (channel features),
|
||||||
|
`crates/zeroclaw-gateway/src` (drive API: config / sessions / workspace / cost / cron),
|
||||||
|
multi-agent runtime + RPC transport (v0.8.0).
|
||||||
|
- Infra & proven pattern (Obsidian vault `~/projects/Valhalla`): `20 Infrastructure/Infrastructure
|
||||||
|
Overview.md`, `20 Infrastructure/Tailscale Network Map.md`, `20 Infrastructure/10 Architecture/
|
||||||
|
SaaS Platform Template.md`, `20 Infrastructure/30 Runbooks/Credentials Inventory.md`.
|
||||||
|
- Reuse: `redclawsystems/zeroclaw → deploy/clawbooks/bootstrap/`; private registry `web-01:5000`;
|
||||||
|
Infisical `icarus.lan:8443`; dev/test box `gw-05`.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Phase-1 drive client: pair with a ZeroClaw gateway, open /ws/chat as an
|
||||||
|
// agent, send one message, and print the streamed turn events. This is the
|
||||||
|
// Clawmates-side seam that (in prod) the control plane uses to run a claw's
|
||||||
|
// turn and journal its events. Node >=22 (global WebSocket + fetch).
|
||||||
|
//
|
||||||
|
// node tools/runtime-spike/drive.mjs <base_url> <agent_alias> "<message>"
|
||||||
|
// e.g. node tools/runtime-spike/drive.mjs http://100.108.129.81:42617 scout "Say hi in 5 words"
|
||||||
|
|
||||||
|
const [base, agent, message] = process.argv.slice(2);
|
||||||
|
if (!base || !agent || !message) {
|
||||||
|
console.error('usage: drive.mjs <base_url> <agent_alias> "<message>"');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Pair: read the one-time code from env (printed in the daemon's startup log).
|
||||||
|
const code = process.env.PAIR_CODE;
|
||||||
|
if (!code) {
|
||||||
|
console.error("set PAIR_CODE=<code from daemon startup log>");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
const pairRes = await fetch(`${base}/pair`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "X-Pairing-Code": code, "Content-Type": "application/json" },
|
||||||
|
body: "{}",
|
||||||
|
});
|
||||||
|
if (!pairRes.ok) {
|
||||||
|
console.error(`pair failed: ${pairRes.status} ${await pairRes.text()}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const pair = await pairRes.json();
|
||||||
|
const token = pair.token ?? pair.bearer ?? pair.access_token;
|
||||||
|
console.log(`✓ paired, token acquired (${String(token).slice(0, 6)}…)`);
|
||||||
|
|
||||||
|
// 2) Open the agent chat socket and run one turn.
|
||||||
|
// Node's global WebSocket can't set headers, so pass the token as ?token=
|
||||||
|
// (gateway accepts header > subprotocol > query).
|
||||||
|
const wsUrl =
|
||||||
|
base.replace(/^http/, "ws") +
|
||||||
|
`/ws/chat?agent=${encodeURIComponent(agent)}&name=${encodeURIComponent("clawmates-spike")}` +
|
||||||
|
`&token=${encodeURIComponent(token)}`;
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
const t0 = Date.now();
|
||||||
|
ws.addEventListener("open", () => {
|
||||||
|
console.log("✓ ws open → sending message");
|
||||||
|
ws.send(JSON.stringify({ type: "message", content: message }));
|
||||||
|
});
|
||||||
|
ws.addEventListener("message", (ev) => {
|
||||||
|
let m;
|
||||||
|
try { m = JSON.parse(ev.data); } catch { return console.log("raw:", ev.data); }
|
||||||
|
switch (m.type) {
|
||||||
|
case "session_start": console.log(` session_start id=${m.session_id} resumed=${m.resumed}`); break;
|
||||||
|
case "chunk": process.stdout.write(m.content ?? ""); break;
|
||||||
|
case "tool_call": console.log(`\n [tool_call] ${m.name} ${JSON.stringify(m.args)}`); break;
|
||||||
|
case "tool_result": console.log(` [tool_result] ${m.name}: ${String(m.output).slice(0, 120)}`); break;
|
||||||
|
case "approval_request": console.log(`\n [APPROVAL REQUIRED] ${JSON.stringify(m)} ← §15 gate hook`); break;
|
||||||
|
case "done":
|
||||||
|
console.log(`\n✓ done in ${Date.now() - t0}ms`);
|
||||||
|
ws.close(); process.exit(0);
|
||||||
|
case "error": console.error("\n✗ error:", JSON.stringify(m)); ws.close(); process.exit(1);
|
||||||
|
default: console.log(" evt:", JSON.stringify(m));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ws.addEventListener("error", (e) => { console.error("ws error:", e.message ?? e); process.exit(1); });
|
||||||
|
setTimeout(() => { console.error("timeout (90s)"); process.exit(1); }, 90_000);
|
||||||
Reference in New Issue
Block a user