Commit Graph
95 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 6e8785f159 fix(missions): a server restart no longer kills a running mission
deploy / test (push) Successful in 4m25s
deploy / build (push) Successful in 5m34s
Mission 01a00538 ("ClawHDF5 REsearch and Refactor") failed 19 minutes and 93,762
tokens into its research phase with `pair failed: 403 Forbidden`, and its coding
phase was then correctly skipped as unreachable. The cause was not the coding
phase and not the model — it was pairing.

A per-mission runtime is authenticated with a SINGLE-USE pairing code, and the
bearer token it returns was cached in memory only. Any restart of the server
discarded that token; the next turn re-paired with a code the gateway had
already spent and got 403 — permanently, for that mission. A deploy, a crash or
an OOM would each do it. The durable-run machinery exists precisely so work
survives a restart; pairing was the one thread that did not, and it failed
closed.

`missions.runtime_token` persists the token at the moment pairing succeeds, and
the worker seeds the executor's cache from it, so a new process reuses the
credential instead of re-pairing. Persisting is best-effort: failing to save
must not fail a turn that just paired successfully.

Verified by reproducing the original failure: launched a mission, confirmed the
token was written, restarted the server MID-PHASE, and watched the mission run
to completion with no pairing failure.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 21:43:05 -07:00
Omar SobhandClaude Opus 5 43436d7181 feat(telemetry): push bus for live agent frames
deploy / test (push) Successful in 4m25s
deploy / build (push) Successful in 5m20s
/api/world/live is a 2s database poll, which is right for queryable state and
wrong for a token stream: reasoning only became visible after a step finished
and its row was written. This adds a process-wide broadcast bus that
topology_exec publishes to as the runtime's WebSocket delivers frames, and the
SSE handler forwards without waiting for the next tick.

Measured: the pushed frame arrived ~2.2s before the polled copy of the same
text.

Design notes worth keeping:
- A global (OnceLock), not an AppState field. The publisher is reached through
  phase_runner -> topology_worker -> MissionTap, none of which hold AppState;
  threading a handle through all of them would put a UI concern into four
  layers that have no other reason to know about one.
- Lossy by design. A slow subscriber lags and skips rather than applying
  backpressure to the agent producing. mission_events remains the durable
  record; this bus is the fast path, never the source of truth.
- Only `claw_<uuid>` aliases are attributed. The governor, door and evaluator
  drive real turns under other names, and attributing their output to an agent
  would put words in someone's mouth. Asserted in a test.
- The poll no longer emits `reasoning`: with both paths live, every turn
  arrived TWICE — once pushed, once polled ~2s later. The row is still written;
  this feed just is not its second mouth.

CEILING, measured rather than assumed: turns are not token-level because the
runtime is not streaming. zeroclaw's claude_cli provider runs
`claude -p --output-format json`, which returns ONE result object when the turn
completes — there are no incremental tokens to forward. Making this genuinely
token-by-token needs `--output-format stream-json` and incremental parsing in
the zeroclaw fork, not here. The bus is in place and will carry them the day it
does.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 16:29:12 -07:00
Omar SobhandClaude Opus 5 ba9d7aa185 feat(telemetry): WORKING ON NOW shows the mission an agent is on
deploy / test (push) Successful in 4m10s
deploy / build (push) Successful in 5m13s
The last of the three declared-but-never-emitted event types.
`agent.task.update` had no producer anywhere in the backend, so the card read
"idle — no active task" for an agent that was mid-turn.

Derived rather than newly instrumented: an agent is working on its crew's
RUNNING mission, and that mission's phases are the steps (completed/skipped →
done, running/evaluating → active, else pending). Nothing is emitted for an
agent with no running mission, so "idle" stays truthful rather than freezing on
a stale last-known task.

Verified on a live mission: 116 agent.task.update events observed on
/api/world/live, carrying the mission title and phase steps, with the state
advancing pending → active as the phase started.

That closes the set. Of the seven cards in the command centre, five were dark:
three had no emitter at all and two read a table the mission path never wrote.
DOORS and LOOPS were correctly wired the whole time and were reporting an honest
zero.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 16:03:42 -07:00
Omar SobhandClaude Opus 5 bf40d10064 feat(telemetry): the reasoning stream actually streams
deploy / test (push) Successful in 4m18s
deploy / build (push) Successful in 5m17s
`agent.reasoning.delta` and `agent.tool.call` have been declared in the taxonomy
and listened for by the command centre since it shipped — and NOTHING ever
emitted them. The world feed emitted five types; neither was among them, so
REASONING STREAM could not populate no matter what an agent did.

The feed is a database poll, not a push bus, so a live card can only show what
was persisted. The worker already holds each step's output text and the claw
that produced it, so it records a `reasoning` mission_event (truncated — the
card renders a tail, not a transcript, and mission_events is capped per phase),
and the feed emits it forward from a cursor that starts at the current max so a
page load streams rather than replaying history.

`tool.call` is emitted from the same place. On the container tier it will stay
empty, and that is correct rather than broken: those agents are tool-free behind
the §15 door. Tool lines appear where agents actually hold tools.

Verified on a live mission: agent.reasoning.delta observed on /api/world/live
carrying the agent's own text, keyed by agentId.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 15:58:56 -07:00
Omar SobhandClaude Opus 5 8be7b3c9b2 feat(telemetry): record per-agent usage for mission turns
deploy / test (push) Successful in 4m31s
deploy / build (push) Successful in 5m23s
The command centre's SPEND, ACTIVITY and THROUGHPUT cards read `usage_events`,
and nothing on the mission path ever wrote a row: `cm_billing::charge` was
called only from the agent-run path. Measured mid-mission with 14 agents live,
`usage_events` was 0 while a crew had just burned 15k tokens — so an agent that
had done real work reported zero cost and zero activity.

The worker already knew everything needed: it logs node, role and token count
per step, and the node's `attrs.agent` carries the `claw_<uuid>` binding the
runtime dispatches on. This routes that to the ledger.

`charge`'s run_id is now Option. `usage_events.run_id` references `agent_runs`,
and a topology turn has no row there — passing its `topology_runs` id was a
foreign-key violation, which is exactly what the first attempt hit. NULL is the
honest value; the agent-run caller still passes its real id.

The executor reports one total rather than an in/out split, so the cost is right
(credits price the sum) and the columns record it as output rather than
inventing a split.

Verified end to end on a real mission: 4 agents, 1046-8670 tokens each, credits
attributed per agent, and the SPEND/ACTIVITY queries now return real numbers.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-15 05:09:56 -07:00
Omar SobhandClaude Opus 5 8ef7067467 fix(repos): a scoped connection can name a user, not just an org
deploy / test (push) Successful in 4m19s
deploy / build (push) Successful in 5m42s
Scoping a Gitea connection to `osobh` — the personal namespace clawmates itself
lives in — failed with "org 'osobh' not found or PAT lacks access". The sync
only ever called /orgs/{owner}/repos, and Gitea serves user namespaces from
/users/{owner}/repos. The error pointed at permissions for what was really a
wrong endpoint, which is the kind of message that sends you to rotate a token
that was fine.

Retry as a user on 404 before giving up, and say what was actually checked.

Verified: owner=osobh now syncs 7 repos, owner=redclaw 22 — 29 instead of the
182 an unscoped connection pulls.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-14 21:00:17 -07:00
Omar SobhandClaude Opus 5 b290025fc4 feat(agents): make delete permanent, and expose the census
deploy / test (push) Successful in 4m25s
deploy / build (push) Successful in 5m10s
A soft delete marked the row and left it. The agent stayed in the table forever,
kept appearing on any surface that forgot `deleted_at IS NULL`, and deleting it
again did nothing — the decision was recorded and never honoured. Two agents on
this deployment had been in that state since June.

`deleted` is now a fifth lifecycle state, collected with NO grace window: a
human already decided, months ago. It takes usage_events with it, which is the
explicit trade — the alternative is rows that outlive the decision to delete
them.

Two endpoints, because this was previously only answerable by reading the
database by hand:

  GET  /api/claws/lifecycle        the census: who is active, completed,
                                   orphaned, deleted — and what is reapable
  POST /api/claws/lifecycle/sweep  run the reap now, rather than waiting out
                                   the hourly timer for a decision already made

Verified end to end: census reported both as `deleted`/`reapable`, the sweep
returned {"reaped":2,"failed":0}, and agents and usage_events both went to 0.

The safety property is unchanged and re-asserted by a new test: adding `deleted`
did not make `owned` or `active` reapable.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-14 18:04:43 -07:00
Omar SobhandClaude Opus 5 ccbc387f4b fix(agents): stop listing soft-deleted agents
deploy / test (push) Successful in 3m54s
deploy / build (push) Successful in 5m20s
Deleting an agent looked like a no-op: it disappeared from the workforce but
stayed on the Team board, and deleting it again did nothing because the row was
already marked. Two queries selected from `agents` without `deleted_at IS NULL`:

  routes/team.rs   the leaderboard — the surface still showing them
  routes/world.rs  the "working" set — a deleted agent holding a stale
                   agent_containers row rendered as live

Observed on this deployment: /api/workforce correctly returned nothing while
/api/team/leaderboard returned two agents soft-deleted back in June.

NOT changed: those rows still exist. Making delete permanent means hard_purge,
which also deletes usage_events — billing history, 6 credits on one of these
two. Discarding that as a side effect of tidying a roster is an explicit
decision, not something a display fix should smuggle in.

.sqlx regenerated: team.rs uses the compile-time-checked query! macro, so the
cached entry no longer matched.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-14 12:20:00 -07:00
Omar SobhandClaude Opus 5 a494634f81 feat(agents): classify agents by lifecycle and reap the finished and orphaned
deploy / test (push) Successful in 4m42s
deploy / build (push) Successful in 5m7s
A mission mints a crew, and the only thing that reaped one was DELETING the
mission. A mission that merely completed left its agents in the roster forever,
and a crew whose reap was skipped or failed left agents bound to nothing —
indistinguishable in the UI from the operator's own staff.

Four states, from one query:

  owned      no agent_template_link row   → hand-created. NEVER reaped.
  active     on a running/draft mission   → working right now. Kept.
  completed  every mission terminal       → reaped after a 24h grace.
  orphaned   minted, bound to nothing     → reaped.

The discriminator is `agent_template_link`, which mission_orchestrator writes
per minted claw. This matters more than it looks: verified on live data, a
hand-created agent and an orphaned crew member both have ZERO team links and are
structurally identical by binding alone. Judging orphanhood by "no team" would
delete the user's workforce. Provenance is the only honest signal.

