Commit Graph
29 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 a02e0cba69 feat(missions): Continuous Research harvests at launch, and cards launch by clicking
deploy / test (push) Successful in 4m21s
deploy / build (push) Successful in 5m14s
The card shipped in e20b321 could not actually be used. Three things were
missing, each of which failed at a different distance from its cause.

**1. `default_team_template` was parsed and never read.** Every recipe declares
one; `WorkflowRecipe` carries the field; nothing consumed it. A mission created
from a card with no explicitly chosen team was rejected at LAUNCH with "no
team_id, no team_template_id, no config.phase_teams" — one step removed from the
real cause, which is that creation ignored the recipe. Create now resolves it
via `team_templates::get_by_key`, only when the caller named no team of any
kind, so an explicit choice still wins. A test asserts every shipped recipe
names a template that has a `templates/teams/<key>.toml`, because a mismatch
there produces an unlaunchable card.

**2. The harvest ran nowhere.** `harvest_for_mission` existed and nothing called
it. `on_launch` now runs it for `continuous_research` missions, before the
phases start, and threads the blob store through from `main` (the route already
had it on `AppState`; the scheduler needed it). Deliberately non-fatal: a
harvest that fails still starts the phases, because the phase is what reports
whether today was quiet or broken and those must stay distinguishable — but
never silent, so both outcomes log their counts.

**3. Nothing wrote the manifest.** `templates/teams/continuous_research.toml`
has pointed its reader role at `ContinuousResearch/<date>/harvest.jsonl` since it
was authored, and the file did not exist — agents aimed at a path nothing
produced. `run_to_vault` now writes it beside the notes and stages it, but only
for a mission-attributed run. `Harvest` carries the shelved `Paper`s to build
it; re-parsing the notes we had just written would have been a parse of our own
output and one more place for the two to drift.

Also: the blob root. `storage.data_dir` defaults to "./data" and the container's
cwd is `/`, so the server tried to create `/data` as uid 65532 and EVERY shelve
failed with "storage io: Permission denied". The image now creates
/var/lib/clawmates-blobs owned by 65532 so a mounted volume inherits it rather
than arriving root:root. Kept off /var/lib/clawmates-missions on purpose: that
tree is swept, and a paper shelved there would be deleted out from under its own
catalogue note.

Proven end to end on a real mission: 15 candidates, 2 already held, 13 shelved,
0 failed; branch auto-merged as additive-only; manifest on vault `main` with
every documented key. The "already held" counts are the seen-set deduping across
topics within a single run, which is the behaviour the whole design exists for.

The project brief now comes from the mission description — `phase_task_text`
already places it under BRIEF verbatim, so no new field was needed.

346 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-17 15:05:37 -07:00
Omar SobhandClaude Opus 5 53da4d7e6d fix(runtime): a mission could not build the repo it was given
deploy / test (push) Successful in 4m20s
deploy / build (push) Successful in 5m21s
`clawmates-runtime` shipped with `gcc` and `make` but no `cmake`, no `g++` and
no `python3-dev`. Measured on clawhdf5, three probes:

  no cmake        → "is `cmake` not installed?"        exit 101 after 13s
  no python3-dev  → "cannot find -lpython3.11"          exit 101 at link
  with both       → cargo test PASSES                   exit 0 after 69s

This is not only the delivery gate. The AGENTS run in this image, so a coding
phase was writing Rust it had no way to compile or test — which reframes the
last run's 11 agent commits as unverifiable by construction.

`images/agent-toolchain/Dockerfile` (the microVM path) has had `cmake
build-essential` all along, and its own header warns about precisely this:
"if `cargo` is present in one image and absent in another, the same mission
passes or fails depending on which backend it landed on, and nothing says why."
Both images now install the same set — it was missing `python3-dev` too.

`images/runtime-toolchain.Dockerfile` is a thin local overlay so the laptop can
run today without recompiling zeroclaw from the fork; it is meant to be deleted
once a runtime image built from the corrected deploy/ Dockerfile is published.

Also: a build failure is no longer reported as a red suite. Both are cargo exit
101, and `verify_tests` mapped every non-zero to `Failed(code)` — so a missing
toolchain was recorded as the USER's tests failing. It now returns
`CouldNotRun` with the reason when the output shows a compile or link failure.
Deliberately narrow: a failing `assert!` still reads as red, because letting
broken code past `on_green_tests` is the expensive direction to be wrong in.
Both directions are pinned by tests built from today's two real samples.

And the coding phase finally has a loop: `research_and_code.toml` declared
`loop = "until_no_more_int_items"`, which `phase_config.rs` lists as
DECLARED_BUT_UNREAD. Iteration is driven by `max_iterations` + `done_when`, and
with `max_iterations = 1` and no `done_when` the phase ran ONCE and was never
judged — reporting `completed` whatever it produced. Now 3 passes against a
stated goal, wording per the measured rule (say what the tree must CONTAIN).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-16 11:02:07 -07:00
Omar SobhandClaude Opus 5 eb120a10dd perf(image): drop chromium from the server image — 875 MB to 212 MB
deploy / test (push) Successful in 4m0s
deploy / build (push) Successful in 1m41s
Chromium and fonts-liberation were 758 MB of an 875 MB image: 87% of the server
image was a browser it never launched.

