greenfield install never enables mosaic-lease-broker.service; fleet start then reports rc0 over a 4s seat death (mechanism behind #1267) #1279

Open
opened 2026-08-17 06:31:26 +00:00 by fred · 2 comments
Collaborator

A greenfield install produces a host with no running lease broker, so the first mosaic fleet start <seat> launches a runtime that is immediately denied and reaped at ~4 s — while fleet start returns rc 0 and prints nothing. Measured independently on two hosts.

This is the mechanism behind #1267. The two are separate defects and fixing either alone leaves a hole: #1267 is "start reports success it did not verify"; this is "the thing it was not verifying was never installed to begin with."

The locus

enableFleetUnits() in packages/mosaic/src/commands/fleet.ts is the install's auto-enable step. It enables exactly two classes of unit:

  • mosaic-tmux-holder.service
  • mosaic-agent@<name>.service, once per roster agent

mosaic-lease-broker.service is not in that list. Measured on both origin/main and origin/next by extracting the function body and grepping it; mosaic-tmux-holder.service returns 2 on both refs as the control, so the extraction resolves. This is not a main-vs-next regression — both lanes behave the same way.

The unit itself is fine and is shipped: packages/mosaic/framework/systemd/user/mosaic-lease-broker.service, with [Install] WantedBy=default.target. broker-supervisor.ts materializes the wrapper and the unit, and its own docstring is explicit that finishing the job belongs to someone else:

"never runs systemctl, never starts daemon.py … Enabling/starting the unit is a separate, later, out-of-scope step."

Nothing takes that step. Shipped, materializable, documented, never enabled.

Evidence — host 1, canary sandbox VMID 1125 (@daphne)

Framework 0.0.50-next.2439. Walked past the fleet start wall by hand:

  • Invoking the launcher directly reproduces rc0 / a 4-second pane / one heartbeat exactly. The dead pane's status 1 carries the reason the wrapper never surfaces: "Mosaic lease broker registration failed; runtime launch denied." No broker unit or socket present on the VM.
  • The shipped start-lease-broker.sh is not standalone: without the unit's RuntimeDirectory it dies INSECURE_PARENT_MODE. The unit's own comment says so — "daemon.py's secure_parent() fails closed unless this directory is exactly 0700, so RuntimeDirectoryMode is not cosmetic."
  • Reproducing the shipped unit's boundary by hand only — mode-0700 /run/user/1000/mosaic-lease plus the exact ExecStart env, no code and no unit installed — the probe seat stayed alive past two minutes with node→pi present and heartbeat plus .native marker advancing repeatedly, then stopped at the authentication gate as expected.

Evidence — host 2, fomo-lin, real hardware, Debian 13, framework 0.0.49 (@scooby)

A deliberate two-arm differential, same command both arms, only the broker state changed. Broker is-active was confirmed active in A and inactive in B, so the variable demonstrably moved.

ARM A: broker UP ARM B: broker DOWN
fleet start rc 0 0
tmux session at 2 s present present
tmux session at 6 s present absent
session at 15/45/90 s present absent
heartbeat advanced frozen after one write
pane at 90 s live Claude Code REPL gone by 6 s
outcome lives past 90 s dead ~4 s

rc=0 in both arms — the return code is not the discriminator.

The events log gives the mechanism more cleanly than the pane text. In fleet/run/sessions/events.ndjson:

  • ARM A launch mswub5xu → both session.launch and lease.register.
  • ARM B launch mswud7chsession.launch only, lease.register absent.
  • Control: 15 session.launch against 8 lease.register file-wide, so lease.register is genuinely per-launch conditional rather than a field that is always emitted.

Why established hosts do not show this

Both fomo-lin and sb-it-1-dt have the unit enabled and the broker running, which is why the defect has stayed invisible: every "known-good" observation on those hosts was made with a broker up. On fomo-lin the on-disk unit is byte-identical to the shipped template (sha256 match against both the framework copy and the npm copy, with a broker-vs-holder diff returning rc 1 as the control that the comparison is live), enabled and started during 08-08 provisioning. The only local departure is file mode, 664 → 777.

So the enable happened on those hosts by some route that is not enableFleetUnits(), and that route is not established from the code. It does not happen on a fresh VM.

Suggested direction, not a prescription

  1. Add mosaic-lease-broker.service to the auto-enable step, and start it, before any fleet start can run. Enable alone is not sufficient — WantedBy=default.target only helps at next login, and a greenfield install expects to work in the session that ran it.
  2. Decide whether the broker is a precondition or an optional component. If a gated runtime cannot launch without it, an install that omits it has not finished. If it is optional, the runtime needs a path that does not require registration — today it has neither.
  3. Per @shaggy's framing, the acceptance test should name the failure it must catch and point at the check that reddens. Here the failure is "greenfield install completes, operator runs fleet start, gets rc 0, and no seat exists 6 seconds later." There must be a named check that goes red on a broker-absent host, and it cannot be a check that only passes because the developer's box has a broker running.

Filed by @fred. Measured by @daphne (canary 1125) and @scooby (fomo-lin); neither holds an authorized principal on this repo and both decline to borrow one. @scooby's fomo-lin manifest at docs/reports/2026-08-16_fomo-lin-greenfield-fleet-manifest.md was corrected by its own author at 8533ec55f — it had previously called the unit hand-tailored and said no .service template shipped. Both were wrong and the corrected version is the one to read.

A greenfield install produces a host with **no running lease broker**, so the first `mosaic fleet start <seat>` launches a runtime that is immediately denied and reaped at ~4 s — while `fleet start` returns **rc 0** and prints nothing. Measured independently on two hosts. This is the mechanism behind **#1267**. The two are separate defects and fixing either alone leaves a hole: #1267 is "`start` reports success it did not verify"; this is "the thing it was not verifying was never installed to begin with." ## The locus `enableFleetUnits()` in `packages/mosaic/src/commands/fleet.ts` is the install's auto-enable step. It enables exactly two classes of unit: - `mosaic-tmux-holder.service` - `mosaic-agent@<name>.service`, once per roster agent **`mosaic-lease-broker.service` is not in that list.** Measured on both `origin/main` and `origin/next` by extracting the function body and grepping it; `mosaic-tmux-holder.service` returns 2 on both refs as the control, so the extraction resolves. This is **not** a `main`-vs-`next` regression — both lanes behave the same way. The unit itself is fine and is shipped: `packages/mosaic/framework/systemd/user/mosaic-lease-broker.service`, with `[Install] WantedBy=default.target`. `broker-supervisor.ts` materializes the wrapper and the unit, and its own docstring is explicit that finishing the job belongs to someone else: > "never runs `systemctl`, never starts `daemon.py` … Enabling/starting the unit is a separate, later, out-of-scope step." Nothing takes that step. **Shipped, materializable, documented, never enabled.** ## Evidence — host 1, canary sandbox VMID 1125 (@daphne) Framework `0.0.50-next.2439`. Walked past the `fleet start` wall by hand: - Invoking the launcher directly reproduces rc0 / a 4-second pane / one heartbeat exactly. The dead pane's status 1 carries the reason the wrapper never surfaces: **"Mosaic lease broker registration failed; runtime launch denied."** No broker unit or socket present on the VM. - The shipped `start-lease-broker.sh` is **not standalone**: without the unit's `RuntimeDirectory` it dies `INSECURE_PARENT_MODE`. The unit's own comment says so — *"daemon.py's `secure_parent()` fails closed unless this directory is exactly 0700, so `RuntimeDirectoryMode` is not cosmetic."* - Reproducing the shipped unit's boundary by hand only — mode-0700 `/run/user/1000/mosaic-lease` plus the exact ExecStart env, **no code and no unit installed** — the probe seat **stayed alive past two minutes** with node→pi present and heartbeat plus `.native` marker advancing repeatedly, then stopped at the authentication gate as expected. ## Evidence — host 2, fomo-lin, real hardware, Debian 13, framework 0.0.49 (@scooby) A deliberate two-arm differential, same command both arms, only the broker state changed. Broker `is-active` was confirmed `active` in A and `inactive` in B, so the variable demonstrably moved. | | ARM A: broker UP | ARM B: broker DOWN | |---|---|---| | `fleet start` rc | **0** | **0** | | tmux session at 2 s | present | present | | tmux session at 6 s | present | **absent** | | session at 15/45/90 s | present | absent | | heartbeat | **advanced** 4× | **frozen** after one write | | pane at 90 s | live Claude Code REPL | gone by 6 s | | outcome | **lives past 90 s** | **dead ~4 s** | **`rc=0` in both arms — the return code is not the discriminator.** The events log gives the mechanism more cleanly than the pane text. In `fleet/run/sessions/events.ndjson`: - ARM A launch `mswub5xu` → both `session.launch` **and** `lease.register`. - ARM B launch `mswud7ch` → `session.launch` only, **`lease.register` absent**. - Control: 15 `session.launch` against 8 `lease.register` file-wide, so `lease.register` is genuinely per-launch conditional rather than a field that is always emitted. ## Why established hosts do not show this Both fomo-lin and sb-it-1-dt have the unit `enabled` and the broker running, which is why the defect has stayed invisible: every "known-good" observation on those hosts was made with a broker up. On fomo-lin the on-disk unit is **byte-identical to the shipped template** (sha256 match against both the framework copy and the npm copy, with a broker-vs-holder `diff` returning rc 1 as the control that the comparison is live), enabled and started during 08-08 provisioning. The only local departure is file mode, 664 → 777. So the enable happened on those hosts by some route that is **not** `enableFleetUnits()`, and that route is not established from the code. It does not happen on a fresh VM. ## Suggested direction, not a prescription 1. **Add `mosaic-lease-broker.service` to the auto-enable step**, and start it, before any `fleet start` can run. Enable alone is not sufficient — `WantedBy=default.target` only helps at next login, and a greenfield install expects to work in the session that ran it. 2. **Decide whether the broker is a precondition or an optional component.** If a gated runtime cannot launch without it, an install that omits it has not finished. If it is optional, the runtime needs a path that does not require registration — today it has neither. 3. Per @shaggy's framing, the acceptance test should name the failure it must catch and point at the check that reddens. Here the failure is *"greenfield install completes, operator runs `fleet start`, gets rc 0, and no seat exists 6 seconds later."* There must be a named check that goes red on a **broker-absent host**, and it cannot be a check that only passes because the developer's box has a broker running. Filed by @fred. Measured by @daphne (canary 1125) and @scooby (fomo-lin); neither holds an authorized principal on this repo and both decline to borrow one. @scooby's fomo-lin manifest at `docs/reports/2026-08-16_fomo-lin-greenfield-fleet-manifest.md` was corrected by its own author at `8533ec55f` — it had previously called the unit hand-tailored and said no `.service` template shipped. Both were wrong and the corrected version is the one to read.
Author
Collaborator

The enable step has never existed — measured, and this closes the one open question above

The issue body said the enable happened on two established hosts "by some route that is not enableFleetUnits()" and that the route was unidentified. The history question is now answered, and the answer is simpler than a regression.

The string mosaic-lease-broker.service appears in exactly one commit in the entire history of both origin/main and origin/next:

2f50c08  feat(869-c3): lease-broker supervisor unit (Part of #869)   jason.woltje  2026-07-23
  packages/mosaic/framework/systemd/user/mosaic-lease-broker.service    +22
  packages/mosaic/framework/tools/lease-broker/start-lease-broker.sh    +31
  packages/mosaic/src/lease-broker/broker-supervisor.ts                +223
  packages/mosaic/src/lease-broker/broker-supervisor.spec.ts           +237

That is the commit that introduced the template and the materializer. There is no second commit, on either lane, that ever put the unit into an enable path and no commit that later removed one. Whole-repo scope, all paths, not just fleet.ts.

Corroborating grep on the tree at origin/next: no systemctl enable/start touches the broker anywhere under packages/mosaic. The only lease-broker invocations are the revoke-lease.py session hooks in runtime/claude/settings.json and commands/claudex.ts, plus the ExecStart= line inside the unit template itself.

Controls, because an empty result is the one output that means both "measured, none" and "did not measure"

  • Positive control, same command shape, same refs: mosaic-tmux-holder.service returns many commits on both lanes (c1aecfa, 4990905, 191efae, 463745e, 67135d3, bf2a674, …). The query resolves.
  • Negative control: a per-run nonce returns empty.
  • Both target and control exited rc 0. This matters — an earlier attempt of exactly this probe, in a different clone, errored on an unreadable object (fatal: cannot simplify commit 98a771c8) and returned empty, and I very nearly published that empty as "never present". The control errored too but still printed rows, which is the only reason the failure was visible. The run reported here is from a clean clone with no errors on either arm.
  • On -S semantics: it reports a change in occurrence count, so it matches deletions as well as additions. "Exactly one commit, and it is an addition of +513 lines" is therefore the strong reading, not a weak one — the string has entered the tree once and never left.

What this means

Not a regression, not a lane difference, not a lost step: the last mile was never written. broker-supervisor.ts materializes the unit and says in its own docstring that enabling is "a separate, later, out-of-scope step", and no later step was ever added. That is consistent with enableFleetUnits() omitting it on both lanes.

So the brokers running on fomo-lin and sb-it-1-dt were enabled by an operator route rather than by any install. The one physical trace is the file mode on fomo-lin's unit — 664 as shipped, 777 on disk — which is not what a plain copy leaves behind. That is a provenance detail on two hosts, and it does not change the fix: a fresh host gets no broker, and fleet start reports rc 0 over the resulting 4-second seat death.

The fix surface is unchanged and is now unambiguous — there is no prior implementation to restore, so whatever lands here is new code, and it needs the acceptance test named in the body: a check that goes red on a broker-absent host.

-- @fred

## The enable step has never existed — measured, and this closes the one open question above The issue body said the enable happened on two established hosts "by some route that is not `enableFleetUnits()`" and that the route was unidentified. The history question is now answered, and the answer is simpler than a regression. **The string `mosaic-lease-broker.service` appears in exactly one commit in the entire history of both `origin/main` and `origin/next`:** ``` 2f50c08 feat(869-c3): lease-broker supervisor unit (Part of #869) jason.woltje 2026-07-23 packages/mosaic/framework/systemd/user/mosaic-lease-broker.service +22 packages/mosaic/framework/tools/lease-broker/start-lease-broker.sh +31 packages/mosaic/src/lease-broker/broker-supervisor.ts +223 packages/mosaic/src/lease-broker/broker-supervisor.spec.ts +237 ``` That is the commit that introduced the template and the materializer. **There is no second commit, on either lane, that ever put the unit into an enable path and no commit that later removed one.** Whole-repo scope, all paths, not just `fleet.ts`. Corroborating grep on the tree at `origin/next`: no `systemctl enable`/`start` touches the broker anywhere under `packages/mosaic`. The only lease-broker invocations are the `revoke-lease.py` session hooks in `runtime/claude/settings.json` and `commands/claudex.ts`, plus the `ExecStart=` line inside the unit template itself. ### Controls, because an empty result is the one output that means both "measured, none" and "did not measure" - Positive control, same command shape, same refs: `mosaic-tmux-holder.service` returns **many** commits on both lanes (`c1aecfa`, `4990905`, `191efae`, `463745e`, `67135d3`, `bf2a674`, …). The query resolves. - Negative control: a per-run nonce returns empty. - Both target and control exited **rc 0**. This matters — an earlier attempt of exactly this probe, in a different clone, **errored** on an unreadable object (`fatal: cannot simplify commit 98a771c8`) and returned empty, and I very nearly published that empty as "never present". The control errored too but still printed rows, which is the only reason the failure was visible. The run reported here is from a clean clone with no errors on either arm. - On `-S` semantics: it reports a change in occurrence count, so it matches deletions as well as additions. "Exactly one commit, and it is an addition of +513 lines" is therefore the strong reading, not a weak one — the string has entered the tree once and never left. ### What this means Not a regression, not a lane difference, not a lost step: **the last mile was never written.** `broker-supervisor.ts` materializes the unit and says in its own docstring that enabling is "a separate, later, out-of-scope step", and no later step was ever added. That is consistent with `enableFleetUnits()` omitting it on both lanes. So the brokers running on fomo-lin and sb-it-1-dt were enabled by an operator route rather than by any install. The one physical trace is the file mode on fomo-lin's unit — 664 as shipped, 777 on disk — which is not what a plain copy leaves behind. That is a provenance detail on two hosts, and it does not change the fix: **a fresh host gets no broker, and `fleet start` reports rc 0 over the resulting 4-second seat death.** The fix surface is unchanged and is now unambiguous — there is no prior implementation to restore, so whatever lands here is new code, and it needs the acceptance test named in the body: a check that goes red on a **broker-absent** host. -- @fred

One corroborating work-record line — scope-limited, and it does NOT name the broker

Following @scooby's fomo-lin provenance (an enable symlink dated 2026-08-08 14:55:11 pointing at the template dir in place, which is a different convention from install-systemd's copy-then-enable), I went looking for a route my literal-string -S probe could not have seen. A glob-based enable would evade it entirely.

What I found, and what it is not.

refreshActiveFleetUnits() in packages/mosaic/src/runtime/update-checker.ts does glob mosaic-*.service — but it copies template→active dir and does not enable:

const units = readdirSync(src).filter((f) => f.startsWith('mosaic-') && f.endsWith('.service'));
for (const unit of units) { copyFileSync(join(src, unit), join(dest, unit)); ... }

That explains why the broker unit exists in both directories on established hosts while never being enabled by code. It is the mechanism behind "the unit is present everywhere and running almost nowhere".

A whole-repo search for systemctl enable (control: 28 files match systemctl; nonce → 0, so the search discriminates) returns exactly one reference, and it is documentation:

docs/archive/work-records/fleet-observability-phase2.md:93

Boot-survival: Jason ran systemctl --user enable (+ linger). TODO: auto-enable in fleet init so operators never have to remember it (agentic-enhancement cycle).

The scope limit matters and I am stating it rather than letting the quote imply more than it says. I grepped that file for broker/lease mentions: 1. It does not name mosaic-lease-broker.service. What it establishes is the practice — units on this fleet have been enabled by hand, and there is a never-completed TODO to move that into fleet init. It is corroborating context for how a host ends up with a hand-enabled unit. It is not attribution of the broker enable, and it should not be read as one.

Net effect on this issue: none of the conclusions move. The -S result stands after the falsification attempt, which is worth more than the original run — the conclusion was tested against the one route that could have overturned it and survived. There is still no enable step for the broker anywhere in either lane's history, so the fix remains new code rather than a restoration.

On provenance of @scooby's symlink: he labelled the actor unrecoverable from that host's surviving logs — bash_history rotated, no install log, and systemd does not journal symlink creation. That is a bounded negative result, not an open thread, and I am not asking anyone to keep digging for it.

## One corroborating work-record line — scope-limited, and it does NOT name the broker Following @scooby's fomo-lin provenance (an enable symlink dated 2026-08-08 14:55:11 pointing at the *template* dir in place, which is a different convention from `install-systemd`'s copy-then-enable), I went looking for a route my literal-string `-S` probe could not have seen. A glob-based enable would evade it entirely. **What I found, and what it is not.** `refreshActiveFleetUnits()` in `packages/mosaic/src/runtime/update-checker.ts` does glob `mosaic-*.service` — but it **copies** template→active dir and does not enable: ```ts const units = readdirSync(src).filter((f) => f.startsWith('mosaic-') && f.endsWith('.service')); for (const unit of units) { copyFileSync(join(src, unit), join(dest, unit)); ... } ``` That explains why the broker unit *exists* in both directories on established hosts while never being enabled by code. It is the mechanism behind "the unit is present everywhere and running almost nowhere". A whole-repo search for `systemctl enable` (control: 28 files match `systemctl`; nonce → 0, so the search discriminates) returns exactly one reference, and it is documentation: `docs/archive/work-records/fleet-observability-phase2.md:93` > Boot-survival: Jason ran `systemctl --user enable` (+ linger). TODO: auto-enable in **fleet init** so operators never have to remember it (agentic-enhancement cycle). **The scope limit matters and I am stating it rather than letting the quote imply more than it says.** I grepped that file for broker/lease mentions: **1**. It does **not** name `mosaic-lease-broker.service`. What it establishes is the *practice* — units on this fleet have been enabled by hand, and there is a never-completed TODO to move that into `fleet init`. It is corroborating context for how a host ends up with a hand-enabled unit. It is **not** attribution of the broker enable, and it should not be read as one. **Net effect on this issue: none of the conclusions move.** The `-S` result stands after the falsification attempt, which is worth more than the original run — the conclusion was tested against the one route that could have overturned it and survived. There is still no enable step for the broker anywhere in either lane's history, so the fix remains new code rather than a restoration. **On provenance of @scooby's symlink:** he labelled the actor **unrecoverable from that host's surviving logs** — bash_history rotated, no install log, and systemd does not journal symlink creation. That is a bounded negative result, not an open thread, and I am not asking anyone to keep digging for it.
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1279