Commit Graph
160 Commits
Author SHA1 Message Date
Omar Sobh 04f8302871 cm-db: threads — drop unnecessary .iter().copied() on participants slice
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 1m33s
ci / rust (push) Failing after 2m55s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m55s
Inherited from the a2a merge. Iterating `&[AgentId]` directly yields
`&AgentId`; `AgentId::as_uuid(&self)` works fine on that, so the extra
`.iter().copied()` was pure noise and tripped clippy's
`unnecessary_to_owned` under `-D warnings`.
2026-07-05 19:04:59 -07:00
Omar Sobh 15cffba2d7 cm-runtime: don't drain the warm pool on a flaky sandbox health check
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 36s
ci / frontend (push) Successful in 1m7s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m27s
`driver.health()` can return `Err(_)` on transient docker daemon hiccups
(connection reset, timeout mid-inspect, daemon busy). exec() used
`.unwrap_or(false)` which treated Err as "dead" and:
  1. Destroyed the agent's assigned sandbox.
  2. Fell through to the warm pool and popped one, draining it by 1.
  3. Provisioned a fresh assigned sandbox on top.

Under CI load — where several test processes hit the docker daemon
concurrently — this fired as the warm_pool.rs:72 "reuse must not drain
the pool" flake. The test asserts pool_size == 2 after a reuse; when
health flaked, the reuse turned into a drain-and-refill and the assert
raced the warmer.

Match only a CONFIRMED `Ok(false)` (container dead or 404). On Err,
assume alive; if it really is dead, the subsequent exec surfaces the
error with a clear message instead of silent sandbox destruction and
warm-pool drainage.
2026-07-05 19:00:19 -07:00
Omar Sobh ebb5b5780b chore: cargo fmt --all — clean up a2a merge's fmt violations
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 49s
ci / frontend (push) Successful in 52s
ci / publish (push) Successful in 3m6s
ci / e2e (push) Has been skipped
The a2a merge (d0d8e7f) landed with a handful of pre-existing rustfmt
diffs that were failing `cargo fmt --all --check` in CI. Pure whitespace
reformatting from `cargo fmt --all`; no semantic changes. Files touched:
mcp_door.rs, quota.rs, routes/a2a.rs, routes/world.rs, runtime_provision.rs,
chat_repos.rs test, and tools/chat.rs.
2026-07-05 18:54:15 -07:00
Omar Sobh 3b382d659a broker: make Postgres pool size configurable, bump default 5 → 8
Under concurrent door executions the broker was queueing on its 5-connection
pool, adding latency to tool calls that fanned out from the same run. Bump
the default to 8 (one connection per concurrent door before queueing) and
expose CLAWMATES_BROKER_POOL_SIZE so the fleet can tune it up as the load
grows without a rebuild.
2026-07-05 18:49:17 -07:00
Omar Sobh e2c82d9a93 cm-api: fleet — bound PTY sink channels so a stalled browser can't OOM the hub
pty_sinks was an unbounded mpsc, which means a runaway PTY (say
`cat /var/log/huge`) with a browser that stopped consuming would
accumulate megabytes in the hub's memory indefinitely. Switching to a
bounded mpsc::Sender means the send fails when the buffer is full; the
NodeHub then closes that session cleanly instead of holding output
forever. Signal sinks stay unbounded — they carry small control frames.
2026-07-05 18:49:09 -07:00
Omar Sobh d2b1f0569e cm-api: quota — enforce max_active_runs on run enqueue
Adds a Quota.max_active_runs ceiling (queued + running topology runs at
once, per workspace) to stop a single workspace flooding the shared queue.
Free tier: 10, Pro: 25, Team: 100. Enforced at every /run enqueue site:
run_org, run_company, run_team, and the webhook trigger. Webhooks return
429 rather than 402 so external callers can back off — the guard is what
stops a leaked webhook token from being weaponized into a queue flood.

A single team run also spawns a tier-tree of children, so the practical
cap grows with the topology — this counts the outer runs, not every step.
2026-07-05 18:49:03 -07:00
Omar Sobh 51d501e36e clawmates-node: bound write.send with 10s timeout so half-open TCP can't wedge daemon
Under a half-open TCP (server side closed, client OS still buffering
writes), the daemon's write.send() inside the tokio::select! branch
blocks forever. tokio::select does not preempt a running future, so
the whole loop freezes — idle_tick never gets to check last_rx, no
'channel ended' log ever fires, and the daemon silently spins on a
dead socket for hours.

Observed on architect Jul 5 2026: daemon connected at 12:59:13,
heartbeats worked for ~2.5 min, then went silent. TCP session showed
ESTABLISHED on the node side, no ESTABLISHED on the gateway side.
Restarting the daemon 'fixed' it — but it re-hung within minutes.

Fix: wrap both write.send() call sites in a tokio::time::timeout of
WRITE_DEADLINE=10s. If a send stalls past that, we log and return Ok()
to trigger the main-loop reconnect. Short enough that it fires long
before the 40s read-idle would (which was our only escape hatch and
never triggered because the loop was frozen).
2026-07-05 18:47:39 -07:00
Omar Sobh d0d8e7fd40 Merge feat/a2a-rooms-delegation-ingress into main
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 24s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 2m41s
Brings a2a rooms, delegation, and A2A ingress work back into main. Prod's
DB has migrations 0026 (group_rooms) and 0027 (a2a) applied from an
earlier hand-tagged fleet21 build cut from this branch, but main never got
them — so the CI-built server image from main refused to start against
prod's DB with "migration 26 was previously applied but is missing".
Landing the branch closes that gap: main + prod DB now share the same
migration state, so images built from main can safely roll onto gw-04.

Included:
- 0026_group_rooms.sql / 0027_a2a.sql — align main with prod's schema
- cm-api routes/a2a.rs + mcp_door.rs updates — A2A ingress and MCP door
- cm-runtime tools/delegate.rs + tools/chat.rs — delegation + N-way rooms
- frontend TeamObserver + FleetPanels + AgentObserver updates
- taxonomy.ts — delegation + A2A signals in the live world feed

Not included (still WIP on the local checkout):
- 0028_backfill_on_delete.sql / 0029_hot_query_indexes.sql
- broker pool + team-run quota changes
- Dashboard.tsx UI rename (Large World → Visualizations)
- Node write.send timeout (aaab663) — separate concern