It was installed for the Slice 6 mission PDF renderer, which no longer exists —
every call site passes `render_pdf: false` because markdown is the deliverable —
and NOTHING in the workspace reads the CHROMIUM_BIN this image set. The only
Chromium the platform actually uses is `browser.goto`, which runs it inside the
agent's dedicated egress-enabled BROWSER container
(cm-runtime/src/tools/browser.rs), never in the server.

Measured on gw-04: 875 MB -> 212 MB. The remainder is debian-slim (75 MB), git
and its dependencies (~95 MB) and the server binary (42 MB). git stays: research
topic clones shell out to it, which is why this image left distroless in the
first place.

Verified in the slimmed image: git 2.39.5 present, CA bundle present, chromium
absent, binary executable, templates and all 8 skills shipped.

That 663 MB was paid on every deploy, every registry push, and every air-gapped
bundle.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 21:24:01 -07:00
Omar SobhandClaude Opus 5 f56d41f5b7 feat(backend): local-ornith — a mission backend served by the node's own GPU
Claude Code pointed at the Ollama already installed on every GPU node. Ollama
has served a native Anthropic-compatible /v1/messages since v0.14, so this is
an env contract rather than a translation layer — the fourth variation on the
same idea as agent-glm and agent-kimi.

The route is NOT the egress proxy, and that is the design. `egress` speaks
CONNECT, takes a destination from the guest, resolves it and decides; every one
of those powers is a liability, which is why it refuses non-443 ports and IP
literals after a unit test caught them being bypassed. Routing a local model
through it would have meant relaxing both.

`local_model` is the opposite shape: there is no destination in the protocol.
fcagent listens on guest 127.0.0.1:11434 and pumps to vsock 9003; the node
splices that onto its own 127.0.0.1:11434 and copies bytes. A compromised guest
cannot redirect it because there is nothing to redirect — it is a pipe, not a
proxy, and strictly narrower than anything an allow-list could express. The
bytes never touch a network, so there is no wire for TLS to protect, and Ollama
stays bound to loopback rather than being exposed on the tailnet.

The socket is bound only for a backend declared to use a local model, so a
`local-ornith` VM reaches the forge through egress and nothing else, while every
other backend's guest port simply refuses. Both halves have negative controls.

`scripts/fleet-model-setup.sh` exists because of one measurement: stock
ornith:9b reported input_tokens=2050 for a 48000-word prompt and answered as
though nothing had been dropped. Ollama's default window is ~2K whatever the
model card says, and it truncates silently — the exact failure an agent turn
would hit and never report. The script pins num_ctx=131072 into a derived tag
and then PROVES both the window and tool calling before declaring success.
Verified on architect: ~65536 words -> 65604 input tokens, stop_reason=tool_use.

Placement needs no new capability key: building the rootfs only on GPU nodes
means `nodes::online_for_backend`'s existing `rootfs @> ["local-ornith"]`
predicate does the affinity, so morpheus never offers the backend.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-09 13:13:54 -07:00
Omar Sobh 8b12245e79 chore(image): promote Claude Code 2.1.226 after the canary passed
Canary first, production second — the point of pinning is that the upgrade is a
decision, and the point of `canary-claude` is that the decision has evidence.

Taken for 2.1.225's fix to a transient 401 that replaced a long-lived
CLAUDE_CODE_OAUTH_TOKEN with a short-lived one and broke HEADLESS sessions until
restart. Our sessions are headless and our VMs are per-mission, so "until
restart" reads as a failed phase.

The 2.1.225 workspace-trust prompt does NOT apply: `--help` states the dialog is
skipped in non-interactive mode (`-p`, or stdout not a TTY) and we satisfy both.
Read from the CLI in a booted 2.1.226 VM rather than inferred from the changelog.

Verified on the production path after the rebuild: guest kernel 6.1.128,
delegation to a subagent, `--settings` stop gate installed, judge independent
(glm-4.7), single writer. 6/6.

`rootfs-canary-claude.ext4` is left on tank as the mechanism for the next
candidate, not as a leftover.
2026-08-08 05:47:25 -07:00
Omar SobhandClaude Opus 5 742724e53c feat(fleet): Kimi as a microVM backend — the URL settled by measurement
The base URL took three measurements to find, and the first two were wrong in
instructive ways.

`api.moonshot.ai/anthropic/v1/messages` EXISTS and speaks the protocol — it
answers with Moonshot's own structured error rather than a 404. It also rejects
an `sk-kimi-` key, because it belongs to the platform.moonshot.ai account
namespace. Two endpoints that both "work" for different accounts is precisely
the shape that makes a guessed URL look like a broken key, and it is why this
was refused rather than guessed for as long as it was.

The Kimi CODE service is the one an `sk-kimi-` key belongs to:
`POST https://api.kimi.com/coding/v1/messages` returns a real Anthropic Messages
body — `msg_` id, `content` blocks, a `thinking` block with a signature. So
`ANTHROPIC_BASE_URL=https://api.kimi.com/coding`, WITHOUT the `/v1`: Claude Code
appends `/v1/messages` itself, and `/v1/v1/messages` would 404 in a way that
reads as a broken image rather than a bad URL.

Two more measured, each otherwise a silent failure at the first turn:
`Authorization: Bearer` is accepted (so ANTHROPIC_AUTH_TOKEN is the right
injection channel), and a `claude-*` model id is ACCEPTED AND ANSWERED — Kimi
maps it onto `kimi-for-coding` exactly as z.ai does, so no ANTHROPIC_MODEL
override is needed.

