greenfield dead end: fleet init writes roster v1, all roster-v2 commands reject v1, migrate-v1 is preview-only #1237
Closed
opened 2026-08-16 03:02:41 +00:00 by fred
·
5 comments
No Branch/Tag Specified
main
docs/ri-050-release-evidence
fred/guides-seat-identity-fleet-comms
next
fred/credential-fail-closed-seat-slots
feat/ri-050-qr-evaluator
docs/ri-050-forge-docs-fastfollow
fix/ri-050-registry-secrets
test/ri-050-publish-gate-negative
fix/ri-050-verify-pglite-path
docs/ri-050-qr-probe-inventory
feat/ri-050-web-stale-safety
docs/ri-050-mission-bootstrap
fix/ri-050-forge-fail-closed
feat/ri-050-publish-gate
fix/1292-lease-broker-activation
fleet/continuation-record-2026-08-17
feat/ri-050-prd-authority
fix/ri-050-macp-fail-closed
fix/1280-identity-first-resolution
feat/w-f4-store
fix/1264-fleet-unattended-first-start
fix/1269-ci-chain-unblock
fix/1256-fleet-runtime-preflight
fix/1256-fleet-pane-path-node
fix/1257-e7-draft-transition
fix/1017-enumeration-guard-population
fix/1240-fleet-transport-check
fix/1017-wire-start-agent-session
e2e-compose
fix/1241-launch-failure-visible
fix/1237-fleet-v2-dispatch
fix/1236-installer-dir-modes
fix/installer-path-and-node
docs/1216-trunk-parameterization
docs/ia-merge-current
fix/869-lease-probe-timeout
feat/workspace-hygiene-tool-enforcement
feat/1080-pr-edit
fix/1182-fail-closed-launch
fix/1179-required-security-di
feat/p3-slice0-task5-chat-runtime-router-shaggy
feat/p3-slice0-task5-chat-runtime-router
feat/wf1-composition
feat/p3-slice0-task4-web-catalog-selection
feat/lease-promotion-and-harness-isolation
ci/provision-pi-runtime
feat/p3-slice0-task3-catalog-selection
feat/p3-slice0-task2-harness-registry
adopt/965-mos-ste-writing-standard
fix/991-comment-url-scheme-normalise
feat/wf2-bundle-migration
feat/wf4-plugin-acquisition
feat/wf5-refresh-safety
fix/1145-coord-di-compiled-boot
feat/p3-slice0-task1-harness-contracts
docs/webui-phase-p-structure
feat/1150-pi-goal-extension
feat/webui-p3-chat
fix/1146-ci-queue-purpose
fix/1138-conditional-federation
feat/webui-p2-data-auth
fix/gateway-runner-image
feat/webui-p1-vite-skeleton
fix/break-c-hooks-and-web-image
docs/webui-fleet-claude-bridge-plan
fix/wizard-gateway-failure
fix/ci-queue-wait-no-status
fix/next-node-gate
fix/mosaic-init-rce
greenfield/fomo-lin
fix/1099-pipefail-wake
fix/1099-pipefail-tests
fix/1099-pipefail-sweep
fix/framework-shell-portability
fix/1043-pane-git-identity
fix/1081-issue-close-silent-comment-failure
fix/1090-enrollment-wallclock-tolerance
feat/1082-tea-stale-token-diagnostic
fix/detect-platform-silent-128-outside-repo
feat/1050-install-state-machine-red-fixture
fix/pr-merge-message-field
feat/1051-mosaic-brain-installer
feat/1045-mosaic-cred
remediation/state
fix/1056-upgrade-rollback-control-race
fix/1019-ci-queue-timeout-harness
feat/rm-02-gate-registry
fix/rm-01-reproducible-checkout
remediation/mission-setup
fix/hygiene-inert-format-gate
fix/1019-queue-guard-stdin
feat/mos-ste-writing-standard
fix/1007-suite-hermeticity
feat/push-guard-null-case-verification
mos-comms-live
docs/heartbeat-framework-layering-ms-lead
feat/869-c4-version-coupling
feat/869-c2-install-ordering-guard
feat/869-c5-doctor-activation-check
feat/per-agent-gitea-identity
fix/875-belongs-case-insensitive-slug
fix/ci-queue-wait-404-branch-absent
feat/869-c1-activation-probe
feat/869-c3-broker-supervisor
fix/865-tea-cli-comment-invocation
feat/glpi-skills
fix/860-deflake-mutator-lease-gate
fix/850-detect-platform-port-normalization
fix/856-worktree-deps-preflight
fix/835-pr-review-approve-reject-comment-flag
fix/848-truthful-evidence
fix/812-pr-review-comment
fix/849-recovery-runtime-fixture-race
docs/758-ledger-m5-001-sync
feat/834-tc-server-side-doc
feat/833-constrained-recovery-command
feat/827-gate0-probe
governance/gate0-probe3-amendment
fix/795-codex-pr-diff
fix/795-ci-base-jq
fix/795-ci-base-git
feat/791-pr3-fleet-regen
feat/791-pr2-snapshot-restore
fix/807-glpi-206
fix/808-agent-send-false-sender
feat/791-upgrade-config-protection
feat/790-mosaic-yolo-claudex-pr2
feat/790-mosaic-yolo-claudex
feat/758-v1-v2-migrator
fix/766-exact-fleet-comms
test/758-reconciler-lifecycle-gates
docs/771-kbn101-db-role-split
test/758-example-profile-dispositions
feat/758-shared-role-resolution
feat/mos-logical-identity-fencing
feat/769-kbn100-unified-schema
docs/753-kbn010-threat-gate
feat/758-roster-v2-compiler
feat/756-official-discord-plugin
docs/758-fleet-config-management
fix/mos-option2-qualification-format
docs/issue-758-m0
docs/mos-option2-qualification
mos-comms
feat/tess-interaction-agent
fix/tess-docs-format
draft/mosaic-platform-prd
fix/installer-provider-gate-and-local-gateway-redis
release/mosaic-cli-0.0.37
feat/framework-constitution-alpha
fix/git-wrapper-repo-detection
fix/woodpecker-wrapper-legacy-mosaic
fix/t-a292e96f-gitea-pr-metadata
fix/gitea-pr-metadata-login-t-a292e96f
fix/t_a292e96f-pr-metadata-gitea
fix/t_3a368a52-gitea-usc-login
fix/bootstrap-hotfix
fix/populate-known-packages-list
fix/idempotent-init
v0.0.39-alpha
mosaic-v0.0.31
fed-v0.2.0-m2
fed-v0.1.0-m1
mosaic-v0.0.29
mosaic-v0.0.28
mosaic-v0.0.27
mosaic-v0.0.26
mosaic-v0.0.25
mosaic-v0.0.24
v0.2.0
v0.1.0
v0.0.8
v0.0.7
v0.0.6
v0.0.5
v0.0.4
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Assignees
be-coder-05
be-coder-06
be-coder-07
be-coder-08
coder-mos1
coder-mos2
coder2
coder3
f10-coder
fargo
fred
happy
jason.woltje (Jason Woltje)
merge-gate
pepper
rev-974 (Rev-974 (Mosaic reviewer seat, web1))
rev-code-01
rev-code-02
rev-security-01
rev-security-02
rev0
sanity
scooby (Scooby)
scrappy
shaggy
tess
tiny
velma
woodpecker
Clear assignees
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mosaicstack/stack#1237
Reference in New Issue
Block a user
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.
On a freshly-installed 0.0.50-next host there is no path from "no fleet" to "a fleet the roster-v2 commands will operate on". The only roster-creation command writes v1; every roster-v2 command rejects v1; and the migration command is preview-only. The CLI cannot get you out of the state its own installer puts you in.
Measured on
mosaic-sbx-canary(Debian 13) rolled back togreenfield, thencurl … | bash -s -- --next --yes→ rc=0, CLI0.0.50-next.2413. (fleet init --writealso needs #1236 worked around first.)The three legs of the trap
1.
fleet initwrites v1. Every preset, includinglocal-canary:It is the only command that creates a roster.
2. Every roster-v2 command rejects v1.
doctor,apply,create,delete,get,planall route throughexecuteReconcilerCommand, which callsparseRosterV2on the canonical roster:The real error is swallowed (filed separately as the diagnosability bug). Recovered by instrumenting
writeOutcomeon a sandbox copy:"use the existing v1 path until migration" is a fair instruction, but it is invisible to the operator, and the v1 path is not what
fleet doctor/create/applyare.3.
migrate-v1cannot write. It has exactly one subcommand:And preview needs artifacts a greenfield host has no way to produce:
--decisionsand--observationshave no documented schema reachable from the CLI, and "failed without publishable detail" is a second swallowed error.Net effect
Roster v2 is unreachable from a greenfield install using shipped commands. I could not find a command that writes one;
roster-v2.jsexportsvalidateRosterV2,parseRosterV2,renderRosterV2,normalizeRosterV2— a renderer exists, nothing surfaces it as a write path.Why this is on the critical path
The north-star goal for web1 requires seats "manageable via mosaic cli tools" and "tracked durably via the Mosaic Fleet system tools". As shipped, a fresh host reaches neither: its roster is v1, and the management surface is v2-only.
What would resolve it
Any one of:
fleet initemits v2 (with--v1to opt out), so a new host starts where the tooling lives.migrate-v1gains a--write, with--decisions/--observationsoptional and defaulted for the trivial case, so v1 hosts have an exit.(1) looks smallest for greenfield and does nothing for the existing v1 hosts — web1 and sb-it-1-dt are both roster v1 today, so (2) is needed regardless. Whichever way, the "use the existing v1 path until migration" instruction needs to reach the operator instead of being replaced with
reconcile-failed.Related
fleet init --writebefore you can even get here.writeOutcomeis why this needed a patched binary to diagnose; filed separately.Reported by fred (orchestrator seat, sb-it-1-dt), measured on canary VMID 1125.
Amendment: I filed this as a v1 dead end. It is worse and more symmetrical than that — there are two fleet implementations sharing one command namespace, and no roster version can reach a working seat.
Also, one softening: I said roster v2 is unreachable. It is unreachable from
fleet init. A hand-written v2 roster works, and there is a documented example to copy —docs/fleet/examples/roster-v2.yaml, with a field reference and an executable schema alongside it. I missed that when I filed. Corrected below.The matrix
Same host, same install, same commands. Only the roster file swapped between the v1 that
fleet init --preset generalwrites and the documented v2 example.fleet initwrites)fleet doctorreconcile-failedfleet get <name>mutation-failedfleet apply --expected-generation 1projections: completereconcile-failedfleet installfleet psfleet statussystemctloutputfleet startlifecycle-precondition-failedfleet statusis the clearest tell: with a v2 roster it prints reconciler JSON, with a v1 roster it prints systemd status. Two different commands, one name, selected by a field in a config file.Note also that the v1-side commands reject
generation— a field that is required by the v2 schema and present in the shipped v2 example. So it is not that the v1 side is version-agnostic and the v2 side is strict; both sides are strict, in opposite directions.Why this blocks the goal
fleet installrefuses to create the systemd units thatfleet apply's lifecycle stage then reports missing.applygets as far asprojections: complete, lifecycle: incomplete, recovery: lifecycle-apply-failed.install+start— the only path that reaches an actual launch — and every reconciler command refuses it.There is no roster you can write that gets both halves. To get from a fresh install to a running, manageable seat you currently need to swap the roster file between commands.
The scope note in the v2 docs may explain this
docs/fleet/reference/roster-v2-fields.mdopens with:If that is still the intended scope, then
apply,create,deleteandupdate— commands whose entire job is mutation and lifecycle — are wired to a compiler that is documented as not authorizing mutation or lifecycle. That would make this a sequencing problem rather than a design disagreement: the M1 structural milestone landed and the commands were pointed at it before the layer above it existed.I am not asserting that is what happened; I am flagging that the shipped docs and the shipped wiring disagree, and whoever owns the milestone can settle it in a sentence.
Revised resolutions
The three options in the original report still stand, but the matrix changes which is cheapest:
install,ps,start,stop,status,add,remove,restart). Then a hand-written v2 roster is a complete, usable path today andfleet initwriting v1 becomes a papercut rather than a wall.fleet initwrite v2. Necessary eventually, insufficient alone — it would move the wall from the reconciler toinstall/start, which is arguably worse because the failure comes later.migrate-v1so it writes. Still needed, still does not fix the split.(1) unblocks the goal fastest. (2) and (3) are the real end state. Doing (2) without (1) makes things worse, which is the part I would most want an owner to weigh in on before anyone starts.
Related
0700on the mosaic home andfleet/, which is what letapplyreach lifecycle at all.Measured on canary VMID 1125, greenfield
--nextinstall, CLI 0.0.50-next.2413.Correction — this is worse than "init writes v1", and one of my three proposed resolutions is not available
Re-measured on canary (Debian 13, CLI
0.0.50-next.2413, framework atfix/1236-installer-dir-modesso the permission defect #1236 is out of the way). Two things I got wrong when I filed this.1. It is not that
fleet initwrites the wrong version. Nothing in the product writes v2.I framed this as a mismatch between one producer and one consumer. It is broader. There are three roster producers and all three emit v1:
fleet init --profile <p> --writeframework/fleet/examples/<p>.yamlverbatim — all seven areversion: 1fleet provision("Materialize a roster.yaml from a system-type profile")version: 1hard-coded,fleet-provision.ts:269fleet profile/ system-type pathAnd no roster v2 exists anywhere in the installed framework. The only
roster-v2.yamlin the repo isdocs/fleet/examples/roster-v2.yaml, anddocs/is not part of whatinstall.shsyncs. I proved this by accident: my probe script looked for a v2 example under~/.config/mosaicon a freshly-installed box, found none, and silently fell through to measuring v1 again.So the accurate statement is: the entire roster-v2 command surface —
create,get,update,delete,plan,apply,reconcile,doctor— is unreachable on a stock install, because the input format it requires cannot be produced or found on the machine. An operator gets a v2 roster only by hand-copying one out of the git repo'sdocs/tree.2.
migrate-v1is not a resolution, and I should not have listed it as oneI offered "finish
migrate-v1so it writes" as option 3. Readingfleet-migration-command.ts: it ismigrate-v1 previewonly, and it requires three inputs —--source,--decisions,--observations— each mandatory, each a file path.--decisionsis an explicit migration-decisions JSON and--observationsis "reviewed lifecycle observations". On a fresh box, an operator has no way to author either. It is a tool for a supervised migration of an existing fleet, not a bootstrap path. Even finished, it would not close this.That leaves two real resolutions, not three.
The measured split, both directions
Same box, same modes, only the roster file swapped:
doctor{"error":{"code":"reconcile-failed"}}statusUnit mosaic-tmux-holder.service could not be found.psFleet roster has unknown field(s): generation.install --no-enableInstalled fleet files for 3 agent(s).Fleet roster has unknown field(s): generation.Neither version drives the whole product. Worth noting the error quality is inverted here: the v1 parser rejecting v2 says exactly what is wrong (
unknown field(s): generation), while the reconciler rejecting v1 saysreconcile-failedand nothing else — that is #1238, and this table is a good argument for it.What I propose to do about it, and what is yours to decide
Two separable pieces. The first is not an architecture decision and I intend to build it unless told otherwise; the second is yours.
Piece A — make the v1-side commands accept a v2 roster (mine). Every one of them (
install,ps,start,stop,restart,status,add,remove,verify) loads through a single function,loadFleetRoster. Teaching that one function to recognizeversion: 2and downshift it in memory to the v1 shape fixes all of them at one site. The fields those consumers actually use — agent name, runtime, class, working directory, tmux socket and holder session, per-runtime reset commands — are all present in v2, so the downshift is total for their purposes and loses nothing they read. It writes no file and changes no format, so it stays compatible with whichever direction you pick below.Piece B — which producer emits v2, and when (yours). This is a product decision about the roster format going forward and I am not going to make it unilaterally. The options are: change
fleet initto emit v2 (needs seven new example files, or rendering v2 from the existing v1 examples at write time), or leaveinitas the v1 path and makeprovisionthe v2 path, or ship v2 examples and let the operator choose. They differ in what an upgrade does to existing installs, which is the part that matters for web1.Piece A alone is enough to get a working seat, using a hand-placed v2 roster as the interim — which is what the sandbox E2E needs and what is currently blocking it.
— fred
Correcting my own Piece A — the codebase already has the right pattern and I proposed fighting it
Ten minutes ago I proposed teaching
loadFleetRosterto downshift a v2 roster in memory, and said that would fixinstall,ps,start,stop,restart,status,add,remove,verify"at one site". That was wrong on both counts. I had not yet read far enough.There is already a version dispatcher in
fleet.ts.usesRosterV2ControlPlane(cmd)reads the roster, checksversion === 2, and routes toexecuteReconcilerCommandJson. It is applied to five commands:start,stop,restart(line 1555)status(line 1619)verify(line 1652)That is why
fleet statusreturns reconciler plan JSON on a v2 roster and a systemd message on a v1 roster — not two implementations by accident, one deliberate fork. The design intent is clear and it is not "make the v1 parser tolerant". Those five commands would never reach a downshift anyway, because the dispatch runs first.The actual gap is the five commands that were not given the dispatch:
ps,install,install-systemd,add,remove. Those are the only ones that fail withunknown field(s): generation, and my measured table only caught two of them because I only probed two.And they do not all want the same treatment, which is the part that matters:
addandremovehave direct roster-v2 counterparts already built —createanddeleteinfleet-agent-crud-command.ts. These want the existing dispatch, exactly likestart/stop/restartgot. Mechanical.psandinstall/install-systemdhave no reconciler counterpart.FleetReconcileCommandisplan | apply | reconcile | start | stop | restart | status | verify | doctor— there is nopsand noinstall, and there should not be:psis a read-only display andinstallcopies tool files and enables units. Neither is a desired-state reconciliation. These two want a read-only v2→v1 projection for the fields they actually read (agent name, runtime, class, working directory, tmux socket and holder, per-runtime reset commands), which v2 carries in full.So it is a dispatch fix for three commands and a read-only projection for two, not one adapter for nine. The projection does not contradict the dispatch design — dispatch owns the control-plane verbs, projection serves the two commands that read the roster without being control-plane operations at all.
I am flagging this against myself because the earlier framing would have made Piece A sound like a single tidy change to one function, and someone deciding Piece B on that basis would be working from a wrong picture of the cost. It is still a contained change and still not an architecture decision. It is just five commands, in two shapes, for stated reasons.
Piece B — which producer emits v2 — is unchanged and still yours.
— fred
Correcting Piece A again — and the good news: the v2 control plane already works end to end
Two things in this comment. The first is me correcting my own proposal for the third time. The second is a measurement that makes this issue smaller than it has looked all along.
1. The "v2 → v1 projection" I proposed would break a stated invariant
I proposed that
install,install-systemdandpsget a read-only v2→v1 projection so they could keep using the v1 code path. I had not yet read whatinstallactually writes.installFleet(fleet.ts:2331) ends by writing every agent's generated env file, using the v1 mappinggenerateAgentEnvValues(fleet.ts:480).The reconciler writes that same file using a different function,
projectRosterV2AgentGeneratedEnv(fleet-reconciler.ts:594), which carries this docstring:So if
installprojected v2 down to v1 and then called the v1 mapping, there would be a third writer offleet/agents/<name>.env.generated, and it would reach that file through the mapping the invariant was written to exclude. The two mappings are not identical either — v1 hasmodelHint ?? '', a per-agentsocketoverride, and aworkingDirectoryfallback toroster.defaults; v2 makes model and workingDirectory required and has no per-agent socket. So the projection is not even lossless in the direction that matters.My proposal was to work around the architecture. The architecture is right and I was wrong about it.
2. What Piece A should actually be — no projection anywhere
On a v2 roster the reconciler already owns env. Measured on canary just now:
regenis already v2-native and is explicitly the recovery path for exactly this file. So:install/install-systemd— on a v2 roster, place the tool files and the unit templates, enable the units, and do not write env at all. Env belongs toapply/regen. The only thing these commands need from a v2 roster is the list of agent names, forenableFleetUnits(which reads nothing butagent.name). Everything elseinstallFleetdoes is roster-independent.ps— render the reconciler plan. The plan already carries per-agentname / desiredState / enabled / systemd / tmux / drift, which is whatpsdisplays. It does not need a roster read of its own.add/remove— dispatch to the already-builtcreate/delete, as I said before. This part stands.That is three dispatches and one narrow change to
install, with no new compatibility layer and no second env writer. Smaller than what I described last time, and it stops fighting #791 instead of quietly re-opening it.Two incidental things noticed while reading:
installandinstall-systemdare registered with byte-identical action bodies (fleet.ts:1524-1542), and both load the roster twice — once insideinstallFleetand once at the call site. Neither is a bug, both are cheap to fold in while touching these lines.3. The measurement that reframes this issue: v2 works
I had been treating "no live seat on a v2 roster" as evidence the v2 path was unfinished. It is not. With tmux present and the holder enabled by hand — that is, standing in for the one thing
installwould have done — the whole control plane goes green on a v2 roster:start --expected-generation 1lifecycle-apply-failedlifecycle: complete, holderownedverifylifecycle-precondition-failedsystemd: activeapply --expected-generation 1regen --writeThe earlier
lifecycle-precondition-failedwas the missing holder and nothing else, which is the check behaving correctly.So the v2 control plane is not the problem. What stands between a greenfield box and a live seat is a chain of four separate things, and this issue is only the second link:
doctor. The holder unit fails to start. Nothing downstream can work.installrejects a v2 roster, so on a fresh v2 box the unit templates never get placed or enabled.startreportslifecycle: completewhile every agent pane is dead.start-agent-session.shcreates the session, the launch command dies immediately, the pane-PID lookup fails all five retries, and the script exits 0 having logged it as a heartbeat warning.set -euo pipefailcannot catch this becausetmux new-session -dgenuinely succeeded — tmux made the session, the command inside it exited, tmux tore it down.claude,pi,codexandopencodeare all absent, so the launch command in step 3 ismosaic yolo pi …against apithat does not exist.I filed #1240 and #1241 earlier and then, in my own notes, filed them mentally under "runtime provisioning" and treated them as downstream of this issue. That was wrong in both directions: #1240 is upstream of this issue, and #1241 is the reason a fix here will look like it worked when it has not. Anyone fixing #1237 should expect a green
installfollowed by a greenstartand still no seat, until #1241 is fixed too.Piece B — which producer emits v2, and what upgrade does to existing installs — is unchanged and still yours.
— fred
Piece A is up: PR #1243 (base
next, branchfix/1237-fleet-v2-dispatch).ps/install/install-systemdnow read the roster through a narrow read-only view, so they work on v2.installon v2 places tool files and units and writes no generated env — the reconciler owns that file, and a second writer here would go through the v1 mapping, which is the #791 invariant.add/removerefuse on v2 and name thecreate/delete→applysequence.Canary red→green, same v2 roster, installed
0.0.50-next.2413vs the branch:fleet psunknown field(s): generationfleet install --no-enablefleet remove <name>delete+applyfleet add <name> …create+applyCorrecting myself on
add/removeI said in this thread that routing
add→createandremove→deletewas a stable rename. It is not, and I repeated it to the reviewer three times. The v1 pair edits the roster and drives systemd; the v2 pair changes desired state without runtime actions and leaves convergence toapply.addcollects four fields whereAGENT_REQUEST_KEYSneeds eleven — two of the invented ones beingproviderandtoolPolicy. Defaulting an agent's tool policy silently picks its privilege surface for the operator. So they refuse.⚠️ Attribution warning for anyone verifying this
A correct fix here shows
installrc=0,startrc=0, and still no live seat. #1240 (tmux absent, and neither the installer nordoctorsays so) is upstream of this one; #1241 (startreportslifecycle: completeover dead panes) and the absent agent runtime are downstream. A dead pane after this change is not a #1237 regression, and #1241 in particular will make a correct fix here look like it failed.Piece B is not in this PR
Which producer emits a v2 roster, and what
upgradedoes to an existing v1 install, is a design decision. Not taking it unilaterally.Review
Reviewed over git comms by @scooby, who has no
mosaicstackprincipal on fomo-lin and will not borrow a token, so their sign-off is carried in the PR body rather than filed as a Gitea review. They found a real boot window I had opened (install enables the unit but writes no env on v2 → a reboot before the firstapplyfails every seat unit); it is closed withConditionPathExistson[email protected], and the effect was measured on canary — with the condition, start is rc=0/Result=success/skipped-clean; with it removed by drop-in and nothing else changed, rc=1/ExecMainStatus=64/failed.