The grace window exists because the results view, the World's 24h replay and
"who did this work?" all read the crew AFTER the run ends; reaping on the
terminal transition deletes the answer exactly when the question gets asked. A
completed crew with no usable timestamp is KEPT — a missing date must never read
as "old enough to delete".

Also fixes the delete summary, which reported how many claws were FOUND rather
than purged: "reaped 4 claw(s)" was printed by a delete that purged none, which
is precisely the log you would read while wondering why the agents are still
there. It now reports purged / kept / FAILED, and failed > 0 is the orphan case.

Verified against live data — all four states observed, including the two that
look alike.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-14 10:55:56 -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 4d07868410 ci: stop leaking a 2.8 GB postgres volume every run
deploy / test (push) Successful in 4m43s
deploy / build (push) Successful in 4m0s
`docker rm -f` without -v orphans the anonymous volume the postgres image
declares. cm-testkit creates a database per test, so each CI run left ~2.8 GB
behind: 38 GB of dangling volumes had accumulated on gw-04, most of the 99 GB
-> 23 GB drop in free space over one day.

Note for anyone reaching for `docker volume prune` to clean this up: don't. On
gw-04 the dangling set also contained traefik-acme (Let's Encrypt certificates)
and all three CI cargo caches. Only the anonymous 64-hex volumes were safe to
remove.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 21:16:38 -07:00
Omar SobhandClaude Opus 5 25a3d6902a ci: only publish a release for an actual tag
deploy / test (push) Successful in 3m55s
deploy / build (push) Successful in 57s
On workflow_dispatch GITHUB_REF_NAME is the BRANCH, so the upload step created a
Gitea release AND a git tag both named "main" — a tag sharing the branch name,
from a run that was only meant to be a smoke test. Both have been deleted.

Gated on github.ref_type == 'tag'. A dispatch now exercises build, SBOM, sign
and offline verify, and stops there.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 21:06:44 -07:00
Omar SobhandClaude Opus 5 837a3d3ff0 ci: don't run the install rehearsal on the production gateway
deploy / test (push) Successful in 3m56s
deploy / build (push) Successful in 56s
Every other step in the release job is inert with respect to prod — build,
SBOM, sign, offline verify. The rehearsal is the only one whose purpose is to
stand a full stack up and tear it down with `down -v`, and it was doing that on
the machine serving production. On 2026-08-13 it adopted the live compose
project and destroyed clawmates_pgdata.

The script itself is now safe (unique -p, a guard against the production project
name, and a health probe pointing at the port the bundle actually publishes) and
is kept for use on a build box or throwaway VM. What changes here is only WHERE
it runs, which was the real problem: a destructive verification step does not
belong on the host it can destroy.

Releases still build, sign, verify offline in a --network none container, and
upload to Gitea.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 20:56:30 -07:00
Omar SobhandClaude Opus 5 875ff948f8 fix(rehearsal): probe the port the bundle actually publishes
deploy / test (push) Successful in 4m0s
deploy / build (push) Successful in 55s
The health check polled 127.0.0.1:18080, but deploy/compose/docker-compose.yml
publishes "8080:8080" and deploy/airgapped/install.sh does not rewrite ports.
Nothing was ever listening on 18080, so the rehearsal always ended in
"platform never became healthy" — regardless of whether the install worked.

Visible now only because the earlier failures (no cargo, compose v1, project
collision) all stopped the script before it got this far.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 20:44:13 -07:00
Omar SobhandClaude Opus 5 e7d2fc9696 fix(rehearsal): never adopt the production compose project
deploy / test (push) Successful in 3m56s
deploy / build (push) Successful in 54s
INCIDENT: the release rehearsal destroyed production data on gw-04.

deploy/compose/docker-compose.yml declares `name: clawmates` at the top level,
and that beats --project-directory. So `compose up` from a temp directory did
not create an isolated stack — it ADOPTED the running production stack of the
same name, recreated its containers, and then the cleanup trap's `down -v`
deleted its volumes, including clawmates_pgdata. Prod came back with an empty
database: 177 repos, all missions and all agents gone. There were no backups.

The fix is `-p rehearse-$$` on every invocation, plus an assertion that refuses
to run under the production project name. Isolation here was implicit and
therefore not isolation at all.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 15:36:51 -07:00
Omar SobhandClaude Opus 5 41854c70e1 ci: the install rehearsal needs compose v2, and says so
deploy / test (push) Successful in 4m23s
deploy / build (push) Successful in 57s
The v1 fallback I added a commit ago cannot work: deploy/compose/docker-compose.yml
uses v2-only syntax — a top-level `name:` and long-form
`env_file: {path, required}` — so docker-compose 1.29 rejects the file outright
("'name' does not match any of the regexes"). A fallback that always fails is
worse than no fallback, so the script now requires v2 and fails immediately with
what to do about it.

$COMPOSE overrides the detection. gw-04 is deliberately left WITHOUT a
`docker compose` plugin: installing one system-wide would flip the production
rolling deploy (clawmates-deploy.sh prefers v2 when present) off docker-compose
v1 as an invisible side effect of a release change. The runner gets a standalone
v2 binary at /opt/act-runner/bin/docker-compose and the workflow passes it in,
so prod keeps rolling exactly as it did.

Verified on gw-04: standalone v2.32.4 runs, and `docker compose` still resolves
to nothing, so clawmates-deploy.sh takes its v1 branch unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 15:22:29 -07:00
Omar SobhandClaude Opus 5 9441cf401c ci: make the install rehearsal work with compose v1
deploy / test (push) Successful in 4m13s
deploy / build (push) Successful in 58s
The rehearsal reached "First boot" — bundle assembled, signed, verified offline,
images loaded, install staged — and then died with
`unknown flag: --project-directory`. That message is misleading: gw-04 has no
docker compose v2 plugin at all, only docker-compose 1.29.2, so `docker compose`
is parsed as `docker` with a bogus flag rather than reported as a missing plugin.

Use the same v2-then-v1 fallback deploy/gw-04/clawmates-deploy.sh already needs.
v1.29.2 supports --project-directory, so the invocations are otherwise unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 15:11:09 -07:00
Omar SobhandClaude Opus 5 bd1c970577 ci: let the install rehearsal use a pre-built bundler
deploy / test (push) Failing after 2m56s
deploy / build (push) Skipped
The rehearsal hardcoded `cargo build -p clawmates-bundler`, so it died with
"cargo: command not found" on the release runner — gw-04 builds Rust inside a
container and has no toolchain of its own. The release job had already built the
bundler two steps earlier, so it was also redundant work.

CLAWMATES_BUNDLER now short-circuits that build when it points at an executable,
falling back to cargo otherwise, so running the script by hand is unchanged.

Everything before this step already passed on the runner: images built, SBOMs
generated, bundle assembled and signed, and "bundle OK: 94 artifacts verified
offline" inside a --network none container.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 15:02:05 -07:00
Omar SobhandClaude Opus 5 c1642a7004 ci: copy the bundler out of the target volume
deploy / test (push) Successful in 4m24s
deploy / build (push) Successful in 58s
First dispatch failed at exit 127, "target/release/clawmates-bundler: No such
file or directory". The bundler builds inside a container where /w/target is a
NAMED VOLUME, so the binary was written somewhere no later host step can see —
the workspace's target/ stays empty. Copy it to .tools/ (bind-mounted) and
assert it landed, so the next occurrence fails at the build step with a clear
message instead of two steps later as a missing file.

deploy.yml does not hit this because it copies clawmates-node into
frontend/public/dl/ from inside the same container.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 14:51:50 -07:00
Omar SobhandClaude Opus 5 de8736c16b ci: move release.yml to Gitea and make it actually runnable
deploy / test (push) Successful in 4m37s
deploy / build (push) Successful in 56s
It could never have run as written: `runs-on: ubuntu-latest` matches no runner
on this forge, and `softprops/action-gh-release` talks to GitHub's API. There
are zero tags and zero releases, which is consistent with it never having fired.

Rewritten for this runner:
- runs-on: gw04 (the only reachable x86_64 host; prod artifacts must be amd64)
- the bundler builds in a rust container with the shared cargo cache volumes —
  gw-04 has no cargo, and installing a toolchain onto the production gateway to
  build a release is the wrong trade
- release creation + asset upload go to Gitea's own API, create-or-reuse so a
  re-run of a tag updates rather than 409s
- syft installs into the workspace, not /usr/local/bin: the host executor runs
  as root on the gateway and a release should leave nothing behind
- a disk-reclaim step, because the artifacts are GBs of image tarballs on a box
  that is also serving production. It removes only the versioned images it
  created — never a blanket prune, since clawmates/agent-*:dev exist in no
  registry and are the source of the microVM rootfs files
- workflow_dispatch added so the pipeline can be exercised without minting a tag

BUNDLE_SIGNING_KEY now exists as a repo secret (fresh ed25519 keypair; nothing
depended on a previous one). The signing and offline-verify steps are unchanged:
verification still runs inside a --network none container, which is the whole
air-gapped contract.

.github/ is now empty and removed.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 14:43:33 -07:00
Omar SobhandClaude Opus 5 26e571fe01 ci: drop .github/workflows/ci.yml
deploy / test (push) Successful in 4m31s
deploy / build (push) Successful in 6m11s
It targets `runs-on: ubuntu-latest`, which no runner on this Gitea provides, so
every push left a failed job in the Actions tab. .gitea/workflows/deploy.yml now
covers the gates that actually hold: the full `cargo test --workspace` (including
the DB- and docker-backed integration suites, which this workflow never ran) plus
frontend typecheck and tests.

What is deliberately NOT carried over, because none of it passes today and
silently keeping a red gate is worse than removing it:
  cargo fmt --all --check      63 files drift
  clippy -D warnings           pre-existing warnings across the workspace
  ci/check-loc.sh              MissionWizard.tsx 1153 lines vs a 1100 soft limit
  ci/check-no-placeholders.sh  false positive on `vec!["rg", "TODO", "src"]`,
                               which is test DATA, not a placeholder
  playwright e2e               needs a browser toolchain on the runner

Re-adopting any of these is a cleanup project, not a workflow edit. The scripts
under ci/ are kept so that work has somewhere to start.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 12:19:43 -07:00
Omar SobhandClaude Opus 5 548f977212 style: rustfmt the four files the repo-less mission fix touched
Found while removing .github/workflows/ci.yml: three of the four files in that
change were unformatted, and four of the diffs were newly introduced (the new
prompt tests and the tool_preamble format! call). Formatting only the files that
change already touched — a repo-wide `cargo fmt` would be 63 files of unrelated
churn and belongs in its own commit.

Mechanical; `cargo test -p cm-api --lib` stays at 322 passed.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 12:19:43 -07:00
Omar SobhandClaude Opus 5 022ef98e44 feat(auth): opt-in local auto-login for single-user deployments
deploy / test (push) Successful in 4m51s
deploy / build (push) Successful in 6m33s
Skips the login form and lands on the dashboard. It performs a REAL backend
login — the API still issues and can revoke the session — so this does not
weaken auth; it only removes a form for a deployment with exactly one operator.

Gated on BOTH LOCAL_AUTOLOGIN_EMAIL and LOCAL_AUTOLOGIN_PASSWORD, and refuses
outright in clerk mode. Prod sets neither, so the route 404s there. Two
conditions rather than one flag: a single misread value should not be able to
hand a session to an anonymous visitor.

The route emits a RELATIVE Location — inside the container request.url is the
0.0.0.0:3000 bind, so NextResponse.redirect would send the browser to a host
that only exists in Docker — and the cookie's secure flag keys on
x-forwarded-proto rather than NODE_ENV.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:47:14 -07:00
Omar SobhandClaude Opus 5 7cf77a9248 feat(ui): level up moves to the agents sidebar; repos open collapsed
Level up now sits at the bottom of the Agents sidebar, labelled with the
selected agent's name, and renders only once an agent is selected — it is hidden
during select mode so the reap bar stays the single footer action there. It is
gone from the ClawCommandCenter header.

Repos open with every org folded. Rather than seeding a "collapsed" set with all
keys on load, the state tracks EXPANDED: a smaller change that also stays correct
for orgs that arrive later from a sync, which a seeded set would render open.
With 182 repos across 8 orgs, an all-expanded default buried the org names the
list is meant to be navigated by.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:47:14 -07:00
Omar SobhandClaude Opus 5 5db695460f fix(missions-ui): the results area could not scroll at all
The canvas host is a position:relative BLOCK, so flex:1 on MissionCanvas's root
was inert and its height collapsed to its content. That starved the scroller
beneath it — scrollHeight === clientHeight — so it never scrolled, and the
overflow spilled past the page and was clipped by the host's overflow:hidden.
Long results were rendered and then thrown away. Every sibling canvas already
used position:absolute; inset:0; missions was the only one that did not.

Measured after, on a brief 5x the viewport: one scroller, clientH 736 vs
scrollH 3244, scrolling 0 -> 2508 (exactly scrollH - clientH, i.e. the true
bottom), zero page overflow, tab strip pinned throughout.

Also removed five nested scrollers (70vh on live events; maxHeight caps on run
streams, phase summaries, artifact bodies and error traces). Those existed only
to work around the missing height and would have become portholes onto the very
content the operator is trying to read. The xterm pane keeps its bounded box —
FitAddon needs one, and a terminal owning its scrollback is correct.

Deleting the header's description peek reclaims 104px for results (header
256 -> 152px); the same text renders in full in Setup -> Overview, as the code's
own comment noted.