Claude Code rather than Moonshot's own `kimi` CLI, deliberately. The mission
harness is Claude-Code-shaped throughout: `--agents` JSON roles, the verifier's
tool allowlist, the `Stop` hook behind the completion gate, the per-subagent
transcripts counted as delegation evidence. `kimi` has none of those flags — its
equivalents are TOML files and markdown agent dirs — so using it would mean a
second executor with its own untested failure modes.

TWO STALE MAPS, caught by the rootfs harness refusing to bless the image: both
`fc-build-rootfs.sh` and the node's `required_cli` expected backend `kimi` to
contain Moonshot's `kimi` binary. That assumption predates the measurement, and
it failed a rootfs that was correct. Both now say `claude` for glm and kimi
alike — the binary is the same in all three images; only the endpoint differs.

Egress for `kimi` is `api.kimi.com` alone: not moonshot.ai (wrong namespace),
not z.ai, not Anthropic. Asserted both ways, like the other two.

The image and rootfs are built on tank and the rootfs passes all four checks
(boots, git, writable /mission, `claude --version`). 534 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 17:52:45 -07:00
Omar SobhandClaude Opus 5 f7f3dfe495 feat(fleet): GLM as a real microVM backend, and per-role models for claws
Three threads, all of which end at the same place: a mission whose verifier does
not share a model with the coder it reviews.

**GLM has a credential contract now.** `microvm_credential_for` returned one env
var name, which quietly assumed every provider reads its secret from the same
place Anthropic does. It returns a `Credential { source, target }` instead —
z.ai's key lives in the server's `ZAI_API_KEY` and Claude Code reads it as
`ANTHROPIC_AUTH_TOKEN`, and collapsing those two names is what forces a guess at
the other end. A wrong guess here sends one provider's credential to another
provider's endpoint.

`images/agent-glm` is the same CLI at the same pinned version as `agent-claude`
with `ANTHROPIC_BASE_URL` baked in. The split is deliberate: the ENDPOINT is a
property of the image, the CREDENTIAL is a property of the turn. That makes the
dangerous mix-up unrepresentable — a GLM VM cannot be handed an Anthropic
subscription token, and a claude VM cannot be pointed at z.ai. Asserted both
ways, because "the GLM VM must not carry CLAUDE_CODE_OAUTH_TOKEN" is the
property that costs a credential if it ever stops holding.

Kimi stays refused. `KIMI_API_KEY` is set and Moonshot serves an
Anthropic-compatible API, but I have not verified its base URL against the
running service, and this function is precisely where guessing a URL is
expensive. It becomes an arm the day someone measures it.

`api.z.ai` joins the node's default egress allow-list. A default that cannot
run the images we ship is a trap rather than a policy — the alternative is an
operator discovering it as a hung agent with no model access.

**Per-role models for claws** (migration 0071). `template_roles` had no model
column, so `mint_team_from_template` bound every role of every mission team to
one literal — a template whose whole point is an independent reviewer minted a
reviewer sharing a model with the coder. A role may now name its own; roles that
say nothing still take the mint's default, so every template written before this
behaves exactly as it did. The literal is now that default rather than a
hardcode.

**A harness scenario for the roster flow.** `verify-mission-delivery.sh roster`
runs the whole Slice 5 loop — planner proposes, human approves, mission runs —
and asserts the roster LANDED on the mission row rather than trusting the API's
answer. That distinction is not theoretical: the first live approval returned an
error while leaving the proposal marked approved.

Built and proven on tank ahead of the deploy: `clawmates/agent-glm:dev` reports
`2.1.223` and `BASE=https://api.z.ai/api/anthropic`, and
`fc-build-rootfs.sh … glm 8G` boots a VM from it that has git, can write
/mission, and answers `claude --version`.

533 tests pass, clippy clean. Migration 0071.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 17:14:53 -07:00
Omar Sobh 11b274edc6 chore(images): Claude Code 2.1.223, and make the verifier foreground
Reviewed the changelog rather than bumping on principle. 2.1.220 → 2.1.223 for one
reason that bears on how we use subagents:

  2.1.222 — "Fixed PreToolUse auto-allow hooks bypassing tool restrictions in
  background agent tasks."

Subagents run in the background by default since 2.1.198, and the `verifier`
role's entire guarantee is a TOOL restriction — no Edit, no Write. So on 2.1.220
the one property we rely on was the one that bug could undo. 2.1.221 also fixes
`--mcp-config` servers not connecting before the first turn in print mode, which
is the mode we run and will matter when the MCP door reaches a VM.

Two findings from the changelog that we already had at 2.1.220, both worth knowing:
  - 2.1.219: subagents can nest to depth 3 (was 1), so our roles can delegate
    further than assumed.
  - 2.1.212: a subagent inherits the parent's permission mode, which confirms the
    verifier's read-only property must come from `tools` and not from permissions.
    That is how it was written; now the reasoning is recorded next to it.

And a correctness fix that follows from the background default: the verifier is now
`background: false`. A background verifier lets the lead carry on and write its
report before the check has finished — the finding would arrive after the
conclusion it was supposed to inform.

