PR3 of #791 — mosaic fleet regen (regeneration-from-SSOT)
Part of #791. Third and final PR of the ratified 3-PR DAG (b strict ownership separation #802✅ merged · a transactional pre-update snapshot #811✅ merged · d regeneration-from-SSOT — this PR). The umbrella #791 stays open until MS-LEAD closes it.
What this adds
mosaic fleet regen — a projection-only recovery command that rebuilds each fleet/agents/<name>.env.generated from the roster.yaml SSOT after an upgrade or partial write leaves the generated projections stale or missing.
Dry-run by default; --write applies; --json for machine output.
Never restarts agents — regen recovers config only. It is structurally incapable of a lifecycle call (the CommandRunner is declared but never referenced on any executable path), and a recordingRunner gate asserts zero runner invocations across every test.
Reuses the merged reconciler plumbing (projectRosterV2AgentGeneratedEnv + the generated-env boundary) — it does not reimplement fleet logic.
Secrev-safe output — report/JSON emit paths and counts only, never a projected KEY=value.
Same semantic gate as reconcile — validates roster persona resolution + tool-policy the same way reconcile/plan/verify do, and now forwards configured rolesDir/overrideDir so a custom-persona-root deployment cannot have reconcile accept a roster that regen rejects.
Concurrency / lock hardening
Regen serializes against agent CRUD and reconcile via both fleet locks (roster.yaml.mutation.lock + roster.yaml.reconcile.lock), acquired mutation→reconcile and released in reverse. Both are non-blocking wx locks that throw on contention → no deadlock. The shared managed-lock helper was hardened:
One ownership-proving tokened lock helper reused for both locks, with per-lock fault labels (a mutation-lock fault no longer mislabels itself as the reconcile lock).
Init-failure cleanup no longer strands a just-created lock: dev/ino-guarded removal, plus a persisted-token fallback for the narrow case where the post-create stat itself fails (dev/ino unavailable) — it removes only a leaf whose content matches our random UUID, so it can never delete a CRUD (empty) or differently-tokened replacement.
Acquire-unwind surfaces a lock-cleanup fault instead of dropping it.
Tests (TDD, red-first, co-located)
fleet-regen-command.spec.ts — 26 tests: dry-run/write dispositions, the load-bearing never-restarts gate, all lock regressions (ownership-proof-before-unlink, acquire-unwind fault, dual-lock stale warning, stranded-lock on init failure, stat-failure strand), and persona-root wiring. Every finding fixed in review was proven red-first.
Known limitation — deferred to a follow-up
A residual check-then-unlink TOCTOU remains in the lock release (between the final ownership proof and the path unlink). It is byte-identical to the already-merged reconcile-lock release on main, unreachable within the wx writer protocol (no Mosaic writer removes a lock it doesn't own; only an external rm/stale-lock reaper can vacate the inode mid-release), and its true fix is a cross-cutting mechanism change — an fd-held advisory lock adopted by all fleet writers (CRUD + reconcile + regen) — out of scope for a projection-only recovery PR. Tracked as a separate follow-up issue. Two independent reviews (author≠reviewer) treated it as non-blocking.
Independent reviews (author≠reviewer): core lock hardening PASS; token-fallback delta PASS; persona-root wiring PASS — each confirmed by revert-to-RED.
Codex code + security review converged to the single deferred TOCTOU; no secrets, no injection, generated-only writes.
Base:31607a4 (fresh main). No self-merge — holding at PR-open for MS-LEAD's exact-head review + durable Reviewer-of-Record before merge.
## PR3 of #791 — `mosaic fleet regen` (regeneration-from-SSOT)
Part of #791. Third and final PR of the ratified 3-PR DAG (b strict ownership separation #802 ✅ merged · a transactional pre-update snapshot #811 ✅ merged · **d regeneration-from-SSOT — this PR**). The umbrella #791 stays open until MS-LEAD closes it.
### What this adds
`mosaic fleet regen` — a **projection-only recovery command** that rebuilds each `fleet/agents/<name>.env.generated` from the `roster.yaml` SSOT after an upgrade or partial write leaves the generated projections stale or missing.
- **Dry-run by default**; `--write` applies; `--json` for machine output.
- **Never restarts agents** — regen recovers *config only*. It is structurally incapable of a lifecycle call (the `CommandRunner` is declared but never referenced on any executable path), and a `recordingRunner` gate asserts *zero* runner invocations across every test.
- **Reuses the merged reconciler plumbing** (`projectRosterV2AgentGeneratedEnv` + the generated-env boundary) — it does not reimplement fleet logic.
- **Secrev-safe output** — report/JSON emit **paths and counts only**, never a projected `KEY=value`.
- **Same semantic gate as reconcile** — validates roster persona resolution + tool-policy the same way `reconcile`/`plan`/`verify` do, and now forwards configured `rolesDir`/`overrideDir` so a custom-persona-root deployment cannot have reconcile accept a roster that regen rejects.
### Concurrency / lock hardening
Regen serializes against agent CRUD and reconcile via **both** fleet locks (`roster.yaml.mutation.lock` + `roster.yaml.reconcile.lock`), acquired mutation→reconcile and released in reverse. Both are non-blocking `wx` locks that throw on contention → no deadlock. The shared managed-lock helper was hardened:
- One ownership-proving **tokened** lock helper reused for both locks, with **per-lock fault labels** (a mutation-lock fault no longer mislabels itself as the reconcile lock).
- **Init-failure cleanup no longer strands a just-created lock**: dev/ino-guarded removal, plus a **persisted-token fallback** for the narrow case where the post-create `stat` itself fails (dev/ino unavailable) — it removes only a leaf whose content matches our random UUID, so it can never delete a CRUD (empty) or differently-tokened replacement.
- **Acquire-unwind surfaces a lock-cleanup fault** instead of dropping it.
### Tests (TDD, red-first, co-located)
`fleet-regen-command.spec.ts` — **26 tests**: dry-run/write dispositions, the load-bearing never-restarts gate, all lock regressions (ownership-proof-before-unlink, acquire-unwind fault, dual-lock stale warning, stranded-lock on init failure, stat-failure strand), and persona-root wiring. Every finding fixed in review was proven red-first.
### Known limitation — deferred to a follow-up
A residual check-then-unlink **TOCTOU** remains in the lock release (between the final ownership proof and the path `unlink`). It is **byte-identical to the already-merged reconcile-lock release on `main`**, unreachable within the `wx` writer protocol (no Mosaic writer removes a lock it doesn't own; only an external `rm`/stale-lock reaper can vacate the inode mid-release), and its true fix is a cross-cutting mechanism change — an **fd-held advisory lock adopted by all fleet writers (CRUD + reconcile + regen)** — out of scope for a projection-only recovery PR. Tracked as a separate follow-up issue. Two independent reviews (author≠reviewer) treated it as non-blocking.
### Review evidence
- Gates green: `typecheck` · `lint` · `format:check` · full mosaic vitest **1278**.
- Independent reviews (author≠reviewer): core lock hardening PASS; token-fallback delta PASS; persona-root wiring PASS — each confirmed by revert-to-RED.
- Codex code + security review converged to the single deferred TOCTOU; no secrets, no injection, generated-only writes.
**Base:** `31607a4` (fresh `main`). **No self-merge** — holding at PR-open for MS-LEAD's exact-head review + durable Reviewer-of-Record before merge.
Add `mosaic fleet regen`, a projection-only recovery command that rebuilds
each `fleet/agents/<name>.env.generated` from the `roster.yaml` SSOT after an
upgrade or partial write leaves the generated projections stale or missing.
- Dry-run by default; `--write` applies; `--json` for machine output. Reuses the
merged reconciler's projection plumbing (projectRosterV2AgentGeneratedEnv +
the generated-env boundary) rather than reimplementing fleet logic.
- Structurally NEVER issues a lifecycle/restart call — regen recovers config
only; a recordingRunner gate proves no runner invocation ever occurs.
- Serializes against agent CRUD and reconcile via BOTH fleet locks
(roster.yaml.mutation.lock + roster.yaml.reconcile.lock), acquired
mutation-then-reconcile and released in reverse; both are non-blocking `wx`
locks that throw on contention, so no deadlock is possible.
- Hardens the shared managed-lock helper: ownership-proving tokened lock reused
for both locks with per-lock fault labels; init-failure cleanup no longer
strands a just-created lock (dev/ino guard, with a persisted-token fallback
when the post-create stat itself fails); acquire-unwind surfaces a lock
cleanup fault instead of dropping it.
- Resolves personas the SAME way reconcile does by forwarding configured
rolesDir/overrideDir, so a custom-persona-root deployment cannot have
reconcile accept a roster that regen rejects.
- Report/output is secrev-safe: paths and counts only, never projected values.
- Docs: upgrade-safety-and-recovery runbook + fleet-local-canary note.
Tests are TDD red-first with co-located specs (regen spec: 26 tests covering
dry-run/write dispositions, the never-restarts gate, all lock regressions, and
persona-root wiring).
A residual check-then-unlink TOCTOU in the lock release remains (byte-identical
to the merged reconcile lock; unreachable within the `wx` writer protocol); its
true fix is an fd-held advisory lock adopted by all fleet writers, tracked as a
separate follow-up.
Part of #791
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Reviewer-of-Record — exact head 1eb37f6f — VERDICT: APPROVE
Independent exact-head adversarial/security review (FRESH standalone, not carried-forward).
Author lane: ms-791 (Claude/Opus) · Reviewer lane: independent Opus adversarial/security reviewer (dispatched by MS Team Lead). author≠reviewer = PROCESS/LANE-attested (single Gitea account jason.woltje; separation by lane, not by provider account).
Head:1eb37f6fd084afbdc8c00e4ff5f57d2c89fc2c14 · tree614169817658f7f280e22054efdd4d8812ad12ff · base31607a4 (merge-base == current main).
CI: authoritative Gitea combined commit-status at this exact head = success (Woodpecker #1884).
Scope: 10 files (+1994/−37), clean — no stray secret/snapshot/session.lock/.env.local.
Findings — all 8 adversarial areas PASS (zero blockers):
Projection-only proven structural — deps.runner never invoked; recordingRunner gate asserts calls==[] && systemctl==[]; sole side-effect = one atomic write.
Lock correctness — dual non-blocking wx, deadlock-impossible (only regen holds both, fixed order); init-failure cleanup dev/ino+token-guarded (no stranded lock on writeFile- or stat-fault); per-lock fault labels correct (no mislabel).
CWE-367 release TOCTOU → SOUND-TO-DEFER (#814): reachable "replaced-before-release" window is CLOSED & pinned by test; residual check→unlink window is identical to the already-merged reconcile-lock release and unreachable within the wx writer protocol (only external rm/stale-reaper vacates the inode). Appropriate to defer for a projection-only PR.
Secrev — result/JSON/report emit paths+counts+timestamps only, never a projected value; test asserts secrets absent from output.
Generated-env boundary — AGENT_NAME validated before path build; writes ONLY <name>.env.generated; legacy .env/.env.local/.env.quarantine untouched.
Persona-root wiring — regen forwards rolesDir/overrideDir + same validateRosterV2Semantics gate → regen and reconcile cannot disagree on persona acceptance.
--write safety — atomic temp(0o600,wx)+rename; fail-closed (all validated before any write; mid-loop fault → incomplete with exact lists).
Test integrity — assertions concrete, non-tautological, load-bearing.
INFO (non-blocking): (a) refactor makes the merged reconcile-lock release functionally equivalent but not byte-identical to main (removed one redundant ownership check; closed no window) — minor correction to the "byte-identical" wording; (b) no explicit test asserts both locks unlinked after a successful --write (covered indirectly). Neither blocks.
Gate status → mergeable: exact-head authoritative-green CI ✓ · fresh independent exact-head RoR APPROVE ✓ · scope clean ✓ · based on current main ✓ · CWE-367 residual deferred to #814 ✓. Durable RoR persisted to provider before queue guard.
## Reviewer-of-Record — exact head `1eb37f6f` — VERDICT: APPROVE
**Independent exact-head adversarial/security review** (FRESH standalone, not carried-forward).
- **Author lane:** ms-791 (Claude/Opus) · **Reviewer lane:** independent Opus adversarial/security reviewer (dispatched by MS Team Lead). author≠reviewer = PROCESS/LANE-attested (single Gitea account `jason.woltje`; separation by lane, not by provider account).
- **Head:** `1eb37f6fd084afbdc8c00e4ff5f57d2c89fc2c14` · **tree** `614169817658f7f280e22054efdd4d8812ad12ff` · **base** `31607a4` (merge-base == current main).
- **CI:** authoritative Gitea combined commit-status at this exact head = **success** (Woodpecker #1884).
- **Scope:** 10 files (+1994/−37), clean — no stray secret/snapshot/session.lock/.env.local.
**Findings — all 8 adversarial areas PASS (zero blockers):**
1. Projection-only proven structural — `deps.runner` never invoked; `recordingRunner` gate asserts `calls==[]` && `systemctl==[]`; sole side-effect = one atomic write.
2. Lock correctness — dual non-blocking `wx`, deadlock-impossible (only regen holds both, fixed order); init-failure cleanup dev/ino+token-guarded (no stranded lock on writeFile- or stat-fault); per-lock fault labels correct (no mislabel).
3. **CWE-367 release TOCTOU → SOUND-TO-DEFER (#814):** reachable "replaced-before-release" window is CLOSED & pinned by test; residual check→unlink window is identical to the already-merged reconcile-lock release and unreachable within the `wx` writer protocol (only external `rm`/stale-reaper vacates the inode). Appropriate to defer for a projection-only PR.
4. Secrev — result/JSON/report emit paths+counts+timestamps only, never a projected value; test asserts secrets absent from output.
5. Generated-env boundary — AGENT_NAME validated before path build; writes ONLY `<name>.env.generated`; legacy `.env`/`.env.local`/`.env.quarantine` untouched.
6. Persona-root wiring — regen forwards rolesDir/overrideDir + same `validateRosterV2Semantics` gate → regen and reconcile cannot disagree on persona acceptance.
7. `--write` safety — atomic temp(`0o600`,`wx`)+rename; fail-closed (all validated before any write; mid-loop fault → `incomplete` with exact lists).
8. Test integrity — assertions concrete, non-tautological, load-bearing.
**INFO (non-blocking):** (a) refactor makes the merged reconcile-lock release *functionally* equivalent but not *byte-identical* to main (removed one redundant ownership check; closed no window) — minor correction to the "byte-identical" wording; (b) no explicit test asserts both locks unlinked after a successful `--write` (covered indirectly). Neither blocks.
**Gate status → mergeable:** exact-head authoritative-green CI ✓ · fresh independent exact-head RoR APPROVE ✓ · scope clean ✓ · based on current main ✓ · CWE-367 residual deferred to #814 ✓. Durable RoR persisted to provider before queue guard.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
PR3 of #791 —
mosaic fleet regen(regeneration-from-SSOT)Part of #791. Third and final PR of the ratified 3-PR DAG (b strict ownership separation #802 ✅ merged · a transactional pre-update snapshot #811 ✅ merged · d regeneration-from-SSOT — this PR). The umbrella #791 stays open until MS-LEAD closes it.
What this adds
mosaic fleet regen— a projection-only recovery command that rebuilds eachfleet/agents/<name>.env.generatedfrom theroster.yamlSSOT after an upgrade or partial write leaves the generated projections stale or missing.--writeapplies;--jsonfor machine output.CommandRunneris declared but never referenced on any executable path), and arecordingRunnergate asserts zero runner invocations across every test.projectRosterV2AgentGeneratedEnv+ the generated-env boundary) — it does not reimplement fleet logic.KEY=value.reconcile/plan/verifydo, and now forwards configuredrolesDir/overrideDirso a custom-persona-root deployment cannot have reconcile accept a roster that regen rejects.Concurrency / lock hardening
Regen serializes against agent CRUD and reconcile via both fleet locks (
roster.yaml.mutation.lock+roster.yaml.reconcile.lock), acquired mutation→reconcile and released in reverse. Both are non-blockingwxlocks that throw on contention → no deadlock. The shared managed-lock helper was hardened:statitself fails (dev/ino unavailable) — it removes only a leaf whose content matches our random UUID, so it can never delete a CRUD (empty) or differently-tokened replacement.Tests (TDD, red-first, co-located)
fleet-regen-command.spec.ts— 26 tests: dry-run/write dispositions, the load-bearing never-restarts gate, all lock regressions (ownership-proof-before-unlink, acquire-unwind fault, dual-lock stale warning, stranded-lock on init failure, stat-failure strand), and persona-root wiring. Every finding fixed in review was proven red-first.Known limitation — deferred to a follow-up
A residual check-then-unlink TOCTOU remains in the lock release (between the final ownership proof and the path
unlink). It is byte-identical to the already-merged reconcile-lock release onmain, unreachable within thewxwriter protocol (no Mosaic writer removes a lock it doesn't own; only an externalrm/stale-lock reaper can vacate the inode mid-release), and its true fix is a cross-cutting mechanism change — an fd-held advisory lock adopted by all fleet writers (CRUD + reconcile + regen) — out of scope for a projection-only recovery PR. Tracked as a separate follow-up issue. Two independent reviews (author≠reviewer) treated it as non-blocking.Review evidence
typecheck·lint·format:check· full mosaic vitest 1278.Base:
31607a4(freshmain). No self-merge — holding at PR-open for MS-LEAD's exact-head review + durable Reviewer-of-Record before merge.Reviewer-of-Record — exact head
1eb37f6f— VERDICT: APPROVEIndependent exact-head adversarial/security review (FRESH standalone, not carried-forward).
jason.woltje; separation by lane, not by provider account).1eb37f6fd084afbdc8c00e4ff5f57d2c89fc2c14· tree614169817658f7f280e22054efdd4d8812ad12ff· base31607a4(merge-base == current main).Findings — all 8 adversarial areas PASS (zero blockers):
deps.runnernever invoked;recordingRunnergate assertscalls==[]&&systemctl==[]; sole side-effect = one atomic write.wx, deadlock-impossible (only regen holds both, fixed order); init-failure cleanup dev/ino+token-guarded (no stranded lock on writeFile- or stat-fault); per-lock fault labels correct (no mislabel).wxwriter protocol (only externalrm/stale-reaper vacates the inode). Appropriate to defer for a projection-only PR.<name>.env.generated; legacy.env/.env.local/.env.quarantineuntouched.validateRosterV2Semanticsgate → regen and reconcile cannot disagree on persona acceptance.--writesafety — atomic temp(0o600,wx)+rename; fail-closed (all validated before any write; mid-loop fault →incompletewith exact lists).INFO (non-blocking): (a) refactor makes the merged reconcile-lock release functionally equivalent but not byte-identical to main (removed one redundant ownership check; closed no window) — minor correction to the "byte-identical" wording; (b) no explicit test asserts both locks unlinked after a successful
--write(covered indirectly). Neither blocks.Gate status → mergeable: exact-head authoritative-green CI ✓ · fresh independent exact-head RoR APPROVE ✓ · scope clean ✓ · based on current main ✓ · CWE-367 residual deferred to #814 ✓. Durable RoR persisted to provider before queue guard.