The python guest agent only ever worked because Firecracker's CI Ubuntu
image happens to ship python3. NONE of our images do — agent-base has
neither python nor git, agent-terminal has git but no python — so it
could never have run in a real mission rootfs. An agent that dictates
what must be installed in the image has the dependency backwards.
crates/bins/fcagent is a 905K static x86_64-unknown-linux-musl binary
that needs nothing from the rootfs it is dropped into. The wire is
unchanged on purpose — 4-byte BE length + JSON, ops ping/exec/put/get —
so microvm.rs and microvm_client.rs needed no edit at all.
std has no AF_VSOCK and the workspace denies `unsafe`, so it uses the
`vsock` crate. `process_group(0)` gives each command its own group without
unsafe, so a command that spawns background children can be killed
wholesale rather than outliving the run.
A unit test caught a bug that would have broken EVERY exec: sourcing the
image-env file with `. env.sh 2>/dev/null; cmd` returns rc=1 WITHOUT
running cmd, because `.` on a missing file makes a non-interactive POSIX
shell exit immediately. On any rootfs lacking that file every command
would have failed while looking like an ordinary non-zero exit. Guarded
with `if [ -f ]` now.
Other places a failure must not borrow an outcome's representation: a
killed command reports ok:false with no rc (not rc=124, which would read
as a build failure); `get` on a missing path is an error, not an empty
archive; a signalled process reports 128+signal rather than success.
Verified on tank: --vm-selftest still 8/8 with the agent swapped
(create 949ms, wire identical), fc-node-setup 8/8, and — the point of the
change — a rootfs built from clawmates/agent-terminal:dev, which has NO
python3, boots and reports `git version 2.39.5` from inside the VM.
Also fixes a shell bug in fc-build-rootfs.sh: $HOME in a double-quoted
default expanded on this Mac, so it looked for the node's binary under
/Users/quantum on a Linux host.
Co-Authored-By: Claude Opus 5 <[email protected]>
The first half of removing the shared bind mount. Not wired yet — this
adds the mechanism and its tests.
One cause, four fixes so far: .git/objects permission denied
(core.sharedRepository), the capture base being overwritten each phase,
COMMIT_EDITMSG root-owned, and reset --hard deleting a prior phase's work
(.git/clawmates-in-use). core.sharedRepository was never a general
solution — it covers objects and refs, and every OTHER file git touches
is a fresh opportunity. Copy-in/copy-out removes the cause instead: the
agent owns its filesystem with no second writer.
Measured before building, because the plan named copy cost as the open
risk: a real 65 MB checkout of this repo copies in 0.23s and out 0.18s on
gw-04. Not a risk at this size; re-measure an order of magnitude larger.
No compression — the payload crosses a local socket, so gzip would spend
CPU to save nothing.
Two safety properties, both tested:
- The archive comes back from a container the agent controls as ROOT, so
it is untrusted input. A `../ESCAPED` entry must not write outside the
destination. The test writes the tar header bytes by hand because the
tar crate refuses to BUILD such an entry through its safe API — which
is reassuring, but means the hostile case has to be constructed the way
an attacker would.
- Symlinks are packed as links, never dereferenced. Following them on
copy-IN would smuggle host files into the container; the test plants a
host secret behind a symlink and asserts its contents never appear in
the archive.
Ownership is deliberately not preserved on unpack: the archive's uids are
the container's root, and re-applying them on the host would recreate the
exact uid split this exists to remove.
413 tests, clippy clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Watches mission_artifacts for MD entries with render_pdf_status='pending'
and turns them into styled PDFs via:
1. Read source MD from <mission_root>/<path>
2. Call configured LLM (default gemini-2.5-flash) with a document-
typesetter system prompt that constrains style to a self-contained
HTML doc with inline CSS + our color palette
3. Print to PDF via `chromium --headless=new --print-to-pdf`
4. Save alongside source MD (foo.md → foo.pdf) + update
mission_artifacts.rendered_pdf_path + render_pdf_status='done'
Graceful degradation: GEMINI_API_KEY unset OR chromium missing =
row marked failed with a descriptive error, worker keeps ticking.
The frontend's "Open PDF" affordance (Slice 2) light up automatically
when render succeeds.
Boot ordering: PDF worker spawns after task_card_worker. Poll every
30s over up to MAX_PARALLEL=2 rows at a time — respects LLM rate
limits and keeps chromium's peak RAM under control.
Env knobs:
GEMINI_API_KEY — required for LLM step
CLAWMATES_PDF_RENDERER_MODEL — model id, default gemini-2.5-flash
CHROMIUM_BIN — chromium binary, default `chromium`
CLAWMATES_MISSIONS_ROOT — artifact dir root, default /var/lib/clawmates-missions
Dockerfile now installs chromium + fonts-liberation and sets
CHROMIUM_BIN=/usr/bin/chromium so the container image has everything
the renderer needs.
Also bumps workspace tokio deps to include the `process` feature
(required for tokio::process::Command).
Follow-ups:
- Anthropic + OpenAI provider variants (only Gemini in this slice)
- SSE stream on /api/missions/{id}/artifacts for the "PDF ready"
notification instead of poll-via-mission-GET
- Per-template PDF style overrides (currently one house style
for all missions)
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A pure async control-flow engine that executes a task across a TopologyGraph
by sequencing safe agent turns. Safety by construction: the engine can only
invoke turns via a generic TurnExecutor — it performs no side effects itself,
so §15 gating (inside each turn) is inherited and switching topology cannot
escalate authority.
- TurnExecutor trait + TurnRequest/TurnOutcome (real impl will wrap
cm-runtime::Runtime; tests use a scripted Echo executor).
- Pure planners (plan.rs): hierarchical (delegate down / synthesize up),
pipeline (topo-ordered threading), swarm (parallel attempts + aggregate).
- RunRecord journal (per-step + RunMetrics: tokens, gated actions, approvals
granted/blocked, turns) — feeds the Phase 4 comparison harness/paper.
- Unsupported kinds return an error (no panic). 5 tests, clippy clean.
Next (Phase 2b): a real TurnExecutor adapter over cm-runtime::send_message.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Full-depth rename per the approved plan; the 'claw' product vocabulary
(claws, /claws routes, clawId, Claw Chat) stays — it is now the brand.
- Display brand: Clawmates (manifest, titles, hero, login/rail logo
'clawmates'); default host app.clawmates.work; registry
ghcr.io/clawmates
- Crates tc-* -> cm-* (16 crates + all imports); binaries
clawmates-server/broker/bundler; images clawmates/*; env prefix
CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config
clawmates.toml; helm chart deploy/helm/clawmates with clawmates-*
resources; db names clawmates*; sockets /run/clawmates; cookie
cm_session; kind cluster clawmates-test; seccomp node profile
clawmates-agent-profile.json
- All 9 Playwright brand assertions updated in lockstep; historical
spec document left untouched as the only remaining 'TeamClaw'
- Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared
test server clawmates-test-pg, kind cluster recreated with image +
profile, compose images rebuilt under clawmates/*
Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright
journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and
the clean-room install rehearsal serving the clawmates login page from
a signed bundle of the rebuilt images.
Co-Authored-By: Claude Fable 5 <[email protected]>
- tc-telemetry: fmt subscriber always; with [telemetry] otlp_endpoint
set, spans batch-export over OTLP/HTTP. Tested against a REAL OTLP
receiver decoding the actual protobuf (official proto types): the
emitted span and service.name arrive on the wire. No endpoint = no
export = no network (air-gap stance). tower-http TraceLayer gives
every API request a span
- The broker finally has its own image (images/broker.Dockerfile,
9.5MB from scratch) — the Helm chart referenced one that never
existed — and the compose deployment now RUNS the broker, sharing a
socket volume with the server (the unix-socket equivalent of the K8s
sidecar). Compose secret flows were silently dead before this
- server.Dockerfile fixes surfaced by the rehearsal: the workspace
build needs tools/ (bundler joined the workspace) and
images/seccomp/ (include_str! profile) in the build context
- scripts/rehearse-install.sh (plan: clean-VM rehearsal): assembles a
REAL signed bundle from the built images (server/frontend/broker/
postgres/socket-proxy), runs the customer path — offline verify,
docker load, compose up — and asserts /healthz plus the served login
page before teardown. Passing locally; wired as a release.yml step,
which also builds/ships the broker + socket-proxy images now
161 Rust tests + 29 journeys; clean-room rehearsal green.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Concurrency soak (exit criterion): 12 concurrent gated runs, every
decision attempted twice concurrently, explicit resumes racing the
durable sweeper — exactly one execution per approval, grants consumed
at most once, every decision audited, zero stuck runs, zero unaudited
executions. (Testkit pool raised to 20 connections; the 5-connection
pool starved the storm.)
- axe a11y sweep (exit criterion): serious+critical violations fail CI on
login, shell, chat, computer home, settings app, all global pages, and
the wizard. Two real violations found and fixed: aria-label on a plain
div (wizard progress -> role=group) and a button directly inside a <dl>
(settings -> plain bordered list).
- tools/bundler (exit criterion): keygen / assemble / verify CLI — copies
artifacts, writes manifest.json + sha256 checksums.txt + a detached
ed25519 signature; verification is fully offline (keyless signing is
internet-dependent and disqualified). Tests: round trip, tampered
artifact caught by hash, tampered checksum list caught by signature,
wrong key refused, missing artifact reported.
147 Rust + 63 frontend tests + 27 Playwright journeys (incl. 4 a11y).
Co-Authored-By: Claude Fable 5 <[email protected]>
- LlmEvent::Usage across all three providers (Scripted deterministic
word-count accounting; Anthropic message_start/delta usage; OpenAI-compat
stream_options include_usage)
- tc-billing: ceil(tokens/1000) min 1 credit; lots drain oldest-first under
FOR UPDATE; balance clamps at zero while the usage ledger records the
full obligation; promo codes redeem exactly once via CAS (migration 0006)
- Runtime charges every completed run (billing failure never fails a run);
proven: 1 token in + 3 out -> 1 credit deducted
- API: GET /api/team/usage, POST /api/credits/redeem (409 on reuse, audited)
- Credits page: balance, 7-day usage meter with runway estimate, PromoRedeem
- /claws/new is the full §9 wizard: ?step=identity|access|slack deep-linked
progress, accent swatches + name randomizer, access toggles, optional
Slack step, explicit review-and-confirm (creation = live agent), animated
provisioning state -> straight into chat
- E2E: chat decrements the visible balance and fills the usage meter;
WELCOME500 adds exactly 500 once then refuses; wizard round trip
140 Rust + 63 frontend tests + 23 Playwright journeys.
Co-Authored-By: Claude Fable 5 <[email protected]>
- tc-db: sessions/messages/steps/runs/run_events repos (atomic seq assignment,
history with ordered step traces, journal replay-from-offset); migration 0003
- tc-llm: provider-neutral ChatRequest/LlmEvent; ScriptedProvider (scenario
TOML, word-level deltas, multi-turn tool legs — ships in production for
e2e/air-gap smoke), AnthropicProvider (Messages SSE), OpenAiCompatProvider
(vLLM/Ollama/llama.cpp); opt-in live tests via TC_LIVE_LLM=1
- tc-runtime: run loop with persist-before-emit event journal, real built-in
clock.now tool, step rows on the reply message, tool-error resilience,
broadcast channels for live attach
- tc-api: agent CRUD + settings/full (tenant-isolated, RBAC'd, audited),
sessions create/list/history?tools=true, POST /api/gateway SSE with
monotonic ids and exact resumeFrom journal replay (tested equal to live)
- teamclaw-server: config-driven provider factory
83 Rust tests green, all against real Postgres / real TCP.
Co-Authored-By: Claude Fable 5 <[email protected]>