Verified on tank: image reports 2.1.223, rootfs rebuilt, `--vm-selftest` all green
including a real agent turn on subscription auth, egress allow and deny both firing.
2026-08-05 21:37:49 -07:00
Omar SobhandClaude Opus 5 ebdba34da6 feat(fleet): B4.6 — a microVM reaches the API through a vsock CONNECT proxy, with an allow-list
The guest still has no network interface, and now that is the design rather than
a gap. Its only route out is an HTTP CONNECT proxy: agent CLI -> 127.0.0.1:3128
in the guest -> vsock 9002 -> a per-VM Unix socket on the host -> TLS to an
allow-listed host.

Why not TAP + iptables, which is what the Firecracker write-ups do — measured,
not argued:
  - `ip tuntap add` is DENIED to the daemon user (needs CAP_NET_ADMIN), so TAP
    would need root to pre-provision devices, the same privilege detour the
    loop-mounted rootfs already forced.
  - tank's FORWARD policy is DROP with Docker and Tailscale chains, so rules
    would have to be inserted at position 1; appended ones die silently.
  - a leaked TAP is a new class of host litter to reap.
CONNECT needs no privilege at all and is better on the merits: the client hands
us the HOSTNAME, so resolution happens host-side and the guest needs no DNS or
resolv.conf; the allow-list is by name, not address; and nothing in the guest can
reach the network except through one function. The guest end parses nothing and
enforces nothing, so a compromised agent cannot argue with the policy.

Rests on one measured fact: `claude` honours HTTPS_PROXY. With the proxy at a
closed port, `claude -p` fails ConnectionRefused instead of answering.

THE RESULT: a real agent turn now completes inside a VM with no network card, on
subscription auth — `claude -p` replies VM-OK. The selftest asks for it whenever
CLAUDE_CODE_OAUTH_TOKEN is present and SKIPS loudly when it is not, since it
spends a little of the plan.

The audit log earns its keep immediately: during that turn the proxy logged
`egress DENIED http-intake.logs.us5.datadoghq.com` — the CLI's telemetry, which
the mission container permits today without anyone deciding to.

Three bugs found by the checks rather than by review:
  - `env_pairs` returned early when a caller sent no env, so the proxy address
    was never added and `curl` in a VM with a working tunnel reported "Could not
    resolve host". Absent env means "the caller sent none", not "this command
    needs no environment".
  - the deny check PASSED for the wrong reason — DNS was failing, so nothing was
    refused by the allow-list at all. It now requires a 403 from the proxy, so it
    cannot go green on a broken tunnel.
  - `host_allowed` accepted `evil.test/api.anthropic.com`, which ends with an
    allowed suffix. Hostnames are now validated against a character class, which
    also refuses IP literals so an address cannot sidestep a list of names.
  - `BufReader::into_inner()` discards buffered bytes: wrapping the stream twice
    would have dropped the start of the TLS handshake and stalled a tunnel that
    looked established. One reader now spans the request, and anything buffered
    past the headers is forwarded as payload.

`iproute2` is in agent-toolchain because it is load-bearing: the guest's `lo`
starts DOWN, and while it is down a listener on loopback BINDS and then refuses
every connection with ENETUNREACH. fcagent finds `ip` by absolute path — as pid 1
its PATH comes from the kernel, and execvp's fallback excludes /usr/sbin, where
Debian puts it.

Egress needs both ends up, so `create` reports `egress` and the guest's `ping`
reports its own half. A VM without it is legal but never silent.

Verified on tank: 16/16 with backend=claude (create 1428 ms), 12/12 on the
default rootfs, no leaked processes, VM dirs or proxy sockets. 457 tests pass,
clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-05 12:44:59 -07:00
Omar SobhandClaude Opus 5 0c291ed1bb feat(fleet): B4.4a — a real agent-claude microVM image, and a check that it has an agent in it
The only rootfs on this track came from clawmates/agent-terminal:dev. Mounted,
it held git and nothing else: no claude, no node, no cargo. A VM booted from it
looks perfect and cannot run a mission, so B4.5 could have been written and
never verified.

images/agent-toolchain — the shared mission toolchain (node 22, git, rust +
cargo-audit, gitleaks/trivy/semgrep, tea/gitea-mcp), lifted from the proven
deploy/clawmates-runtime image minus the zeroclaw daemon: a microVM mission runs
the direct-session model, so there is no daemon to host. A base image rather
than three self-contained Dockerfiles because this layer is ~3 GB and the real
risk is scanner and toolchain versions drifting between per-CLI images — the
evaluator runs the project's own suite to check a claim, so `cargo` present in
one image and absent in another makes the same mission pass or fail by backend
with nothing saying why.

images/agent-claude — plan A6, first of three: the pinned CLI and its env
contract only, so bumping Claude Code does not rebuild the toolchain and cannot
disturb agent-kimi / agent-glm. HOME=/root with an empty .claude for B4.4 to
inject into; no ANTHROPIC_API_KEY, since it silently overrides the subscription
OAuth we already pay for.

Both the builder and the node selftest now ASK the guest for the CLI the image
is named for, instead of trusting the name. `required_cli` maps claude/kimi/glm
to a probe; an unrecognised backend reports unchecked and prints SKIP rather
than passing quietly.

