Installer PATH block: sentinel-managed, default-home-only profile writes (4c / D25) #1327

Closed
opened 2026-08-19 23:18:29 +00:00 by code-infra-01 · 0 comments
Member

Defect

setupPath() in packages/mosaic/src/stages/finalize.ts guards its shell-profile append on
whether the profile already contains the binDir it is about to write. That prevents a duplicate
for one Mosaic home and is blind to accumulation across different homes: every wizard run
against a fresh temp home appends a permanent # Mosaic PATH block to the operator's real
shell profile.

Measured on sb-it-1-dt (D25, corrected 2026-08-19): 1,061 appends across ~/.bashrc and
~/.profile naming 1,058 unique /tmp/mosaic-* dirs (402 mosaic-wizard-test-*, 656
mosaic-unified-wizard-*), of which zero exist. Root harm: a run against a temp home mutates
operator state outside the home under test.

Requirements (docs/MOSAIC-IMPROVEMENTS.md 4c)

  • S1 Guard on a sentinel, not the value: a # Mosaic managed block with begin/end markers,
    rewritten in place. N runs leave exactly one block whatever the home.
  • S2 Never write outside the home under test: when the target home is not the resolved default,
    the profile write is skipped. A test must not be able to touch $HOME/.bashrc.
  • S3 Idempotent by construction: two runs, same home or two different homes, leave a
    byte-identical profile after the second run.
  • S4 Migration: collapse pre-existing unmarked # Mosaic blocks into the managed block.
  • S5 Windows parity: the $env:Path arm gets the same fix.

Caller census (complete, 2026-08-19)

setupPath is module-private with a single call site (finalize.ts:289, inside finalizeStage).

Reaches setupPath:

  1. src/cli.ts:590 - mosaic wizard command -> runWizard -> finalizeStage
    (src/wizard.ts:335,406,466; plus quick-start path src/wizard.ts:215 ->
    quickStartPath -> src/stages/quick-start.ts:65)
  2. __tests__/integration/full-wizard.test.ts:23 - mkdtemp prefix mosaic-wizard-test-
    (402 blocks measured on host), calls runWizard in-process with real $HOME
  3. __tests__/integration/unified-wizard.test.ts:73 - mkdtemp prefix mosaic-unified-wizard-
    (656 blocks measured on host), same in-process shape

Verified non-entries: mosaic-wizard bin (src/index.ts) is an export barrel and never invokes
the wizard; finalize-skills.spec.ts and finalize-sync-abort.spec.ts mock
getShellProfilePath to null (write path returns early); no shell script appends PATH or runs
the wizard.

Completeness: repo-wide grep for setupPath (one call site), finalizeStage (wizard.ts,
quick-start.ts, two null-profile specs), runWizard (cli.ts, the two integration tests); the
two host-measured prefixes appear in exactly those two test files.

Acceptance

  • Test appending against two distinct temp homes asserts exactly one managed block.
  • Test pointing the installer at a temp home asserts the operator profile is unmodified. This
    arm fails against today's code (S2).
  • Legacy # Mosaic blocks collapse into the managed block (S4).
  • Windows arm covered by the same block logic (S5).

Scope note: ~/.config/mosaic/bin being empty while mosaic resolves from ~/.npm-global/bin
is a separate defect (filed separately by fred); not addressed here.

## Defect `setupPath()` in `packages/mosaic/src/stages/finalize.ts` guards its shell-profile append on whether the profile already contains the binDir it is about to write. That prevents a duplicate for one Mosaic home and is blind to accumulation across different homes: every wizard run against a fresh temp home appends a permanent `# Mosaic` PATH block to the operator's real shell profile. Measured on sb-it-1-dt (D25, corrected 2026-08-19): 1,061 appends across `~/.bashrc` and `~/.profile` naming 1,058 unique `/tmp/mosaic-*` dirs (402 `mosaic-wizard-test-*`, 656 `mosaic-unified-wizard-*`), of which zero exist. Root harm: a run against a temp home mutates operator state outside the home under test. ## Requirements (docs/MOSAIC-IMPROVEMENTS.md 4c) - S1 Guard on a sentinel, not the value: a `# Mosaic` managed block with begin/end markers, rewritten in place. N runs leave exactly one block whatever the home. - S2 Never write outside the home under test: when the target home is not the resolved default, the profile write is skipped. A test must not be able to touch `$HOME/.bashrc`. - S3 Idempotent by construction: two runs, same home or two different homes, leave a byte-identical profile after the second run. - S4 Migration: collapse pre-existing unmarked `# Mosaic` blocks into the managed block. - S5 Windows parity: the `$env:Path` arm gets the same fix. ## Caller census (complete, 2026-08-19) `setupPath` is module-private with a single call site (`finalize.ts:289`, inside `finalizeStage`). Reaches `setupPath`: 1. `src/cli.ts:590` - `mosaic wizard` command -> `runWizard` -> `finalizeStage` (`src/wizard.ts:335,406,466`; plus quick-start path `src/wizard.ts:215` -> `quickStartPath` -> `src/stages/quick-start.ts:65`) 2. `__tests__/integration/full-wizard.test.ts:23` - mkdtemp prefix `mosaic-wizard-test-` (402 blocks measured on host), calls `runWizard` in-process with real `$HOME` 3. `__tests__/integration/unified-wizard.test.ts:73` - mkdtemp prefix `mosaic-unified-wizard-` (656 blocks measured on host), same in-process shape Verified non-entries: `mosaic-wizard` bin (`src/index.ts`) is an export barrel and never invokes the wizard; `finalize-skills.spec.ts` and `finalize-sync-abort.spec.ts` mock `getShellProfilePath` to null (write path returns early); no shell script appends PATH or runs the wizard. Completeness: repo-wide grep for `setupPath` (one call site), `finalizeStage` (wizard.ts, quick-start.ts, two null-profile specs), `runWizard` (cli.ts, the two integration tests); the two host-measured prefixes appear in exactly those two test files. ## Acceptance - Test appending against two distinct temp homes asserts exactly one managed block. - Test pointing the installer at a temp home asserts the operator profile is unmodified. This arm fails against today's code (S2). - Legacy `# Mosaic` blocks collapse into the managed block (S4). - Windows arm covered by the same block logic (S5). Scope note: `~/.config/mosaic/bin` being empty while `mosaic` resolves from `~/.npm-global/bin` is a separate defect (filed separately by fred); not addressed here.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1327