cm_decide:🚪 three Nouls per outbound action (data leaving the
organisation, a credential in the content, unsolicited/abusive), the max
is the deny probability. Measured on 24 hand-labelled door actions
(eval/door-actions.json): AUROC 1.000, [email protected] 0.96, no false denies, no
misses, 4 of 24 in the review band — three deny-labelled actions it would
not refuse alone (db dump 0.71, delegate-exfil 0.74, threat 0.77) and the
one genuinely borderline allow (repo name to a contractor 0.56). 168 ms,
~600 tokens per action, off the z.ai quota.
mcp_door: PolicyOutcome::Hold. With TYPESAFE_API_KEY set, above DENY_AT
(0.8) refused, below ALLOW_BELOW (0.2) executed, between them the action
gets a pending approval (session_key door:<id>) and the agent is told it
is queued and not to retry. The approvals route recognises a held door
action and executes it on approve — the grant decide mints, the tool
consumes — rather than resuming a chat run. The chat-model governor
stays as the fallback without a key; it has no middle band. Fail-closed
on an unreachable or malformed answer. Thresholds overridable per
deployment (CLAWMATES_DOOR_DENY_AT / _ALLOW_BELOW).
decide-eval --kind door reports the band outcome, not only a threshold.
Harness: a door scenario exercising all three bands directly against /mcp
with email_send (its effect is an outbox row), then approving the held
one and checking it executes then and not before.
Co-Authored-By: Claude Opus 5 <[email protected]>
The `decide()` helper (used by both approve and reject) wrapped
`runtime.resume_run(ready).await` inside `tokio::spawn`, so the handler
returned 200 to the client before the resume had even started. When a
client (or test) immediately reconnected to /api/gateway with resumeFrom,
the run's broadcast channel didn't exist yet — `runtime.subscribe(run_id)`
returned None, the journal was still empty (no new events yet), and the
SSE stream closed with zero events. Symptom: approvals_api tests flaky in
CI (`unwrap on None` at line 282 of reject_over_http_executes_nothing,
sometimes `missing step_finished` on the accept path).
resume_run's *own* awaited portion is only the setup — claim_resume,
checkpoint load, and open_channel. It internally spawns the long-running
multi-step work. Awaiting it inline means we wait milliseconds for setup,
then return. By the time the client reconnects, the channel exists, so
subscribe() finds it and live-tail works. The sweeper still handles the
crash-between-decision-and-resume case for durability.
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]>