The SSE resume fix (202e853) is preserved by auto-merge — approvals.rs
still awaits resume_run inline so the channel is ready before reply.
2026-07-05 17:20:29 -07:00
Omar Sobh 202e8535f0 fix(approvals): await resume_run so channel is ready before response returns
ci / rust (push) Failing after 3m25s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped
ci / gates (push) Successful in 33s
ci / frontend (push) Successful in 1m33s
The `decide()` helper (used by both approve and reject) wrapped
`runtime.resume_run(ready).await` inside `tokio::spawn`, so the handler
returned 200 to the client before the resume had even started. When a
client (or test) immediately reconnected to /api/gateway with resumeFrom,
the run's broadcast channel didn't exist yet — `runtime.subscribe(run_id)`
returned None, the journal was still empty (no new events yet), and the
SSE stream closed with zero events. Symptom: approvals_api tests flaky in
CI (`unwrap on None` at line 282 of reject_over_http_executes_nothing,
sometimes `missing step_finished` on the accept path).

resume_run's *own* awaited portion is only the setup — claim_resume,
checkpoint load, and open_channel. It internally spawns the long-running
multi-step work. Awaiting it inline means we wait milliseconds for setup,
then return. By the time the client reconnects, the channel exists, so
subscribe() finds it and live-tail works. The sweeper still handles the
crash-between-decision-and-resume case for durability.
2026-07-05 09:40:20 -07:00
Omar SobhandClaude Opus 4.8 85b0e1ac33 feat(observe): surface delegation + A2A in the live world feed via audit poll
Edge-initiated inter-agent events (gated delegation, A2A ingress) bypass the
run loop, so the world SSE now polls the append-only audit log (cursor on the
BIGINT id, seeded to max on first pass) and emits:
  - delegation.invoked -> agent.delegate {fromAgentId,toAgentId,toName,task}
  - a2a.invoked        -> a2a.invoked   {agentId}
