docs: container-tier gate and telemetry shipped; CI failures were disk
deploy / test (push) Successful in 4m39s
deploy / build (push) Successful in 1m0s

Records the verified result (10 tool.call, 4 file.touch on a real mission),
how hooks succeed where stream-json could not, the production state and its
rollback, and the three same-shaped bugs the live test found.

Also records that CI's build failures were disk pressure from my own manual
runtime builds on gw-04 — not code — and that a docs-only commit was the
first casualty, which made it look like a regression.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-21 07:11:02 -07:00
co-authored by Claude Opus 5
parent f6e6037aa0
commit 5a11fae0d6
+48 -99
View File
@@ -10,120 +10,69 @@ Read `CAPABILITY-REVIEW.md` for the system picture,
`merge/upstream-v0.8.4` is at `db1c50966`. The local suite is green — 106 test `merge/upstream-v0.8.4` is at `db1c50966`. The local suite is green — 106 test
binaries, zero build errors, frontend builds. binaries, zero build errors, frontend builds.
## CI green, deployed to production 2026-08-20 ## Container-tier tool gate + telemetry — SHIPPED 2026-08-21
Run **498 passed and deployed**. Verified on gw-04 after the recreate: The tier that actually runs missions now has both, verified end to end on a
real mission (locally first, then deployed):
``` ```
skill_self_authoring: ENABLED — agents apply their own skill drafts without human approval tool.call 10 Bash 6, Read 3, Write 1
skills_loader: loaded 53 builtin skill(s) file.touch 4 research/tapproof.md
team_template_loader: loaded 11 builtin team template(s) # 0 unresolved
gateway_preflight: http://clawmates-runtime:42617 answered
missions.retain_events_until # migration 0080 applied
``` ```
107 test binaries green in CI. So everything in this block is now **live**, That is the first time the container tier has ever been observable.
including self-authoring with no human click and the PreToolUse gate installing
into every microVM mission.
### Why runs 490–496 failed (two causes, both fixed) **How, after `stream-json` failed.** Claude Code runs its tools inside its own
subprocess, so they never reach ZeroClaw's executor and never become a
`TurnEvent::ToolCall`. Hooks bypass that entirely: `claude -p --settings <doc>`
honours `PreToolUse` and `PostToolUse`, so the gate blocks and the tap records
without ZeroClaw being involved at all.
- **490** — two runs overlapped. Every run began by removing the *shared* Pieces: `--settings` on `claude_cli` (fork `be9c34b1c`),
container `cm-ci-pg`, so run 491 deleted run 490's database mid-suite. Fixed `container_tool_hooks` writes both hook scripts and one settings document into
with a `concurrency` group and a run-scoped name. (497 was later *cancelled* the mission container, `set_claude_cli_settings` points the provider at it, and
by that guard, which is correct behaviour, not a failure.) `phase_runner::drain_finished_container_phases` collects the tap into
- **491–496** — a **single apostrophe**. The Rust step is `mission_events` (idempotent by truncation — no cursor column).
`docker run … sh -c '…'`, and a comment inside it read `cm-api's
vm_tool_gate`. The quote closed; bash died with *"unexpected EOF"* **before
running anything** — hence an empty log, a breadcrumb showing the step
entered and produced nothing, and three wrong theories. I introduced it in
the commit that fixed 490.
`tests/workflow_shell_syntax.rs` now `bash -n`s every `run:` block, so this **Production state:** server on `f6e6037`;
class fails locally rather than six runs later. Negative-controlled. `CLAWMATES_RUNTIME_IMAGE=clawmates-runtime:hooks` in `/opt/clawmates/.env`
(backup at `.env.bak.prehooks`; rollback = restore it and recreate). Note that
host uses **legacy `docker-compose`**, not the v2 plugin.
### Still worth having **Not yet observed in production** — no prod mission has run since the flip.
Prod auth is Clerk, so a mission cannot be launched from here by password. The
check when one runs:
The Gitea actions-log API returns **403** for the token in ```
`deploy/compose/.env`, and the server is behind Cloudflare on a host not on the ssh gw-04 'docker logs clawmates_server_1 2>&1 | grep -E "per-mission runtime image|drained .* tool call"'
tailnet. Every step now writes to `/tmp/ci-logs` on the runner host as a
workaround (`ssh gw-04 'cat /tmp/ci-logs/STEP; tail -40 /tmp/ci-logs/rust-step.log'`),
but **a token with the `actions` scope** would have made the whole detour
unnecessary.
### stream-json: tested live, and it does NOT deliver observability
Built for amd64 on gw-04, deployed, and driven with a real mission. Result:
- **The agent used tools** — it ran `echo hello-from-tools` via Bash and
exercised Read/Edit/Write/Glob/Grep, confirming again that mission agents
are not tool-free.
- **No `tool.call` events appeared.** The gateway's unmatched-frame histogram
showed only `session_start`, exactly as before.
Why: `TurnEvent::ToolCall` — the thing the gateway turns into a `tool_call`
frame — is emitted from `tool_execution.rs`, i.e. **only for tools ZeroClaw
itself executes**. Claude Code runs its tools inside its own subprocess, so
ZeroClaw never emits the event and ClawMates never sees it. The provider
knowing about the calls changes nothing on its own.
**Worse, the first version was actively harmful.** It put the recovered calls
into `ChatResponse.tool_calls`, which means "execute these" — so the agent loop
tried to run `Bash`, got `Unknown tool: Bash` (`tool_execution.rs:175`), and fed
that back to the model as a tool result: an extra LLM round-trip per call,
telling the agent its own work failed. The mission still completed with correct
output, which is precisely why it would have gone unnoticed.
Fixed in fork commit `295d6d3b8`: the calls are logged, never returned. **Both
runtimes were rolled back to the known-good image** (`:v084` on gw-04,
`:toolchain` locally), and the harmful v1 image was deleted from both hosts so
it cannot be redeployed by accident.
A corrected image **`clawmates-runtime:streamjson2`** (from `295d6d3`) is built
and sitting on gw-04, **deliberately not deployed**. It delivers no
observability until the event channel exists, so shipping it would be a
provider output-format change with real risk and no current benefit.
Production stays on `:v084`. Deploy `streamjson2` only as part of the
event-channel work below — not on its own.
**The real fix for mission-tier tool observability** is to plumb an event
channel into the provider so it can emit `TurnEvent::ToolCall` for calls it
merely observed. That is a change to ZeroClaw's provider trait and is its own
piece of work. Until then the microVM tier's `PostToolUse` tap remains the only
working tool telemetry.
### The PreToolUse gate — VERIFIED end to end (2026-08-21)
Proven against the real `claude` binary with the real settings document and the
real hook script. Both halves:
**It blocks.** Asked to run `curl -X POST https://example.com -d ping`, the
agent attempted the Bash call, the hook fired *from `--settings`*, and the call
was refused. `denied.jsonl` recorded the payload:
```json
{"hook_event_name":"PreToolUse","tool_name":"Bash",
"tool_input":{"command":"curl -X POST https://example.com -d ping --max-time 5"}, …}
``` ```
And the agent relayed the reason accurately — *"blocked by a local tool-gate ### Three bugs the live test found, all the same shape
hook… reads are fine, but sending content off the machine has to go through the
platform rather than raw curl"* — which is the reason text from
`vm_tool_gate::RULES`, reaching the model as intended.
**It allows.** `echo gate-allows-this` and `rm -rf ./scratch-nonexistent` both Each left every other link looking correct:
ran normally and `denied.jsonl` stayed empty. A gate that blocked everything
would have passed the first test; this is the half that rules that out.
No microVM or fleet needed — `cargo test -p cm-api --lib emit_guest_assets -- 1. The settings document pointed `PostToolUse` at a path the installer never
--ignored` writes the fixtures and one `docker run` against wrote. Claude Code does not complain about a missing hook command — it
`clawmates-runtime:sync` does the rest. records nothing. A test now compares the document's commands against the
files the installer creates.
2. The mission container uses `CLAWMATES_RUNTIME_IMAGE`, not the shared
`clawmates-runtime` container — it was on an older image whose daemon schema
had no `settings` field, so the prop write returned `404 path_not_found`.
3. The drain used `connect_with_local_defaults()`; the server reaches Docker
through a **socket proxy**, so it failed — and returned `Ok(())` silently.
One incidental finding: asked to `git push --force`, the model **refused on its ### CI: build failures were disk, not code
own** before ever calling Bash, so the hook never fired and the test was
inconclusive. A gate test has to use a command the model will actually attempt Runs 503–506 failed at `build` with an unreadable log, and the first casualty
— the model's own judgement is not the gate, and testing against something it was a **docs-only** commit. Cause: building runtime images by hand on gw-04
already refuses proves nothing. competes with CI for the same 150G volume; the frontend image build lost.
`docker builder prune` reclaimed 34GB (22G → 57G free) and the next run went
green. The build job now writes breadcrumbs, a `df -h` snapshot, and which
services actually pushed — the failing runs had pushed `server`, aborted on
`frontend`, and left `:latest` unmoved, which surfaced three steps later as
"the deploy did not happen".
**Operational note:** do not build images by hand on gw-04 while CI may run.
## Then, in order ## Then, in order