Streaming now follows only when already at the bottom, via a shared
useStickToBottom hook replacing two byte-identical copies, plus a "jump to
latest" pill neither had. Defaults collapse by mission state, and a remount key
fixes scrollTop leaking between tabs — a bug that only appears once scrolling
works.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:47:00 -07:00
Omar SobhandClaude Opus 5 4dec77ae6d fix(missions): give repo-less container missions the workspace they are promised
Every agent on a research_only mission refused to work, each reporting it was
"in Claude Code", had no /mission/repo, and only had Read/Edit/Bash. All three
statements were true. The run still recorded completed — 5 turns, 7.4k tokens,
0 artifacts, no error.

The machinery is correct when a repo IS bound (verified on a live prod
per-mission container: /mission/repo present, all 5 agents pinned). Only the
repo-less path was broken, in three layers that disagreed by construction:

- sync_in no-oped without a host checkout and copy mode does not bind /mission,
  so NOTHING created /mission/repo. The microVM tier already creates it, for the
  stated reason that "the guest needs the workspace to exist before the agent
  writes into it". Creating it host-side also un-breaks sync_out, equally a
  no-op before, so work survives across phases instead of being wiped.
- pin_agent_workspaces returned Ok after pinning ZERO agents, so the
  deliberately-fatal guard in mission_orchestrator could never fire. Its error
  text already described the exact outcome we got.
- The prompt advertised ZeroClaw tool names and explicitly denied `bash`, while
  every executor ends in `claude -p`: microVM passes Read/Edit/Write/Bash/Agent,
  session passes Read/Edit/Write/Bash, and claude_cli agents get Claude Code's
  native toolset — ZeroClaw's gating never reaches the subprocess. It was
  telling agents to use missing tools and avoid present ones.

And it went green because mission_outputs logged the failed collect and
continued — with the fail-empty rule and the NO-OUTPUT marker both BELOW that
continue, so the phase was retried forever and never failed. The retry is now
bounded by a grace window off completed_at.

Verified end to end: mission completed, agent wrote
/mission/repo/research/firecracker_vs_docker.md, collected and registered as a
document artifact (6.6 kB of real content).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:46:45 -07:00
Omar SobhandClaude Opus 5 af89020dfd ci: put the docker CLI on PATH for the sandbox integration tests
deploy / test (push) Successful in 4m34s
deploy / build (push) Successful in 8m42s
cm-runtime/cm-sandbox tests shell out to `docker` via std::process, so the
mounted socket alone was not enough — browser_tool failed with
`docker available: NotFound`. Mount the host binary rather than apt-installing
docker.io: the container is fresh every run, so an install would re-download
~100 MB each time and cache nothing.

Verified on gw-04 that a mounted /usr/bin/docker talks to the host daemon
(client=29.1.3 server=29.1.3), and that the agent-*:dev images these tests need
are already present there.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:18:01 -07:00
Omar SobhandClaude Opus 5 ee1cea72d9 ci: mount the docker socket so the testcontainers suite can run
deploy / test (push) Failing after 2m54s
deploy / build (push) Skipped
cm-files' s3_store test starts a real MinIO via testcontainers. Without the
socket it does not skip — it fails with
`Client(Init(SocketNotFoundError("/var/run/docker.sock")))`, which looks like a
broken test rather than a missing capability. It passed locally only because the
Mac's docker socket was visible to the test process.

Sibling containers testcontainers starts are reachable because the test
container already shares the host network.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:13:26 -07:00
Omar SobhandClaude Opus 5 8129f58845 ci: give cargo the credential for the private clawhdf5 git dep
deploy / test (push) Failing after 5m38s
deploy / build (push) Skipped
First run failed in `cargo test --workspace`: "failed to load source for
dependency clawhdf5", preceded by three "spurious network error: invalid packet
line" retries. Two separate causes, both needed:

- libgit2 cannot fetch from Gitea's smart-HTTP. images/server.Dockerfile already
  sets CARGO_NET_GIT_FETCH_WITH_CLI for exactly this; the test step did not.
- quantumclaw/clawhdf5 is private (401 anonymous), so the CLI fetch needs a
  credential. Supplied via an insteadOf rewrite from a repo secret, so the token
  is masked in logs and never committed.

The server image build does not hit this — it builds only clawmates-server,
which does not pull cm-brain's clawhdf5 path.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:06:34 -07:00
Omar SobhandClaude Opus 5 b1bf50160a ci: build and deploy to production from a push to main
deploy / test (push) Failing after 42s
deploy / build (push) Skipped
Closes the one manual step left in the pipeline. gw-04 has run
clawmates-deploy.timer every minute since July, pulling :latest and rolling on
drift — the CD half already worked. What was missing was anything that moved
:latest, since the old build host (tank) is packed for the move.

The runner lives on gw-04 because it is the only reachable x86_64 host and prod
images must be linux/amd64: web-01 is aarch64 and the fleet build boxes are
offline. Host executor, capacity 1, so builds serialize rather than competing
with production traffic.

Three details that are not obvious:
- `docker push :latest` does NOT move the tag on this registry once the manifest
  exists under another tag. The PUT-the-manifest step is what actually moves it,
  and its absence is how a "successful" deploy could leave prod on a stale image.
- The final step verifies the image prod is RUNNING, not the one we pushed. A
  green edge on the old image is the failure this pipeline exists to prevent.
- broker is built here too. It had no :latest tag at all, so gw-04's deploy loop
  logged a pull failure every single cycle since 2026-08-11.

Also ignore the local env backups: `.env` was ignored but `.env.bak.*` was not,
and those copies hold real credentials.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 10:03:35 -07:00
osobhandClaude Opus 4.8 96d1409a23 style: cargo fmt --all
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 2m39s
ci / e2e (push) Has been skipped
ci / frontend (push) Successful in 24s
Apply rustfmt (toolchain 1.96.0) to satisfy the CI Format check.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-27 11:54:28 -07:00
Omar SobhandClaude Fable 5 e6f50c7ecb Computer panel: fixed 652px card, centered (not stretched), matching WorkClaw
The card used h-full, so it stretched to fill the column — on tall windows
it grew past the reference (808px at a 920px viewport) instead of staying a
content-sized panel. WorkClaw centers a fixed ~652px card that floats with
empty space above/below. Now phone/tablet → md:h-[652px] (md:max-h-full so
it still caps on short windows), centered via justify-center on the column
wrapper; full + mobile keep h-full to fill. Verified at a 920px window: card
652px, top 166 / bottom 818 (centered, floating) instead of 808 stretched.
At the 720px test viewport it caps to the column either way, so no baseline
change — full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 20:36:46 -05:00
Omar SobhandClaude Fable 5 c174c94594 Chat header: remove the agent identity block
Dropped the avatar + name + role + online dot from the top of the chat
header (the claw is already shown in the rail and the welcome state). The
header now holds only the right-aligned action cluster (and the device/
Close overlay while the panel is open). Removed the now-unused agent prop
+ Avatar/Agent imports; ChatWorkspace no longer passes agent to the header.
Regenerated chat-welcome + computer-home baselines; full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 18:14:48 -05:00
Omar SobhandClaude Fable 5 8c9b750673 Left rail: match WorkClaw sizing/weights/container
Measured sidebar parity:
- Container: dropped the aside's px-1 so the rail is the full w-rail 176px
  (was 168px inner); added bg-background (solid #0a0a0a, was transparent);
  padding now 8px 0 24px (pt-2 pb-6).
- Bottom nav (Skills/Apps/Team/Credits): 14px/400 → 12px/500 (text-xs
  font-medium, leading-[1.3]); trailing credits balance likewise.
- Claw-name labels: active 400→600 (font-semibold), inactive 400→500
  (font-medium); kept 12px.
Verified live at 1440: rail width 176, bg rgb(10,10,10), nav 12px/500,
active claw name 12px/600. Rail-wide change → regenerated all 7 visual
baselines; full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 15:25:38 -05:00
Omar SobhandClaude Fable 5 ddc1f8fbe0 Fonts: drop body antialiased so text renders at the reference weight
The <body> carried Tailwind's `antialiased` (-webkit-font-smoothing:
antialiased), which inherits through the whole tree and renders every
chat/message/UI text thinner & lighter than WorkClaw. WorkClaw leaves the
browser default (`auto`), giving the crisper/heavier weight. Removed the
class — body is now just `min-h-full`; computed -webkit-font-smoothing is
`auto`. The font itself was always 14px/400 Geist; only the smoothing
differed. Global rendering change → regenerated all 7 visual baselines;
full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 14:06:03 -05:00
Omar SobhandClaude Fable 5 ea27715202 Fonts: add ui-sans-serif to the sans fallback chain (match WorkClaw)
--font-sans was 'var(--font-geist), system-ui, sans-serif'; WorkClaw's
chain includes ui-sans-serif before system-ui. Added it so the fallback
order matches: geist, geist Fallback, ui-sans-serif, system-ui, sans-serif.
Fallback-only — Geist always loads (vendored locally), so rendering is
unchanged; full suite (37, incl. visual) green with no baseline diff.

