248948cc847b4d229291fa65785d940b02fd36ca
46
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2668191e30 |
feat(auth): a credential narrow enough to hand to an agent
`docs/TOOL-CALL-ARCHITECTURE.md` §3 calls deploying the MCP door "config, not code". It is not, and the reason is authentication. `/mcp/skills` authenticates with `AuthService::authenticate`, which returns a full `AuthedUser` carrying the user's role. There is no narrower credential in the system. So pointing a mission container at the door means writing a bearer token into a file inside that container — and mission agents run arbitrary `Bash` with egress and no read gate, which is this platform's own documented security posture. An owner-scoped token there turns "the agent runs commands in a sandbox" into "the agent drives the whole ClawMates API as the owner". Checked before building this rather than assumed: no such credential is in a mission container today. The runtime's config.toml has no `[mcp.servers]` block and no bearer, so the door would have been a NEW exposure, not an existing one. So: `auth_sessions.scope`, defaulting to `full`. `authenticate` now delegates to `authenticate_scoped(token, SCOPE_FULL)`, which means **every existing caller rejects a narrow token** and a route must opt in by naming the scope it accepts. `/mcp/skills` is the only opt-in. Fail closed on purpose. The likely mistake here is adding a scope and forgetting to wire its check; this way that mistake grants nothing rather than granting everything. `mint_scoped` refuses to mint a `full` token — a caller reaching for it wants a narrow credential, and handing back a full one because an argument was wrong is exactly the failure the column exists to prevent, and it would be invisible because the token would work. The test that matters is not that the door accepts the token, it is that nothing else does. Negative-controlled: removing the scope comparison fails `a_scoped_token_is_refused_by_every_unscoped_caller`. `.sqlx` regenerated — `authenticate` is a compile-checked query and CI builds with SQLX_OFFLINE=true. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9 |
||
|
|
ccbc387f4b |
fix(agents): stop listing soft-deleted agents
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]>
|
||
|
|
fdb8cfeecc |
slice 9 cleanup: drop legacy research/loops backend + tables
Retires the legacy research/loops backend after the missions arc
(slices 1-9) fully replaced it. Frontend cutover was 4663348; this
commit finishes the job on the backend + database.
Migration:
- 0053_drop_legacy_research_loops.sql — drops the 8 legacy tables
(research_topics, research_topic_agents, research_outcomes,
research_publish_approvals, loops, loop_agents, loop_orgs,
loop_teams) and the 3 topology_runs FK columns
(research_topic_id, loop_id, iteration). parent_run_id stays;
recursive_exec still uses it.
Files deleted (11):
- crates/cm-api/src/routes/{research,loops,research_setup,
research_pipeline,wizard_repo,probe}.rs
- crates/cm-api/src/research_container.rs
- crates/cm-db/src/repo/{research_topics,research_outcomes,
research_publish_approvals,loops}.rs
- crates/cm-runtime/src/loops.rs
- crates/cm-api/tests/research_publish_role.rs
Files edited:
- crates/cm-api/src/lib.rs — dropped 20 legacy route registrations
(all /api/research/* + /api/loops/* + /webhooks/loops + probe)
and module decls
- crates/cm-api/src/topology_worker.rs — deleted legacy dispatch
(freeze_research_outcome, advance_loop_after_completion,
continue_initial_burst, maybe_transition_research_topic,
parse_reorder_rationale, per-topic/loop gateway resolver).
reap_stuck_runs now keys on mission_id (not topic_id).
Executor path unconditionally uses ZeroClawDriveExecutor::from_env
— mission_orchestrator provisions each claw as an agent inside
the shared runtime via RuntimeProvisioner, so per-team gateway
resolution is no longer applicable.
- crates/cm-api/src/routes/topology.rs — deleted container-log SSE
endpoint (research/loop-specific), dropped loop_id filter and
iteration field from ListRunsQuery/RunSummary
- crates/cm-api/src/routes/world.rs — removed
active_research_topics/active_loops/preseed_repo_paths;
World SSE no longer emits repo:{topic}/loop:{id} landmark orbs
(follow-up task #21 tracks adding mission:{id} equivalents)
- crates/cm-api/src/runtime_provision.rs — removed now-unused
mint_workspace_service_token
- crates/cm-db/src/repo/topology_runs.rs — removed 9 legacy
helpers (research_topic_id lookup, loop_id_for_run,
iteration_for_run, active_runs_for_research_topic, etc.)
- crates/cm-db/src/repo/teams.rs — removed 4 dead helpers
(team_for_loop, team_for_research_topic + setters)
- crates/cm-api/tests/topology_jobs.rs — removed loop/topic
tests, dropped enqueue_run_with_topic helper
- crates/bins/clawmates-server/src/main.rs — removed
spawn_loop_scheduler call
- crates/cm-api/src/routes/mod.rs, crates/cm-db/src/repo/mod.rs,
crates/cm-runtime/src/lib.rs — module decls stripped
sqlx cache: regenerated against post-migration schema
(71 files changed, ~+70 / -8896 net)
Test/build: SQLX_OFFLINE=true cargo check --workspace clean;
cargo test --workspace --no-run clean.
Follow-up (task #21): World view lost the in-flight-work landmarks
when repo:{topic} / loop:{id} orbs disappeared. Add mission:{id}
orbs as the missions-era replacement.
|
||
|
|
5f988ce022 | fix: legacy skills::create mirrors title into new name column | ||
|
|
bffa790dbc |
sqlx: cache owner_of_workspace query for offline CI
Co-Authored-By: Claude Opus 4.7 <[email protected]> |
||
|
|
acd2a0f287 |
structure polish: post-reify nav + ensure-chain + TeamWizard auto-parent
Two small quality-of-life fixes on top of the reify commit:
Post-reify navigation
OrphanMigrationDialog already returned team_id in its result;
Dashboard now pushes /?team=<team_id> before router.refresh() so
the user lands on the freshly-materialized team and sees exactly
where their agents just moved. Previously they had to hunt for it
in the newly-rebuilt sidebar.
Wizard auto-materialize (POST /api/structure/ensure-chain)
cm-db: ensure_chain(pool, ws, fallback_org, fallback_company) —
fast path returns coordinates of the first org+company already
bound in this workspace (workspace's oldest org, oldest company
under it). Slow path inserts a new org+company with the
fallback names ("My Workspace" / "General") + binds them via
org_companies. Returns { org_id, company_id, created }. Small
txn — leaves the workspace consistent whether it was already
wired or not.
cm-api: POST /api/structure/ensure-chain accepts optional
fallback_org_name and fallback_company_name in the body (trimmed,
else default). Returns the ids.
CreateTeamRequest gains an optional attach_to_company_id. When
set, after build_team() completes, we look up the company
(workspace ownership check enforced by companies::get), count
its existing teams for a stable n_i node id, and insert a
company_teams binding — so the team lands under the parent
atomically instead of a follow-up round-trip.
TeamWizard now calls ensure-chain before POST /api/teams and
passes the returned company_id in attach_to_company_id. Both
calls are best-effort — if ensure-chain fails (network etc.)
we still try to create the team, and the migration dialog stays
available as the fallback UX. Wizard flow now: fresh workspace's
first team is fully wired from the moment it appears in the
tree — no synthetic "My Workspace" scaffolding ever gets
rendered around it.
The Team/Company create paths not touched here (create_team_from_claws,
company create, org create, MasterPlannerModal scaffold) still
work as before — they just won't auto-parent yet. Later commits
can wire them the same way.
|
||
|
|
8b789beec0 |
structure: reify-orphans endpoint + "give these a home" dialog
Turns the four synthetic tree containers into a real migration path.
Clicking any of them ("My Workspace", "Teams", "Direct",
"Ungrouped") opens a dialog that creates a real
org → company → team chain and re-parents every orphan into it, all
in one DB transaction.
cm-db (new module structure_reify)
- orphan_agents / orphan_teams / orphan_companies: workspace-scoped
SELECTs of entities without a parent binding in team_members /
company_teams / org_companies. Used both by the dialog's counter
and internally by the migration.
- count_orphans: cheap combined-count via three subqueries in a
single SELECT so the dialog only round-trips once for the header.
- reify_orphans(pool, ws, org_name, company_name, team_name):
1. begins a tx
2. inserts a new org + company + team (all `flat`, empty graphs
— user can shape them later via the existing PATCH endpoints)
3. binds company under org (org_companies "n0")
4. binds team under company (company_teams "n0")
5. inserts team_members rows for every orphan agent (n1, n2, …)
6. inserts company_teams rows for every orphan team
7. inserts org_companies rows for every orphan company
8. commits, returns the created ids + moved counts
cm-api (routes/structure)
- GET /api/structure/orphan-counts → { agents, teams, companies }
- POST /api/structure/reify-orphans → { org_id, company_id, team_id,
moved_* }. Trims + rejects any empty name; validates before
starting the transaction so a 400 never rolls anything back.
Frontend
- New OrphanMigrationDialog: fetches counts on open, three name
fields (defaults: Organization "My Workspace", Company "General",
Team "Everyone"), POSTs on save. "Nothing to migrate" state
disables the save button when the workspace is already fully
wired. Copy explicitly notes that everything is renameable in the
sidebar afterward.
- Dashboard: onTreeSelect now branches on SYNTHETIC_TREE_IDS —
clicking a synthetic node opens the dialog instead of falling
through to the (nonexistent) selection. On successful reify,
router.refresh() so the sidebar + world viz reflect the new real
chain.
What this doesn't do yet (next commit)
- Wizard auto-materialize: when creating a team/company via wizard,
auto-create parent placeholders if they don't exist. Deferred so
this commit stays focused.
|
||
|
|
21ac35c8d4 |
research: spawn per-topic ZeroClaw team container on start (commit 1/3)
Commit 1 of the path-B (real per-topic isolation) plan. The
container spawns and its coordinates persist — nothing talks to
it yet; commit 2 wires ZeroClawDriveExecutor to prefer the topic's
URL when populated. This split keeps each landing verifiable.
Backend
- Migration 0038: research_topics gets zeroclaw_container_name +
zeroclaw_gateway_url columns. Both nullable so a topic can exist
before a spawn and teardown just NULLs them out.
- cm-db: ResearchTopic struct extended; get/list SELECTs updated;
new set_zeroclaw_container(id, workspace_id, name, url) helper
used both for spawn (Some/Some) and teardown (None/None).
- cm-api: bollard added as a workspace dep (matches cm-sandbox's
version). New research_container module:
· connect() → uses DOCKER_HOST when set (prod's socket-proxy
at tcp://socket-proxy:2375) else the local socket. Same
pattern cm-sandbox already uses.
· container_name_for(topic_id) → "research-<uuid>-team"
(deterministic so a re-start reattaches to the same
container instead of orphaning it).
· inherited_env() → propagates ZEROCLAW_*, OPENAI_*,
ANTHROPIC_*, GEMINI_*, GROQ_* from the parent server env
(provider config + tokens), stripping the server's own
ZEROCLAW_GATEWAY_URL/WORKSPACE so the team runtime doesn't
loop back on itself. Appends ZEROCLAW_GATEWAY_PORT=42617
and ZEROCLAW_WORKSPACE=/zeroclaw-data/workspace for the
team's own listener.
· spawn(docker, topic_id, repo_host_path, state_host_path):
- inspect: if the container already exists, start it if
stopped and return its coordinates (idempotent restart).
- else create with:
image = CLAWMATES_RESEARCH_TEAM_IMAGE or
clawmates-runtime:latest
cmd = [daemon, --host, 0.0.0.0]
env = inherited_env()
mounts = repo_host_path → /workspace/repo (rw)
state_host_path → /zeroclaw-data (rw)
network = CLAWMATES_RESEARCH_TEAM_NETWORK or
clawmates_core
labels = clawmates.role=research-team,
clawmates.research.topic_id=<uuid>
- creates state_host_path first so bind doesn't ENOENT.
· stop(docker, name) → stop + remove. Idempotent on 404/304.
- start_topic wires spawn after the clone completes:
· state root = CLAWMATES_RESEARCH_WORKSPACE_ROOT / <topic> /
state
· on success, persists (name, url) on the topic row so commit
2 can look them up when constructing the executor
· every failure (docker connect, docker create/start, DB
persist) is best-effort: logs and continues. A missing team
container leaves the topic pointing at the workspace-wide
gateway URL (env), preserving prior behavior.
Deploy prerequisites (not in this commit)
- The compose stack's clawmates_server service needs bind-mounts
of CLAWMATES_RESEARCH_WORKSPACE_ROOT (e.g.
/var/lib/clawmates-research:/var/lib/clawmates-research) so
paths the server writes to are visible on the host and the
spawned team container mounts the same underlying data.
- socket-proxy's ACL must allow POST + DELETE on /containers
(already the case in prod per the audited compose file).
|
||
|
|
3465bb7a6d |
research: persist bound repo + shallow-clone on start_topic
This is the minimum viable version of the "agents actually work on
a repo" architecture. Full vision (isolated ZeroClaw container per
topic, dynamic agent provisioning inside, pause/resume, commit
gate) is real weeks of work — this closes the first, most-visible
gap so the ClawHDF5 topic can actually run against its codebase.
Backend
- Migration 0037: research_topics gets repo_id UUID (nullable, FK
to repos ON DELETE SET NULL) and repo_workspace_path TEXT for
the on-disk checkout location. Index on repo_id when set.
- research_topics::create takes repo_id: Option<Uuid>. get + list
select it and repo_workspace_path. set_repo_workspace_path
persists the path once the first clone lands.
- CreateTopicRequest accepts `repo: Option<TopicRepoRef>` — the
same denormalized shape the wizard already sends. Only repo_id
is authoritative; other fields are ignored (dead_code-allowed
so serde still deserializes the full body).
- start_topic branches on topic.repo_id. When set, it calls
ensure_repo_workspace:
· resolves repo.clone_url + repo.default_branch
· target path = CLAWMATES_RESEARCH_WORKSPACE_ROOT
// <topic_id> // repo (defaults under $TMPDIR)
· runs `git clone --depth 1 --single-branch --branch <b>` via
tokio::process. Reuses the checkout if .git already exists.
· persists the path so re-starts skip the clone
· runs `git ls-files` to sample the tree (first 60 entries,
total count reported honestly so the prompt doesn't lie
about coverage)
All best-effort — a clone failure logs but still starts the run
without repo context rather than aborting.
- build_coordinator_task takes Option<&RepoContext>. When present,
the framing gets a REPO block (slug / path / branch / file
sample) and a USING THE REPO section instructing the coordinator
to ground every recommendation in a concrete file reference and
never fabricate paths. The per-topology bodies are unchanged —
the repo guidance sits above them so it applies to every shape.
What this unblocks / doesn't unblock
Unblocks: The coordinator prompt now knows the repo exists, where
it lives on disk, and what's in it. Even without file-editing
tools wired to the checkout, the coordinator can point spokes at
concrete modules and the final artifact can reference real files.
For a spec-shaped outcome like ClawHDF5's, that's the difference
between abstract advice and a spec grounded in the actual crates.
Does NOT unblock: The agents themselves editing files, running
tests, or committing. That requires either mounting the checkout
into the ZeroClaw sandbox or exposing a new MCP tool for
repo-scoped file ops — separate follow-up.
|
||
|
|
7c1af2e070 |
research: pipeline-running signal + spinners so users aren't guessing
The prior flow was ambiguous: after hitting Start research, status flipped to "processing" and a "Submit for review" button appeared immediately with no indication that anything was actually running. Users had to guess whether the pipeline was working or stalled. Backend surfaces the truth as a signal: - new topology_runs::active_runs_for_research_topic counts queued+running runs whose research_topic_id matches - TopicDetail includes runs_in_flight: i64 alongside the existing status field, so the canvas can distinguish "pipeline still working" from "runner stalled". ResearchCanvas is now honest about state: - while runs_in_flight > 0, the header status pill grows a cyan "N runs in flight" badge with an inline SVG spinner - the stage-explainer card turns cyan-bordered and shows a "pipeline is running" hint, plus copy pointing the user at the Agents tier where each teammate's activity streams live - the "Submit for review (manual)" button is HIDDEN while any run is in flight — it's an escape hatch for stalled runs only, not the happy-path action. It reappears if runs_in_flight drops to zero but the topic is still marked processing, so a stalled runner can still be nudged along. - the canvas polls getTopic every 4s while status is processing/ publishing or runs_in_flight > 0, so the spinner + outcome swap in automatically when the pipeline completes. ResearchList sidebar: - each row's status dot becomes a spinner when the topic's status is processing or publishing, matching the canvas at a glance - the list also polls every 6s while ANY topic is active, so transitions land in the sidebar without waiting on a parent bump. The poll is gated on a derived boolean to avoid effect thrash. Follow-up: same pattern belongs on LoopsList / LoopsCanvas for loop iterations in flight — same signal (queued+running runs per loop) but not wired here. |
||
|
|
316cdbf929 |
research sidebar: delete-with-confirm per topic
Mirror the row-level delete affordance the loops sidebar already
has. Loops was wired earlier; research had a bare title-only card
with no way to remove a stale topic.
- cm-db: research_topics::delete cascades via existing FK rules
(research_topic_agents, research_publish_approvals, and the new
research_outcomes all CASCADE on topic_id; topology_runs's
research_topic_id back-ref is SET NULL so historical runs stay).
- cm-api: DELETE /api/research/{id} → 204. Idempotent.
- Frontend: deleteTopic helper. ResearchList row is now a card
with the existing title/status/outcome header plus a trash icon
that flips the card into an inline "Delete topic + all outcomes?"
confirm strip. Confirm → red Delete / gray Cancel. If the
deleted topic was selected, selection clears; local counter
bumps the list refetch without waiting on a parent.
|
||
|
|
a2d3d85ebe |
research pipeline v2: topology-aware start + persisted draft
Three connected changes that turn "Start research" from a status flip into a real pipeline that produces a reviewable artifact: - Migration 0036: adds research_topics.topology_kind (default 'hub_spoke') and a new research_outcomes table (id, topic_id, version DESC, body_md, produced_by_run_id, created_at) so each run's final synthesis is versioned and persistent. - Wizard now has a topology picker in the Outcome step — hub_spoke / pipeline / hierarchical / star_moe — with copy that steers users to the right shape (Pipeline for research → distill → analyze → implement rosters, hub_spoke for the coordinator- and-specialists default). - start_topic reads the chosen topology_kind, parses it into a cm_topology::TopologyKind, and dispatches a per-shape coordinator prompt via build_coordinator_task. Pipeline explicitly tells stage 1 not to write the final artifact and propagates a "final stage MUST emit a complete markdown document with measurable acceptance criteria" instruction downstream. The graph builder is called with the topology the user actually picked instead of hard-coded HubSpoke. - topology_worker::freeze_research_outcome fires after every successful complete(). It looks up research_topic_id on the run; if set and final_output is non-empty, it inserts a new research_outcomes row (version auto-derived server-side via coalesce(max(version), 0) + 1). Best-effort — a DB hiccup logs but doesn't fail the run. - TopicDetail now includes topology_kind and latest_outcome. ResearchCanvas swaps in the outcome's body_md (rendered as pre-wrap markdown, versioned header, produced-at timestamp) whenever an outcome exists; the original prompt collapses into an "Original prompt" <details> below so it's still one click away. Pre-run topics still show the description as before. Follow-ups still open: reject-with-revision loop feeding the coordinator, publishing → published transition + real artifact export (md / pdf), and an approvals inbox surface for reviewers. |
||
|
|
984d9a1274 |
reap: cascade orgs → companies → teams → agents
Selecting a team/company/org in the Agents sidebar used to only
delete the grouping row; the agents inside survived, ungrouped.
Not what the user wanted, and it left a trail of orphaned runtime
state (containers, .brain files, DB rows) behind.
Backend — POST /api/claws/batch-delete is now a universal cascade
reaper. Body accepts { ids?, teams?, companies?, orgs? } in any
combination. The server walks org → companies_of_org →
teams_of_company → agents_of_team, dedupes against explicit ids,
and hard-purges every unique agent (deprovision ZeroClaw runtime,
tear down sandbox container, unlink .brain/.onion files,
transactional agents::hard_purge). Group rows are deleted last; FK
cascades on team_members, company_teams, org_companies, and
loop_agents/teams/orgs clean up the join tables. Every stage
streams SSE.
Three new cm-db helpers wire the walk: agents_of_team,
teams_of_company, companies_of_org — all DISTINCT selects on the
existing join tables.
Frontend — Dashboard's Agents-tier StructureTree now sets
selectLevel="*" (was "claw"), so the Wrench → checkbox affordance
appears on org/company/team/agent nodes alike; the same-level
invariant in onToggleSelect still prevents mixed batches.
ReapProgressModal collapses to a single POST regardless of kind —
body key derived from kind — and its subtitle is honest:
"Cascading through every agent inside — permanent."
|
||
|
|
6e10528bc2 |
loops: restore .sqlx entries for test-only queries
An earlier `cargo sqlx prepare --workspace` (without --all-targets) deleted two query cache files used only by tests (cm-runtime/tests/run_loop.rs and one topology_runs insert). CI builds with SQLX_OFFLINE=true and needs them present. Regenerate with --all-targets to include the integration test binaries. |
||
|
|
a6da19430f |
loops: repo picker + agent/team/org staffing + sidebar edit/delete
Adds the missing pieces the wizard needed and the sidebar controls around it: - LoopsWizard is now a 6-step flow (identity → repo → task/topology → triggers → repeat → assign agents) plus the existing secrets card. ResearchWizard picks up the same repo step and a hard gate when the workspace has zero agents. - New LoopStaffingStep with three tabs — Individual / Team / Organization — that mix freely per loop; selections persist via new loop_agents / loop_teams / loop_orgs join tables (0035 migration), each cascading on loop_id so hard-delete stays a single-row DELETE. - Backend CreateLoopRequest / UpdateLoopRequest accept the three lists and apply_staffing does a transactional replace-all; list_loops / get_loop hydrate the lists via a flattened LoopWithStaffing response. - LoopsList sidebar gains per-row enable/disable, edit (reopens the wizard prefilled with the current loop, PATCHes on submit), and delete with an inline confirm. - NoAgentsGate blocks launching a loop or research topic from a workspace with no roster; the sidebar `+` buttons also disable with a tooltip pointing at the TEAM tier. Not yet wired: the run driver still fills role slots from the workspace-wide pool; teaching enqueue_iteration to prefer loop_agents/loop_teams/loop_orgs is a follow-up. |
||
|
|
637e1bdd69 |
repos: sidebar actions (sync/edit/remove) + edit modal
Sidebar: - Each connection header now has three inline icon buttons: Sync now (spins while in flight), Edit (opens the modal), Remove (opens an inline confirm strip). Removes cascade repos via ON DELETE CASCADE. - The connection's last_sync_error surfaces as a red inline banner under the header — no more 'error status with nowhere to see why'. - Sync is POST /api/repos/connections/:id/sync (already existed); after either sync or delete the sidebar re-fetches so state stays consistent. Edit modal (RepoConnectionEditModal): - Loads GET /api/repos/connections/:id, pre-fills owner/base_url/label - PATCHes only the fields that actually changed; empty string on a Some(&str) field sends explicit null so the backend clears it - Sync-now + Remove reachable from inside the modal too - Rotating the token is out of scope: the modal says as much and points the user at delete + re-create through the wizard (the broker doesn't expose an update path, and rotating in place would require duplicating the whole broker->store_secret flow here) Backend: - GET /api/repos/connections/:id — same ConnectionSummary shape - PATCH /api/repos/connections/:id — owner/base_url use Option<Option<T>> double-nesting so 'omit = leave alone' and 'null = clear' round-trip distinctly through serde - repo_connections::update with COALESCE-per-field so the SQL matches the double-Option semantics without an OR-chain per field |
||
|
|
6d087bf537 |
repos: backend — schema, /api/repos routes + GitHub sync provider
Migration 0034: two tables. repo_connections carries the workspace's per-provider config (owner, base_url, label, last_synced_at, last_sync_error) and points at an app_connections row for the PAT. repos is the per-connection cache with (connection_id, external_id) unique so upsert is idempotent across re-syncs. Cascading deletes clean up cleanly on connection removal. cm-secrets grows a FetchAuthorized op — GET with the stored PAT injected as bearer, returns status + JSON body without ever exposing the credential to cm-api. This is the least-privilege door for read-only provider APIs (list repos), distinct from the InvokeHttp path that still requires a single-use approval grant for outbound writes. cm-api::routes::repos wires: - POST /api/repos/connections (broker store_secret + insert both rows + initial sync + mark_synced) - GET /api/repos/connections - DELETE /api/repos/connections/:id - POST /api/repos/connections/:id/sync - GET /api/repos (500 cap, newest provider_updated first) - GET /api/repos/:id (full detail incl. clone_url + html_url) GitHub provider inline for v1 — paginated pull of /orgs/:owner/repos (when owner set) or /user/repos (when absent), 100/page, capped at 20 pages (~2k repos) to keep first-sync latency bounded. Non-2xx surface back to the caller as sync_error; parse failures are best-effort per repo (skipped, logged, don't abort the batch). Gitea + GitLab providers land in a follow-up — mostly URL swap + response-shape adapter. |
||
|
|
806ba869e5 |
teams: ephemeral lifecycle for Scheduled + Triggered planner modes
Migration 0033: adds teams.lifecycle ('permanent' | 'ephemeral') and a
topology_runs.team_id back-ref with a partial index for the sibling-in-
flight check.
cm-db repo:
- teams::insert_team_with_lifecycle (insert_team keeps the permanent default)
- topology_runs::enqueue_run_for_team (populates team_id)
- topology_runs::check_ephemeral_teardown — atomic SELECT that only
returns Some when the team is ephemeral AND no siblings are still
queued/running; carries the workspace + bound claw ids for cleanup.
cm-api:
- topology_worker post-terminal hook maybe_teardown_ephemeral_team
runs deprovision_claw on each bound claw (best-effort; failures log
but don't block Postgres deletion), then hard_purge each agent row,
then delete_team.
- routes::teams::build_team_with_lifecycle (build_team keeps default);
run_team enqueues with team_id.
- planner ScaffoldRequest gains mode; lifecycle_for(mode) sets the team
to ephemeral for scheduled + triggered, permanent otherwise.
Frontend MasterPlannerModal passes mode in the scaffold payload so the
backend can derive lifecycle without duplicating the mode taxonomy.
Tests: 3 new (returns claws when no siblings, holds when siblings queued,
ignores permanent teams). 10/10 topology_jobs green; workspace clippy
--tests clean.
|
||
|
|
8a4e222aec |
research: auto-transition processing → reviewing on last run
Hooks the topology_worker's post-terminal path into a new notify_run_completed repo helper that atomically transitions the topic processing → reviewing when the completed run has research_topic_id set AND no siblings for that topic are still queued or running. Guarded on status='processing' so a retry, a re-fire, or a topic already past processing are all no-ops. Best-effort at the worker; DB hiccups are logged and never fail the run. The manual /submit-review endpoint stays as an escape hatch for topics that end up parked in processing with nothing to complete (updated the doc comment). |
||
|
|
6d1dda6197 |
loops: iteration timeline — GET /api/topology-runs?loop_id=X
Extend the topology-runs list route with an optional loop_id filter that returns iterations for a single loop, newest-iteration-first. Adds the iteration and finished_at columns to the summary (skip-null on the JSON so compares stay compact). Backed by list_by_loop in the repo, which uses the existing topology_runs_loop_idx partial index. LoopsCanvas fetches the runs in parallel with the loop detail and renders an iteration timeline card (iteration #, status pill, start time, duration, run id prefix) between the graph section and the actions row. |
||
|
|
973eeb272e |
research: publish approval gate + explicit state transitions
Fourth commit of the Research + Loops arc. Completes the state machine
for research topics with the publish approval gate the spec asked for.
Migration 0032 — research_publish_approvals
Dedicated small table (id, workspace_id, topic_id, requested_by,
status, decided_by/at, created_at). Keeping it separate from the
existing `approvals` table (0001) because that one is tightly coupled
to gated tool calls inside an agent run — session_key + run_id +
action_type + category + payload + preview + requested_by_agent, all
NOT NULL. Forcing those nullable would ripple through cm_safety;
cleaner to give publish approvals their own two-transition state
machine.
New endpoints
POST /api/research/:id/submit-review processing → reviewing
(v1 caller-driven; the
orchestrator hook comes
when we wire actual runs)
POST /api/research/:id/request-publish creates a pending
approval. Rejects with
409 if the topic already
has one open.
GET /api/research/publish-approvals list workspace's pending
POST /api/research/publish-approvals/:id/approve flips approval to
approved + transitions
the topic
reviewing → publishing
(which stamps
published_at)
POST /api/research/publish-approvals/:id/reject stays in reviewing; new
requests allowed
The approve/reject write is an atomic UPDATE ... WHERE status = 'pending';
the decide() repo function returns whether the caller won the race so
concurrent double-approves collapse to a single topic transition.
State machine after this commit:
standby ─POST /start─▶ processing ─POST /submit-review─▶ reviewing
─POST /request-publish + approve─▶ publishing ─(future: artifact
assembly)─▶ published
|
||
|
|
4b48c521eb |
loops: backend routes + repo + cron scheduler + HMAC webhook
Third commit of the Research + Loops arc. Lights up loops as durable
recurring topology executions:
GET /api/loops list workspace's loops
POST /api/loops create — returns webhook_token +
signing_key ONCE when webhook trigger
is enabled; never exposed again
GET /api/loops/:id detail
PATCH /api/loops/:id update definition
DELETE /api/loops/:id delete
POST /api/loops/:id/run trigger one iteration NOW
POST /api/loops/:id/enable set enabled=true
POST /api/loops/:id/disable set enabled=false
POST /webhooks/loops/:token public; HMAC-SHA256-verified
Scheduler (cm_runtime::spawn_loop_scheduler) wakes every 10s, queries the
partial index on (next_fire_at) for due loops, enqueues one topology_runs
row per fire with loop_id + iteration + parent_run_id chained back to the
previous iteration. Uses croner via the existing scheduling::next_occurrence
helper. Missed windows fire ONCE and skip the backlog — next_fire_at is
always computed strictly AFTER now(), so a late scheduler doesn't drain a
buildup.
Webhook signatures follow the same pattern as the Stripe billing webhook
(HMAC-SHA256 with constant-time hex compare). Token + signing key are
24-byte OS-RNG values; the URL uses base64-url for the token, and the
signing key is base64-std. Both surface exactly once at create time.
All three fire paths (scheduler, immediate-run, webhook) funnel through
`cm_db::repo::loops::enqueue_iteration` so the invariants stay in one
place. `iters` repeat policy is enforced by the scheduler tick; `until`
and `on_completion` land with the orchestrator hook in commit 4.
Adds cm-llm as a direct cm-api dep, getrandom for the webhook material
generator, and wires the scheduler spawn into the server binary alongside
the resume sweeper and outbox drainer.
|
||
|
|
fb047879c2 |
research: backend routes + repo + wizard refine (behind /api/research)
Second commit of the Research + Loops arc. Builds on 0030 by lighting up
the container CRUD, the agent-slot attach/detach, the standby→processing
transition, and the wizard's one-shot LLM refine.
GET /api/research list workspace's topics
POST /api/research create (accepts wizard output)
GET /api/research/:id detail (topic + attached agents)
PATCH /api/research/:id update non-status fields
POST /api/research/:id/agents attach agent (idempotent)
DELETE /api/research/:id/agents/:agent detach
POST /api/research/:id/start standby → processing
POST /api/research/wizard/refine one-shot LLM refine
The refine endpoint accumulates the workspace's default LLM provider's
stream into a JSON object (`{title, description}`) with the tight system
prompt at the top of the module. Same provider routing as agent runs
(via runtime.provider()), so a workspace already using GLM/Kimi gets it
for free.
State machine's remaining transitions (processing → reviewing on last
run_completed; reviewing → publishing via approvals gate) land with the
orchestrator hookup + approvals extension. Publish approval and loops
are separate commits still to come.
Adds cm-llm as a direct cm-api dep (previously only pulled transitively
via cm-runtime) so the refine endpoint can build a ChatRequest. Uses
sqlx::query! for compile-time verification; .sqlx cache generated on
morpheus against a fresh migrated DB.
|
||
|
|
e5e049921f |
migrations: 0028 — backfill ON DELETE clauses on legacy FKs
Retrofits the ON DELETE pattern learned after v1 shipped (CASCADE for tenant-scoped children, SET NULL for historical references, RESTRICT where the domain type is non-Option) onto tables from 0001-0006 and 0026. Two deliberate exceptions kept as NO ACTION: audit_log.workspace_id (audit is append-only and must outlive workspace deletes) and thread_messages.from_agent (history stays attributable via agents.deleted_at). Two stay NOT NULL as RESTRICT (agents.managed_by, installed_skills.installed_by) because the Rust domain type is UserId, not Option<UserId>. Also includes the previously-orphan .sqlx cache for the usage_events query in cm-runtime/tests/run_loop.rs, which needed re-recording after the FK changes touched the metadata. |
||
|
|
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]>
|
||
|
|
3554a3aaf2 |
CI: remove k8s stages, fix the Docker-level pipeline green
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]>
|
||
|
|
148705769f |
Reaping: sandbox orphan reaper + DB expiry/retention sweeper + volume-init retry
Closes the cleanup gaps found in review (latent today; bites under load/crashes).
Sandbox containers (cm-sandbox / cm-runtime):
- label every sandbox `clawmates.sandbox={agent|browser}` at create
- SandboxDriver::list_managed(kind) (Docker label filter + K8s label selector)
- SandboxManager::reconcile_orphans(ttl) + spawn_reaper: removes engine
containers no live handle owns (ZERO = all)
- boot reconciliation (every pre-existing sandbox is an orphan from a dead
process) + periodic reaper (5m interval / 10m TTL)
- SIGTERM graceful drain: serve().with_graceful_shutdown → shutdown() both
managers so a redeploy can't leak; destroy errors now logged not swallowed
DB expiry/retention (cm-db cleanup.rs + cm-api cleanup_sweeper, hourly):
- expire auth_sessions + oauth_states past expires_at (security)
- prune sent outbox(7d), run_events(14d), routine_runs(30d), terminal
topology_runs(90d), consumed execution_grants(7d)
Compose: volume-init restart "no" → on-failure:5 (retry instead of wedging boot).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
||
|
|
540c74f42e |
Adopt design comps: dark system, new landing/auth, dashboard shell + canvas
Re-skins the whole app to the dark design comps and wires the new surfaces to
the backend (the /api proxy + auth + schemas are unchanged).
Design system:
- globals.css: remapped @theme tokens to the comp palette (#08080a base, coral
#ff6f61, status cyan/green/amber/purple/teal); token names preserved
- MeshMark: triangle + 3-node brand glyph; cm-flow/cm-blink/cm-halo keyframes
- marketing flipped light → dark
Backend (migration 0012):
- agents.model_binding (persisted on team deploy) + GET /api/claws/{id}/runtime-config
- routine_runs table + scheduler journaling + GET /api/routines/runs
- GET /api/claws/{id}/compartments (anatomy aggregate)
- GET /api/structure/stats (workspace counts)
Frontend:
- Landing: full dark marketing page (hero constellation, deploy ladder,
12-topology taxonomy, recursive execution, compare/Pareto, safety, self-host)
- Auth: dark split-panel AuthShell + comp LoginForm + Clerk SignIn themed dark
- Dashboard shell: TopBar (breadcrumb + live stats + deploy + user) + StatusBar
(runner/sandbox/doors); rail slimmed to 60px + 252px context column
- ConstellationCanvas (radial recursive) replaces the graph view in StructureCanvas;
selecting a claw opens ComputerPanel (apps/now-running/dock); RoutinesPanel
- Claw anatomy view (/claws/[id]/anatomy) from compartments + runtime-config
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
||
|
|
3eca4ed70c |
Recursive deploy ladder: Company + Org tiers, mesh mark, two-tier rail
Completes the scale ladder (single → team → company → org). Every tier is a
topology whose nodes are the tier below; running a parent recursively runs each
child's sub-topology down to the leaf claws.
Backend:
- migration 0011: companies/company_teams, orgs/org_companies, topology_runs.tier
- cm-db repos for companies + orgs (mirror teams)
- TurnRequest.attrs (forwarded from node.attrs) for child-id binding
- SubTopologyExecutor (recursive_exec.rs): a parent "turn" runs the child's
sub-topology; durability via parent updated_at keepalive + cancel propagation
+ depth cap; boxed future breaks the org→company recursion
- topology_worker selects executor by job.tier
- routes: /api/companies, /api/orgs (create/list/get/run) + unified
/api/structure/{level}/{id} for the zoom canvas
Frontend:
- MeshMark: node-mesh brand glyph (replaces the claw PNG), tier variants
- TopologyGraphView: optional onNodeClick/nodeMeta + dark-token theming
- StructureCanvas + Breadcrumb: one recursive zoom view for every tier
(drill down on node click, breadcrumb up); TeamRunPanel extracted + shared
- two-tier Discord-style rail: StructureRail (mesh mark + org/company/team
glyphs + tools popover + deploy + user) | RosterColumn (selected group's
children, or your claws); SecondaryNav for cross-cutting tools
- ComposeWizard (company/org) wired into DeployWizard; /companies + /orgs pages
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
||
|
|
8123a27bcf |
Teams (deploy ladder rung 1): schema + provisioning + API
A team = a baseline topology staffed with real claws. Migration 0010 (teams + team_members node→claw bindings) + cm-db repo/teams.rs. runtime_provision.rs turns a claw into a live runtime agent claw_<id> via the synced gateway config API (#7468): create agent + bind model_provider (mapped from chosen model) + risk_profile=toolfree + clawmates_door bundle — atomic, immediately drivable. routes/teams.rs: POST /api/teams (create claws + provision + build(kind,roles) + bind node.attrs["agent"]=claw_<id> + persist), GET /api/teams[/{id}], POST /api/teams/{id}/run (enqueue a durable run of the team graph — reuses the topology worker + SSE). v1 persona = topology role via the prompt builder; the claw's system_prompt stays its chat identity. Spike confirmed: runtime agent provisioning works; IDENTITY.md persona works for API models (Gemini/Groq), masked by CLI models (Claude/Kimi Code). 16 cm-api tests + provision unit tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
e8486ee57c |
Door delivery: outbox drainer + SMTP transport (inert until configured)
The §15 door's email_send queues to `outbox` but nothing delivered it. Add a real transport: cm-db outbox repo (list_queued/mark_sent/mark_failed) + a cm-runtime drainer — an EmailSender trait (testable), a lettre STARTTLS LettreSender, drain_once (queued -> sent/failed), and spawn_drainer wired into the server beside the scheduler/sweeper/topology-worker. Config-gated: inert (logs "outbox delivery DISABLED") until CLAWMATES_SMTP_* is set, so it ships safely before credentials exist. The agent never holds the SMTP credential — it only writes to outbox through the gated door; the server owns the transport. NOTE: live delivery is still credential-blocked — Migadu's API can't send (SMTP-only) and the admin token is invalid; no SMTP creds exist. The transport is built + tested (drain_marks_sent_and_failed via a mock sender); set CLAWMATES_SMTP_* to go live with zero further code. clippy clean. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
6e87433c66 |
Close gaps: GLM-judge (anthropic registry) + run cancel + deep-link
#2 GLM judge (closes #114): registry NamedProvider gains a `format` field; build_provider_registry builds an AnthropicProvider for format="anthropic". GLM's coding/OpenAI endpoint is ToS-throttled for raw SDK, but its Anthropic endpoint (api.z.ai/api/anthropic) accepts raw API calls (verified x-api-key -> glm-4.7), so CLAWMATES_JUDGE_MODEL=glm:glm-4.7 routes the door governor / topology judge through GLM with no runtime-routing. (Kimi-as-judge still needs a Platform key — coding key is agent-only.) #3 run-control: POST /api/topology-runs/{id}/cancel (workspace-scoped, queued/running only); the worker honors it at the step boundary (checks current_status in the checkpoint callback) and won't clobber a cancel with `failed`. Frontend Run tab gains a Cancel button and clickable recent runs that deep-link into a live/replayed stream (SSE replays from checkpoint). cm-* tests (incl. new cancel test) + clippy + frontend lint/typecheck green. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
8dee01c77b |
Durable topology jobs (1/4): job-table schema + repo
Evolve topology_runs into a durable-job table (migration 0009): status state machine (queued/running/completed/failed/cancelled), kind, input graph, per-step checkpoint, error, last_event_id, timestamps. comparison becomes nullable (the result blob, absent until completion). Back-compat: existing rows default to completed/compare. cm-db repo gains the durable-job ops: enqueue_run, claim_next_queued (CAS via FOR UPDATE SKIP LOCKED), checkpoint, complete, fail, requeue_stale (resume sweep), and status(). Regenerated .sqlx cache. Also fix two pre-existing test RuntimeConfig literals missing the providers field (from the registry work). Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
7baf2082d0 |
feat(topology): persist comparison runs + history
Save each comparison and let users reload past ones.
- migration 0008: topology_runs (workspace-scoped; full comparison as JSONB).
- cm-db repo::topology_runs (insert / list_recent / get) + regenerated .sqlx.
- cm-api: compare persists best-effort (never loses the LLM result on a DB
hiccup); GET /api/topology-runs (recent) + GET /api/topology-runs/{id}.
Integration test asserts persist → list → get.
- frontend: "Recent comparisons" list on the Compare tab; click to reload a
saved run. e2e p8 green (39 suite); offline build + clippy clean.
Server self-migrates at boot (cm_db::MIGRATOR), so 0008 applies on deploy.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
|
||
|
|
6071e1fd61 |
fix(auth): make JIT provisioning idempotent under concurrent first-login
On a user's first login the authed shell fires several API calls at once; each ran the JIT-provision path and raced to INSERT the same new user row, tripping the partial unique index on auth_subject. The losing requests 500'd and the post-login SSR errored out. Use INSERT ... ON CONFLICT (auth_subject) DO UPDATE ... RETURNING so concurrent callers converge on the row the winner created. Regenerated the .sqlx offline query cache. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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]> |