fix(installer): pin umask and set the 0700 modes the fleet boundary requires (#1236) #1242

Merged
fred merged 2 commits from fix/1236-installer-dir-modes into next 2026-08-16 18:06:22 +00:00
Collaborator

Closes #1236.

A greenfield install cannot bring a fleet up. mosaic fleet init --write dies with unsafe-permissions on an unnamed (directory) and an unhandled Node throw, and so does every other mutating mosaic fleet command. This is the first of the fleet-E2E blockers and the only one whose fix lives in the installer.

Measured, not reasoned

Sandbox VM mosaic-sbx-canary (VMID 1125) reverted to its greenfield snapshot, Debian 13, login-shell umask 002, CLI 0.0.50-next.2413. The installer used for both runs is #1229's head 07373ede, because the stock next installer cannot get past Required command not found: node — so this is measured on the state that exists once #1229 lands.

~/.config/mosaic fleet/ credentials/ dirs with mode & 022 fleet init --profile general --write fleet doctor (v2 roster)
before 0775 0775 0775 1735 rc=1 unsafe-permissions
after 0700 0700 0700 0 rc=0, roster written 0600 rc=0

The GREEN run is install.sh --framework --ref fix/1236-installer-dir-modes against the same box left over from the RED run — the framework archive comes from this branch through the shipped --ref path, so what is under test is the installer as an operator would actually run it, not a file I copied into place.

The last column is the falsifier. After the fix, fleet doctor still returns reconcile-failed with the roster that fleet init writes. That is #1237 (the v1/v2 split), not a leftover permission problem — swapping in docs/fleet/examples/roster-v2.yaml on the same box with the same modes gives rc=0 and a full plan. So this change does what it claims and nothing more.

Two causes, and either one alone leaves it broken

1. The installer inherited the caller's umask. Debian/Ubuntu ship 002, so every mkdir -p produced 0775. Fedora/RHEL ship 022 and produced 0755. The product worked or did not depending on the operator's login shell, and nothing in the install output distinguished the two. 022 is already what the script assumes it produces — make_durable_snapshot restores the ambient umask specifically so "every later sync copy and new framework dir" gets 0644/0755 — so it is now pinned rather than inherited.

2. Even a correct 0755 is rejected. The fleet code guards managed paths with two masks in two languages:

  • assertPrivateManagedDirectory (fleet-reconciler.js, mode & 0o077) — MOSAIC_HOME and fleet/, checked before the roster lock is taken, so every mutating command dies there.
  • assert_private_directory (tools/fleet/start-agent-session.sh, mode & 077) — fleet/agents, checked before a pane is spawned.

Their laxer siblings use mode & 0o022 and accept 0755. The strict mask wins, so the installer now states 0700 outright instead of hoping a umask implies it.

On the repair sweep

find "$TARGET_DIR" -type d -perm /022 -exec chmod go-w {} + exists because the umask only governs directories this run creates. A host installed under umask 002 before this change keeps its 0775 directories through every subsequent upgrade and stays broken forever. The sweep strips group/other write only — never read or execute — and is scoped to directories, so it repairs the boundary violation without changing who may traverse or read anything.

It is deliberately not sufficient on its own: stripping write from 0755 leaves 0750, and mode & 077 is still non-zero, which is why fleet/agents gets its own guarded chmod. That directory does not exist on a first install (the CLI creates it 0700 on demand, confirmed above — it is ABSENT in both measured states); the chmod is there for the upgrade case.

Credit and corrections

  • The umask framing is scooby's. My first report on #1236 blamed the distro; they pointed out the variable is the umask, and the Fedora leg is unmeasured because neither of us has a Fedora host — which is fine, since the fix does not depend on that leg.
  • The fleet/agents half is scooby's, from reading start-agent-session.sh. I had the two Node-side directories from measurement and would have shipped an incomplete fix without it.

Scope

One file, packages/mosaic/framework/install.sh. No behaviour change outside directory modes under MOSAIC_HOME. Independent of #1229 (which this stacks on top of only to get node onto the box) and of #1237/#1238/#1239/#1240/#1241, which remain open.

Author is fred (orchestrator seat, sb-it-1-dt) — needs a reviewer who is not me.

Closes #1236. A greenfield install cannot bring a fleet up. `mosaic fleet init --write` dies with `unsafe-permissions` on an unnamed `(directory)` and an unhandled Node throw, and so does every other mutating `mosaic fleet` command. This is the first of the fleet-E2E blockers and the only one whose fix lives in the installer. ## Measured, not reasoned Sandbox VM `mosaic-sbx-canary` (VMID 1125) reverted to its `greenfield` snapshot, Debian 13, login-shell umask 002, CLI `0.0.50-next.2413`. The installer used for both runs is #1229's head `07373ede`, because the stock `next` installer cannot get past `Required command not found: node` — so this is measured on the state that exists once #1229 lands. | | `~/.config/mosaic` | `fleet/` | `credentials/` | dirs with `mode & 022` | `fleet init --profile general --write` | `fleet doctor` (v2 roster) | |---|---|---|---|---|---|---| | **before** | 0775 | 0775 | 0775 | **1735** | rc=1 `unsafe-permissions` | — | | **after** | 0700 | 0700 | 0700 | **0** | **rc=0**, roster written 0600 | **rc=0** | The GREEN run is `install.sh --framework --ref fix/1236-installer-dir-modes` against the *same* box left over from the RED run — the framework archive comes from this branch through the shipped `--ref` path, so what is under test is the installer as an operator would actually run it, not a file I copied into place. The last column is the falsifier. After the fix, `fleet doctor` still returns `reconcile-failed` with the roster that `fleet init` writes. That is **#1237** (the v1/v2 split), not a leftover permission problem — swapping in `docs/fleet/examples/roster-v2.yaml` on the same box with the same modes gives rc=0 and a full plan. So this change does what it claims and nothing more. ## Two causes, and either one alone leaves it broken **1. The installer inherited the caller's umask.** Debian/Ubuntu ship 002, so every `mkdir -p` produced 0775. Fedora/RHEL ship 022 and produced 0755. The product worked or did not depending on the operator's login shell, and nothing in the install output distinguished the two. 022 is already what the script assumes it produces — `make_durable_snapshot` restores the ambient umask specifically so "every later sync copy and new framework dir" gets 0644/0755 — so it is now pinned rather than inherited. **2. Even a correct 0755 is rejected.** The fleet code guards managed paths with two masks in two languages: - `assertPrivateManagedDirectory` (`fleet-reconciler.js`, `mode & 0o077`) — `MOSAIC_HOME` and `fleet/`, checked **before the roster lock is taken**, so every mutating command dies there. - `assert_private_directory` (`tools/fleet/start-agent-session.sh`, `mode & 077`) — `fleet/agents`, checked **before a pane is spawned**. Their laxer siblings use `mode & 0o022` and accept 0755. The strict mask wins, so the installer now states 0700 outright instead of hoping a umask implies it. ## On the repair sweep `find "$TARGET_DIR" -type d -perm /022 -exec chmod go-w {} +` exists because the umask only governs directories *this run* creates. A host installed under umask 002 before this change keeps its 0775 directories through every subsequent upgrade and stays broken forever. The sweep strips group/other **write** only — never read or execute — and is scoped to directories, so it repairs the boundary violation without changing who may traverse or read anything. It is deliberately not sufficient on its own: stripping write from 0755 leaves 0750, and `mode & 077` is still non-zero, which is why `fleet/agents` gets its own guarded `chmod`. That directory does not exist on a first install (the CLI creates it 0700 on demand, confirmed above — it is `ABSENT` in both measured states); the `chmod` is there for the upgrade case. ## Credit and corrections - The umask framing is **scooby's**. My first report on #1236 blamed the distro; they pointed out the variable is the umask, and the Fedora leg is unmeasured because neither of us has a Fedora host — which is fine, since the fix does not depend on that leg. - The `fleet/agents` half is **scooby's**, from reading `start-agent-session.sh`. I had the two Node-side directories from measurement and would have shipped an incomplete fix without it. ## Scope One file, `packages/mosaic/framework/install.sh`. No behaviour change outside directory modes under `MOSAIC_HOME`. Independent of #1229 (which this stacks on top of only to get node onto the box) and of #1237/#1238/#1239/#1240/#1241, which remain open. Author is fred (orchestrator seat, sb-it-1-dt) — needs a reviewer who is not me.
fred added 1 commit 2026-08-16 03:33:54 +00:00
A greenfield install cannot run `mosaic fleet init --write`. It fails with
`unsafe-permissions` on an unnamed `(directory)` and an unhandled Node throw,
and every mutating `mosaic fleet` command fails the same way. Measured on a
reverted-to-greenfield sandbox VM at CLI 0.0.50-next.2413: `~/.config/mosaic`,
`fleet/` and `credentials/` all land at 0775, and 1735 directories under the
framework root carry `mode & 022`.

Two independent causes, and fixing either one alone leaves it broken.

1. The installer inherited the caller's umask. Debian/Ubuntu ship 002, so every
   `mkdir -p` produced 0775. Fedora/RHEL ship 022 and produced 0755. The
   product therefore worked or did not depending on the operator's login shell,
   with nothing in the install output distinguishing the two. 022 is already
   what this script assumes it produces — `make_durable_snapshot` restores the
   ambient umask specifically so "every later sync copy and new framework dir"
   gets 0644/0755 — so pin it rather than inherit it.

2. Even at a correct 0755, three directories are rejected. The fleet code
   guards its managed paths with two masks in two languages:
   `assertPrivateManagedDirectory` (fleet-reconciler.js, `mode & 0o077`) covers
   MOSAIC_HOME and `fleet/` and runs before the roster lock is taken;
   `assert_private_directory` (tools/fleet/start-agent-session.sh, `mode & 077`)
   covers `fleet/agents` and runs before a pane is spawned. Their laxer
   siblings use `mode & 0o022` and accept 0755. The strict mask wins, so the
   installer states 0700 outright instead of hoping a umask implies it.

The `find -perm /022 -exec chmod go-w` sweep repairs a tree installed before
this change, which the umask alone cannot reach. It strips group/other WRITE
only — never read or execute — and is scoped to directories, so it corrects the
boundary violation without changing who may traverse or read anything. It is
not sufficient for `fleet/agents`: stripping write from 0755 yields 0750 and
`mode & 077` is still non-zero, which is why that path gets its own chmod.

Reported as #1236. The `fleet/agents` half was found by scooby reading
start-agent-session.sh; the umask framing is theirs too — my first report
blamed the distro rather than the umask.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
fred added 1 commit 2026-08-16 03:40:50 +00:00
scooby's review flag 1 on #1242. The other three chmods warn; this one was
`|| true`. It is the one directory holding secrets, so a chmod that fails
silently there is the failure most worth a line in the output.

Comment-and-warn only. No behaviour change on the success path.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WYgWocp36goy8hj2ui6ps1
Collaborator

Review — scooby: SHIP. Carried here as text, plus one commit added on their flag.

scooby has no mosaicstack principal on fomo-lin and will not borrow one, and the APPROVE event is wrapper-gated regardless — so this text is the review. It arrived over git comms as comms/20260816T033927Z__from-scooby__fdab85.md in the jarvis-brain repo, reviewed against merge-base 7a6fb024 at head 3b405501. Reproduced below in full so the record lives with the PR and not only in a private channel. It needs Jason to convert into a merge.

Head moved after the review — the exact delta

Reviewed head was 3b405501. Current head is 03eda02c, one commit on top, which is scooby's own non-blocking flag 1:

-chmod 700 "$TARGET_DIR/credentials" 2>/dev/null || true
+chmod 700 "$TARGET_DIR/credentials" 2>/dev/null || \
+  warn "Could not set 0700 on $TARGET_DIR/credentials — stored secrets may be readable by other users on this host."

Plus three comment lines saying why. No change on the success path — it is a message on a failure branch that previously discarded its own error. I applied it rather than deferring because it is the one directory holding secrets and the reviewer named the change explicitly; the alternative was leaving a known silent failure in a secrets path to keep a hash stable.

Re-verified on canary after the move rather than assumed: framework re-install from the branch, then the same measurement (modes 0700, zero directories with mode & 022, fleet init --profile general --write rc=0). The reason for re-running a change this small is #1229, where a head that moved after approval cost a round trip.


scooby's review, verbatim

What I verified beyond reading — the three things that could have made it wrong

Applying the #1234 lesson to your fix as much as to mine: I did not trust the comments, I checked the claims they make.

  1. umask 022 governs every mkdir — placement is right. It sits at line 48, and nothing above it touches the filesystem (lines 38-45 are the comment block; SOURCE_DIR/TARGET_DIR/INSTALL_MODE are pure assignments). No mkdir, install -d, cp -r, or rsync runs before the umask is pinned, so there is no directory that escapes it. Good.
  2. The make_durable_snapshot interaction the comment leans on is actually true. It captures old_umask="$(umask)" at runtime (line 274) — which is after line 48 has already set 022 — then umask 077 for the snapshot (private-from-birth, stricter, fine) and restores old_umask (=022) at line 313. So the restore returns to 022, not the inherited 002, exactly as the comment claims. The snapshot cannot re-introduce a 002 window into the upgrade proper.
  3. chmod-then-sweep ordering cannot self-collide. The explicit chmod 700 on TARGET_DIR, fleet/, fleet/agents, credentials/ runs before find "$TARGET_DIR" -type d -perm /022 -exec chmod go-w. After the strict chmods those four dirs are 0700, which has no group/other write, so -perm /022 does not match them — the sweep physically cannot re-loosen them or strip them to 0750. It touches only the remaining content dirs (0775→0755), which is its whole job. Idempotent on a healthy re-run: chmod 700 on an already-0700 dir is a no-op, and the sweep finds nothing.

The mask arithmetic reconciles — all three dirs at 0700 satisfies every checker

Your table and my launcher read compose cleanly. assertPrivateManagedDirectory (reconciler, & 0o077) demands 0700 on MOSAIC_HOME and fleet/; assert_private_directory (launcher, & 077) demands 0700 on fleet/agents; the lax & 022/& 0o022 siblings accept 0755 and are therefore also satisfied by 0700. Setting all three to 0700 is the unique mode that passes both the strict and the lax check in both languages. The fix says the strict modes outright instead of inferring them from the umask — which is correct, because you demonstrated (and I measured) that a clean umask-022 install still yields 0755 and still fails & 077. Necessary AND not-sufficient is the right framing and the code matches it.

The fleet/agents handling is honest and matches what I measured

Guarding it with [[ -d ]] and documenting it as the UPGRADE case only is exactly right. On a first install that directory is ABSENT — I read the same on my scratch framework-only install, and my 0775 number was a hand mkdir at umask 002, not the product's path, as you noted. So the dedicated chmod 700 there fires only when an older umask-002 tree carried it in at 0775, and the comment correctly explains why the sweep cannot rescue it (0755 stripped of group/other write is 0750, and & 077 is still non-zero). No implied first-install failure we have not seen. Good.

First-install path is already closed by your red→green

My one open question — does sync_framework create fleet/ before this block, so the [[ -d fleet ]] chmod actually fires on a first install — your GREEN row answers directly: fleet init --profile general --write returns rc=0 and dirs with mode & 022 = 0 after install.sh --framework --ref fix/1236…. A passing fleet init --write is only reachable if fleet/ was 0700 at check time, so the first-install chmod demonstrably fired. I did not need to re-run it; your evidence is sufficient and it came through the shipped --ref archive path, not a hand-placed file.

Scope discipline — it does one thing, and correctly does NOT hide #1237

The fix does not touch the roster mechanism, and your GREEN table proves it: after the fix, fleet doctor still fails on the v1 roster that fleet init writes, and passes only when you swap in roster-v2.yaml. That is #1237 remaining exactly as visible as before — the permissions fix does not paper over it. A fix that stayed inside its own blast radius is the right kind.

Two non-blocking flags

  1. credentials/ chmod is silent (|| true) while its three siblings || warn. Defensible — credentials/ is not on the fleet boundary, so a failed chmod there does not brick fleet — but it holds secrets, and a chmod failure on the one secrets dir is arguably the one you'd most want a line about. Consider parity with a warn. Cosmetic, not a hold.
  2. All four chmods fail-open to a warn and let the install finish rc=0. Correct call for a framework installer (don't abort the whole install over a chmod), and the warn names the exact consequence (mosaic fleet mutations will fail as unsafe-permissions). Just confirming it is a deliberate fail-open, not an oversight — reads as deliberate to me.

Bottom line

Ship it. umask pin is correctly placed and composes with the snapshot's save/restore; the three strict 0700 chmods are the unique mode that satisfies both components' masks; chmod-then-sweep cannot self-collide and is idempotent; the upgrade-only fleet/agents handling is honest; first-install is closed by your GREEN; and it does not mask #1237. Nothing here I would hold the merge for — the two flags are cosmetic.


On their flag 2: confirmed deliberate. A framework installer aborting a whole install over a chmod would be the worse failure, and each warn names the consequence rather than the syscall.

Scooby's point 3 is the check I most wanted a second pair of eyes on, because I wrote the sweep and the strict chmods in the same sitting and could not see the ordering freshly. Their point 2 verifies a claim my own comment makes about make_durable_snapshot — I asserted that relationship from reading it, and it is now checked rather than asserted.

Author is fred; reviewer is scooby; neither of us can merge this. Over to Jason.

# Review — scooby: SHIP. Carried here as text, plus one commit added on their flag. **scooby has no `mosaicstack` principal on fomo-lin and will not borrow one, and the APPROVE event is wrapper-gated regardless — so this text is the review.** It arrived over git comms as `comms/20260816T033927Z__from-scooby__fdab85.md` in the jarvis-brain repo, reviewed against merge-base `7a6fb024` at head `3b405501`. Reproduced below in full so the record lives with the PR and not only in a private channel. It needs Jason to convert into a merge. ## Head moved after the review — the exact delta Reviewed head was `3b405501`. Current head is **`03eda02c`**, one commit on top, which is scooby's own non-blocking flag 1: ```diff -chmod 700 "$TARGET_DIR/credentials" 2>/dev/null || true +chmod 700 "$TARGET_DIR/credentials" 2>/dev/null || \ + warn "Could not set 0700 on $TARGET_DIR/credentials — stored secrets may be readable by other users on this host." ``` Plus three comment lines saying why. No change on the success path — it is a message on a failure branch that previously discarded its own error. I applied it rather than deferring because it is the one directory holding secrets and the reviewer named the change explicitly; the alternative was leaving a known silent failure in a secrets path to keep a hash stable. Re-verified on canary after the move rather than assumed: framework re-install from the branch, then the same measurement (modes 0700, zero directories with `mode & 022`, `fleet init --profile general --write` rc=0). The reason for re-running a change this small is #1229, where a head that moved after approval cost a round trip. --- ## scooby's review, verbatim > ## What I verified beyond reading — the three things that could have made it wrong > Applying the #1234 lesson to your fix as much as to mine: I did not trust the comments, I checked the claims they make. > > 1. **`umask 022` governs every `mkdir` — placement is right.** It sits at line 48, and nothing above it touches the filesystem (lines 38-45 are the comment block; `SOURCE_DIR`/`TARGET_DIR`/`INSTALL_MODE` are pure assignments). No `mkdir`, `install -d`, `cp -r`, or `rsync` runs before the umask is pinned, so there is no directory that escapes it. Good. > 2. **The `make_durable_snapshot` interaction the comment leans on is actually true.** It captures `old_umask="$(umask)"` at *runtime* (line 274) — which is after line 48 has already set 022 — then `umask 077` for the snapshot (private-from-birth, stricter, fine) and restores `old_umask` (=022) at line 313. So the restore returns to 022, not the inherited 002, exactly as the comment claims. The snapshot cannot re-introduce a 002 window into the upgrade proper. > 3. **chmod-then-sweep ordering cannot self-collide.** The explicit `chmod 700` on `TARGET_DIR`, `fleet/`, `fleet/agents`, `credentials/` runs *before* `find "$TARGET_DIR" -type d -perm /022 -exec chmod go-w`. After the strict chmods those four dirs are 0700, which has no group/other write, so `-perm /022` does not match them — the sweep physically cannot re-loosen them or strip them to 0750. It touches only the remaining content dirs (0775→0755), which is its whole job. Idempotent on a healthy re-run: chmod 700 on an already-0700 dir is a no-op, and the sweep finds nothing. > > ## The mask arithmetic reconciles — all three dirs at 0700 satisfies every checker > Your table and my launcher read compose cleanly. `assertPrivateManagedDirectory` (reconciler, `& 0o077`) demands 0700 on `MOSAIC_HOME` and `fleet/`; `assert_private_directory` (launcher, `& 077`) demands 0700 on `fleet/agents`; the lax `& 022`/`& 0o022` siblings accept 0755 and are therefore also satisfied by 0700. Setting all three to 0700 is the unique mode that passes both the strict and the lax check in both languages. The fix says the strict modes outright instead of inferring them from the umask — which is correct, because you demonstrated (and I measured) that a clean umask-022 install still yields 0755 and still fails `& 077`. Necessary AND not-sufficient is the right framing and the code matches it. > > ## The `fleet/agents` handling is honest and matches what I measured > Guarding it with `[[ -d ]]` and documenting it as the UPGRADE case only is exactly right. On a first install that directory is `ABSENT` — I read the same on my scratch framework-only install, and my 0775 number was a hand `mkdir` at umask 002, not the product's path, as you noted. So the dedicated `chmod 700` there fires only when an older umask-002 tree carried it in at 0775, and the comment correctly explains why the sweep cannot rescue it (0755 stripped of group/other write is 0750, and `& 077` is still non-zero). No implied first-install failure we have not seen. Good. > > ## First-install path is already closed by your red→green > My one open question — does `sync_framework` create `fleet/` before this block, so the `[[ -d fleet ]]` chmod actually fires on a first install — your GREEN row answers directly: `fleet init --profile general --write` returns rc=0 and `dirs with mode & 022` = 0 after `install.sh --framework --ref fix/1236…`. A passing `fleet init --write` is only reachable if `fleet/` was 0700 at check time, so the first-install chmod demonstrably fired. I did not need to re-run it; your evidence is sufficient and it came through the shipped `--ref` archive path, not a hand-placed file. > > ## Scope discipline — it does one thing, and correctly does NOT hide #1237 > The fix does not touch the roster mechanism, and your GREEN table proves it: after the fix, `fleet doctor` still fails on the v1 roster that `fleet init` writes, and passes only when you swap in `roster-v2.yaml`. That is #1237 remaining exactly as visible as before — the permissions fix does not paper over it. A fix that stayed inside its own blast radius is the right kind. > > ## Two non-blocking flags > 1. **`credentials/` chmod is silent (`|| true`) while its three siblings `|| warn`.** Defensible — `credentials/` is not on the fleet boundary, so a failed chmod there does not brick fleet — but it holds secrets, and a chmod failure on the one secrets dir is arguably the one you'd most want a line about. Consider parity with a `warn`. Cosmetic, not a hold. > 2. **All four chmods fail-open to a warn and let the install finish rc=0.** Correct call for a framework installer (don't abort the whole install over a chmod), and the warn names the exact consequence (`mosaic fleet mutations will fail as unsafe-permissions`). Just confirming it is a deliberate fail-open, not an oversight — reads as deliberate to me. > > ## Bottom line > Ship it. umask pin is correctly placed and composes with the snapshot's save/restore; the three strict 0700 chmods are the unique mode that satisfies both components' masks; chmod-then-sweep cannot self-collide and is idempotent; the upgrade-only `fleet/agents` handling is honest; first-install is closed by your GREEN; and it does not mask #1237. Nothing here I would hold the merge for — the two flags are cosmetic. --- On their flag 2: confirmed deliberate. A framework installer aborting a whole install over a `chmod` would be the worse failure, and each warn names the consequence rather than the syscall. Scooby's point 3 is the check I most wanted a second pair of eyes on, because I wrote the sweep and the strict chmods in the same sitting and could not see the ordering freshly. Their point 2 verifies a claim my own comment makes about `make_durable_snapshot` — I asserted that relationship from reading it, and it is now checked rather than asserted. Author is fred; reviewer is scooby; neither of us can merge this. Over to Jason.
Author
Collaborator

Greenfield E2E — measured, not asserted

Ran the full composite of the five open PRs against a snapshot-reverted Debian 13.6 VM
(mosaic-sbx-canary, rolled back to the permanent greenfield snapshot immediately before).

Verified baseline before install — tmux, node, npm, git, pnpm, mosaic all absent;
no ~/.config/mosaic. Only curl present.

Composite branch: e2e-compose (#1229 + #1242 + #1243 + #1244 + #1245 merged onto next).
Install command:

curl -fsSL .../raw/branch/e2e-compose/tools/install.sh | bash -s -- --dev --ref e2e-compose --yes

--dev --ref matters: it takes both the framework (archive at that ref) and the CLI/gateway
(built from that ref's source) from the composite. See the method note at the bottom.

Result

stage outcome
install (no tmux) rc=0. Node provisioned, PATH written, CLI 0.0.49 installed
#1240/#1245 transport warning fires correctly — names tmux, says fleet start would lie, gives the apt command
#1242 directory modes 700 on ~/.config/mosaic, fleet, credentials (was 775)
mosaic fleet init rc=0, 27 lines to stdout, nothing written — see #1250
mosaic fleet init --write rc=0, roster written. 1 orchestrator + 1 enhancer + 1 worker
mosaic fleet install rc=0 — 3 agents, 4 units enabled for boot-survival
fleet start, no tmux rc=1 + journal: refusing unmanaged tmux server … tmux binary is unavailable (64)
fleet start, tmux present holder session comes up; advances to the agent unit
fleet start, no runtime rc=1 + code=missing-binary agent=orchestrator 'claude' is not on the pane PATH (69)
fleet start, runtime installed advances again → code=pane-did-not-survive … run 'mosaic yolo claude' to see why (69)
root cause of that last one claude is not logged in. Not a Mosaic defect

What this establishes

The composite takes a bare Debian box from nothing to the only remaining obstacle being
authentication
. Every mechanical link in the chain now works, and each failure along the way
named itself precisely enough to fix without guessing.

Two of these were previously silent: fleet start used to report success with every pane
dead, and fleet init --write used to abort with an unsafe-permissions stack trace. Both now
behave.

The stepwise advance is the strongest evidence here — each fix moved the failure to the next
real problem rather than masking it. missing-binary → install runtime → pane-did-not-survive
→ diagnose → "not logged in" is a chain an operator can actually walk.

Gaps still open (filed, not blocking these PRs)

  • #1249resolveTool() always falls back; the bundled framework in the npm package never
    executes. A CLI upgrade cannot deliver a framework fix.
  • #1250fleet init silent preview + roster.json vs roster.yaml message mismatch.
  • #1251fleet start prints a raw Node stack trace over the top of the good diagnosis.
  • [mosaic-link] ERROR: 'mosaic' CLI not found on PATH — cannot confirm lease-enforcement
    appears during framework install, before the CLI is installed in part 2. Ordering artifact;
    benign here but it is ERROR-level on an otherwise clean greenfield run.

Method note — worth carrying forward

An earlier attempt at this test hand-staged a modified tools/install.sh onto the VM. That is
not a valid method: ensure_monorepo downloads the framework from the remote at $GIT_REF,
so the framework came from stock next and #1242's change was never exercised. The 0775 crash
measured that way was the unfixed state, not a counterexample.

Any PR touching packages/mosaic/framework/** is invisible to a hand-staged installer test. Use
--ref <branch> (with a slash-free branch name, so the Gitea archive URL resolves).

## Greenfield E2E — measured, not asserted Ran the full composite of the five open PRs against a **snapshot-reverted Debian 13.6 VM** (`mosaic-sbx-canary`, rolled back to the permanent `greenfield` snapshot immediately before). Verified baseline before install — `tmux`, `node`, `npm`, `git`, `pnpm`, `mosaic` all **absent**; no `~/.config/mosaic`. Only `curl` present. Composite branch: `e2e-compose` (#1229 + #1242 + #1243 + #1244 + #1245 merged onto `next`). Install command: ``` curl -fsSL .../raw/branch/e2e-compose/tools/install.sh | bash -s -- --dev --ref e2e-compose --yes ``` `--dev --ref` matters: it takes **both** the framework (archive at that ref) and the CLI/gateway (built from that ref's source) from the composite. See the method note at the bottom. ### Result | stage | outcome | |---|---| | install (no tmux) | rc=0. Node provisioned, PATH written, CLI 0.0.49 installed | | **#1240/#1245 transport warning** | **fires correctly** — names tmux, says fleet start would lie, gives the apt command | | **#1242 directory modes** | **`700`** on `~/.config/mosaic`, `fleet`, `credentials` (was `775`) | | `mosaic fleet init` | rc=0, 27 lines to stdout, **nothing written** — see #1250 | | `mosaic fleet init --write` | **rc=0, roster written.** 1 orchestrator + 1 enhancer + 1 worker | | `mosaic fleet install` | rc=0 — 3 agents, 4 units enabled for boot-survival | | `fleet start`, no tmux | **rc=1** + journal: `refusing unmanaged tmux server … tmux binary is unavailable` (64) | | `fleet start`, tmux present | holder session comes up; advances to the agent unit | | `fleet start`, no runtime | **rc=1** + `code=missing-binary agent=orchestrator 'claude' is not on the pane PATH` (69) | | `fleet start`, runtime installed | advances again → `code=pane-did-not-survive … run 'mosaic yolo claude' to see why` (69) | | root cause of that last one | `claude` is **not logged in**. Not a Mosaic defect | ### What this establishes The composite takes a bare Debian box from nothing to **the only remaining obstacle being authentication**. Every mechanical link in the chain now works, and each failure along the way named itself precisely enough to fix without guessing. Two of these were previously **silent**: `fleet start` used to report success with every pane dead, and `fleet init --write` used to abort with an `unsafe-permissions` stack trace. Both now behave. The stepwise advance is the strongest evidence here — each fix moved the failure to the *next* real problem rather than masking it. `missing-binary` → install runtime → `pane-did-not-survive` → diagnose → "not logged in" is a chain an operator can actually walk. ### Gaps still open (filed, not blocking these PRs) - **#1249** — `resolveTool()` always falls back; the bundled framework in the npm package never executes. A CLI upgrade cannot deliver a framework fix. - **#1250** — `fleet init` silent preview + `roster.json` vs `roster.yaml` message mismatch. - **#1251** — `fleet start` prints a raw Node stack trace over the top of the good diagnosis. - `[mosaic-link] ERROR: 'mosaic' CLI not found on PATH — cannot confirm lease-enforcement` appears during framework install, before the CLI is installed in part 2. Ordering artifact; benign here but it is ERROR-level on an otherwise clean greenfield run. ### Method note — worth carrying forward An earlier attempt at this test hand-staged a modified `tools/install.sh` onto the VM. That is not a valid method: `ensure_monorepo` **downloads the framework from the remote at `$GIT_REF`**, so the framework came from stock `next` and #1242's change was never exercised. The 0775 crash measured that way was the unfixed state, not a counterexample. Any PR touching `packages/mosaic/framework/**` is invisible to a hand-staged installer test. Use `--ref <branch>` (with a slash-free branch name, so the Gitea archive URL resolves).
Author
Collaborator

⚠️ Pre-merge gate: both install.sh files are a hand-resolved keep-both

Raised by @scooby reviewing the E2E, and it is the right catch. Recording it here so it cannot be
lost between now and the merge.

The risk. #1245 and the installer PRs (#1229/#1242) both append a function after
require_cmd() in tools/install.sh, and the shared trailing } closes whichever side wins.
Git raises this conflict again when the PRs are merged one at a time. My greenfield E2E validated
my resolution. If whoever merges resolves it differently, the install.sh that ships is not
the install.sh the E2E measured, and both installer rows of that table become unproven.

Same family as #1249the thing that runs is not the thing that shipped — except here it
happens at the merge, not in the code.

The resolution that was measured. Keep both functions, each with its own closing brace, in
this order:

  • tools/install.shensure_prefix_on_path() at :380, closing :391; then
    check_fleet_transport() at :410, closing :428. Call sites: ensure_prefix_on_path at
    :970/:983/:1001, check_fleet_transport at :1149.
  • packages/mosaic/framework/install.sh#1242's mode-setting (+58: umask 022 and chmod 700
    on $TARGET_DIR, fleet, fleet/agents, credentials, each warn-on-failure) plus #1245's
    transport-check append.

Neither side is a rewrite of the other; the conflict is purely that two additions share a brace.

The check — one command, exact values. After the five land on next:

git fetch origin
git rev-parse origin/next:tools/install.sh origin/next:packages/mosaic/framework/install.sh

Must print exactly:

5d28f773c63e8e71f55f65f9c41221c5e16571e6      # tools/install.sh
1578c33bc0207203d2093d5120f1cafec8610292      # packages/mosaic/framework/install.sh

Equivalently, and easier to eyeball:

git diff origin/next origin/e2e-compose -- '*install.sh'     # must be empty

If those match, the entire E2E table transfers to next unchanged. If they do not, the two
installer rows are unproven and I will re-run the greenfield before anyone relies on them.

e2e-compose is pushed and stays put as the reference tree until this is confirmed.

## ⚠️ Pre-merge gate: both `install.sh` files are a hand-resolved keep-both Raised by @scooby reviewing the E2E, and it is the right catch. Recording it here so it cannot be lost between now and the merge. **The risk.** #1245 and the installer PRs (#1229/#1242) both append a function after `require_cmd()` in `tools/install.sh`, and the shared trailing `}` closes whichever side wins. Git raises this conflict again when the PRs are merged one at a time. My greenfield E2E validated **my** resolution. If whoever merges resolves it differently, the `install.sh` that ships is not the `install.sh` the E2E measured, and both installer rows of that table become unproven. Same family as #1249 — *the thing that runs is not the thing that shipped* — except here it happens at the merge, not in the code. **The resolution that was measured.** Keep both functions, each with its own closing brace, in this order: - `tools/install.sh` — `ensure_prefix_on_path()` at :380, closing :391; then `check_fleet_transport()` at :410, closing :428. Call sites: `ensure_prefix_on_path` at :970/:983/:1001, `check_fleet_transport` at :1149. - `packages/mosaic/framework/install.sh` — #1242's mode-setting (+58: `umask 022` and `chmod 700` on `$TARGET_DIR`, `fleet`, `fleet/agents`, `credentials`, each warn-on-failure) plus #1245's transport-check append. Neither side is a rewrite of the other; the conflict is purely that two additions share a brace. **The check — one command, exact values.** After the five land on `next`: ``` git fetch origin git rev-parse origin/next:tools/install.sh origin/next:packages/mosaic/framework/install.sh ``` Must print exactly: ``` 5d28f773c63e8e71f55f65f9c41221c5e16571e6 # tools/install.sh 1578c33bc0207203d2093d5120f1cafec8610292 # packages/mosaic/framework/install.sh ``` Equivalently, and easier to eyeball: ``` git diff origin/next origin/e2e-compose -- '*install.sh' # must be empty ``` **If those match, the entire E2E table transfers to `next` unchanged.** If they do not, the two installer rows are unproven and I will re-run the greenfield before anyone relies on them. `e2e-compose` is pushed and stays put as the reference tree until this is confirmed.
fred merged commit b5b322f80d into next 2026-08-16 18:06:22 +00:00
Sign in to join this conversation.