(The lowercase 'geist' / 'geist Fallback' names are next/font's
auto-generated, metric-adjusted family names — CSS font-family matching is
case-insensitive, so they render identically to 'Geist'/'Geist Fallback'.)

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 13:50:48 -05:00
Omar SobhandClaude Fable 5 2910189717 Regenerate computer-home baseline: chat condenses beside the panel
Full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 12:19:35 -05:00
Omar SobhandClaude Fable 5 21da4dca19 Computer panel: reserve chat space (--computer-width) so the chat condenses, not hides
The absolute panel overlaid the full-width centered chat column, hiding the
messages/composer behind it (WorkClaw avoids this by reserving the panel's
width as padding on the chat). Introduced a shared --computer-width var,
set once on the ChatWorkspace row per ?device= (phone 448px, tablet 67%,
full 100%; 0 when closed). Both the panel <aside> width and a new chat-
column wrapper's md:pr read it, so they never drift: opening the panel
squeezes the centered max-w-3xl column into the left space (re-centered)
while the panel overlays the reserved right area. Animated with
transition-[padding]/[width] + the signature easing for the 'condense'
effect. The header is outside the padded wrapper, so its cluster shift is
unaffected. Verified at 1440: composer right clears the panel left edge in
both tablet (561<593) and phone (920<992) — nothing hidden.

86 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 12:17:52 -05:00
Omar SobhandClaude Fable 5 914ffae8f9 Regenerate computer-home baseline: action cluster in the middle column when expanded
Full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 11:59:43 -05:00
Omar SobhandClaude Fable 5 0d49fb7099 Chat header: action cluster slides to the panel's left edge when expanded
When the panel opened the cluster only shifted a fixed 168px, so the
Slack/Sessions/New icons stayed near the right instead of moving to the
middle. Now the open-state margin matches the panel width (phone 448px,
tablet/full 67%), so the cluster lands at the panel's LEFT edge — the
middle column beside the chat — mirroring WorkClaw's shrinking-pane move.
Animated with transition-[margin]. Verified at 1440: collapsed Computer
ends 1420 (flush right); tablet → 600 (panel left 593); phone → 972
(panel left 992). Device/Close overlay stays far-right; no overlap.