Verified on tank:
  - rootfs-claude.ext4 boots; git, node, cargo, a real git commit all work
  - `claude --version` → 2.1.220 over vsock, in both the builder and
    `--vm-selftest` (11/11, create 1498 ms)
  - negative control: the same builder run against agent-terminal with
    FC_CLI forced reports `cli rc=127 claude: not found` and exits 1, so the
    green result above is a measurement and not a default
  - `claude -p hello` fails with "Not logged in · Please run /login" — the CLI
    runs headless in the VM, and B4.4 only has to supply the credential
  - no leaked firecracker processes or vm dirs afterwards

437 tests pass, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-05 11:33:07 -07:00
Omar SobhandClaude Opus 4.8 11e1379c5f fix(build): normalize /etc/clawmates seed-dir perms for the nonroot user
The server COPYs templates/ and skills/ then drops to USER 65532. When the
build context arrives with mode-700 dirs (e.g. rsync -a preserving a dev's
local perms), COPY bakes 700 into the image and the nonroot runtime user
can't read them — the skills/team-template builtin seed silently skips
("Permission denied (os error 13)"). chmod -R a+rX after the COPYs makes
the seed dirs readable regardless of source perms.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-28 09:07:42 +02:00
Omar Sobh a1d1097b52 ci + ops: cargo-build retry wrapper + runtime systemd unit
ci / rust (push) Successful in 4m27s
ci / e2e (push) Skipped
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 26s
ci / publish (push) Successful in 3m39s
Two durability fixes closing recurring flakes:

CI flake wrapper (broker + server Dockerfiles):
  Wrapped the cargo build step in a 3-attempt retry loop with
  linear backoff (10s / 20s). Directly targets the crates.io
  transient network errors that keep hitting CI on the runners
  ('curl failed: SSL_ERROR_SYSCALL, errno 0'). Each build only
  loses time on transient failures; a real compile error still
  fails all 3 attempts and surfaces the last error normally.

