5b49d5a1a8f66b0e10dd7b701f8a018b9d36c020
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f7f3dfe495 |
feat(fleet): GLM as a real microVM backend, and per-role models for claws
Three threads, all of which end at the same place: a mission whose verifier does
not share a model with the coder it reviews.
**GLM has a credential contract now.** `microvm_credential_for` returned one env
var name, which quietly assumed every provider reads its secret from the same
place Anthropic does. It returns a `Credential { source, target }` instead —
z.ai's key lives in the server's `ZAI_API_KEY` and Claude Code reads it as
`ANTHROPIC_AUTH_TOKEN`, and collapsing those two names is what forces a guess at
the other end. A wrong guess here sends one provider's credential to another
provider's endpoint.
`images/agent-glm` is the same CLI at the same pinned version as `agent-claude`
with `ANTHROPIC_BASE_URL` baked in. The split is deliberate: the ENDPOINT is a
property of the image, the CREDENTIAL is a property of the turn. That makes the
dangerous mix-up unrepresentable — a GLM VM cannot be handed an Anthropic
subscription token, and a claude VM cannot be pointed at z.ai. Asserted both
ways, because "the GLM VM must not carry CLAUDE_CODE_OAUTH_TOKEN" is the
property that costs a credential if it ever stops holding.
Kimi stays refused. `KIMI_API_KEY` is set and Moonshot serves an
Anthropic-compatible API, but I have not verified its base URL against the
running service, and this function is precisely where guessing a URL is
expensive. It becomes an arm the day someone measures it.
`api.z.ai` joins the node's default egress allow-list. A default that cannot
run the images we ship is a trap rather than a policy — the alternative is an
operator discovering it as a hung agent with no model access.
**Per-role models for claws** (migration 0071). `template_roles` had no model
column, so `mint_team_from_template` bound every role of every mission team to
one literal — a template whose whole point is an independent reviewer minted a
reviewer sharing a model with the coder. A role may now name its own; roles that
say nothing still take the mint's default, so every template written before this
behaves exactly as it did. The literal is now that default rather than a
hardcode.
**A harness scenario for the roster flow.** `verify-mission-delivery.sh roster`
runs the whole Slice 5 loop — planner proposes, human approves, mission runs —
and asserts the roster LANDED on the mission row rather than trusting the API's
answer. That distinction is not theoretical: the first live approval returned an
error while leaving the proposal marked approved.
Built and proven on tank ahead of the deploy: `clawmates/agent-glm:dev` reports
`2.1.223` and `BASE=https://api.z.ai/api/anthropic`, and
`fc-build-rootfs.sh … glm 8G` boots a VM from it that has git, can write
/mission, and answers `claude --version`.
533 tests pass, clippy clean. Migration 0071.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
1797669296 |
feat(missions): Slice 5 — let a model size the mission's team
`routes/planner.rs` has had Opus proposing rosters since the Master Planner
shipped, and none of it ever reached a mission: the proposal lived in React state
and died with the tab. A mission's shape came from a team template instead —
fixed roles, and every claw minted `claude-sonnet-5` from a literal in
`mint_team_from_template`. That literal is why no mission has ever run more than
one provider.
A roster is `(topology_kind, [(role, backend)])`, which is exactly what the
composed executor already consumes: `Roster::graph` builds a `TopologyGraph` with
the backend in `attrs`, and `MicroVmTurnExecutor` reads `attrs["backend"]` per
node. So a verifier on another provider's rootfs stops being a bolt-on and
becomes a graph node — the correlated-failure break the independent judge exists
for, one layer down.
Three verbs, and the split is the point. **suggest** asks the model and persists
the answer, changing nothing. **decide** approves (writes `config.roster` and
switches the mission to the composed engine) or rejects. A proposal is never
applied on arrival: a model sizing a team is a suggestion about how many VMs to
boot, and this codebase treats model output that costs money as evidence for a
decision, not the decision.
Fail-closed at every seam, because each of these otherwise surfaces much later
and much more expensively:
- a backend no ONLINE node can boot is refused when PROPOSED, naming the ones
the fleet actually has. Placement would refuse it too — at launch, after the
roster was approved and someone believed the mission would run. The model is
handed that same list in its prompt, so the usual case never arises.
- an invented `topology_kind` is refused, not defaulted. `parse_topology_kind`
defaults to hub-spoke, which is right for a template we wrote and wrong for a
string a model just produced: running a `pipeline` proposal as a hub-and-spoke
changes what every node sees and nothing would say so.
- the roster is validated BEFORE it is stored, so a stored proposal is always
one that could be approved; and again at approval, against the fleet as it is
then — a node can go offline in between.
- `MAX_MEMBERS = 6`. Each member is a whole VM, not a subagent, and a model
asked to size a team proposes twelve happily.
Two properties live in SQL rather than in the handler: at most one approved
roster per mission (partial unique index — two approved rosters are two answers
to "what shape is this mission", and the executor reads one field), and
decide-once (`WHERE status = 'proposed'`, so a double-clicked approve claims
nothing the second time). Both tested against a real database, including that the
second approval is refused by Postgres rather than merely losing a race.
NEGATIVE CONTROL, run rather than assumed: with the roster preference removed
from `composed_graph`, `an_approved_roster_outranks_the_template` FAILS — 3 nodes
from the template instead of the roster's 2. A stored roster that is silently
ignored at launch is precisely the shape this project keeps paying for.
Not closed: per-role models for CLAWS. `template_roles` has no model column, so a
ZeroClaw team still mints one model for every role. The literal is now a named
constant that says so and points at the roster path, rather than sitting inline
where nobody reads it.
527 tests pass, clippy clean. Migration 0070. Not yet exercised against the
deployed stack — the route has never been called with a live model.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
12147a1e01 |
feat(missions): Slice 4 — the two engines composed, with the file handoff proven
`team_engine='composed'` (the third name migration 0069 anticipated) runs a
mission as a durable ZeroClaw graph whose every node is a whole
Claude-Code-in-a-microVM session. Engine Z owns checkpoint/resume, cancellation
and per-node heterogeneity; Engine C owns shared context and cheap fan-out;
neither has the other's asset, which is why this is a composition and not a
compromise.
`MicroVmTurnExecutor` implements the existing `TurnExecutor`, so it inherits the
planners, the checkpoint, the stale-run recovery, `close_finished_phases`, the
evaluator, capture and delivery unchanged — the same trick `SubTopologyExecutor`
already plays with a heavy `run_turn`. Producer side emits ONE `queued` row
carrying the real graph and lets the worker claim it: the durability IS being
worker-driven, and the solo path's `tokio::spawn` has none of it. Still exactly
one `topology_runs` row per unit of work and one completion path — `finish()` is
now that one place, shared by every tier.
THE TRAP, solved and proven. A VM is inject → run → collect → destroy, so a
per-node VM with text-only handoff silently loses every file an earlier node
wrote: node 2 boots from the original checkout, sees nothing, and still reports
success. The mission's host checkout is the medium — every node injects from it
and collects back over it — and two properties make that safe rather than lucky:
`execute_resumable` is strictly sequential, so two VMs never write one directory;
and the vm id is deterministic per (phase, iteration, step), so a duplicate is
refused by the node ("vm already exists") instead of becoming a second writer.
NEGATIVE CONTROL, run rather than assumed: with `repo` swapped for a private
per-node workspace, `a_later_node_sees_an_earlier_nodes_files` FAILS with
`saw:[]`; restored, it passes. The `PhaseVm` seam exists for exactly this — it
models inject/collect through the real `mission_fs` tar path in milliseconds.
Two durability traps this tier walks into, both closed:
- `requeue_stale` fires at 180s on `updated_at`, and one node here can run for
an hour. `SubTopologyExecutor` keeps its parent alive from each leaf step;
there is nothing between the start and end of a VM turn, so the turn holds a
ticker that touches `updated_at` every 30s and aborts on drop. Without it a
healthy composed run is requeued mid-node and boots a second VM.
- the 15-minute stuck-run reaper asks "any step records since it was CREATED?",
which describes a healthy composed run as readily as a wedged one. Hence
`REAPABLE_TIERS` — worker-driven minus this tier. Reaping it would be #54 in
a different costume.
`on_launch` mints no team for a microVM mission, deliberately: claws in
containers are what a VM mission does not use. So `mission_orchestrator::
composed_graph` builds the shape from the team template directly — nodes, roles
and pattern, zero claws provisioned. Per-node `attrs["backend"]` and
`attrs["node_id"]` override the mission's, which is what makes a validator node
on another provider's image a first-class graph node; a malformed `node_id`
fails the node rather than quietly running it where the graph did not ask.
Refusals are recorded as a failed run, not returned as an error: `launch_phase`
is swept every ten seconds, so a returned error is a phase that retries forever
while the log repeats itself.
501 tests pass, clippy clean. NOT yet proven end to end: no composed mission has
run on the fleet, so the resume-after-a-killed-worker leg is argued from the DB
test and the step-numbering test, not from a real two-node run.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
7696009b25 |
fix(fleet): name the BACKEND in a placement refusal, not just "microvm capability"
Observed on the negative control: a mission with backend='kimi' was correctly refused, but the message read "no online node reports microvm capability" — and both nodes do report it. What one lacked was the image. That first clause would have sent an operator to reinstall firecracker on a node that already had it. The refusal now names the backend, and the remedy still names both halves. |
||
|
|
d9f53a3f96 |
fix(fleet): placement requires the backend's rootfs image, not just KVM
The first real microVM mission was placed on morpheus because it reports
{"microvm": true}, while only tank had rootfs-claude.ext4. It failed by name
rather than booting the wrong image — but whether a mission ran came down to
which capable node was listed first, which is a coin flip dressed as scheduling.
`missions.backend` was invisible to the scheduler.
The node now enumerates the images on its disk and reports them as a `rootfs`
ARRAY. `microvm::available_backends` lives beside `rootfs_for`, its inverse,
because the two must agree on what a backend name means; split apart, one drifts
and the scheduler starts promising images the booter cannot find. It only
advertises names `rootfs_for` would accept, and reports an empty array rather than
omitting the key — set_capabilities REPLACES, so a deleted image stops being
advertised instead of leaving a stale claim.
`nodes::online_for_backend` requires microvm AND that the node's list contains the
mission's backend. A node on an older daemon has no `rootfs` key and matches
nothing: unknown is not permission, the same treatment every other capability
gets. `backend_key` maps the three spellings of "the default image" to the one
name the node advertises, and is tested — a mismatch there would reject every node
for an ordinary mission with no backend set.
The launch error now names both halves of the fix, since "no capable node" was
true but unhelpful when the node was capable and merely lacked the image.
Mission gains `backend` on the domain struct; it was a column the executor read
from the phase query while the struct that placement uses could not see it.
464 tests pass, clippy clean.
|
||
|
|
1cd81a8b2a |
fix(missions): place a microvm mission before returning from on_launch
Self-inflicted, one commit old, and found by running a real mission: the early return I added for "a microvm mission materialises no team" sat ABOVE the microVM placement block in the same function, so on_launch returned before ever choosing a node. The mission then failed with the executor's own guard — "mission has no target_node_id ... a microvm mission cannot run on the gateway, which has no /dev/kvm" — which is the guard working exactly as designed, on a cause one layer further up. Placement now runs first. Worth noting the shape: adding an early return to a long function silently skipped everything below it that the same runtime_kind depends on. 461 tests pass, clippy clean. |
||
|
|
521b8dea10 |
fix(missions): the third team gate, and a container a microvm mission never uses
on_launch demanded a team template too — "pick teams in the wizard" — so a microvm mission still could not launch after the first two gates were exempted. Three separate places required a claw graph for a path that runs one `claude -p` inside a VM: routes::missions (draft→running), phase_runner::launch_phase (no matching teams → stay pending), and here. Returning before team materialisation rather than filtering its picks: claws that never run are not a cheaper version of the same thing, they are a runtime binding and a pairing code describing something nothing speaks to. Also stops provisioning the per-mission ZeroClaw container for a microvm mission. The first real run was observed starting one and leaving it holding a pairing code and ~3 GB of image for the life of a mission that never contacts it. 461 tests pass, clippy clean. |
||
|
|
4f07430e92 |
feat(missions): B4.5 — phase_runner runs a microvm mission in a VM
`runtime_kind='microvm'` placed a mission on a KVM-capable node and then nothing
executed it: config accepted without a reader, one of the four seams this project
keeps closing. This is the reader.
`microvm_executor` — inject → run → collect → destroy, the shape copy mode
already proved for containers with a VM boundary instead of a namespace one. The
checkout goes in as a tar, the work comes back as a tar over the SAME host path,
so `mission_delivery::capture_phase_diff_at` needs no change at all.
The agent is told NOT to push, unlike the container path's session prompt. Two
reasons: delivery is already host-side and diffs the collected tree against the
recorded clone point (covering committed, staged and unstaged work in one pass),
so pushing would add a second untested way for work to arrive; and pushing would
mean forge credentials inside the VM, when the point of collecting is that the
guest never holds them.
Exactly ONE topology_runs row (tier='microvm'), mirroring launch_direct_session:
close_finished_phases, evaluation, capture and delivery all key off those rows,
and a second completion path would be a second way for a phase to finish with one
of them untested. The row and the phase flip happen BEFORE any fallible VM work,
so a missing token or a node that lost its capability shows up as a failed run an
operator can see — not a phase that stays pending and retries every ten seconds.
Fail-closed points, each the reader for a guarantee built earlier:
- credentials resolve BEFORE the VM boots, so a missing subscription token
fails the phase instead of booting a VM whose agent sits unauthenticated
- a VM reporting egress:false is REFUSED, which is what makes create's
egress/egress_host/egress_guest fields more than decoration — a turn without
egress does not fail, it hangs
- the injected checkout is PROVEN present in the guest before an agent turn is
spent on it; an inject that reports success while landing nothing would
otherwise become an agent reporting an empty repository
- work is collected even when the agent exits non-zero — a turn that failed
partway still wrote files, and a retry needs to see them
- a turn that ran but could not be collected is a FAILED phase, not a happy one
- destroy runs on every exit path, or an 8 GB sparse rootfs leaks
Two integration gaps found while wiring, both of which would have produced a
mission that completed having delivered nothing:
- `capture_finished_coding_phases` pulls work out of a CONTAINER before
capturing. A microvm mission has none, so the docker connect would fail, the
loop would `continue`, and capture would be skipped forever while the phase
sat marked completed. Its work is already collected by the executor.
- `launch_phase` provisioned a runtime container, copied the checkout into it
and wrote a runtime binding + pairing code describing a runtime nothing uses;
and the orchestrator's workspace pin — deliberately FATAL — would have failed
a microVM launch on a container it was never going to use.
461 tests pass, clippy clean.
NOT YET PROVEN END TO END: no mission has run through this path. The pieces under
it are each verified on tank (image, credentials, egress, a real agent turn), but
this executor has only been compiled and unit-tested. Deploy + one real microvm
mission is the remaining step.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
0f7fa31f86 |
feat(fleet): B1 — microvm runtime kind and KVM placement predicate
Phase B step 1, on top of the B0 spike that proved microVMs boot here.
KVM is a HARD predicate, not a preference. gw-04 — where every mission
runs today — is itself a VM without nested virtualisation and has no
/dev/kvm, so a microvm mission landing there cannot start at all. The
scheduler therefore has to be able to tell nodes apart, which means the
node has to report what it can host.
Nodes gain a `capabilities` jsonb, populated from a probe on the node
rather than from configuration: /dev/kvm either exists there or it does
not, and nothing on the server can make it appear. The probe OPENS the
device rather than stat-ing it, because it can exist while being
unopenable (wrong group, or a container without the device passed
through) — which is precisely how firecracker will fail.
`microvm` requires BOTH kvm and a firecracker binary. A node with KVM
but no binary looks capable by the obvious test and fails at launch; a
node with the binary but no KVM is gw-04.
Placement fails the launch when no capable node exists, rather than
letting a mission sit in 'running' with nowhere to run. An explicit
target_node_id is treated as a request, not a guarantee — it is honoured
only if that node actually reports the capability.
`capabilities` defaults to '{}' NOT NULL so a node that has never
reported fails every predicate: an unqueried node and an incapable node
must be indistinguishable to the scheduler, because scheduling onto a
node whose abilities are unknown is how you get a mission that cannot
start and does not say why. The report replaces rather than merges, so a
capability the node has LOST disappears instead of leaving a stale true.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
|
|
1253595ba7 |
fix(missions): stop three launch failures from passing as success
A verification run against the deployed stack found a chain mission whose phase 0 reported `completed` with zero files, no commit error and no push error — indistinguishable from a phase that correctly had nothing to do. Three separate defects had to line up, each of them the same shape: a failure sharing its representation with a legitimate negative result. 1. `pin_agent_workspaces` embedded the whole config in one `sh -c` argv. That works until the file grows — config gains a block per provisioned claw — then fails with `argument list too long`. Now written through the tar upload API, which has no argv limit, so the failure mode is gone rather than merely further away. 2. A failed pin was logged "(continuing)". Without the pin, agents write to their sandboxes and the committer finds nothing in /mission/repo — the mission cannot deliver, so the launch now fails where someone is still looking. The restart that applies the pin is fatal for the same reason. 3. `capture_phase_diff_at` swallowed `git diff` failures with `unwrap_or_default`, so an unreadable base landed `empty: true, files_changed: 0` — byte-identical to an honest no-op. The error is now recorded as `diff_error`, and an empty patch that came from a failed diff is no longer trusted to mean an unchanged tree. Adds scripts/verify-mission-delivery.sh, which found #1 and #2 on its first real run. Its probes are fail-closed: no placeholder values, a self-test that proves the uid probe can detect the split it looks for, and FAIL-NORUN for a scenario that never executed. Its own first version had this bug too — a `die` inside `$(...)` exited the subshell, so a run that could not authenticate printed "all checks passed" and exited 0. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
2eb0880fc0 |
fix(skills): reconcile team-template skill names so role bindings actually bind
Every skill reference in every team template was failing to resolve. The TOMLs used snake_case slugs (`write_rust`, `index_selection`) while the authored skills under `skills/**/*.md` declare kebab-case names (`write-rust-current-edition`, `postgres-index-selection`), so `get_by_name` missed on all of them: 128 skipped bindings across 51 distinct names, and no mission agent received any of its template's skills. The mirror-image half was equally invisible: ten authored skills — including `int-xx-marker-protocol`, whose own `when_to_use` says "pin on every coding role" — were referenced by no role at all, so nothing could ever load them. - Rename the 14 references that have authored skills behind them, and dedupe the two that now collapse onto the commit-protocol skill. - Attach all ten orphaned skills to the roles their `when_to_use` names. All 23 authored skills now reach at least one role. - Aggregate the loader's per-name logging into one line per template. The old per-name spam is why this went unnoticed; a bound/unresolved count is noticeable. References with no authored skill are kept and listed — they record intent for skills not yet written. - Two regression tests: no authored skill may be orphaned, and every authored skill must be referenced by its exact name. Also clears the two standing clippy warnings: group `mint_team_from_template`'s eight positional args into `TeamMint`, and make `provider_alias_for` branch on `is_exact_provider_match` so the helper is live code and the two can't disagree about what counts as an exact family match. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
95bd65540c |
docs(missions): record why template role prose is not deletable
Plan §12 proposed deleting the ~1,250 lines of `system_prompt` prose across the 23 team templates as instruction-shaped injection. Tracing the two prompt paths shows that would be strictly harmful: - Missions never see it. `topology_exec::build_prompt` synthesizes its own one-line system text from the role slot, so the prose costs zero mission tokens and deleting it saves zero. - Chat depends on it. `mission_orchestrator` copies it into `agents.system_prompt`, which is the base prompt `cm_runtime::brain::compose_system` augments for a claw's chat turns. Deleting it leaves every mission-minted claw with no identity in chat. The prose is also mostly information (domain standards, wire discipline) rather than instructions restating general competence, which is the kind ablation keeps. Comment left at the one injection site so this isn't re-derived. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
06ae608d0c |
fix(missions): provision claws into the mission's own daemon + reload the pin
Mission turns execute against the per-mission runtime container, but claws were provisioned via RuntimeProvisioner::from_env() — i.e. the GLOBAL gateway. That daemon loads config once at boot and never re-reads the file, so the per-mission daemon had no claw_* agents at all: querying it for a mission claw's risk_profile returned 404 while the global daemon returned 200. With the alias unresolvable, the daemon silently fell back to the default `scout` agent, which is jailed to the global workspace — agents reported "the scout agent workspace" and "/mission/repo isn't accessible", produced no files, and burned tokens. This is the deeper cause behind the empty-output runs; the tool-allowlist and workspace-pin fixes were necessary but not sufficient. - RuntimeProvisioner::for_gateway(url) — aim the provisioner at a specific gateway (mirrors ZeroClawDriveExecutor::from_env_for_gateway); from_env now delegates to it. - mission_orchestrator captures the per-mission endpoint from ensure_container and provisions every claw there, falling back to the global gateway only when there is no per-mission runtime (dev/no-docker). - workspace.path is file-only (the config prop API cannot set a PathBuf), and the daemon never re-reads the file, so pin_agent_workspaces is now followed by restart_container(): restart + wait for /health to answer. Agents created through the daemon's own config API are already persisted to that file, so they survive; the pairing code is re-minted on every launch. The readiness probe inspects the /health BODY — exec_capture only fails on docker errors, so a curl that cannot connect still "succeeds". Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
34409bca0c |
fix(missions): grant coding tools + pin claw workspace to /mission/repo
Mission agents were burning ~275K tokens producing nothing: the coder had only file_read and its workspace was the empty ephemeral sandbox, so it dumped a full spec inline instead of writing files. Two root causes: 1. Risk-profile allowlists used pre-0.8 tool names. `coding_readwrite` allow-listed `file_write` (renamed to `file_edit` in ZeroClaw 0.8, and `file_write` now refuses on ephemeral workspaces) and omitted file_edit / content_search / glob_search / git_operations — the exact tools the phase prompt tells agents to use. Since allowed_tools is a strict allowlist, agents were effectively read-only. Documents the correct profiles in agent.config.example.toml (they only lived in host config; the live runtime profiles were corrected via its config API). 2. workspace.path never got set. `agents.<alias>.workspace.path` is an Option<PathBuf> the ZeroClaw Configurable macro skips from prop enumeration, so provision_claw's set_prop always 404'd and the whole call errored into a swallowed eprintln. Removes the dead set_prop and pins the workspace out-of-band: MissionRuntimeProvisioner:: pin_agent_workspaces patches the shared config file on the per-mission container (format-preserving via toml_edit, atomic temp+mv); the daemon applies it on the same reload that surfaces the freshly-provisioned claws. Covered by unit tests for the TOML stamp. Co-Authored-By: Claude Opus 4.8 <[email protected]> |
||
|
|
6d5e7c87d7 |
fix(claws): point per-mission workspace at /mission/repo + tool-inventory preamble
Two stacked issues after risk_profile was fixed: 1. Claws had file_edit + 46 other tools available, but the templates trained the agents to expect file_read/file_write (older ZeroClaw tool names). Result: agent output kept saying "I only have file_read" and dumped implementations into the context window as text. 2. Even with file_edit, the sandbox pointed at /zeroclaw-data/.zeroclaw/agents/<alias>/workspace/ — NOT /mission/repo where the checked-out mission repo actually lives. unrestricted_filesystem=false blocked agents from reaching it. Fixes: - provision_claw now takes workspace_path. mission_orchestrator passes /mission/repo — pins the per-claw workspace via agents.<alias>.workspace.path to the bind-mount path so file_edit / content_search / glob_search operate on the mission's git checkout. - phase_task_text prepends an explicit tool inventory (file_edit, content_search, glob_search, git_operations, git_forge, ...) plus a WORKSPACE line pinned at /mission/repo. Each phase directive is rewritten to reference file_edit / git_operations explicitly and to call out "do NOT paste code in your reply expecting the platform to save it." |
||
|
|
84572186e9 |
fix(runtime_provision): use team-template risk_profile, not hardcoded toolfree
The provisioner was hardcoding risk_profile=toolfree for every claw, which the ZeroClaw config explicitly configures to EXCLUDE every usable tool (shell, file_read, file_write, http_request, browser). Result: coder/tester/committer claws had zero tools and produced text in the context window with no ability to actually write files or run tests — exactly what the last mission summary showed. Fixes: - provision_claw now takes risk_profile: &str, passed through from the team template (development teams already had coding_readwrite, which now actually gets applied). - Research team templates updated from toolfree → research_readonly (file_read) and papers_research → research_web_readonly (file_read + web_search + web_fetch). Applied to both the on-disk TOML files and the live DB rows. - Added RuntimeProvisioner::default_risk_profile_for_role for auto-provision code paths that lack a template context — picks coding_readwrite for coder-like roles, research_readonly otherwise. - Split rebind_model out of provision_claw so the model-change UI path doesnt inadvertently clobber the existing risk_profile. Templates DB fixup for missions launched pre-deploy is already applied via manual UPDATE. |
||
|
|
b569688e04 |
fix(mission_runtime): per-mission auto-pair via container log scrape (C3 auth)
The seed-mount approach didnt work: even with the shared runtimes data dir bind-mounted, a fresh gateway instance mints a new pairing key and requires re-pairing. The topology_worker connect returned 401 forever. New approach — per-mission gateways self-pair: - Provisioner tails container logs after start, extracts the X-Pairing-Code from the boot banner - Persists it on missions.runtime_pairing_code (migration 0059) - topology_worker constructs ZeroClawDriveExecutor with THAT code via from_env_for_gateway_with_code, which triggers the lazy /pair handshake on first turn and caches the returned bearer Drops the shared-runtime data-dir mount — each per-mission gateway now owns its own state, restoring the C3 isolation guarantee. |
||
|
|
7649b213ad |
missions: wire per-mission runtime container into launch + retry (C3 slice 2)
- mission_orchestrator::on_launch now calls ensure_container after the repo checkout, persists the container_name + endpoint on the missions row. Non-fatal — logs and continues on docker errors so dev-mode + tests keep working. - phase_runner::launch_phase does the same as a fallback for any mission whose runtime_endpoint is null (pre-C3 or torn down). Nothing reads the endpoint yet; slice 3 swaps topology_worker over. |
||
|
|
1e91a19707 |
missions: fix repo checkout for retries + tokenize git.redclaw.dev clones
- mission_orchestrator: run ensure_checkout BEFORE the team_id short-circuit. Previously, a re-launched or retried mission bailed out at the team_id=already-bound guard and skipped repo checkout entirely, so agents ran against an empty workspace. - mission_workspace: inject GITEA_TOKEN into git.redclaw.dev URLs so clone auth works from the server container. Redact any token echoed back on failure. - refresh buttons on MissionCanvas + MissionsList now spin the icon while loading so clicks are visibly acknowledged. - refresh-spinner keyframe added to motion.css. Requires operator on gw-04: sudo chown 65532:65532 /var/lib/clawmates-missions (applied 2026-07-21 pre-commit). |
||
|
|
b8b8cb452e |
missions: multi-team model — pick research + development teams
Directly addresses "we want to pick one or more teams to assign to a
mission, first screen research teams, next screen dev teams." A
mission now materializes N teams, each tagged with a phase purpose.
Backend:
- 0056_mission_teams.sql — new join table
mission_teams(mission_id, team_id, purpose). team_id PK because a
team belongs to one mission-purpose. missions.team_id kept as
legacy pointer to the first minted team for single-team surfaces.
- mission_orchestrator::on_launch — reads mission.config.phase_teams
(JSONB shape { research: [tid,...], coding: [tid,...] }), mints
one team per (purpose, template) pair, records each in
mission_teams, binds the first to mission.team_id. Legacy fallback:
if config.phase_teams is absent, uses missions.team_template_id.
Hard error if both are absent.
- GET /api/missions/{id}/teams — returns
[{ team_id, purpose, team_name }], sorted by created_at asc.
Frontend wizard (step 3 rewrite):
- researchTeamIds / devTeamIds — Set<string> multi-selects
- Reusable TeamMultiSelect component (checkbox-style cards)
- Panels rendered conditionally by preset:
hasResearchPhase → "Research teams" panel
hasCodingPhase → "Development teams" panel
neither → "Teams" panel (bench/security-only missions)
- canNext enforces at least one pick in every visible panel
- submit builds config.phase_teams and passes it via CreateMissionRequest
- Review step shows both selections by name
MissionTeamTab:
- Fetches /api/missions/{id}/teams and groups by purpose
- Each purpose renders a section with per-team cards
- Falls back to a single "mission" pseudo-row for legacy missions
that only have missions.team_id (no mission_teams rows)
CreateMissionRequest no longer sends team_template_id from the wizard
— the multi-team config.phase_teams path supersedes it. The backend
still accepts team_template_id for API callers.
Verified: cargo check --workspace + tsc + eslint --quiet all green.
|
||
|
|
0ee689f590 |
missions: hard-require team template — block empty-team launches
Root-cause fix for the "mission runs with zero agents" bug. Three
enforcement layers now guarantee a launched mission has a team:
1. mission_orchestrator::on_launch — the previous
\`return Ok(None)\` when both team_id and team_template_id are
None is now \`return Err(...)\`. That branch was never a real
"auto-provision later" path; it was a silent no-op that let
the mission flip to running with nothing to run.
2. routes::missions::set_status — the draft→running transition
now (a) rejects with 400 when team_id + team_template_id are
both null, and (b) runs on_launch BEFORE flipping status +
returns 500 on failure. No more orphan "running" missions
with no materialization.
3. MissionWizard step 3 — removed the misleading "LLM
auto-provision" tile (fake code path). First real template is
pre-selected on mount; canNext requires teamTemplateId set;
empty state surfaces a red warning if no templates loaded.
4. MissionCanvas Launch button — disabled with a "No team" label
and explanatory tooltip when the mission has neither team_id
nor team_template_id (defense-in-depth for legacy rows or
direct-API missions).
Also flipped the mission_orchestrator test that expected
Ok(None) → now expects a specific error message.
Prod cleanup: reset the stuck mission
019f814c-d36f-7d60-8915-1ce100683133 (running with team_id=NULL) back
to draft so the operator can delete or attach a template.
Verified: cargo check --workspace + tsc + eslint all green;
mission_orchestrator test updated to match new contract.
|
||
|
|
a2bc06d313 |
validation sweep: close residual TODO + strip 'future' stubs
- mission_orchestrator: replace hardcoded cli=\"claude\" with real precedence chain: mission.config.cli → template.config.default_cli → \"claude\". Missions can now A/B by CLI without a schema change. - fleet.rs: scope clippy::too_many_arguments allow on NodeHub::open_pty — 8 params (session address x4 + target address x3) is the actual dimensionality; a struct would be ceremony. - security_scan.rs: delete future_artifact_root — pure hint-stub, no callers, nothing depends on it. - routes/world.rs: delete the empty normalize() seam + its 30-line comment block. normalize_run_event higher up does the real work; the empty stub was pre-cleanup scaffolding. Post-sweep validation across the whole workspace: * cargo check --workspace → clean * cargo test --workspace --no-run → all bins build * cargo test -p cm-api --test mission_orchestrator → 3/3 pass * cargo clippy -p cm-api -p clawmates-node --tests → 0 warnings * tsc --noEmit → 0 errors Wiring audit: every recent route handler is registered in lib.rs. Every recent frontend component has at least one importer. Remaining #[allow(dead_code)] entries are deprovision_claw + unpublish_claw on RuntimeProvisioner — real rollback paths waiting on the team-delete route. Documented future hooks, not stubs. |
||
|
|
2b3ec27757 |
herdr phase 1c: wizard runtime picker + on_launch auto-dispatch
Closes the operator loop for the second-runtime path. Missions
created with runtime='local_herdr' now spawn a Herdr pane on their
target_node automatically on draft→running.
Frontend (MissionWizard):
- Step 4 grows a "Runtime" section above Schedule
- Radio: "Hosted (ZeroClaw)" default | "On a fleet node (Herdr)"
- Local-Herdr shows a dropdown of ONLINE nodes only (from
/api/nodes filtered by status='online')
- canNext blocks Next when local_herdr picked without a node
- Review step shows "Runtime: Herdr on <node-name>" or "Hosted"
- Empty-online-nodes state hints "Connect one from INFRA first"
Backend:
- mission_orchestrator::on_launch grows a NodeHub param; when
mission.runtime_kind='local_herdr' + target_node_id set +
hub present → calls fleet_herdr::dispatch(). Non-fatal:
logs and continues so a research_only mission with a Herdr
runtime chosen accidentally still boots the team.
- routes::missions::set_status passes state.node_hub through.
- Test call sites updated to pass None for the new param
(integration tests don't drive real fleet nodes).
CLI stub: on_launch currently hard-codes cli="claude" for the
Herdr pane. Phase 4 will read that from the team template so a
research team → kimi, gpu team → claude, etc.
Verified: cargo check --workspace + cargo test
-p cm-api --test mission_orchestrator + tsc --noEmit all green.
|
||
|
|
214d0c5e9f |
task #25: per-mission repo checkout on mission launch
Closes the follow-up gap flagged when task #23 landed. security_scan and benchmark_runner now exec against $CLAWMATES_MISSIONS_ROOT/ {mission_id}/repo — this commit is what actually puts a repo there. - crates/cm-api/src/mission_workspace.rs — new module. ensure_checkout(pool, workspace_id, mission_id): * mission with no repo_id → Ok(None), no-op * repo cloned into $ROOT/{id}/repo (--depth 1) * dir already a git repo → fetch + reset --hard origin/{branch} (idempotent — every launch brings the tree in sync with the remote default_branch) Auth uses the process's ambient git credential setup (SSH agent / .netrc / helper). Tokens deliberately not embedded in URLs. - crates/cm-api/src/mission_orchestrator.rs — on_launch calls ensure_checkout after team materialization + team_id bind. Non-fatal: clone failures log and continue so research_only missions (no repo needed) don't get blocked. - deploy/compose/docker-compose.yml — new named volume missions_workspaces mounted at /var/lib/clawmates-missions on both the server (writer) and where the clawmates-runtime container will mount it (reader for docker exec). CLAWMATES_ MISSIONS_ROOT + CLAWMATES_RUNTIME_CONTAINER env vars set on the server so mission_workspace + exec_target read the same canonical values. The scan/bench trigger buttons now actually produce findings once you (a) run a mission whose repo_id is set, (b) have the clawmates-runtime container bind-mounting missions_workspaces at /var/lib/clawmates-missions. Verified: SQLX_OFFLINE=true cargo check -p cm-api + cargo test -p cm-api --test mission_orchestrator both green. |
||
|
|
565f6cae65 |
slice 4: 5 workflow templates as TOML recipes + mission-launch orchestrator
Two things land together:
1. Ships the 5 workflow recipes as TOML files under
templates/workflows/*.toml:
- research_only (hub_spoke research → MD + PDF)
- research_and_code (research → coding loop until INT-XX drained)
- security_hardening (scan → research patches → coding with
reviewer approval + full MCP bundle)
- refactor (single-pass coding with dep audit
preamble + before/after benchmarks)
- benchmark (author + baseline benchmarks per stack)
Each declares phases[], per-phase config, default_team_template.
Loaded read-only into an in-memory registry (workflow_registry)
via OnceLock — no DB row per recipe.
2. Ships the mission-launch orchestrator that closes the loop from
Slice 3.5d's mechanics. When a mission transitions draft→running,
`mission_orchestrator::on_launch`:
- Reads mission.team_template_id (skips if unset)
- Loads the team template detail (roles + skills bindings)
- Builds a topology graph from role slots via cm_topology::build
- Inserts the teams row + stamps template_id/version/risk_profile/mcp_bundles
- For each role: agent insert, model binding, runtime provision
(opt-in via RuntimeProvisioner::from_env), brain_seed::ingest
(Slice 3.5d), agent_template_link::upsert (Slice 3.5d),
team_members bind, audit trail
- UPDATE missions SET team_id = ...
Wired into routes::missions::set_status when prior.status='draft'
and new='running'. Failures log + are non-fatal (mission still
flips to running so the user can inspect + retry).
With this, Slice 3.5d's brain-seed + link machinery actually gets
populated, and the MCP skills server's template-defaults-merge path
(Slice 3.5b/d) starts serving real bindings to real agents.
Follow-ups (Slice 5-8):
- Task-card parser watches run events for TASK/COMPLETED markers
→ mission_tasks rows
- PDF renderer worker turns MD artifacts into PDFs
- Before/after benchmark runner honors phases[].config.benchmark
- Security scan MCP bundle exposes cargo-audit/gitleaks/trivy/semgrep
- Level-up endpoints diff learned-vs-seeded via agent_template_link
Co-Authored-By: Claude Opus 4.7 <[email protected]>
|