86 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 11:58:04 -05:00
Omar SobhandClaude Fable 5 fa1f64803c Regenerate chat-welcome + computer-home baselines for the right-aligned cluster
Full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 11:17:14 -05:00
Omar SobhandClaude Fable 5 672bfb5631 Chat header: right-align the action cluster (ml-auto), matching WorkClaw
The cluster was absolute-centered (left-1/2 -translate-x-1/2), pinning it to
the header center. WorkClaw right-aligns it with ml-auto in normal flow.
Switched to: title back to min-w-0 flex-1, action cluster ml-auto in flow
(vertical centering via the header's items-center). Verified at 1440:
collapsed cluster is flush right (Computer ends at 1420 = px-5 from edge).
When the panel is open the cluster shifts left (md:mr-[168px]) so the
absolute device/Close overlay sits to its right with a clean gap (Computer
ends 1252, toggles 1296-1420) — no overlap.

86 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 11:15:36 -05:00
Omar SobhandClaude Fable 5 fbff4cbbc8 Regenerate chat-welcome + computer-home baselines for the Slack-chip cluster
Action cluster now Slack → Sessions → New → separator → Computer, chip-styled.
Full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 08:42:10 -05:00
Omar SobhandClaude Fable 5 35d0754b8d Collapsed toolbar: Slack chip + separator + raised-chip action group
Match WorkClaw's collapsed action cluster. Left-to-right now reads
Slack → Sessions → New → │ separator │ → Computer (coral):
- New Slack quick-launch chip (brand /services/slack.svg) as the first
  action, opening the Computer → Slack app via setParams({app:'slack'})
  (same handler as the panel home tile; connect-gated there).
- Slack/Sessions/New wrapped in their own md+ sub-group and given the
  raised-chip treatment (bg-neutral-900 + ring-1 ring-inset ring-white/8
  + shadow-bubble), preserving hover/active.
- Thin 24px vertical separator (w-px h-6 bg-white/12) before the coral
  Computer toggle, which stays size-[42px]/shadow-launcher and always
  visible (mobile-reachable; the action group hides <md per WorkClaw).
Handlers/aria-labels/icons of Sessions/New/Computer unchanged. The
header 'Slack' button doesn't collide with the panel-scoped Slack tests.
Verified live: cluster order Slack→Sessions→New→Computer; Slack opens
?app=slack.

86 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 08:40:32 -05:00
Omar SobhandClaude Fable 5 e0a59e3b95 Regenerate chat-welcome + computer-home baselines for the two-zone header
Action cluster centered, device/Close zone top-right. Full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 07:46:22 -05:00
Omar SobhandClaude Fable 5 906ab50bb8 Chat header: two-zone toolbar (centered action cluster + right-side device zone)
Was a single right-aligned row where the title's flex-1 bunched the action
icons and the device controls together on the right. Now matches WorkClaw's
two-zone pattern:
- Identity stays left (capped width so it never reaches center).
- Action cluster (Sessions / New / Computer) is its own group, absolutely
  centered in the header (measured at ~808, the chat-header center).
- Device-size toggles + Close are a separate, absolutely-positioned
  top-right overlay (z-50, above the panel), shown only while the panel is
  open. Toggles are md+ only; Close stays (now also reachable above the
  mobile full-screen panel). Measured far-right at 1296–1420.
Button classes, icons, aria-labels and handlers are unchanged — only the
grouping/positioning wrappers differ. The panel card starts below the
header (pt-88), so both zones stay visible/clickable when the panel is open.

86 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 07:41:15 -05:00
Omar SobhandClaude Fable 5 40e7e66eea Regenerate computer-home baseline for the 67% tablet overlay
The panel is now a 847px (67%) absolute overlay at tablet, covering the
right of the chat which stays full-width underneath. Fresh capture; full
suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 06:38:40 -05:00
Omar SobhandClaude Fable 5 4d9bcc74ec Computer panel: absolute overlay in ALL states + proportional widths + rail collapse
The previous pass left 'full' as an in-flow flex sibling, so it fought the
chat for layout space and overlapped (chat crushed to a sliver, panel
relative on top). Now the panel is an absolute right-anchored overlay in
EVERY state (never in-flow) — the chat keeps full width and never reflows;
opening the panel just covers the right N%:
- phone  → md:w-[448px]
- tablet → md:w-[67%]   (≈ the measured 847px at 1440)
- full   → md:w-full    (covers everything right of the rail)
Transition is now plain transition-[width] on the absolute aside.

Rail collapse in full mode: LeftRail reads ?device, and when the panel is
open in 'full' it collapses 176→80px, icon-only — labels/credits/user-text
hidden (md+) via a group/rail data-collapsed flag, animated in parallel
with the panel via transition-[transform,width]. Verified at 1440: tablet
panel=847 rail=176, phone=448, full panel=1360 rail=80; chat composer.left
stays 472 in phone/tablet (no shift). 24px card inset + 32px radius kept
throughout.

86 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 06:36:43 -05:00
Omar SobhandClaude Fable 5 dab08e47aa Regenerate rail baselines + de-flake the visual signIn helper
Rail logo (coral paw-print brand tile) and 58px agent tiles updated in
workspace-home, chat-welcome, computer-home (the last also shows the
panel as a right overlay — chat no longer shifts). Fixed the visual
signIn helper: it matched the login page's lowercase 'clawmates' heading
and could screenshot mid-navigation, so workspace-home captured the login
page — now it waits for navigation off /login and matches 'Clawmates'
exact. Avatar unit test updated for the 58px/rounded-20 rail tile.
Full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 05:30:02 -05:00
Omar SobhandClaude Fable 5 96e0134fc6 Left rail restructure + Computer panel as a right-anchored overlay
PART 2 — panel positioning (the 'pushes the chat left' root cause):
DevicePanel was an in-flow flex sibling, so opening it consumed width and
shoved the chat. Now phone/tablet anchor it as an ABSOLUTE right-edge
overlay (md:absolute right-0 top-0 h-full z-30, w-[448px]/w-[550px]) so
the chat keeps full width and does NOT reflow; only 'full' stays in-flow
(grow-[4]) and lets the chat shrink. The chat row is now relative; the
aside is pointer-events-none with the screen card pointer-events-auto so
the header toggles/close stay clickable under the overlay's transparent
top padding. Verified live: composer.left is identical (472px) panel
open vs closed in tablet — the chat no longer shifts.

PART 1 — left rail:
- Logo in its own 80px header row as a 48px black rounded-2xl brand tile
  with a coral PawPrint mark (our placeholder logo) + sr-only 'clawmates'.
- Agent tiles bumped to 58px (Avatar 'rail' size + rounded-[20px]
  squircle), centered, gap-1, 2px coral active ring (rounded-[22px]).
- Hover-revealed ⋯ menu per agent row (new AgentRowMenu): a 176px #1A1A1A
  popover with Pin + Settings rows (coral 15px icons, 14px labels), closes
  on outside-click/Escape. Pin pins the claw to the top of the rail
  (persisted in localStorage, client-side); Settings deep-links to that
  claw's Computer → Settings (the claw redirect now forwards ?app=).

86 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 05:26:09 -05:00
Omar SobhandClaude Fable 5 9456edf198 Regenerate chat-welcome + computer-home baselines for the new welcome stack
Composer centered in the welcome stack with attach/screenshot icons and
no Send, 12px subtitle, rounded 126px hero avatar, and the larger chat
header (58px avatar / 18px name / 14px role). Captured fresh; full suite
(37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 04:29:15 -05:00
Omar SobhandClaude Fable 5 7393b6538b Chat welcome + active conversation: composer controls, layout, header
Measured welcome-screen + active-conversation feedback:

Composer redesigned with two variants (no Send button — Enter to send,
Shift+Enter for newline; attach + screenshot icon affordances on the
left). 'welcome' = short single-row 672px-capped pill, icons inline;
'active' = taller two-row card (textarea on top, 36px icon-button row
below), placeholder switches to 'Type your message…'. A cream Stop pill
appears while the agent is generating, wired to a real useChat.stop()
that aborts the SSE and finalizes the partial reply (new 'stopped'
reducer action).

Empty state un-pinned: the composer is now part of the centered welcome
stack (avatar → heading → subtitle → composer → chips) instead of being
pinned to the viewport bottom. Subtitle dropped to 12px. Hero avatar
kept at 126px but with a proportional 40px squircle radius (a flat 17px
read as square at that size).

Chat header sized up to match: 58px avatar (new Avatar 'header' size),
name 18px/600 neutral-200, role 14px. The neutral-500 role/subtitle tone
nudged to #7d7d7d to clear AA contrast on #0a0a0a.

84 unit + 31 functional E2E green; verified both states live.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-11 04:27:27 -05:00
Omar SobhandClaude Fable 5 660dbc6583 Regenerate computer-home baseline for the new panel layout
Tablet-width card, size toggles + close in the global header, and the
enlarged dock (56px tiles / 34px glyphs / 11px labels). Captured from a
verified-fresh server; full suite (37) green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 22:05:24 -05:00
Omar SobhandClaude Fable 5 2df933b0c0 Computer panel: header toggles, device-state widths, bigger dock
Measured panel feedback — four fidelity gaps closed:

1. Size toggles + close moved OUT of the panel card header into the
   global chat header (where the reference mounts them), beside a thin
   divider. DeviceSizeToggle restyled to 28px p-1.5 rounded-md with
   inactive at opacity-30, active at full. The panel card header now
   carries only the home/back button + title + status dot.
2. Device-state widths wired (were stuck at 400px). DevicePanel now
   manages its own complementary <aside> sized by flex per ?device=:
   phone basis-[448px] (400 card), tablet basis-[550px] (502 card),
   full basis-0 grow-[4] — fluid, fills the row beside the chat. Verified
   live: aside measures 448 / 550 / 1011 at a 1440 viewport. Wrapper
   px-3→px-6 (24px gutters); card is now w-full (width driven by aside).
3. The grow animates — transition-[flex-basis,flex-grow] with the
   signature --duration-normal / ease-app on the aside.
4. Dock glyphs enlarged: tiles size-12→size-14 (56), glyph 22→34, plus
   the 11px label under each tile.

Mount/unmount preserved via transitionend (flex-basis|flex-grow), as is
role=complementary aria-label=Computer, the device-panel-theme testid,
and the mobile full-screen overlay. p3/p4 updated to find the toggle +
close at page scope (now in the header, outside the panel region).
83 unit + 31 functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 22:03:26 -05:00
Omar SobhandClaude Fable 5 b135c114df Regenerate visual baselines against the corrected build
The previous baselines were captured against a stale next-start server
(Playwright reuseExistingServer reused a ghost on :3100 serving the
pre-fix bundle), so they encoded the broken render. Deleted and
regenerated from a verified-fresh server: chat-welcome (coral name, 2×2
pill chips, #1A1A1A composer), computer-home (real Chrome/Slack brand
tiles, coral Claw Chat glyph, frosted dock, icon size-toggle), credits,
marketing, team-orgchart. Full suite (37) green against them.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 21:34:40 -05:00
Omar SobhandClaude Fable 5 0383d8fd77 Fix interface fidelity: invalid Tailwind classes + restore lost HomeScreen
Side-by-side comparison surfaced that several restyle classes silently
generated NO CSS (confirmed against the compiled .next CSS) — invisible
to tests because unit tests assert class strings, not computed styles,
and visual baselines were captured from the broken build.

Root causes fixed:
- rounded-radius-button (51) / rounded-radius-squircle (3) generated
  nothing → every pill/button/chip was border-radius:0. Tailwind v4 maps
  --radius-button to 'rounded-button', not 'rounded-radius-button'.
  Replaced with rounded-full / rounded-[17px].
- duration-normal/fast/slow (58) generated nothing → transitions had
  easing but 0 duration (snapped). Replaced with the token-arbitrary
  form duration-(--duration-normal). (Also fixed an over-replace that
  double-wrapped var(--duration-normal) inside animate-[…] values.)
- HomeScreen.tsx was the OLD emoji version — the R3 rewrite was never
  committed. Restored: brand Chrome/Slack squircle tiles, coral
  GradientGlyph icons, 56px tiles with press-scale, and the frosted
  glass dock (bg-white/[0.07] backdrop-blur-[40px] saturate-150).

Targeted gaps from the feedback:
- raised surface --color-surface-warm-muted #1f1f1f → #1a1a1a (composer,
  chips, tabs)
- welcome chips: max-w-[422px] gap-3 → centered 2×2 grid
- composer input + rail nav labels → 14px
- DeviceSizeToggle: Full/Tablet/Phone text → lucide monitor/tablet/
  smartphone icons (aria-label keeps the radio names)
- SW cache bumped v1→v2 so redeployed clients purge the stale bundle
- LeftRail drawer: ref-during-render → prev-state pattern (lint)

Compiled CSS now emits .rounded-full, transition-duration:var(--duration-
normal), border-radius:17px, and shadow-dock-capsule. Button/Avatar unit
tests updated to the real class names. 83 unit tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 21:21:20 -05:00
Omar SobhandClaude Fable 5 889c65685e e2e harness: refuse to run when port 8080 is already serving
The Playwright backend harness now aborts if something else (e.g. the
compose stack) is already on :8080, instead of letting reuseExistingServer
silently point every journey at the wrong backend with the wrong seed —
the failure mode that surfaced mid-restyle. Also gitignore the local
data/ blob-store artifacts.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 20:32:29 -05:00
Omar SobhandClaude Fable 5 337f059afd R7: test reconciliation + visual baseline lock
- Regenerated all 7 @visual darwin baselines for the restyled surfaces
  (login, workspace home, chat welcome, computer panel, credits) and
  added two new ones: the marketing landing (full page) and the Team
  org-chart tab.
- Credits visual made deterministic: balance + the whole usage card are
  masked (testids), and the runway line always renders so the card
  height is constant — shared-backend credit spend during the full run
  no longer shifts the diff.
- Full gate sweep green end to end: 173 Rust tests + clippy/fmt clean,
  83 frontend unit + lint + typecheck, all 37 Playwright journeys
  (functional + visual), axe serious/critical at zero across every
  surface, LOC ≤1250 and no-placeholder gates.

The interface now matches the reference design system across the app,
the Computer panel, the global pages, and a new light-theme marketing
site — design system only, our brand throughout.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 20:26:10 -05:00
Omar SobhandClaude Fable 5 51677fef2d R6: responsive — mobile rail drawer + full-screen Computer overlay
- SlidePanel gains an  prop: at ≤md the open Computer panel
  becomes a fixed full-screen layer (max-md:!w-full beats the inline
  width) instead of a cramped docked rail — the measured mobile/tablet
  behavior. The screen card goes full-bleed (no radius/shadow/max-width)
  on small screens; desktop docking is unchanged.
- LeftRail collapses behind a hamburger at ≤md and slides in as a drawer
  over the content with a backdrop, auto-closing on navigation
  (render-phase route check, no effect). Desktop keeps the static 176px
  rail (the hamburger and drawer transforms are max-md-scoped, so the
  1440 desktop suite is untouched).

83 unit tests; desktop p0/p3/a11y journeys green; verified at 390px the
rail collapses to the toggle.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 20:18:31 -05:00
Omar SobhandClaude Fable 5 200bb56299 R5: marketing landing — light-theme public site at /
A new (marketing) route group with its own LIGHT layout (slate palette +
marketing coral #E95656 + periwinkle cards on near-white), the inverse of
the dark app. The middleware rewrites '/' → /marketing for logged-out
visitors (keyed on the session cookie / Clerk __session); authed users
fall through to the workspace app unchanged.

Built to the measured marketing system with OUR copy and honest claims —
NO SOC 2 / compliance badges: floating white pill nav, the 75px/800/-3px
display hero with a single coral keyword span, dual pill CTAs (coral
primary / white-bordered secondary), trust badges ('free credits',
'no credit card', 'self-hostable & air-gapped'), periwinkle feature
cards, a structural-security section (approval-gated, broker-held creds,
air-gappable — our real differentiators), an interactive FAQ accordion,
CTA band, and footer.

E2E (p7-marketing): renders for logged-out visitors, FAQ accordion works,
asserts NO SOC 2 claim, and an axe pass (coral CTAs sized to clear the
AA large-text threshold). p0-shell updated: logged-out '/' now shows
marketing with a Sign-in link to /login.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 20:13:09 -05:00
Omar SobhandClaude Fable 5 1bfbccf172 R4 frontend: global pages restyled, Team tabs, /apps page, Buy credits
Shared PageChrome (28px/600 title + muted desc + top-right action pill)
now fronts every global page. Skills → 2-col r24 Card grid with team
install counts. Credits → three-card layout (balance w/ Buy credits;
usage meter w/ runway; promo; Talk-to-sales → mailto). The Stripe Buy
credits button only mounts when /api/billing/config reports it enabled
(honest degradation) and opens a real Checkout Session.

Team page gains the three reference tabs via SegmentedTabs: Members
(restyled), Claw org chart (real /api/team/orgchart — members grouped
with the claws they manage, each a deep link into chat), and Leaderboard
(real /api/team/leaderboard — claws ranked by usage with a coral bar).

New /apps global page (workspace-wide connections via ?workspace=true):
category pills + SearchPill + 2-col rows with inline API-key connect;
Apps added to the rail nav.

Wizard restyled to the system: coral-fill white-text CTAs with the glow
shadow, coral progress bars, swatch enter animation, system inputs —
all step text/behavior preserved.

83 unit + 29 functional E2E + a11y green; contrast fixed (subtle-fg →
muted-fg on cards).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 20:04:34 -05:00
Omar SobhandClaude Fable 5 f3f08a8edd R4 backend: team org-chart + leaderboard, Stripe credits, workspace apps
- GET /api/team/orgchart — each member grouped with the claws they manage
  (agents.managed_by); GET /api/team/leaderboard — every claw ranked by
  its real usage_events rollup (credits/tokens/runs, zeros included).
  Both tested against real Postgres.
- Stripe Buy-credits (the Slack/Clerk integration pattern): [billing]
  config (stripe keys + price + webhook secret + credits_per_pack);
  POST /api/credits/checkout opens a real Checkout Session; POST
  /api/billing/stripe verifies Stripe's t=,v1= HMAC (constant-time) and
  grants one credit lot, idempotent on the session id; GET
  /api/billing/config gates the button (honest degradation when unset).
  Offline tests: signed grant + replay no-double-grant + forged-sig 400 +
  config flag. Live checkout creation deferred to a CM_LIVE_STRIPE test.
- /apps global page support: clawId now optional on connect + directory;
  absent => workspace-wide connection (app_connections.agent_id NULL) via
  new connections::list_for_workspace.
- ApiError gains a From<sqlx::Error> so inline queries use ? cleanly.

cm-api 10 test files incl. team_tabs (2) + stripe_billing (3); clippy clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 19:58:09 -05:00
Omar SobhandClaude Fable 5 67b80da695 R3: Computer panel rebuilt to the reference 'computer' system
Panel chrome (CONSOLIDATED-computer-panel-handoff): the 448px docked rail
now hosts a 400×652 r32 'screen' card with the measured wallpaper stack —
135° slate/navy base gradient tinted by the agent accent, a #000/.40
vignette, and a frosted blur(40px) white/5 overlay with grain on top.
Header carries the rose-500 status dot (home) or a back chevron (drilled),
keeping the 'Computer home' / 'Close computer' accessible names stable.

Home screen: 56px tiles with 16px-radius icon containers (Chrome/Slack
brand marks on white squircles, coral-gradient lucide glyphs elsewhere),
11px labels, active:scale-[0.92] press; the frosted dock capsule uses the
exact blur(40px) saturate(1.5) bg-white/7 r24 + inset/drop shadow with
48px black tiles.

App window choreography: opening an app zooms it out of the tapped tile
(transform-origin + --app-origin-scale captured at click time → the
app-shell-zoom-in keyframe), ~0.28s ease-app.

All 8 apps restyled to the inventory spec via shared AppShell helpers
(SubViewHeader, PanelEmptyState = muted lucide glyph + 16/600 + 12 muted):
Files 45px rows w/ folder/file/chevron glyphs; Slack segmented tabs
(active white bg / aubergine text) + cream connect gate; Skills pinned
cream 'Add Skill'; Add Apps category pills + SearchPill + coral connect;
Settings grouped r12 cards + coral radios + cream Save + lucide delete;
Browser/Routines/ClawChat lucide empty states. Every test marker text and
aria-label preserved.

83 unit + 29 functional E2E green; lucide icons throughout (emoji gone).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 19:49:49 -05:00
Omar SobhandClaude Fable 5 6e061515b8 R1+R2: shell and chat restyled to the reference system
Shell (main-chat-shell-spec): rail rebuilt around a 48px squircle claw
avatar stack with a 2px coral active ring + name beneath + online dot,
dashed 'New claw' tile, lucide-icon nav pills with the live credits
balance shown inline (coral when negative, aria-hidden so the nav link
name stays 'Credits'); 80px transparent chat header with 36px round
sessions/new-session icon buttons and the 42px coral-glow Computer
launcher; 768px centered content rail. RosterList computes the active
claw from the pathname so AgentRosterItem stays presentational.

Chat (chat-message-components): asymmetric layout — user pill #1A1A1A
radius 24/24/4 with inset white ring + dual shadow, capped 75%, vs the
bubble-less assistant message (50px squircle avatar, 15px gap, plain
14px/1.7 text); messageSlideIn entrance; 126px welcome avatar with the
24px/600/-0.6px heading (claw name in coral) and lucide-led suggestion
chips; floating 24px-radius neutral-800 composer card with the cream
Send pill; StepTrace rows restyled to the system.

Brand marks (Chrome, Slack) vendored to public/services from the bundle.
83 unit tests, full 29-journey functional E2E green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 19:41:31 -05:00
Omar SobhandClaude Fable 5 3ec1d3b829 R0: design foundations — reference tokens, motion set, lucide primitives
- globals.css @theme merged with the measured reference system
  (docs/tokens.css): hover/divider/subtle-fg colors, cream + bubble
  tokens, squircle/toggle radii, spec-verbatim shadow set (bubble, card,
  dialog, dock-tile, CTA glow, launcher glow, screen-card ambient, dock
  capsule), marketing light palette, default 0.25s ease-app transition
  tokens. Legacy aliases (--color-accent, --spacing-rail/panel-*)
  retained so existing classes keep working
- motion.css: full app-shell choreography (zoom-in/out from tile origin
  w/ --app-origin-scale, push/pop slides, cross-fade) + messageSlideIn,
  create-claw step/swatch, device-* set — keyframes verbatim from
  docs/motion.md
- lucide-react installed (ISC). New tested primitives: Button (cream/
  coral/ghost/icon/launcher variants per measured states), GradientGlyph
  (per-instance coral linearGradient stroke, the reference glyph
  technique), SegmentedTabs, Card + SectionLabel + GroupedRows,
  SearchPill; Avatar extended (squircle r17, rail/chat/welcome sizes,
  online-dot overlay)

82 unit tests; functional e2e suite green (visual baselines regenerate
in R7).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 18:07:35 -05:00
Omar SobhandClaude Fable 5 447f7039d8 Compose: production README, env knobs, first-owner bootstrap — deployed & live
Made the Docker Compose route turn-key for a real self-host, then stood
the whole stack up and drove a live chat through it.

- First-owner bootstrap (cm-auth::bootstrap_owner): a fresh local-auth
  install has no users and no signup route, so the initial Owner +
  workspace are provisioned ONCE from CLAWMATES_BOOTSTRAP_* env on first
  boot — idempotent, never clobbers an existing install (keys on 'any
  workspace exists'). Two real-Postgres tests (creates + signs in;
  second call is a no-op). Wired into server boot, guarded on a
  non-empty password
- deploy/compose/README.md: full production bring-up — services, the
  security topology, every config knob, Anthropic vs local-LLM, the
  broker-key backup, ops, and TLS/SSE proxy notes
- .env.example fleshed out (bootstrap, LLM, auth mode, OTLP); compose
  uses optional env_file so only the knobs you set are injected (unset
  options never override clawmates.toml with empty strings)
- volume-init one-shot chowns the broker's named volumes so the non-root
  scratch broker can write its socket + generated master key

Deployed locally and verified end to end: all 5 containers healthy,
broker generated its key, server bootstrapped owner@…, login + /api/user/me
work, and a real message streamed a live Anthropic response through the
gateway. Captured screenshots of login, workspace home, chat, and the
Computer panel.

166 Rust tests (+2 bootstrap).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 14:06:15 -05:00
Omar SobhandClaude Fable 5 b9fdec9173 Clerk deployment smoke: validated against a real instance, both halves
Last open item from the roadmap + post-1.0 list. Run against the live
Clerk instance closing-seasnail-39.clerk.accounts.dev.

- Backend (crates/cm-auth/tests/live_clerk.rs, CM_LIVE_CLERK=1): pulls
  REAL discovery + JWKS from the live instance, mints a REAL session JWT
  via Clerk's Backend API (create user -> open session -> session token),
  and runs it through AuthService::authenticate — verify + JIT provision
  (keyed on the real sub), duplicate-subject suppression, tamper
  rejection against the live JWKS. Decodes the instance domain from the
  publishable key; cleans up the test user after. PASSING
- Frontend: built with AUTH_MODE=clerk + real keys, next start serves
  Clerk's <SignIn /> at /login wired to the instance (instance domain +
  data-clerk attributes present in the HTML). Both halves confirmed
  end to end against production Clerk
- docs/clerk.md: documented the smoke procedure for both halves

166 Rust tests (+6 live, key-gated). Keys used via env only, never
stored — rotate them (they passed through chat).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:39:26 -05:00
Omar SobhandClaude Fable 5 e65bcd4130 Post-1.0 finale: Calico-enforced egress denial + visual-regression lock
- scripts/netpol-cluster.sh: a kind cluster with Calico (default CNI
  disabled) — the only way to PROVE the §15 default-deny NetworkPolicy,
  since kindnet accepts the object but never enforces it. New live test
  on that cluster: outbound connect to 1.1.1.1 dropped, DNS egress
  dropped, while API-server exec keeps working (not pod network).
  Kernel-level enforcement of the sandbox egress claim, demonstrated
- K8sDriver::connect_with_context: pin a kubeconfig context instead of
  ambient. The whole k8s suite now pins its cluster explicitly — the
  netpol cluster's creation had silently switched the current context
  and stranded the seccomp test on the wrong cluster (fixed and made
  impossible to recur)
- CI: netpol-cluster up + calico egress test in the sandbox-k8s job
- Visual-regression lock (plan P6): @visual Playwright spec with
  animation-disabled, masked-dynamic-region screenshots of login,
  workspace home, chat welcome, computer panel, credits; darwin
  baselines committed (5 PNGs); CI excludes @visual until linux
  baselines are generated there. Full local suite: 33 journeys

165 Rust tests + 5 live kind tests (2 clusters) + 33 journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:30:10 -05:00
Omar SobhandClaude Fable 5 5407111a89 Live Anthropic validation — and the platform-breaking bug it caught
Running the opt-in live suite (CM_LIVE_LLM=1 + ANTHROPIC_API_KEY) against
the real API immediately surfaced a launch blocker: Anthropic (and
OpenAI) restrict tool names to ^[a-zA-Z0-9_-]{1,128}$ — our ENTIRE
registry uses dotted names (clock.now, email.send, shell.exec, ...).
The scripted provider never enforced the pattern, so every real-model
deployment would have 400'd on the first tool call.

- Fix at the provider boundary, where it belongs: wire_tool_name /
  internal_tool_name codec (dots <-> __) applied in BOTH HTTP providers
  at all three sites (tools list, assistant tool_use echo, inbound
  tool_use decode). Internal naming (DB step rows, scenarios, UI traces)
  unchanged. Offline unit test round-trips every registry name through
  the wire pattern
- New live tests, all passing against api.anthropic.com (Haiku 4.5):
  - provider tool ROUND TRIP: real ToolUse arrives, ToolResult ships
    back exactly as a checkpoint would reassemble it, model completes,
    real usage events on the wire
  - full runtime loop: real model calls clock.now, run completes, REAL
    token usage metered, credits decremented
  - the #1-risk validation: a real model's email.send intercepted ->
    suspended -> approved -> checkpoint RESUMED against the live API ->
    completed -> outbox exactly 1 (checkpoint/resume fidelity end to end)
- Stray TC_OPENAI_COMPAT_* envs renamed to CM_OPENAI_COMPAT_*

No credentials stored anywhere; the key was passed via env only.

163 Rust tests (+5 live, key-gated).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 12:55:51 -05: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 05e9612688 Post-1.0: Localhost seccomp on K8s, warm sandbox pool, server HPA
- K8sDriver.with_localhost_seccomp(profile): sandbox pods run under the
  STRICT allowlist instead of the runtime default. Proven live on kind:
  the harness installs the profile onto the node, a pod runs ordinary
  work as uid 10001, and unshare is kernel-denied inside the pod — the
  same probe the Docker suite uses, now passing on both targets
- Helm: sandbox.seccomp=localhost renders a DaemonSet that installs the
  chart-shipped profile into /var/lib/kubelet/seccomp on every node
  (ConfigMap + hostPath); ci/check-helm.sh enforces the chart copy stays
  byte-identical to images/seccomp/agent-profile.json and asserts the
  hardened render (DaemonSet + profile + HPA)
- server HPA (autoscaling/v2, CPU target) behind
  server.autoscaling.enabled
- SandboxManager.warm(n): a background warmer keeps n pre-provisioned
  sandboxes ready so an agent's first exec skips container startup;
  unhealthy pool entries are discarded, reuse never drains the pool,
  shutdown destroys assigned AND pooled. [sandbox] warm_pool config
  (default 0). Real-Docker test: prefill -> assign -> refill -> reuse ->
  clean shutdown

160 Rust tests + 4 live kind tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 11:15:49 -05:00
Omar SobhandClaude Fable 5 a26939d7bc Plan hardening: socket-proxy allowlist, gateway load test, offline verify
- The compose server NEVER sees the raw Docker socket (plan risk #5):
  tecnativa/docker-socket-proxy on an isolated engine_net with exactly
  CONTAINERS/POST/EXEC/DELETE/VERSION allowed; server reaches it via
  DOCKER_HOST. DockerDriver honors DOCKER_HOST (connect_to). Proven by a
  REAL proxy test: full sandbox lifecycle works through the allowlist
  while /networks, /secrets, and /images all 403 — the blast-radius cap
  if the server is ever owned. (This also fixes compose deployments,
  where sandbox provisioning previously had no engine access at all.)
- Gateway load test (plan P6): 40 concurrent SSE streams against one
  server — every run completes with the full §13 event vocabulary,
  every journal strictly monotonic, every resumeFrom=0 replay byte-equal
  to its live stream
- release.yml: SBOMs (syft, spdx-json) for all four images shipped
  INSIDE the signed bundle; final verification now runs in a
  --network none container — proving the customer's verify path needs
  no internet, not just claiming it

159 Rust tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 10:50:55 -05:00
Omar SobhandClaude Fable 5 ceca21ca79 Clerk frontend integration: one image, runtime-switched identity
- src/lib/auth/bearer.ts is the single identity dispatch for both
  server-side token consumers (RSC apiFetch and the /api proxy route):
  local -> httpOnly tc_session cookie; clerk -> Clerk getToken() session
  JWT. The Clerk SDK is imported lazily, so the air-gapped/local path
  never loads it
- Runtime env (AUTH_MODE / CLERK_PUBLISHABLE_KEY / CLERK_SECRET_KEY),
  deliberately NOT build-time NEXT_PUBLIC_*: the same standalone image
  serves both deployment targets
- Conditional <ClerkProvider> in the root layout (publishableKey passed
  at render from runtime env); /login renders Clerk's <SignIn /> in
  clerk mode and the local form otherwise; proxy.ts middleware delegates
  to clerkMiddleware() only when active
- Helm: frontend deployment injects the Clerk keys from a Secret when
  auth.mode=clerk
- mode.ts unit-tested (default local, exact-match clerk, loud failure
  without the publishable key); the local path stays proven by all 29
  journeys; the Clerk branch is thin delegation to the SDK, exercised in
  deployment smoke per docs/clerk.md

157 Rust + 68 frontend tests + 29 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 10:36:51 -05:00
Omar SobhandClaude Fable 5 cbc8d35a2e Clerk authentication: hosted-identity session JWTs as a first-class mode
- tc-auth JwtVerifier: OIDC discovery -> JWKS, RS256 with the issuer
  pinned, 5s leeway (the crate's default 60s would double the life of
  Clerk's 60s session tokens), key cache with one refresh on unknown kid
  (Clerk rotates). Serves auth.mode = clerk AND generic oidc — a Clerk
  instance IS an OIDC issuer, so one verifier covers both
- AuthService.authenticate dispatches: JWT-shaped bearers take the
  hosted-identity path, everything else stays a local opaque session.
  External users JIT-provision keyed by the stable sub claim
  (users.auth_subject, unique partial index in migration 0007); an
  existing local account with the same email is LINKED, not duplicated;
  role tracks the issuer claim every request (org:admin -> Owner)
- Config auth.mode = "clerk" (requires issuer_url; validated), server
  pins the issuer at boot, Helm values/configmap accept mode=clerk
- Tests with REAL crypto, no mocks: fresh RSA keypairs, a live local
  issuer publishing real discovery + JWKS docs, Clerk-shaped tokens —
  JIT + role mapping, repeat-subject no-dup, expired refused (leeway
  regression), wrong-key forgery refused, foreign issuer refused, and
  the full router round trip with Authorization: Bearer <session JWT>
- docs/clerk.md: dashboard session-token customization (email + org
  role claims), config, @clerk/nextjs getToken() wiring, what CI proves

157 Rust + 63 frontend tests + 29 journeys. Air-gapped installs keep
local auth — Clerk is a cloud-only alternative, not a replacement.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 10:29:27 -05:00
Omar SobhandClaude Fable 5 ace66d7ffb P6 complete: PWA, route motion, dex browser-flow OAuth, release pipeline
- PWA (§16): hand-rolled 60-line service worker (network-first pages with
  offline fallback, cache-first hashed statics, /api NEVER touched — SSE
  and approvals stay live), app manifest with §2 identity, stdlib-
  generated coral claw icons, prod-only registration. E2E asserts
  manifest, real PNG icons, an ACTIVATED service worker, and the /api
  bypass. (Serwist was tried and dropped: its webpack plugin fights
  Next 16's Turbopack builds; sixty lines we own beat a plugin we fight.)
- Route motion (§3): (workspace) template re-mounts per navigation with a
  quiet fade-rise, zeroed under prefers-reduced-motion. The a11y sweep
  now settles running animations before scanning — axe was reading
  mid-fade opacity as contrast failures
- OAuth browser flow vs REAL dex: the e2e harness boots dexidp/dex with
  static client + password; the journey drives the actual dex login form
  from /api/apps/oauth/start through the callback 303 and asserts the
  app reads connected (closing the P4 deferral honestly)
- release.yml: tag-triggered — builds all four images + postgres, saves
  tarballs, assembles the SIGNED air-gapped bundle (compose, config,
  migrations, seccomp profile, installer, bundler binary), derives the
  public key via the new Could not find command "pubkey". subcommand (tested), verifies
  the bundle customer-style with the public half only, attaches tarball
  + public key to the GitHub release

153 Rust + 63 frontend tests + 29 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 09:57:21 -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 7392ce1d08 P6: shell.exec — agents execute code in their real hardened sandbox
- SandboxManager (tc-runtime): one container per agent, provisioned
  lazily on first use, reused for the manager's lifetime, replaced
  transparently if dead, destroyed on shutdown
- shell.exec tool: sh -lc inside the agent's sandbox; stdout/stderr/
  exit_code return to the model as the step output. No external effects
  declared — the sandbox boundary (uid 10001, no caps, seccomp
  allowlist, read-only rootfs, zero egress) is the §15 control here,
  not an approval gate
- RuntimeConfig.sandboxes (+ with_sandboxes builder); [sandbox] config
  {image, enabled}; the server connects the Docker driver at boot and
  tolerates an absent engine (shell.exec reports it per-call)
- Tests with the REAL DockerDriver: a scripted run executes two
  commands — output proves uid 10001 from inside, and /home/agent state
  written by the first call is read by the second (same sandbox); a
  deployment without a sandbox runtime records honest error steps and
  the run still completes

151 Rust tests + 27 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 09:25:38 -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 70ec39f696 P6: S3 blob store, Helm chart, air-gapped installer verify loop
- S3BlobStore (object_store, path-style) behind the same BlobStore trait,
  tested against a REAL MinIO container (round trip, overwrite, NotFound
  on get and delete, nested keys); [storage] backend=local|s3 config with
  validation + server-side selection (S3 creds via env overlay)
- Helm chart: server pod with the secret broker as a SIDECAR sharing a
  private emptyDir unix socket (no network hop carries credentials),
  frontend, optional local PVC vs S3, OIDC/oauth values, unbuffered-SSE
  ingress annotations, NetworkPolicies (frontend->server only), hardened
  securityContexts; ci/check-helm.sh lints AND asserts the rendered
  topology properties
- deploy/airgapped/install.sh: offline signature+checksum verification via
  the bundled teamclaw-bundler BEFORE any docker load; --verify-only mode;
  ci/test-install.sh rehearses clean/tampered/wrong-key paths with the
  real binary
- CI: helm gate + installer rehearsal wired in

149 Rust tests; helm lint + rendered assertions green; installer
verify-path rehearsal green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 08:19:43 -05:00
Omar SobhandClaude Fable 5 ccf96053e6 P6: security soak, axe a11y sign-off, signed air-gapped bundle tooling
- 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]>
2026-06-10 07:36:15 -05:00
Omar SobhandClaude Fable 5 a8efada690 P5 exit: usage metering, credit billing, promo codes, 3-step wizard
- 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]>
2026-06-10 07:19:34 -05:00
Omar SobhandClaude Fable 5 91327e3618 P4 complete: OAuth authorization-code flow, MCP-OAuth, AddApps connects
- migration 0005 oauth_states: one-time states (10-min TTL), consumed by a
  CAS DELETE on callback — replays and forgeries both 404
- POST /api/apps/oauth/start: OIDC discovery on the configured issuer (or
  the custom MCP issuer for authType=mcp_oauth), state row, authorize URL
- GET /api/apps/oauth/callback: code exchanged at the REAL token endpoint
  (client id+secret form POST); the access token goes straight to the
  broker (test proves it never appears unencrypted in Postgres); connection
  row + audit; redirects to the claw's Add Apps panel
- [oauth] config (issuer/client/redirect_base) wired through AppState
- Tests against a real local IdP server (discovery + validating token
  endpoint): full round trip, broker-held token, replay/forged state
  refused, bad code fails exchange, mcp_oauth uses the custom issuer while
  plain oauth refuses without a configured IdP
- AddAppsApp: live connection badges + inline API-key connect per app
  (E2E: connect Notion by key from the directory)

136 Rust + 63 frontend tests + 21 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 06:50:41 -05:00
Omar SobhandClaude Fable 5 6dbdd20ee0 P4: Slack inbound @mention — broker-verified signatures drive real runs
- Broker op VerifySlackSignature: v0 HMAC-SHA256 computed INSIDE the broker
  (constant-time compare); the signing secret never crosses the socket.
  Slack secrets are one JSON credential {bot_token, signing_secret}; the
  broker extracts the right field per operation
- Public POST /api/slack/events: signature verified against connected slack
  connections via the broker; forged signatures 401; url_verification
  handshake echoed only when signed; app_mention starts a real run in the
  agent's dedicated '💬 Slack' session — and the agent's reply is itself a
  gated outbound post
- SlackApp Connection tab captures bot token + signing secret
- Integration test: forged 401, signed challenge, signed mention -> run ->
  slack.post pending in the approval queue
- E2E: full loop — connect, gated outbound (sink empty -> exactly one post),
  then a node-crypto-signed mention -> approval card -> approve -> 'On it!'
  lands in the sink

134 Rust + 63 frontend tests + 21 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 06:40:04 -05:00
Omar SobhandClaude Fable 5 000b9b3a4b P4 core: broker-held app connections + gated, broker-executed Slack posting
- app_connections repo; POST /api/apps/connect (keys/basic): the credential
  goes to the secret broker over its socket and only the encrypted ref lands
  in the row; disconnect endpoint; /api/apps directory merged with live
  connection status; audit rows for connect/disconnect
- Broker protocol: InvokeHttp carries a JSON body
- slack.post tool (SendsExternally -> gated): marked broker_executed — the
  runtime skips its own grant consumption and the BROKER independently
  verifies + consumes the single-use grant, then calls Slack with the bot
  token injected; the runtime never sees the credential
- Config: [broker] socket_path + [slack] base_url; e2e harness spawns the
  real teamclaw-broker daemon and the server hosts an e2e-only /__slack sink
- SlackApp: Connection tab stores the token via the broker; connected state
- Integration test: blocked while pending -> approved -> sink received
  exactly one post with 'Bearer xoxb-test-token' -> grant replay refused
- E2E journey: connect Slack in the panel -> gated post card with preview ->
  sink empty while pending -> approve -> exactly one post, queue clear

133 Rust + 63 frontend tests + 21 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 05:51:19 -05:00
Omar SobhandClaude Fable 5 1fd2c287f1 P3 exit: Computer panel with all 8 apps, themed and deep-linkable
- DevicePanel in the 448px SlidePanel: ?app= routes (home + 8 sub-apps via
  dynamic imports), ?device=full|tablet|phone size toggle, per-agent
  accent-derived wallpaper theme + feTurbulence grain, glassy dock + grid
  home screen, Computer button in the chat header
- Apps: Files (3 drives, real listings), Skills (installed + add from
  library), Routines (list/refresh/empty state), Claw Chat (threads +
  sensitive badge + detail), Settings (push/pop nav: edit profile PATCHes
  the system prompt, Other-Claws access toggle PUTs the policy, confirmed
  destructive delete), Slack (§7.3 pre-connect gate), Add Apps (live
  /api/apps directory + search), Browser (chrome + spec'd empty state)
- /skills Skill Library page + nav entry; curated /api/apps directory
  endpoint; e2e seed gains a catalog skill
- P3 exit E2E (6 journeys): themed home screen + device toggle in URL,
  agent-written file appears in Files, agent-scheduled routine appears in
  Routines, system-prompt edit persists across reload, deep-link cold-load
  of ?app=settings&device=full, every app reachable, library installs

132 Rust + 63 frontend tests + 20 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 05:37:18 -05:00
Omar SobhandClaude Fable 5 67f918439c P3 backend: files, skills, routines, claw chat with LIVE taint plumbing
- tc-files: BlobStore trait + LocalBlobStore (traversal-proof keys); wired
  through Runtime (config storage.data_dir in deployments)
- File tools: files.write/files.list (workspace-internal) + files.delete
  (gated FileDeletion — tested: file survives pending, gone after approve);
  GET /api/openclaw/files + /api/shared-drive/files (drive/agent scoped)
- Skills: catalog/library + idempotent install with counter, uninstall;
  GET /api/skills[?clawId=], POST install/uninstall
- tc-scheduler: croner cron math (clock-controlled tests), SKIP LOCKED
  claim-and-advance firing REAL runs into dedicated ' name' sessions
  (reused, exactly-once), paused routines skipped; routines API + agent
  tool routine.schedule; loop spawned in server
- Claw chat: 1:1 threads, chat.send enforcing the target's Other-Claws
  policy, chat.inbox whose output carries inter_agent taint; the run loop
  now ACCUMULATES taint from tool outputs into LoopState, classifies with
  it, and stamps steps + approvals — a poisoned inbox followed by
  email.send produces an approval whose taint_sources says inter_agent
- ScriptedProvider scenario selection now keys on the most recent marker
  (session history kept earlier markers alive)

132 Rust tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 05:28:50 -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 de38449b41 P2 BLOCKING exit green: approval interception chain end-to-end
- tc-tools: Effect declarations -> §15 GatedCategory mapping, deny-by-default
  external reach, taint invariant property-tested (tainted external effects
  are NEVER auto-allowed)
- tc-safety: pending approvals with exact payload+preview, CAS decide with
  audit + single-use grant in one tx, checkpoint suspend/load, exclusive
  resume claim, expiry sweep, decided-unresumed work queue (migration 0004
  adds the outbox the gated email.send tool writes)
- tc-runtime: resumable LoopState checkpointed to agent_runs; gated tool ->
  approval row -> approval_required/run_suspended events -> suspend; resume
  consumes the grant BEFORE executing (spent grant = no execution), rejection
  feeds a structured refusal in-band; durable resume sweeper; continuous
  journal seq across suspension (tested). ContentPart::Text became a struct
  variant — internally-tagged newtype primitives don't serialize
- tc-api: GET/decide approvals endpoints (409 double-decide, tenant
  isolation), decision triggers in-process resume; full chain proven over
  HTTP incl. gateway resumeFrom continuation
- frontend: approval_required/run_suspended events, suspended reply state,
  inline ApprovalCard (§10: summary, category, exact payload preview,
  approve/reject -> decide + stream re-attach), /approvals queue page, nav
- E2E (14 journeys, workers:1 to serialize the shared backend): gated email
  blocks with disabled composer -> approve -> continuation + ✓ step + reload
  replay; reject -> ✗ step, nothing executed; queue page decides pending

106 Rust + 61 frontend tests + 14 Playwright journeys green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 04:58:47 -05:00
Omar SobhandClaude Fable 5 9f9f507c15 P1 exit: chat UI, sessions column, agent create — 11 E2E journeys green
- Stream layer: incremental SSE parser tested against bytes captured from
  the REAL gateway (chunking-independent, UTF-8-safe, heartbeat-tolerant);
  Zod gateway event schemas; pure transcript reducer (optimistic send,
  delta streaming, step traces, resume dedupe, error states)
- Same-origin /api proxy route: httpOnly cookie -> bearer, unbuffered SSE
  passthrough; NuqsAdapter in root layout
- Chat workspace: route /claws/{id}/chat/{key} (RSC history + settings),
  WelcomeState with suggested prompts, MessageList (right user bubbles,
  left agent messages, blink caret), collapsible StepTrace, Composer
  (Enter sends, Shift+Enter newline)
- SessionsColumn in 208px SlidePanel (?sessions=1): search, relative
  times, active coral border, new session; /claws/{id} resumes latest or
  opens fresh; minimal create-claw form; rail + button and roster links
- P1 exit E2E: scripted reply streams, tool step trace survives reload,
  separate transcripts across sessions with column switching, create claw
  and chat immediately

83 Rust + 56 frontend unit/component tests + 11 Playwright journeys.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-09 23:29:27 -05:00
Omar SobhandClaude Fable 5 32008c9ef0 P1 backend: chat persistence, tc-llm providers, runtime loop, gateway SSE
- 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]>
2026-06-09 23:16:06 -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
Omar SobhandClaude Fable 5 172a3c8fed P0: Next.js shell — tokens, motion, SlidePanel, LeftRail, auth plumbing
- Tailwind v4 @theme block encoding all spec §2 tokens; full §3 keyframe
  inventory with prefers-reduced-motion handling; Geist vendored (air-gap)
- SlidePanel width-animation primitive (component-tested: exit-transition
  unmount, fixed-width inner content, a11y region semantics)
- session-key.ts mirroring the Rust codec + URL-param encoding; nuqs
  panel-params with spec ?sessions=1 flag shape and routines->scheduled alias
- Zod-typed API client; httpOnly cookie session bridge (/auth/session);
  login page; (workspace) layout with LeftRail roster/nav/user; Team and
  Credits pages on real endpoints (+ GET /api/team/members in tc-api)
- Vitest + Testing Library harness (26 tests); ESLint max-lines 1250 +
  no-warning-comments mirroring the CI gates

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-09 22:43:50 -05:00
Omar SobhandClaude Fable 5 c4349bf292 P0: tc-auth local sessions, tc-api P0 endpoints, teamclaw-server binary
- tc-auth: argon2id passwords, hashed opaque bearer tokens in auth_sessions
  (migration 0002), anti-enumeration login errors, redacted token Debug
- tc-api: axum router with /healthz, /api/auth/login|logout, /api/user/me,
  /api/team/{claws,credits,permissions}; Authed bearer extractor + RBAC
  permission derivation; integration-tested over real TCP vs real Postgres
- teamclaw-server: config -> pool -> self-migrate -> serve

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-09 22:30:33 -05:00
Omar SobhandClaude Fable 5 0afb359183 P0: workspace scaffold, CI gates, tc-domain, tc-config, tc-db vs real Postgres
- Cargo workspace with 1250-line and no-placeholder CI gates wired first
- tc-domain: id newtypes, SessionKey codec (proptest round-trip), Role,
  GatedCategory (spec §15), AccessPolicy, core entities
- tc-config: figment TOML+env config, DeployTarget/provider/auth selection
  with semantic validation
- migrations/0001: full spec §14 schema incl. DB-enforced append-only audit_log
- tc-db: compile-time-checked sqlx repos (workspaces, users, agents+policies,
  credits, audit) with committed .sqlx offline metadata
- tc-testkit: per-test real-Postgres databases (testcontainers or
  TC_TEST_DATABASE_URL), embedded migrations

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