TeamObserver renders agent.delegate as an A->B handoff in the team timeline;
adds the agent.delegate taxonomy type. Reliable live (the synthesized-run path
never streamed — active_runs + first-sight cursor jump skip it).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-29 05:55:49 -07:00
Omar SobhandClaude Opus 4.8 7589f62aca feat(a2a): External access panel + GET /api/a2a/settings
Adds GET /api/a2a/settings (enabled + publicBaseUrl) and an A2ASection in the
Fleet overview: enable/disable A2A for the workspace, mint/list/revoke external
bearer tokens (token shown once), and the discovery URL. Claw/skill publishing
is configured per-claw (follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-28 16:14:22 -07:00
Omar SobhandClaude Opus 4.8 cbfa0ff24f feat: agent-to-agent platform on ZeroClaw 0.8.2 — rooms, delegation, A2A ingress
Builds on the v0.8.2 runtime. Four workstreams, all behind the §15 MCP door:

- Group rooms (Phase 1): migration 0026; N-way threads repo with a DM/room
  count-guard; chat.send {room} + room.create/invite/leave tools; RoomMessage
  -> room.message SSE; /api/claw-chat/rooms* APIs; Observer room badge.
- Per-claw door identity: door caller_agent resolves the X-ZeroClaw-Agent
  header (set by the fork) to the specific claw, falling back to roster[0].
- Gated delegation bridge (Phase 3): clawmates__delegate door tool drives a
  sibling via the existing /ws/chat ZeroClawDriveExecutor (not A2A); self-deny,
  per-workspace hourly budget, audit trail, untrusted-banner result. Native
  in-daemon delegation stays off (it would bypass the door).
- A2A tenant ingress (Phase 2): migration 0027 (workspace_a2a + a2a_tokens);
  runtime_provision enable_a2a_server/publish_claw; routes/a2a.rs tenant-aware
  proxy (per-workspace tokens, injected internal bearer, daemon stays internal,
  cards URL-rewritten to the cm-api edge); a2a.invoked taxonomy.

Tests: cm-db room repos, cm-runtime chat tools, door units. sqlx cache updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-28 16:11:01 -07:00
Omar SobhandClaude Opus 4.8 1a531e91cd Fleet tools: add Rust version card (probe rustc + nightly latest + rustup update)
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 24s
ci / rust (push) Failing after 2m39s
ci / e2e (push) Has been skipped
Adds Rust to the per-node dev-tool cards: daemon probes rustc → reports 'rust';
nightly checker fetches latest stable from GitHub rust-lang/rust; GET endpoint maps
rust→Rust (after docker); one-click update runs 'rustup update stable'. Frontend is
data-driven (no change). $HOME/.cargo/bin added to probe candidate dirs.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-27 13:04:16 -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 Opus 4.8 d9ee9f5328 Agents page: collapse anatomy into icon cards → pretty editable modal
ci / rust (push) Failing after 8s
ci / frontend (push) Successful in 25s
ci / e2e (push) Has been skipped
ci / gates (push) Successful in 7s
The agent command center's BRAIN+SURFACE columns are now a grid of compact icon
cards (system prompt / how it operates / personality / skills / capabilities /
tools / memory / safety). Clicking a card opens a modal with the full content,
nicely formatted (markdown/persona/tags), editable for the four brain text files
(system_prompt, agent_md, persona, skills_md) and saved back to the .brain via
PATCH /api/claws/{id}/brain. Capabilities/tools/memory/safety render read-only
(tools keeps its Add affordance). LIVE column unchanged.

- frontend: new AnatomyGrid (icon cards + SectionModal); ClawCommandCenter swaps the
  two verbose columns for it; skills_md added to RawBrain types.
- backend: cm-brain skills_md() getter + skills_md in ClawBrainResponse so the skills
  editor pre-fills (avoids blank-overwrite).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-27 11:25:20 -07:00
Omar SobhandClaude Opus 4.8 66c4a80985 Agents: read-only agent-to-agent observer in the chat card + live agent.message
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 12s
ci / frontend (push) Successful in 26s
ci / e2e (push) Has been skipped
A new "Observe" button (Eye) sits next to "+ New" in the agent chat header; clicking
flips the card into a read-only observer of that agent's conversations with other
agents, updating live as messages happen.

- frontend: AgentObserver (history from /api/claw-chat/* + live agent.message overlay
  filtered to the agent, read-only banner, no composer); ClawChatSection toggle + flip.
- backend: emit a live agent.message run-event when chat.send succeeds — events.rs
  AgentMessage variant, chat.send returns to_id, runtime emits in both tool paths,
  world.rs normalizes agent_message → agent.message SSE. No migration, no new table.

Roadmap (not built): group/multi-party rooms; A2A protocol (a2a-rs) adoption.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-27 09:57:29 -07:00
Omar SobhandClaude Opus 4.8 b82a71d4ea Fleet tools: 8s probe cap + post-update refresh retries (updates now reflect)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 7s
ci / frontend (push) Successful in 24s
ci / e2e (push) Has been skipped
After 'claude update' replaces the binary, the daemon's re-probe ran the fresh
binary which macOS Gatekeeper re-verifies (>2s) — the 2s probe cap missed the new
version, so the UI didn't refresh (update worked but looked stale). Bump the cap to
8s; frontend polls the tools endpoint a few times post-update to catch the re-probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-27 06:55:25 -07:00
Omar SobhandClaude Opus 4.8 ed339c2121 Fleet tools: one-click per-node update (Phase 2)
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 7s
ci / frontend (push) Successful in 23s
ci / e2e (push) Has been skipped
The ↑ badge on each tool card is now a button: confirm → POST
/api/nodes/{id}/tools/{tool}/update → daemon runs the tool's own updater + re-probes.

- daemon: tool_update op (spawned task so the 170s update can't stall the WS loop;
  re-probes + re-sends node_tools after). Fixed command allow-list (no arbitrary
  shell): claude/glm → `claude update`; kimi → `uv tool upgrade kimi-cli`; ollama →
  brew upgrade (mac) / install.sh (linux); else unsupported. 4KB output cap.
- cm-api: call_timeout/request_timeout (long ops); POST .../tools/{tool}/update
  (workspace-scoped, allow-list) → {ok,output}.
- frontend: ↑latest becomes an Update button → confirm → spinner → refresh/err.

Note: claude/kimi/glm are user-space (no sudo); ollama on Linux uses install.sh
(needs sudo — works on passwordless nodes, returns an error otherwise; surfaced in UI).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-27 06:25:30 -07:00
Omar SobhandClaude Opus 4.8 9263418fcb Fleet: per-node dev-tool version cards + nightly latest-check (Phase 1, read-only)
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 7s
ci / frontend (push) Successful in 23s
ci / e2e (push) Has been skipped
Each node card now shows installed versions of Docker / Claude Code / Kimi / GLM /
Ollama (conditional per node) under the ssh card, with an "update available" badge.

- daemon: probe_tools() finds docker/claude/kimi-cli/ollama across candidate bin dirs,
  extracts semver from --version, reports {"t":"node_tools",...} on connect + every 15m.
- migration node_tools + tool_latest; cm-db repo node_tools (upsert/list/latest).
- cm-api: fleet.rs NodeTools uplink → upsert; tool_versions.rs spawn_latest_checker
  (24h, npm/pypi/github; docker display-only); GET /api/nodes/{id}/tools (glm mirrors
  claude). Spawned in clawmates-server.
- frontend: NodeTools cards on each HostCard with the ↑latest badge.

Phase 2 (one-click update execution) intentionally deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-27 06:01:51 -07:00
Omar SobhandClaude Opus 4.8 ce7a5d9aed Remove orphaned k8s artifacts (Docker-only now)
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 20s
ci / frontend (push) Successful in 24s
ci / e2e (push) Has been skipped
CI no longer references k8s; delete the dead k8s surface:
- deploy/helm/ (the chart), ci/check-helm.sh, scripts/netpol-cluster.sh
- cm-sandbox: the feature-gated K8sDriver (src/k8s.rs) + k8s_security test +
  the `k8s`/`k8s-tests` features + the optional kube/k8s-openapi/rustls deps
  (Cargo.lock drops the kube-rs tree). Nothing outside cm-sandbox referenced it.

Docker (bollard) DockerDriver is the sole sandbox driver. cm-sandbox + cm-runtime
compile, fmt + clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 18:55:17 -07:00
Omar SobhandClaude Opus 4.8 3554a3aaf2 CI: remove k8s stages, fix the Docker-level pipeline green
ci / frontend (push) Successful in 23s
ci / rust (push) Failing after 27s
ci / e2e (push) Has been skipped
ci / gates (push) Successful in 5s
Survey + fixes so the pipeline passes at the Docker level (no k8s).

- Remove k8s: drop the `sandbox-k8s` job (kind/Calico/--features k8s-tests) and the
  "Helm chart lints" gate step. release.yml was already k8s-clean.
- Rust job:
  - `cargo fmt --all` — fix pre-existing formatting drift (fmt --check was failing).
  - clippy -D warnings: fix 3 lib warnings (cm-brain sort_by_key→Reverse, cm-api
    fleet.rs doc list indentation, node_rules map_or→is_none_or).
  - Regenerate the .sqlx offline cache (was missing the cm-runtime run_loop test
    query → offline compile failed). DB-backed tests use testcontainers at runtime.
  - Set SQLX_OFFLINE=true on the rust + e2e jobs so query! macros compile against
    the committed cache deterministically (no DB needed at compile time).
- Frontend job:
  - Fix the 1 ESLint error (useAgentTelemetry: no setState-synchronously-in-effect;
    tag the slice with agentId + derive null on mismatch).
  - Fix 2 stale panel-params tests (`terminal` is a valid app id now; assert the
    current APP_IDS + use a genuinely-unknown id for the reject case).

Verified locally: fmt clean, clippy --all-targets -D warnings clean (offline),
frontend lint 0 errors, tsc clean, 86/86 frontend tests pass, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 18:15:31 -07:00
Omar SobhandClaude Opus 4.8 89c147b742 Command center: edit brain sections inline from the cards
ci / gates (push) Failing after 11s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Add PATCH /api/claws/{id}/brain (edit_brain): writes any of system_prompt / agent_md
/ persona / skills_md into the claw's .brain (best-effort) + persists system_prompt
to Postgres (authoritative) + commits a ClawSync revision.

Frontend: a reusable EditableSection (pencil → textarea → Save/Cancel → PATCH →
re-fetch brain). The SYSTEM PROMPT, HOW I OPERATE (AGENTS.md), and PERSONALITY cards
in the command center are now editable inline; saving writes back to the mapped
brain section. AGENTS.md card now always shows (so it can be authored when empty).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 13:30:58 -07:00
Omar SobhandClaude Opus 4.8 9fca3f6676 Brain: inject identity into the live prompt + surface AGENTS.md in the command center
ci / gates (push) Failing after 11s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Gap 1 — the brain's identity sections were stored but UI-only. cm-runtime/brain.rs
`compose_system` now folds the brain's AGENTS.md ("## How I operate") + personality
into the live system prompt (after the Postgres-authoritative base, before skills +
memory; falls back to the brain's soul_md when the base is empty). Mirrors the
OpenClaw/ZeroClaw render order.

Mapping — expose `agent_md` on `GET /api/claws/{id}/brain` (ClawBrainResponse) and
render it as a collapsible "HOW I OPERATE · AGENTS.md" card in the command center's
BRAIN column (RawBrain gains agent_md; richBrain passes it through). So the section
that's now in the prompt is also visible in the UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 12:59:17 -07:00
Omar SobhandClaude Opus 4.8 11a1f22daa Agents page: reorganize into the Agent Command Center (per-agent live metrics)
ci / gates (push) Failing after 13s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Replace the single centered "anatomy profile" + resizable chat split with an
operator command center: compact 62px identity strip → 5-tile per-agent metrics
band → three independently-scrolling LIVE · BRAIN · SURFACE columns. Chat moves to
the computer's Chat app; the right computer pullout (DevicePanel) is untouched.

- backend: cm-api/routes/world.rs emits per-agent `telemetry{agentId,tokensPerMin,
  costPerHr,loops,doorsPending}` in the SSE loop, from 4 batched GROUP BY queries
  (usage_events tokens/min + credits/hr, active routines, pending approvals) — all
  real, no migration. taxonomy `telemetry` gains optional agentId; stateKey now
  keys it per-agent so slices don't clobber.
- frontend: new ClawCommandCenter + anatomy-cards (shared cards extracted from
  Dashboard); useAgentTelemetry(agentId) feeds the metric band (Doors amber>0/
  green=0); LIVE column streams the agent's task.update / reasoning.delta /
  tool.call (replaces the mocked VitalsCard heatmap with a live activity chart).
- Dashboard: left region → full-height ClawCommandCenter; chat launcher opens the
  computer Chat app; removed the dead anatomy cluster + unused imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 11:34:12 -07:00
Omar SobhandClaude Opus 4.8 10c89f5157 Node-placed agent terminal: container PTY on the agent's node + WebRTC, shared node-local drives
Completes "agent on a node" (single-node): when an agent's placement points at a
fleet node, its terminal container runs there and the browser reaches it over a
direct WebRTC DataChannel (LAN speed), sharing a node-local volume with the
sandbox. gw-04-local agents are byte-identical to before.

- cm-sandbox/docker.rs: empty drive subpath → mount the whole volume at the target
  (volume_options None), so a per-agent node-local volume auto-creates at ~/drives.
- cm-api/fleet.rs: NodeHub.open_pty/webrtc_offer carry optional container+session
  (injected only when Some); node-terminal caller passes None (host shell unchanged).
- cm-runtime/terminals.rs: TerminalManager gains node_provider + placement
  (mirrors SandboxManager, draining-aware); node_local_drive_mount(agent) =
  clawmates_agent_<id> at ~/drives; placement_for() ensures + locates the container;
  attach uses driver_for(node) (local byte-identical).
- cm-runtime/sandboxes.rs: a node-placed agent sandbox mounts the same per-agent
  volume → shares files with the terminal on that node.
- cm-api/routes/terminal.rs: ticket response gains `node`; ws() bridges node-placed
  agents through the NodeHub relay (WebRTC + fallback) execing into the container;
  local path unchanged. server main wires with_node_provider.
- frontend: agentTerminalConnector mints the ticket then picks WebRTC (node-placed,
   direct / relayed badge) vs WS (local); webrtcConnector generalized to be
  endpoint-agnostic (node terminal reuses it).

Known follow-up: terminal (uid 65532) and sandbox (uid 10001) share the volume but
differ in uid — cross-container writes need an aligned uid/gid (group-writable).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 09:11:18 -07:00
Omar SobhandClaude Opus 4.8 12212c72ac Daemon: PtyTarget — exec a terminal PTY into an agent container (node-placed terminal foundation)
Generalize the daemon's PTY spawn so a session can target either the node's host
shell (today) or `docker exec -it <container> tmux …` (the node-placed agent
terminal, which shares the container's node-local ~/drives). A `container` (+
optional `session`) field on pty_open/webrtc_offer selects the container path;
absent it, the host shell path is byte-identical to before — so the Infra node
terminal is unaffected. Threads PtyTarget through open_pty + rtc handle_offer →
build_peer → bridge_pty. Additive; nothing emits `container` yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 09:00:06 -07:00
Omar SobhandClaude Opus 4.8 c94784bab2 Fleet: actionable executions — rules engine + metrics-aware placement (Phase 2)
ci / gates (push) Failing after 16s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Turn the Beszel-tapped metrics into a self-managing loop.

- migration node_rules (workspace/node-scoped: metric op threshold, for_seconds,
  action JSONB, last_fired).
- cm-db: repo/node_rules.rs (CRUD + list_enabled); node_metrics::eval_all merges
  Beszel + heartbeat scalars per node + a headroom() heuristic; nodes::status_of;
  heartbeat now PRESERVES a `draining` status across heartbeats (so a cordon sticks).
- cm-api: node_rules.rs evaluator (spawn_evaluator, 20s) — when a metric condition
  holds for the rule's window it fires drain / undrain / alert (in-memory sustained
  + cooldown tracking, modeled on the node sweeper); routes/beszel.rs rules CRUD
  (GET/POST/PATCH/DELETE /api/fleet/rules); spawned in clawmates-server.
- cm-runtime: placement_node() is metrics-aware — a `draining` node stops receiving
  new agent sandboxes (falls back to local), so the drain rule is actionable.
- frontend: FleetRules section in the Local view — build rules (node · metric · op ·
  threshold · duration → action), toggle/delete, with fired-history.

The loop: hot/overloaded node → rule drains it → placement avoids it → recovers →
undrain rule brings it back. Deployed; node_rules migration applied.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-25 23:43:49 -07:00
Omar SobhandClaude Opus 4.8 36a227566b Fleet: Beszel hub integration — rich per-node metrics + per-node monitor (Phase 1)
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Tap each node's Beszel metrics (GPU/temps/disk-IO/network/per-container — beyond
our basic heartbeat) by reading the workspace's Beszel hub. The agents run in
WS-only mode with no locally-readable socket, so (per the de-risk) the server taps
the hub's PocketBase API instead of the daemon reading agents — no daemon changes.

- migrations: workspace_beszel (BYO hub URL + login, server-side only, mirrors the
  Tailscale BYO pattern) + node_metrics (latest scalar columns + JSONB blob).
- cm-db: repo/fleet_beszel.rs, repo/node_metrics.rs; nodes SELECT joins node_metrics
  (gpu_pct/temp_max surfaced on node_json for the live cards).
- cm-api: beszel.rs client (auth-with-password, poll `systems`, map to nodes by
  hostname, upsert metrics) + a 15s spawn_poller; routes/beszel.rs (connect/status/
  disconnect + GET /api/nodes/{id}/metrics with history proxied live from the hub).
- frontend: HostCard gains a GPU/temp readout + a Monitor button; NodeMonitor is a
  full-width per-node page (current panel + CPU/mem/GPU/temp/net/disk charts from the
  hub's 1m history); a "Beszel monitoring" connect form in the Local view.

Reachability confirmed: gw-04 → the hub over the tailnet (100.123.224.84:8090). Needs
the user to connect their hub login to activate the poller.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-25 23:28:29 -07:00
Omar SobhandClaude Opus 4.8 4de2f31b50 Fleet terminal: WebRTC DataChannel direct path (low-latency) + WS fallback
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Terminal keystrokes were ~400ms because every byte relayed browser→Cloudflare→
gw-04 (Europe)→tailscale→node, even when the node is on the user's own LAN. Add a
direct browser↔node WebRTC DataChannel so co-located terminals run at LAN speed;
the gateway is reduced to signaling; the WebSocket relay stays as the automatic
fallback (graceful degradation — never worse than before).

Daemon (clawmates-node v0.4.0, new src/rtc.rs):
- Add the `webrtc` crate (reuses the ring crypto provider we already install — no
  conflict). Browser is the offerer; we answer, trickle ICE back over the control
  channel, and on DataChannel open spawn a host PTY (tmux) bridged DIRECTLY to the
  channel. Refactor open_pty → spawn_terminal_pty shared by both transports.
  iceServers: STUN + auto host/tailnet candidates (direct, no relay, for LAN/tailnet).

Server (cm-api):
- NodeConn.signal_sinks; Uplink WebRtcAnswer/WebRtcIce/WebRtcFailed routed to the
  browser; NodeHub webrtc_offer/ice/close + open_session/open_pty (open_terminal
  split so the PTY opens only once the transport is chosen). bridge_terminal relays
  signaling over the existing ticket-authed WS and opens the relay PTY on
  {type:"fallback"}.

Browser (NodeTerminalApp):
- RTCPeerConnection + reliable/ordered DataChannel; offer/answer/ICE over the WS;
  2.5s race → use the DataChannel if it opens, else fall back to the WS relay.
  Reconnect wraps both. A direct/relayed indicator shows the live transport.

Deployed; both nodes (morpheus, tank) updated to v0.4.0 and steady online. Direct-
path proof is a browser action (the  indicator + latency); confirmable from the
daemon's [rtc] logs.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-25 16:55:32 -07:00
Omar SobhandClaude Opus 4.8 94828ed887 Fleet: robust real-time connectivity + mosh-inspired reconnecting terminal
ci / gates (push) Failing after 6s
ci / frontend (push) Has been skipped
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / e2e (push) Has been skipped
Nodes flapped online/offline and the terminal died on the first blip. WebSockets
are the right transport (outbound, NAT-friendly); the fixes harden around it.

Server (cm-api):
- Anti-clobber connection epoch: a reconnecting daemon gets a fresh epoch; a stale
  run_channel's teardown only clears the hub + sets offline if it still owns the
  slot — so a lingering old channel can't flip a live reconnection offline (the
  main false-offline cause).
- WS keepalive: run_channel now pings every 15s and tears down if no inbound
  frame (incl. pong) for 35s — dead links detected in seconds, not minutes.
- Staleness sweeper backstop: spawn_node_sweeper (8s tick / 20s window) wired in
  clawmates-server, so a vanished node goes offline within ~28s even if its
  channel hangs (mark_stale_offline was defined but never called).

Daemon (clawmates-node v0.3.0):
- Heartbeats off the select thread (dedicated thread owns System + blocking
  docker/tailscale/disk CLIs) so a slow op never starves heartbeats/pongs.
- Each handle_frame runs on its own task; added a 40s inbound idle deadline so a
  half-open socket triggers a reconnect.

Frontend:
- useNodes streams /api/nodes/live (SSE push) instead of a 3s poll; isLive()
  derives online from lastSeen freshness (<15s) so a transient column flip never
  shows a healthy node down.
- Node terminal: clean auto-reconnect loop (re-mint ticket -> reconnect -> tmux
  re-attaches and redraws the live screen = mosh-style snap-to-state over TCP),
  replacing the [disconnected] dead-end.

Mosh evaluated: harvest principles (session/transport decoupling, snap-to-state,
already given by tmux), don't adopt — UDP is incompatible with our browser+CF+NAT
topology and it's GPLv3. Removed temporary terminal debug traces + /api/debug route.

Verified: node holds steadily online (heartbeat 1-3s, no flap) and goes cleanly
offline when the daemon stops.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-25 07:18:05 -07:00
Omar SobhandClaude Opus 4.8 95bd022d07 Fleet terminal: fix tmux nesting-refusal + frontend resize storm
ci / gates (push) Failing after 7s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Root cause (from the server trace: 69 banner bytes then immediate "to_browser
ended" = pty_exit): tmux spawned and exited instantly, writing its "sessions
should be nested with care" warning to stderr (not the PTY). The operator runs
the daemon inside their own tmux, so $TMUX was inherited and the spawned tmux
refused to nest → browser saw nothing.

- daemon (v0.2.2): spawn tmux on a DEDICATED socket (`tmux -L clawmates
  new-session -A -s main`) and `env_remove("TMUX")`, so it can never collide with
  or be refused by the operator's tmux.
- NodeTerminalApp: debounce the ResizeObserver (150ms). The pull-out animates
  open, firing the observer on every pixel — previously ~80 resize frames per
  open, each fit()+SIGWINCH. Now one resize after layout settles.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-25 06:01:10 -07:00
Omar SobhandClaude Opus 4.8 27f5d05f96 Fleet terminal: daemon self-diagnosis + immediate banner + debug route
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
The server trace showed pty_open is sent but the daemon (morpheus, v0.2.0) emits
no pty_out — so the PTY spawn was dying silently. Instrument it:
- daemon open_pty: log open/tmux/first-read/EOF/error/total to stdout, and send
  an IMMEDIATE banner pty_out ("[clawmates] host shell on <host> — starting…") so
  the browser confirms the relay even before the shell draws. If open_pty fails,
  send the error as pty_out (was a silent pty_exit). Bump to v0.2.1.
- cm-api: temp GET /api/debug/node-pty/{id}?dbg=… opens a node terminal and reads
  ~2s of output with no browser/auth, to test the relay in isolation.
- NodeTerminalApp + ticket/ws routes already log each hop ([node-term]/[fleet-term]).

Diagnostic logic: banner shows + shell doesn't → relay ok, shell is the problem;
nothing shows → relay broken; daemon "EOF after N bytes" → shell exited.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-25 00:06:51 -07:00
Omar SobhandClaude Opus 4.8 8e71c8403b clawmates-node: bump to v0.2.0 (selftest + tmux re-hosted)
ci / gates (push) Failing after 6s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 21:43:00 -07:00
Omar SobhandClaude Opus 4.8 c2a0309ad7 clawmates-node --selftest + server terminal tracing (diagnose blank terminal)
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
- Daemon: factor the terminal command into terminal_command(); add `--selftest`
  which opens the host terminal PTY locally and prints ~2.5s of raw output, so
  you can confirm tmux/zsh actually draws on a given node without the browser.
  (Verified locally: tmux spawns zsh + draws its status bar.)
- cm-api: temporary [fleet-term] eprintln tracing in open_terminal, the pty_out
  router, and the browser bridge (byte counts + sink presence) to locate where
  output stops between daemon→server→browser. To be removed once diagnosed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 21:28:26 -07:00
Omar SobhandClaude Opus 4.8 6e81eaa52b clawmates-node: host terminal uses tmux (resumable, redraw-on-attach)
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
The node terminal opened a bare bash PTY — bash prints its prompt once, so a
freshly-attached xterm showed nothing. Switch to `tmux new-session -A -s
clawmates` on the HOST (mirrors the agent terminal, but on the node itself, not
in a container): tmux redraws the whole screen on attach (no blank), and the
session is resumable across reopens. Starts in $HOME. Falls back to a login
shell if tmux is unavailable. ensure_tmux() best-effort installs tmux via the
host package manager when the daemon runs as root (systemd); otherwise logs a
hint to `apt install tmux`. Rebuilt + re-hosted both binaries.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 20:27:51 -07:00
Omar SobhandClaude Opus 4.8 cf6c331b02 Fleet: node hostname/IP on register + node terminal in the infra computer
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Hostname/IP:
- Daemon reports the machine's hostname (sysinfo) + primary outbound IPv4 on each
  heartbeat. migrations/0021 adds nodes.hostname/local_ip; cm-db heartbeat stores
  them; node JSON exposes them. Cards now title on the real hostname (falling back
  to name) + show the IP, instead of the "New node" placeholder. `name` stays
  user-overridable (rename).

Terminal moved into the pull-out computer (no more per-card modal):
- New infra computer app NodeTerminalApp (computer/apps/infra) — xterm bridged to
  a node's host shell over the node control channel, filling the app window
  (mirrors the agent Terminal's layout + ResizeObserver). Added "terminal" to the
  INFRA_CATALOG grid; a ?node= panel param targets a specific node (picker when
  unset). Clicking Terminal on a node card now opens the infra computer to that
  node's shell instead of a separate full-screen window. Deleted NodeTerminal.tsx.

Rebuilt + re-hosted both daemon binaries (hostname change).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 20:13:15 -07:00
Omar SobhandClaude Opus 4.8 cfa16751c5 clawmates-node: fix rustls CryptoProvider panic + non-intrusive Tailscale
ci / gates (push) Failing after 6s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Two bugs surfaced running the daemon on a real node:
- Linking cm-sandbox (bollard) brought a second rustls provider into the graph,
  so rustls couldn't auto-pick one and panicked at the WSS TLS handshake. Install
  the ring provider explicitly at startup (rustls dep + install_default()).
- The daemon auto-ran `tailscale set --ssh`, which tries to reroute the user's
  live SSH session and aborts ("will result in your session disconnecting"). Now
  Tailscale is only touched when an auth key is explicitly passed (opt-in), with
  --accept-risk=lose-ssh to avoid the interactive abort.

Rebuilt + re-hosted both binaries (linux-amd64, darwin-arm64).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 16:10:42 -07:00
Omar SobhandClaude Opus 4.8 fb59378aa2 Fleet P2b: run agent sandboxes on connected nodes (RemoteDriver + placement)
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Agents can now provision their sandbox on a connected fleet node instead of the
gateway host. Local stays the strict default, so existing agents are byte-for-
byte unaffected until explicitly placed elsewhere.

Security parity: the daemon links the REAL cm-sandbox DockerDriver and runs the
typed container ops (sb_provision/sb_exec/sb_destroy/sb_health/sb_list) through
it — identical hardening (cap-drop ALL, seccomp, no-net, read-only, non-root) to
local sandboxes. cm-sandbox spec types are now Serialize/Deserialize so the spec
crosses the channel.

- cm-api: RemoteDriver (impl SandboxDriver over the node channel) + HubDriverProvider
  (impl cm_runtime::NodeDriverProvider, hands out a driver only for connected
  nodes via a sync online set) + NodeHub.call/is_connected. AppState.with_node_hub
  so the hub is shared with the placement provider.
- cm-runtime SandboxManager: driver_for(node_id) routes by the recorded
  agent_containers.node_id (local default = existing driver, identical path);
  placement_node() reads the workspace setting and falls back to local if the
  node is offline; exec/release route accordingly. NodeDriverProvider trait.
- DB: 0020_workspace_placement + repo (for_agent/get/set/clear).
- main.rs: build the NodeHub first; inject HubDriverProvider into the agent
  manager + share the hub with AppState.
- API+UI: GET/PUT /api/fleet/placement + a "Run agents on: Local / <node>"
  selector in the Fleet overview.

Note: a node must be able to pull the agent image (the daemon docker-pulls it);
interactive PTY for agent containers on remote nodes is not wired (Terminal app
stays local) — the in-dashboard node shell already covers host access.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 12:48:50 -07:00
Omar SobhandClaude Opus 4.8 33aa9c0693 Fleet P2b: node sandbox-readiness check (hardened workload on a node)
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Proves a connected node can host hardened agent workloads end-to-end, without
touching the agent run loop (zero blast radius on existing agents).

- Daemon: typed `sb_check` op — pulls a tiny image and runs it fully locked down
  (cap-drop ALL, no-new-privileges, no network, read-only rootfs, non-root,
  memory/pids caps), then tears it down. Fixed command; nothing caller-supplied
  runs (preserves the exec-hardening invariant).
- cm-api: NodeHub.sandbox_check + POST /api/nodes/{id}/sandbox-check.
- UI: a shield "sandbox check" button on each online node card streams the
  result (✓ SANDBOX READY + container id/uname).

This validates the full provision→run→destroy mechanism on nodes. The remaining
P2 work — wiring real agent deploys to auto-place onto nodes — is its own
subsystem (a RemoteDriver reusing the local DockerDriver for security parity,
agent-image distribution to nodes, and node-routing in SandboxManager) and is
best done as a focused pass; it is intentionally NOT bundled here to keep the
core agent path untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 12:31:50 -07:00
Omar SobhandClaude Opus 4.8 f5f96508eb Fleet P2a: in-dashboard remote terminal (PTY over the WSS channel)
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
You can now open a real shell on any connected node from the dashboard — the
daemon spawns a host PTY and streams it over the existing outbound control
channel (no inbound port, no Tailscale brokering needed).

Daemon:
- portable-pty host shell sessions: pty_open/pty_in/pty_resize/pty_close ops; a
  reader thread streams base64 pty_out frames. Outbound frames now funnel through
  one mpsc channel so PTY output and heartbeats interleave.

cm-api NodeHub:
- per-connection pty_sinks + sid multiplexing; open_terminal/terminal_input/
  terminal_resize/terminal_close; in-memory single-use terminal tickets (the
  browser WS can't carry a bearer, and the session is instance-local anyway).
- routes/nodes.rs: POST /api/nodes/{id}/terminal/ticket + GET .../terminal/ws
  (bridges browser xterm <-> node PTY: binary = keystrokes, text = resize).

Frontend:
- NodeTerminal xterm modal (reuses the agent Terminal's xterm setup); a Terminal
  button on each online node card opens a shell.

This proves the bidirectional streaming-over-channel mechanism the RemoteDriver
will reuse. Remaining P2: RemoteDriver + placement (run agent workloads on nodes).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 12:13:24 -07:00
Omar SobhandClaude Opus 4.8 7332d69f8a Fleet P1: BYO Tailscale + network metrics, Tailscale SSH, exec hardening
ci / gates (push) Failing after 6s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Security hardening:
- The gateway no longer sends arbitrary shell to nodes. The WSS exec op is
  replaced by a typed `verify` op the daemon runs itself (fixed host+docker
  check); future container ops are typed too. cm-api NodeHub.verify() + the
  daemon's handle_command only dispatches vetted ops.

BYO Tailscale:
- migrations/0019_workspace_tailscale.sql + cm-db fleet_tailscale repo (store the
  user's Tailscale API key + tailnet, server-side only).
- cm-api routes/tailscale.rs: POST/GET/DELETE /api/fleet/tailscale + GET
  /api/fleet/tailscale/devices (proxies api.tailscale.com device list).
- Daemon: --tailscale-authkey → `tailscale up --authkey … --ssh` (enables
  Tailscale SSH for keyless user access); else `tailscale set --ssh=true`. Reports
  its tailscale IP (already).

UI:
- Fleet overview gains a Tailscale section: connect (key+tailnet) + live tailnet
  device status (online/last-seen/IP/os). Node cards show a copyable Tailscale SSH
  target (ssh <ip>).

Remaining: P2 — RemoteDriver + placement (run agents on nodes) and the in-UI
remote terminal (PTY proxied over the WSS channel).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 10:27:58 -07:00
Omar SobhandClaude Opus 4.8 2bdd0a23e8 Fleet P0: node registry + daemon + health + connect-host wizard
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Users can connect their own local-hardware nodes into a fleet. Each node runs a
new Rust daemon that dials home over an outbound WebSocket, reports host health,
and runs commands we send.

Backend:
- migrations/0018_fleet_nodes.sql: nodes + node_health tables + agent_containers
  (node_id, workspace_id) index. cm-domain NodeId.
- cm-db repo/nodes.rs: create/auth/list+health/get/heartbeat/set_status/delete
  (unchecked sqlx, no .sqlx regen).
- cm-api fleet.rs NodeHub: live daemon channels (node_id→sender) + the WS channel
  runner (heartbeat→DB upsert, exec request/response framing). routes/nodes.rs:
  POST /pair, GET /nodes, SSE /nodes/live, POST /{id}/exec-test, DELETE /{id},
  WS /nodes/agent (token-auth). Wired into AppState + router.

Daemon (new crate crates/bins/clawmates-node):
- sysinfo host metrics (cpu/mem/pressure/swap/disk/load/containers), outbound WSS
  dial + reconnect, heartbeat loop, exec command handling, tailscale-ip probe.
  install.sh convenience installer.

Frontend:
- Fleet sidebar item + FleetOverview + LocalHardware node-health cards (live via
  /api/nodes, 3s poll) + ConnectHostWizard (install → verify connection →
  exec-test). InfraStage dispatches fleet/local; default selection = fleet.

Deferred: P1 (BYO Tailscale + network metrics), P2 (RemoteDriver + placement so
agents actually run on connected nodes).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 08:09:14 -07:00
Omar SobhandClaude Opus 4.8 3b012b12bf World: explosive file sparks + 3 real views (Flat 2D / Live Gource / Brain)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
- Explosive sparks on file/project I/O: /api/world/live tags file-ish tools
  (read/write/drive/vault/obsidian/edit/save) with touch weight 1; the engine
  carries it as a node "burst" and the renderer fires a big fast particle blast.
- The formation tabs are now genuinely different views:
  - Flat — the previous 2D React-Flow forest (WorldFlow), overlaid.
  - Live — the WebGL Gource clone (glow sprites + sparks + trails + beams).
  - Hierarchy — a NEW neural-brain view: agents are neurons in a dormant
    two-hemisphere mesh; active agents fire signal particles along pathway edges
    to their nearest neighbours (chaining), so the brain lights up as agents work.
- Render groups (gource/brain) + 3D particle system so signals travel in depth;
  shared particle update; OrbitControls (rotate + pinch-zoom) across all WebGL views.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 22:49:23 -07:00
Omar SobhandClaude Opus 4.8 380b95da2a World: normalize real run_events into the live taxonomy (the data unlock)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
/api/world/live now tails each active run's journal (run_events) and normalizes
the runner's existing events into taxonomy events — no runner change needed:
- text_delta       -> agent.reasoning.delta (the live reasoning stream)
- step_started     -> agent.tool.call + node.activity + world.touch on the tool
                      node (agents visibly converge on the tool they're using)
- approval_required -> door.request
Per-run seq cursor streams forward only (skips backlog on first sight). This
lights up REAL data for both the World view and the upcoming Observe surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:52:51 -07:00
Omar SobhandClaude Opus 4.8 53279e6339 World: Phase C — replay scrubber (Gource-style playback)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
- /api/world/replay?hours= — reconstructs a sorted, timestamped taxonomy timeline
  from the workspace's run history (agent_runs ⋈ sessions) for client playback.
- engine.clearWorldNodes() — drop transient world nodes/targets for a loop restart.
- WorldCanvas: a WorldClock control (Live ⇄ Replay) feeding the same engine —
  play/pause, 1x/2x/4x speed, seek bar, loop. Live subscriptions detach in replay.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:39:03 -07:00
Omar SobhandClaude Opus 4.8 05c54de47d World: real-data convergence — pawns roam the live tree; runs drive world.touch
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
- engine: when there are no service/event touch-nodes (a quiet feed), pawns roam
  the real org/company/team structure so the view is always alive with zero
  synthetic data; real touch-nodes take priority when present.
- /api/world/live: emit world.touch + node.activity per currently-running run
  (agent_runs joined to sessions, state='running') so each working agent visibly
  converges on its active-run node; fold running agents into working status;
  telemetry.loops = active run count.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:29:06 -07:00
Omar SobhandClaude Opus 4.8 39bacbd1d2 World page: live WebGL Gource-style visualization (Phases A+B)
Replaces the static React-Flow world forest with a real-time WebGL view of the
swarm working, plus the live event contract that feeds it.

Phase A — the live contract + feed:
- frontend/src/lib/live/taxonomy.ts — the 13-event Clawmates Event Taxonomy as
  typed TS (the shared World/Observe contract).
- frontend/src/lib/live/useClawmatesLive.ts — native shared-singleton live client
  (EventSource to /api/world/live, typed fan-out, replay-of-last-state to late
  subscribers, refcounted, synthetic fallback so views are always alive).
- crates/cm-api/src/routes/world.rs — GET /api/world/live, authed + workspace-
  scoped SSE emitting the taxonomy (real agent.status from live-container state,
  a topology.update of the workspace's agents, telemetry with real doorsPending),
  mirroring run_events_sse. normalize() seam documented for run_events->world.touch.

Phase B — the WebGL engine:
- frontend/src/components/world/engine.ts — Gource-inspired force-directed model:
  org/company/team tree (sibling repulsion + parent spring + friction), agent
  pawns that converge on the touched node and beam it, world nodes that glow with
  heat and fade when idle. Framework-agnostic (renderer-independent) state+math.
- frontend/src/components/world/WorldCanvas.tsx — three.js scene (ortho cam,
  UnrealBloom), render loop syncing engine state, camera auto-fit, HTML labels,
  raycast click->select, Hierarchy/Flat/Live formation switch.
- Dashboard.tsx: swap <WorldFlow/> -> <WorldCanvas/> at the world-tier seam
  (shared claw-tier pieces untouched; WorldFlow.tsx kept for now).
- Adds three (+ @types/three).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:23:34 -07:00
Omar SobhandClaude Opus 4.8 e9ce368ec1 Scaling Phase 1: multi-tenant onboarding + replica-safe coordination
Decouples "many users" + "many server replicas" from "many machines" so the
platform is tenant-isolated and horizontally safe on the current single node.

- Per-signup workspaces (cm-auth): a new hosted-identity sign-in provisions and
  owns its own workspace instead of joining the first. Config-gated by
  auth.per_signup_workspace (default off); concurrent first-logins serialized by
  a per-subject advisory lock so no duplicate workspaces.
- Terminal tickets in Postgres (migration 0016, hashed, single-use): any replica
  can redeem a ticket minted by another. Drops the in-process ticket map.
- Container registry in Postgres (migration 0017, agent_containers): Terminal
  and Sandbox managers resolve an agent's container through a shared registry,
  so a 2nd replica reuses it instead of spawning a duplicate. node_id recorded
  as 'local' (Phase 2 hook). Boot reconcile removes only true orphans, so
  terminals now survive a redeploy (tmux sessions resume).
- Per-workspace quotas (cm-api/quota.rs): plan-tier caps on agents + live
  containers, enforced at agent create + terminal spin-up (reconnects allowed),
  returned as HTTP 402. New GET /api/quota surfaces usage vs limits.

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-22 23:21:54 -07:00