Runtime systemd unit (deploy/clawmates-runtime/):
  Replaces the manual 'docker run' that had been starting the
  ZeroClaw runtime with no persistence for its network topology.
  Ephemeral prod fixes at 09:30 PDT 2026-07-21 (task #38) were:
    - anthropic.default provider block added to
      /root/clawmates-runtime/data/.zeroclaw/config.toml (already
      durable — bind-mounted from host)
    - docker network connect clawmates_edge clawmates-runtime
      (NOT durable — vanishes on container recreate)
  New systemd unit clawmates-runtime.service (installed +
  enabled on gw-04):
    - ExecStart docker-runs the container attached to
      clawmates_core, then connects clawmates_edge in the same
      shell command, then docker waits.
    - Bind-mounts both /root/clawmates-runtime/data and
      /var/lib/clawmates-missions (for security_scan +
      benchmark_runner).
    - --rm so upgrading is just docker pull + systemctl restart.
    - Restart=on-failure with 5s backoff.

Closes task #38 and preemptively closes the CI flake pattern.
2026-07-21 12:53:30 -07:00
Omar SobhandClaude Opus 4.7 3ac3d53da7 slice 6: LLM + Chromium PDF renderer worker
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 38s
ci / rust (push) Successful in 3m28s
ci / e2e (push) Skipped
ci / publish (push) Successful in 3m4s
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]>
2026-07-19 15:16:57 -07:00
Omar SobhandClaude Opus 4.7 7b23f61632 slice 3.5c: seed 15 built-in skills across the 6 stacks
ci / frontend (push) Successful in 25s
ci / gates (push) Successful in 4s
ci / rust (push) Failing after 3m41s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Hand-authored skill catalog anchored to real 2026-07 versions:
  - Rust 1.97.1 (stable), edition 2024
  - React 19.2.7, Server Components + Actions
  - TailwindCSS 4.3.3 (CSS-first config, Oxide engine)
  - three.js r185 (WebGPURenderer stable, BatchedMesh matured)
  - React Native 0.86 / Expo SDK 54+ (New Architecture default)
  - cargo-nextest 0.9.140, gitleaks 8.20+, cargo-audit 0.21+
  - Postgres 17 (18 in beta, don't rely on)
  - CUDA Blackwell, Metal Apple7+, ROCm CDNA3

Ships 15 skills across the categories:
  foundation/  workspace-repo-commit-protocol
               small-focused-commits
               tdd-red-green-refactor
               code-review-checklist
               int-xx-marker-protocol
               decompose-int-items
  rust/        write-rust-current-edition
               rust-error-handling
               cargo-test-driven-development
               rust-async-tokio-idioms
  backend/     postgres-migrations-forward-only
               postgres-index-selection
               api-pagination-day-1
  frontend/    react-19-server-components
               tailwind-v4-idioms
               component-4-state-model
  mobile/      expo-managed-vs-bare
               rn-flashlist-perf
  gpu/         gpu-coalescing-and-occupancy
               roofline-model
  threejs/     threejs-perf-and-teardown
  security/    cargo-audit-workflow
               secret-scanning-gitleaks

skills_loader.rs walks skills/**/*.md, parses YAML frontmatter
(name, description, when_to_use, tags), upserts via
skills_catalog::upsert_builtin. Idempotent per boot — bumps version
+ appends skill_versions row ONLY when body changes. Deterministic
sha256-derived ids so builtins are stable across boots.

Dockerfile copies skills/ to /etc/clawmates/skills. Server boot
task spawns loader alongside team_template_loader.

Follow-ups (Slice 3.5c continuation, future PRs):
  - 20-30 more skills (duckdb, shadcn composition, a11y, WebGPU
    migration, metal frame capture, rocprof, deep gitea forge
    integration, semgrep rulepacks)
  - Bind skills to team template roles (add [role.skills] refs to
    templates/teams/*.toml + wire template_role_skills population
    in team_template_loader)

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 13:55:44 -07:00
Omar SobhandClaude Opus 4.7 9ba5c06a1a slice 3: 6 team templates seeded from TOML recipes
ci / rust (push) Failing after 11s
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 28s
ci / e2e (push) Skipped
ci / publish (push) Skipped
Team templates are the canonical rosters + tool bundles that mint
concrete teams for a mission. Every builtin ships as a TOML recipe
under templates/teams/*.toml, loaded into the DB at server boot.

Migration 0048 adds:
  - team_templates    (id, key, name, stack, default_topology,
                       risk_profile, mcp_bundles, version, source,
                       workspace_id)
  - template_roles    (m2m: template_id + slot; system_prompt,
                       skills[], brain_seed)
  - teams gets template_id + template_version for level-up lineage

Ships 6 builtins:
  - rust_sdlc  — planner/coder/tester/reviewer/committer for Rust
  - backend    — api_designer/db_engineer/coder/tester/committer
                 (Postgres, DuckDB, graph DBs, wire protocols)
  - frontend   — designer/coder/tester/committer (React + Tailwind + ShadCN)
  - mobile     — designer/coder/tester/committer (Expo, RN, iOS, Android)
  - gpu        — arch_analyst/kernel_author/bench_engineer/coder/committer
                 (CUDA, Metal, ROCm from Rust)
  - threejs    — scene_designer/coder/shader_author/perf_engineer/
                 committer (three.js, WebGL, WebGPU)

Each role has a versioned system_prompt + skill list + brain_seed
markdown. Skills column is a name array today; Slice 3.5a promotes it
to a typed m2m join with the real skills catalog.

Server boot:
  - team_template_loader::load_builtins reads TOML from
    /etc/clawmates/templates/teams (container) or templates/teams (dev),
    upserts idempotently. Deterministic uuid per template key (sha256
    of a fixed namespace + key) so ids are stable across boots.
  - Dockerfile copies templates/ to /etc/clawmates/templates.

Read API:
  - GET /api/team-templates       — list all
  - GET /api/team-templates/{id}  — detail with roles

Wizard:
  - Step 3 rewired from a raw team_id text field to a template picker
    with "LLM auto-provision" as the default option + one card per
    builtin, showing stack, topology, risk profile, and description.
  - Mission create now passes team_template_id (not team_id) so phase
    execution knows which template to mint from.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-19 12:41:15 -07:00
Omar Sobh 8417f6e65c server-image: switch base to debian:12-slim + install git
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m3s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m46s
Distroless static-debian12 has no git binary, so
routes/research_setup::ensure_repo_workspace's `tokio::process::Command::new("git")`
returned "No such file or directory" for every wizard-materialized
research topic. Silent failure — the topic's repo_workspace_path
stayed NULL, prepare_topic_runtime bailed out, the run was enqueued
against the shared workspace-wide gateway which then 401'd or timed
out on cold auth.

Switching to debian:12-slim + apt-installing git + ca-certificates
(needed by clone-over-https). Same UID 65532 as before so bind-mount
hosts don't need re-chown.

Image size grows from ~40MB to ~80-100MB but this is the correct
base for a service that shells out to git. Static distroless was a
premature optimization once the server's job description grew a
"clone repos" responsibility.

Follow-up: research migrating to the `gix` pure-Rust crate so we can
go back to distroless. Nontrivial refactor of ensure_repo_workspace
so not attempted in this fix.
2026-07-10 11:23:55 -07:00
Omar Sobh 4b66930796 image(server): install musl target after rust-toolchain.toml, matching broker
ci / frontend (push) Successful in 24s
ci / gates (push) Successful in 1m53s
ci / publish (push) Successful in 2m12s
ci / rust (push) Failing after 4m44s
ci / e2e (push) Has been skipped
Same fix pattern that unblocked broker in 534eb72. The `rustup target add`
was landing on the base image's default toolchain, but rust-toolchain.toml
(channel = 1.96.0) can prompt rustup to resolve to a distinct pinned
toolchain the target hasn't been installed on. Move the target install
after the COPY so it lands on the toolchain cargo actually uses.
2026-07-05 16:19:42 -07:00
Omar Sobh 534eb72a95 image(broker): install musl target AFTER rust-toolchain.toml is copied
ci / gates (push) Successful in 8s
ci / frontend (push) Successful in 2m11s
ci / rust (push) Failing after 3m17s
ci / e2e (push) Has been skipped
ci / publish (push) Failing after 3m21s
Ordering matters. `rust:1.96-slim` ships with toolchain 1.96 already
installed under one identifier, but rust-toolchain.toml (channel = 1.96.0)
can prompt rustup to resolve to a distinct pinned toolchain. `rustup
target add` runs BEFORE rust-toolchain.toml lands, so it adds musl to the
wrong toolchain — cargo's later build picks up the workspace pin and can't
find core for the target. Move the COPY of rust-toolchain.toml before the
rustup target add so the target lands on the toolchain cargo actually
uses. Matches server.Dockerfile's known-working structure.
2026-07-05 16:12:24 -07:00
Omar Sobh bb3040c5db image(broker): pin base to rust:1.96-slim so target install matches toolchain
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 25s
ci / publish (push) Failing after 54s
ci / rust (push) Failing after 2m12s
ci / e2e (push) Has been skipped
rust-toolchain.toml pins the workspace to 1.96.0. broker.Dockerfile used
rust:1-bookworm — some newer 1.x version — and `rustup target add` inside
that image installs the musl target under the container's default
toolchain. When cargo then reads rust-toolchain.toml and activates 1.96.0,
the target isn't there for that toolchain, so the build fails with
`E0463: can't find crate for core`. server.Dockerfile already uses
rust:1.96-slim; align broker to match.
2026-07-05 16:08:34 -07:00
Omar Sobh f351d0d495 image(broker): mirror server's git-cli + musl toolchain setup
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 34s
ci / publish (push) Failing after 37s
ci / rust (push) Failing after 2m28s
ci / e2e (push) Has been skipped
server.Dockerfile already has CARGO_NET_GIT_FETCH_WITH_CLI=true plus git,
cmake, make, pkg-config, and the musl-gcc CC vars needed for the clawhdf5
git dep (libgit2 fails against Gitea smart-HTTP with "invalid packet line";
the git CLI works fine). broker.Dockerfile was missing all of it, so the
CI publish job failed at the very first `cargo build --release -p
clawmates-broker` inside the Docker builder. Copy the pattern.
2026-07-05 16:05:59 -07:00
Omar SobhandClaude Opus 4.8 731adb6449 agent-terminal image: pre-create ~/drives owned by the runtime user
So a fresh per-agent node-local volume (node-placed terminal) seeds with 65532
ownership and the non-root shell can write ~/drives, instead of a root-owned mount.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 09:19:12 -07:00
Omar SobhandClaude Opus 4.8 e61724ff82 Agent computer: terminal (tmux + drives + tabs), Obsidian vault, UI polish
Terminal app (xterm ⇄ WebSocket ⇄ per-agent themed container):
- zsh + oh-my-zsh + powerlevel10k image (agent-terminal), runs as uid 65532 to
  share read-write ownership of the file-drive volume with the server.
- Interactive PTY in cm-sandbox (bollard exec tty/attach + resize) + a
  TerminalManager; ticket-authed WS bridge routed straight to the backend via a
  Traefik PathRegexp(/ws) rule. MOTD greets the user by name.
- tmux resumable sessions; multi-tab (one tmux session per tab, same container),
  drag-to-reorder, rename, and a Save that persists named tabs to the server
  (terminal_tabs, migration 0014) so they survive logout / a new device.
- Files drives mounted per-agent (subpath) at ~/drives/{documents,received,
  shared}; a reconciler keeps the Files app's index in sync with terminal writes.
  Storage moved to a shared `filedata` volume (CLAWMATES_STORAGE__DATA_DIR).

Obsidian vault (a markdown "second brain" per agent):
- New `vault` FileDrive (migration 0015) mounted into the terminal at ~/obsidian;
  a file-content read route; a purple Obsidian tile + a vault viewer app.

Computer UI:
- Draggable computer-panel width (min = phone preset) keeping the size presets.
- Green Terminal glyph, "Claw Chat" → "Chat", colored gradient-outline app icons.
- Agent page: avatar↔activity-grid spacing + larger, uniform section fonts with
  colored section-tinted tag chips.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 16:52:35 -07:00
Omar SobhandClaude Opus 4.8 34f744734b Large World graph, agent platform, brain stack & dashboard rebuild
Frontend
- Large World: collapse org/company/team tiers into one expandable React Flow
  hierarchy (WorldFlow) with per-click expand, persisted node positions, a
  compact tree sidebar, wrench multi-select delete across levels, and a sized
  right slide-out (phone/tablet/full) showing an agent summary + drill button.
- Agent page: GitHub-style animated contribution grid (VitalsCard), collapsible
  System Prompt + Personality cards, restructured anatomy cards, bigger avatar
  with name/title header row, Markdown/JSON-aware rendering, brain registry +
  history, avatar generate/upload.
- User-icon menu (Infrastructure/Brains/Tools/Profile/Credits) + ToolPanel;
  Master Planner deploy wizard (Specialists/Swarm/Scheduled/Triggered);
  Team Runs view; reap-progress modal; dashboard is the single live interface.

Backend
- cm-brain crate (.brain as the agent definition) + brain apply/history.
- Hard-purge reap (FK-ordered) + sandbox release + SSE batch-delete.
- Swarm self-verifying loop, mode-aware planner, web.search tool, webhooks
  (migration 0013), org/company/team delete endpoints, scheduler sweeps.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22 23:21:54 -07:00
Omar SobhandClaude Fable 5 add4f79fed Rebrand: TeamClaw -> Clawmates (clawmates.work)
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]>
2026-06-10 12:31:25 -05:00
Omar SobhandClaude Fable 5 8046853feb Post-1.0: OTLP tracing, broker image + compose service, install rehearsal
- 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]>
2026-06-10 11:35:32 -05:00
Omar SobhandClaude Fable 5 4f253bec93 P6: browser.goto — real Chromium browsing with live web taint
- SandboxSpec gains an egress flag (default false — the kernel suite
  still proves zero-network for agent sandboxes). Egress-enabled
  containers exist ONLY for the browser: no credentials, no broker
  route, bridge network with host-gateway alias for local test pages
- images/agent-browser: Alpine Chromium, uid 10001, setuid bits
  stripped — same non-root hardening as agent-base
- browser.goto tool: headless chromium --dump-dom in the agent's
  browser container; HTML stripped to readable text (4k cap) and
  returned with output_taint=web; viewport screenshot captured,
  base64'd out of the container, stored in the blob store
- Taint semantics tightened: the step that PRODUCED untrusted output
  now carries its own taint (recorded before the step row), not just
  later steps — chat.inbox test updated to the stricter §15 reading
- GET /api/claws/{id}/browser/viewport.png serves the latest capture;
  BrowserApp polls it and renders the live viewport (spec §7.1),
  keeping the empty state until the agent has browsed
- Proven end to end with REAL Chromium against a REAL local page:
  content 'Revenue up 14 percent' returned tainted web; the gated
  email.send that follows carries 'web' in its approval taint_sources
  (untrusted content can never quietly reach outward); screenshot
  verified by PNG magic bytes

152 Rust tests + 63 frontend + 27 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 09:41:40 -05:00
Omar SobhandClaude Fable 5 84c51168be P6: strict seccomp allowlist + K8s sandbox driver proven on kind
- images/seccomp/agent-profile.json is now a TRUE ALLOWLIST: Docker's
  default profile (vendored from moby v27.5.1, defaultAction ERRNO) with
  18 syscalls an agent never needs stripped from the allow groups
  (unshare, ptrace, bpf, mount family, setns, module loading,
  perf_event_open, process_vm_*, reboot, quotactl, ...); arch map trimmed
  to x86_64 + aarch64. All 6 Docker kernel assertions still green.
- K8sDriver (tc-sandbox feature 'k8s', kube-rs): one hardened pod per
  sandbox — runAsUser 10001, cap-drop ALL, no-new-privs via
  allowPrivilegeEscalation=false, RuntimeDefault seccomp, read-only
  rootfs with emptyDir /tmp + /home/agent, resource limits, no service
  account token — in a PSS-restricted namespace carrying a default-deny
  NetworkPolicy (applied server-side apply, idempotent). Exec via the API
  server attach channel with exit codes parsed from v1.Status.
- Live suite (feature 'k8s-tests') against a REAL kind cluster: uid /
  CapEff==0 / NoNewPrivs / rootfs probes from inside pods, PSS label +
  deny-all policy asserted via the API, lifecycle. Honest limits in the
  rustdoc: Localhost seccomp profile and CNI-enforced egress are
  per-cluster provisioning (kindnet does not enforce NetworkPolicy).
- rustls 0.23 process provider pinned to ring at driver connect.
- CI: dedicated sandbox-k8s job (helm/kind-action) running the suite.

149 Rust tests + 3 live kind tests; clippy clean including the k8s feature.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 09:00:56 -05:00
Omar SobhandClaude Fable 5 ea5162ac65 P2 complete: Docker sandbox with kernel assertions + secret broker
tc-sandbox:
- SandboxSpec/SandboxDriver + DockerDriver (bollard): uid 10001, cap-drop
  ALL, no-new-privileges, embedded seccomp deny profile (unshare/ptrace/
  bpf/keyctl/mount/...), read-only rootfs with tmpfs /tmp + /home/agent,
  network=none, mem/cpu/pids limits
- agent-base image: non-root, all setuid binaries stripped
- 6 kernel-level assertion tests probing from INSIDE real containers:
  uid + CapEff==0, rootfs read-only, seccomp EPERM on unshare, zero
  traffic-carrying interfaces + failed egress connect, no setuid +
  NoNewPrivs=1, lifecycle

tc-secrets:
- ChaCha20-Poly1305 envelope encryption under a FileKey (generated 0600,
  AEAD tamper detection tested); secrets table ciphertext-at-rest
- teamclaw-broker daemon: length-prefixed JSON over a unix socket; no
  protocol operation ever returns plaintext; InvokeHttp independently
  consumes the single-use execution grant against Postgres BEFORE touching
  any credential, then performs the call itself with the secret injected
- Tests over the real socket + real Postgres + a real local HTTP receiver:
  encrypted at rest, pending approval refused, approved call carries the
  bearer token exactly once, grant replay refused, non-http URLs rejected

116 Rust + 61 frontend tests + 14 E2E journeys green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 05:07:46 -05:00
Omar SobhandClaude Fable 5 fc173f170d P0 exit: e2e harness, Playwright shell journeys green, deploy skeleton
- teamclaw-server e2e mode (TEAMCLAW_MODE=e2e): idempotent deterministic
  seed through the real registration paths
- Playwright suite (6 journeys) against the real backend + prod Next build:
  login redirect, bad-password error, shell/roster/online-dot, team members,
  seeded credits, sign-out revocation — P0 exit criterion met
- Dockerfiles: musl-static server -> distroless, Next standalone -> distroless
  node (multi-arch via TARGETARCH)
- Air-gapped compose topology with edge/core/sandbox_net/secrets_net
  segmentation (engine-validated in CI), config-file + env-overlay pattern
- CI: compose validation + e2e job with trace upload

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-09 22:48:28 -05:00