diff --git a/docs/ADMIN-GUIDE/operations/fleet-unattended-first-start.md b/docs/ADMIN-GUIDE/operations/fleet-unattended-first-start.md index 1754fc59..d802c82a 100644 --- a/docs/ADMIN-GUIDE/operations/fleet-unattended-first-start.md +++ b/docs/ADMIN-GUIDE/operations/fleet-unattended-first-start.md @@ -27,7 +27,8 @@ The fleet path never falls back to an interactive wizard. It exits nonzero befor when: - `MOSAIC_AGENT_NAME` is not an exact roster member; -- an ambient `MOSAIC_AGENT_CLASS` disagrees with that member's canonical class; +- a defined ambient `MOSAIC_AGENT_CLASS` is blank/whitespace or disagrees with that member's + canonical class; - a missing destination has no safe regular default source; - a source or existing destination is a symlink (including dangling), directory, unavailable, or over the bounded size; - the fleet communications helper/roster cannot be validated; or @@ -56,8 +57,8 @@ The build leg is load-bearing: `dist/` is ignored, so a direct Vitest invocation absent or stale CLI output. The gate runs the exact-source built CLI in subprocesses with piped stdin, temporary homes, a canonical fixture roster, fake runtime/broker executables, and no provider call. It covers no-TTY launch, exact identity, -private modes, no-clobber, missing/symlink defaults, unknown members, class mismatch, -standalone wizard preservation, and concurrent first start. +private modes, no-clobber, missing/symlink defaults, unknown members, blank/mismatched class, +portable standalone composition/wizard preservation, and concurrent first start. Do not use this fixture as proof that a real provider credential is present or that a package has been deployed. Those require separate environment-specific evidence. diff --git a/docs/DEVELOPER-GUIDE/architecture/fleet-first-start-identity.md b/docs/DEVELOPER-GUIDE/architecture/fleet-first-start-identity.md index 2d940134..44cd9199 100644 --- a/docs/DEVELOPER-GUIDE/architecture/fleet-first-start-identity.md +++ b/docs/DEVELOPER-GUIDE/architecture/fleet-first-start-identity.md @@ -16,8 +16,8 @@ The fix remains at `checkSoul()` and does not add flags to `yolo`, fleet command 1. A present, nonblank, whitespace-exact `MOSAIC_AGENT_NAME` selects the fleet path. 2. `resolveFleetIdentity()` must resolve that exact member through the existing roster/helper - boundary, and any ambient `MOSAIC_AGENT_CLASS` must canonicalize to the roster class, before any - identity seed. + boundary, and any defined `MOSAIC_AGENT_CLASS` (including blank/whitespace) must canonicalize to + the roster class, before any identity seed. Only undefined means absent. 3. `lstatSync()` preflights every destination directory entry without following links, so a dangling link is rejected before its counterpart can be published. 4. Safe bounded snapshots are read from only the missing contracts under `defaults/`. @@ -26,10 +26,12 @@ The fix remains at `checkSoul()` and does not add flags to `yolo`, fleet command concurrent seat or operator won; the existing path is preserved and revalidated. 7. Temporary files are removed, and both installed contracts are re-opened through the no-symlink secure-file reader before launch continues. -8. `composeContract()` independently re-resolves the roster, securely reads `USER.md` through a - descriptor at the point of use, and injects exact member identity and communications data. +8. `composeContract()` independently re-resolves the roster, securely reads fleet `USER.md` through + a Linux descriptor at the point of use, and injects exact member identity and communications data. -A standalone launch with no `MOSAIC_AGENT_NAME` retains the interactive wizard. +A standalone launch with no `MOSAIC_AGENT_NAME` retains the portable tolerant USER read and the +interactive wizard. Fleet-only no-follow enforcement must not make supported standalone macOS +composition depend on Linux `/proc` descriptor traversal. ## Identity and authority @@ -42,9 +44,9 @@ not the source of a fleet seat's identity. The canonical roster controls: - tmux socket and helper target; and - communications generation. -An unknown/padded ambient name or mismatched ambient class fails before any file is seeded. This -avoids replacing the interactive wall with a fleet of indistinguishable or ambiently invented -identities. +An unknown/padded ambient name, mismatched class, or explicitly blank/whitespace class fails before +any file is seeded. This avoids replacing the interactive wall with a fleet of indistinguishable or +ambiently invented identities. ## Concurrency and filesystem properties @@ -64,8 +66,9 @@ no-TTY subprocess, not a direct wizard test. The package `test:vitest` gate buil Vitest, while the clean-checkout command builds its workspace dependencies first, so ignored `dist/cli.js` cannot be absent or stale. A fake lease launcher records whether execution reached the runtime boundary and captures the composed prompt. -Positive and negative cases prove the check can both proceed and refuse. Composition coverage also -replaces a previously validated `USER.md` with an external symlink and proves point-of-use refusal. +Positive and negative cases prove the check can both proceed and refuse. Fleet composition coverage +replaces a previously validated `USER.md` with an external symlink and proves point-of-use refusal; +a standalone unreadable-optional-USER case proves the portable tolerant branch remains separate. Real Pi authentication and provider task execution remain environment tests, not claims of this fixture. diff --git a/docs/USER-GUIDE/workflows/fleet-unattended-first-start.md b/docs/USER-GUIDE/workflows/fleet-unattended-first-start.md index 8425971e..c178a796 100644 --- a/docs/USER-GUIDE/workflows/fleet-unattended-first-start.md +++ b/docs/USER-GUIDE/workflows/fleet-unattended-first-start.md @@ -25,8 +25,8 @@ base. The canonical roster row supplies each seat's exact name, class, peers, so ## Operator behavior -A normal standalone launch without a fleet identity still uses the interactive wizard when -`SOUL.md` is absent: +A normal standalone launch without a fleet identity retains its portable configuration path and still +uses the interactive wizard when `SOUL.md` is absent: ```bash mosaic pi @@ -38,8 +38,8 @@ A roster-owned seat may be started without attaching to its pane: mosaic fleet start ``` -Mosaic refuses before runtime execution if the requested member is absent, its ambient class -conflicts with the roster, a required default is missing or unsafe, or an existing identity contract +Mosaic refuses before runtime execution if the requested member is absent, its explicitly supplied +ambient class is blank or conflicts with the roster, a required default is missing or unsafe, or an existing identity contract is not a safe regular file. Repair the named component and retry the same exact roster member; do not copy another seat's personalized identity. diff --git a/docs/reports/code-review/1264-code-review.md b/docs/reports/code-review/1264-code-review.md index 76f74be1..fe1756b8 100644 --- a/docs/reports/code-review/1264-code-review.md +++ b/docs/reports/code-review/1264-code-review.md @@ -71,6 +71,25 @@ Result: risk `none`, confidence `0.93`, 9 files, no critical/high/medium/low fin could not run Vitest because Vite attempted to create a temporary config artifact on its read-only mount (`EROFS`); executor-owned focused and full results are recorded in the QA report. +## Second exact-head review + +Daphne reviewed exact head `9dc90be7e13b1cd609f6df97d43d890ef5392ca0` and filed Gitea review +ID 169 as `REQUEST_CHANGES`. Review 169 confirmed all review-168 closures, then found: + +1. the new point-of-use reader was Linux-only but had been applied to every standalone USER read, + breaking supported non-fleet macOS composition; and +2. explicit blank/whitespace `MOSAIC_AGENT_CLASS` was treated as absent and could seed before + runtime, while only undefined should mean absent. + +Red-first remediation preserves legacy `readOptional()` for standalone composition, keeps descriptor +no-follow consumption fleet-only, moves the replacement-symlink case under a valid fleet identity, +and rejects defined blank/whitespace classes before seeding. Three blank-class CLI cases and one +tolerant standalone composition case failed before the source change and pass after it. + +Review-169 remediation code review approved at confidence `0.90` (4 files, no findings). Security +review reported risk `none` at confidence `0.90` (4 files, no findings). The review sandbox retained +its known Vite `EROFS` limitation; executor-owned tests are in the QA report. + ## Remaining review gate Daphne must re-review the next exact pushed head. This report cannot record that future verdict diff --git a/docs/reports/documentation/1264-documentation-checklist.md b/docs/reports/documentation/1264-documentation-checklist.md index 4cc3c0ba..c65d5cc0 100644 --- a/docs/reports/documentation/1264-documentation-checklist.md +++ b/docs/reports/documentation/1264-documentation-checklist.md @@ -25,6 +25,8 @@ - [x] Initial padded-name finding remediated and automated re-review approved. - [x] Daphne formal review ID 168 completed on exact first head `43fa0477` and requested changes. -- [x] Four formal-review groups reproduced red and remediated; automated remediation code/security - reviews are clean. -- [ ] Daphne exact-remediation-head re-review completed after push (Fred/goals excluded). +- [x] Four review-168 groups reproduced red and remediated; automated reviews are clean. +- [x] Daphne review ID 169 completed on exact head `9dc90be7` and confirmed review-168 closures. +- [x] Review-169 standalone-portability and blank-class blockers reproduced red and remediated; + automated reviews are clean. +- [ ] Daphne exact-second-remediation-head re-review completed after push (Fred/goals excluded). diff --git a/docs/reports/qa/2026-08-16-1264-unattended-first-start.md b/docs/reports/qa/2026-08-16-1264-unattended-first-start.md index 15486656..d208d14e 100644 --- a/docs/reports/qa/2026-08-16-1264-unattended-first-start.md +++ b/docs/reports/qa/2026-08-16-1264-unattended-first-start.md @@ -1,7 +1,7 @@ # #1264 Unattended Fleet First-Start Verification -> Status: **IN PROGRESS — review remediation complete locally; push/re-review pending** | Executor: -> goals | Date: 2026-08-16 | Target: isolated local fixtures only +> Status: **IN PROGRESS — review-169 remediation complete locally; push/re-review pending** | +> Executor: goals | Date: 2026-08-16 | Target: isolated local fixtures only ## Objective @@ -44,7 +44,7 @@ executables. | ---------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------- | | No-TTY fleet first start avoids wizard and reaches runtime | Exact-source built CLI with piped stdin | CLI GREEN | | Missing top-level identity files are initialized from shipped defaults | Exact-byte and `0600` assertions | CLI + filesystem GREEN | -| Exact seat identity remains roster-owned | Captured argv; name/class mismatch no-side-effect refusals | CLI GREEN | +| Exact seat identity remains roster-owned | Captured argv; mismatched/blank class no-side-effect refusals | CLI GREEN | | Existing operator identity is never overwritten | Custom bytes/mode with defaults removed | CLI + filesystem GREEN | | Concurrent/repeated first start is safe | Four parallel CLIs plus repeated launch | CLI GREEN | | Missing/unsafe defaults and destinations fail before partial mutation | Missing, target/dangling symlink, oversized, invalid-root cases | Filesystem/CLI GREEN | @@ -85,7 +85,18 @@ passed: 3. dangling `USER.md` allowed `SOUL.md` to be published before refusal; and 4. replacing a securely validated `USER.md` with an external symlink was followed by composition. -This establishes that all four reviewer findings were observable on the pushed implementation. +This establishes that all four review-168 findings were observable on the pushed implementation. + +Daphne's review ID 169 then found two more exact-head failures at `9dc90be7`. Before production +changes, four new assertions failed: + +1. standalone composition routed an unreadable optional `USER.md` through the Linux-only descriptor + reader instead of the legacy portable tolerant path; and +2. explicit `MOSAIC_AGENT_CLASS` values `""`, `" "`, and tab were treated as absent, seeded both + identity files, and reached runtime. + +The replacement-symlink case was also moved under a valid roster identity so it tests the fleet-only +security boundary rather than standalone behavior. ## Final GREEN @@ -105,16 +116,17 @@ env -u DATABASE_URL sh -c ' ' ``` -Exit `0`: `6/6` files, `124/124` tests. +Exit `0`: `6/6` files, `128/128` tests. -- 12 real-CLI/no-TTY tests cover exact roster name/class, byte-equal `0600` seeds, no-clobber, - partial seed, missing/symlink defaults, unknown/padded/blank name, mismatched class, standalone - wizard preservation, and four concurrent starts. +- 15 real-CLI/no-TTY tests cover exact roster name/class, byte-equal `0600` seeds, no-clobber, + partial seed, missing/symlink defaults, unknown/padded/blank name, mismatched or explicitly blank + class, standalone wizard preservation, and four concurrent starts. - 12 direct filesystem tests cover complete publication, existing operators, idempotence, source prevalidation, target and dangling destination links, invalid roots, oversized input, and unexpected link errors. -- Composition coverage deterministically replaces validated `USER.md` with an external symlink and - requires refusal at point of use. +- Composition coverage deterministically replaces a valid fleet seat's validated `USER.md` with an + external symlink and requires refusal at point of use. A separate standalone case proves tolerant + optional composition remains outside the Linux-only fleet reader. Full package gate (which rebuilds Mosaic itself after the clean-checkout dependency build): @@ -122,13 +134,13 @@ Full package gate (which rebuilds Mosaic itself after the clean-checkout depende env -u DATABASE_URL pnpm --filter @mosaicstack/mosaic run test:vitest ``` -Exit `0`: `88/88` files, `1,573/1,573` tests. +Exit `0`: `88/88` files, `1,577/1,577` tests. Focused helper + point-of-use coverage: ```text -2 files, 52/52 tests -Statements 97.97% | Branches 88% | Functions 100% | Lines 97.97% +2 files, 53/53 tests +Statements 97.84% | Branches 91.66% | Functions 100% | Lines 97.84% Exit 0 ``` @@ -164,9 +176,14 @@ because this checkout prohibits the PostgreSQL-dependent gateway isolation path. - Codex security review of the remediation: risk `none`, confidence `0.93`, 9 files, no findings. Its sandbox could not execute Vitest because Vite attempted a write on a read-only mount; the executor-owned results above are the test evidence. -- Daphne formal review ID 168 at exact head `43fa0477`: `REQUEST_CHANGES`, four blocking groups. All - four have red-first regressions and local green remediation. Re-review of the next pushed exact - head is necessarily pending until that head exists. +- Daphne formal review ID 168 at exact head `43fa0477`: `REQUEST_CHANGES`, four blocking groups; all + closed by review 169. +- Daphne formal review ID 169 at exact head `9dc90be7`: `REQUEST_CHANGES`, two blocking groups + (standalone portability and explicit blank class). Both now have red-first regressions and local + green remediation. +- Codex review of review-169 remediation: `approve`, confidence `0.90`, 4 files, no findings. +- Codex security review of review-169 remediation: risk `none`, confidence `0.90`, 4 files, no + findings. Exact-new-head Daphne re-review is pending until that head is pushed. ## CI evidence and external blocker @@ -183,7 +200,9 @@ FAIL: host provides 'pi' in the system path; missing-binary cases are not measur That line comes from the inherited `test-start-agent-session.sh` CI-fit guard, not #1264. Fred filed the correction as PR #1270. Its pipeline 2448 is terminal green and proves the four formerly masked suites execute, but #1270 is not merged, so `next` still carries the failing chain. A new #1268 -pipeline is pending the remediation push. Terminal-green #1268 CI is not claimed. +pipeline 2449 at `9dc90be7` reproduced the same single inherited `FAIL:` after Mosaic passed +`1,573/1,573`. A new pipeline is pending the review-169 remediation push. Terminal-green #1268 CI is +not claimed. PR #1268's envelope was read back as `user.login=mos-dt-0`; its commit is explicitly authored and committed by `goals `. No goals Gitea login exists on this host, and no diff --git a/docs/scratchpads/1264-unattended-first-start.md b/docs/scratchpads/1264-unattended-first-start.md index bb077e17..31d2223d 100644 --- a/docs/scratchpads/1264-unattended-first-start.md +++ b/docs/scratchpads/1264-unattended-first-start.md @@ -7,13 +7,14 @@ - Branch: `fix/1264-fleet-unattended-first-start` - Base: `origin/next@476db12b92971634b67fd2057b7577ee5894e449` - First pushed head: `43fa0477877e0d0f110da8d11c3033b40ddeb191` -- Current remediation worktree: `/var/home/jason.woltje/agent-work/1264-review-remediation` +- Current remediation worktree: `/var/home/jason.woltje/agent-work/1264-review2-remediation` - Coordinator: Fred; reviewer must be neither Fred nor this implementation seat. - `docs/TASKS.md` is orchestrator-owned and is not modified by this worker. -The original `/var/home/jason.woltje/agent-work/1264-unattended-first-start` worktree was removed -without force after its pushed head and clean state were verified. The Fred-authorized plain-Git -worktree exception was reused for exact-head review remediation because `/src` remains unavailable. +The original `/var/home/jason.woltje/agent-work/1264-unattended-first-start` and first remediation +worktrees were removed without force after each pushed head and clean state were verified. The +Fred-authorized plain-Git worktree exception was reused for exact-head review remediation because +`/src` remains unavailable. ## Objective @@ -37,14 +38,16 @@ preserving the standalone interactive wizard. - PRD IDs: `FCM-REQ-12`, `AC-FCM-10`; `FCM-REQ-11` is reserved by #1256. - A present fleet name must be nonblank, whitespace-exact, and resolve through the canonical roster. -- Any ambient class must canonicalize to the roster class before mutation. +- Any defined ambient class, including blank/whitespace, must canonicalize to the roster class before + mutation; only undefined means absent. - Preflight all destination directory entries with no-follow existence semantics so dangling links fail before counterpart publication. - Seed only missing top-level files from bounded regular defaults with owner-private, atomic, no-clobber hard links. - Generic defaults are behavior, not identity or authority. -- Securely consume `USER.md` through a descriptor at composition time. -- Standalone missing identity retains the wizard. +- Securely consume fleet `USER.md` through a Linux descriptor at composition time. +- Standalone composition retains the portable tolerant USER read and missing identity retains the + wizard. ## Progress @@ -54,9 +57,11 @@ preserving the standalone interactive wizard. - [x] Initial automated review finding (padded name before write) remediated. - [x] Commit `43fa0477` pushed; PR #1268 opened against `next`; original worktree removed cleanly. - [x] Daphne formal review ID 168 completed on exact first head: `REQUEST_CHANGES` with four groups. -- [x] All four groups reproduced red before remediation and now pass locally. -- [x] Remediation Codex review approved; remediation security review risk `none`. -- [ ] Commit/push remediation with explicit goals author/committer; verify remote object/content. +- [x] All four review-168 groups reproduced red before remediation and passed at `9dc90be7`. +- [x] Daphne review ID 169 completed on `9dc90be7`: review-168 closures confirmed; two new blockers. +- [x] Review-169 portability and blank-class blockers reproduced red and now pass locally. +- [x] Review-169 Codex review approved; security review risk `none`. +- [ ] Commit/push second remediation with explicit goals author/committer; verify remote object/content. - [ ] Daphne exact-new-head re-review. - [ ] Terminal #1268 CI. Pipeline 2445's only `FAIL:` was the inherited Pi-PATH CI-fit guard; PR #1270's pipeline 2448 is green, but #1270 is not merged. @@ -73,13 +78,15 @@ and never created the fake runtime-boundary capture. Against exact first-head production code, four new tests failed while 1,568 existing tests passed: class mismatch mutated before refusal; each dangling destination left its counterpart; and a -replacement `USER.md` symlink was consumed by composition. +replacement `USER.md` symlink was consumed by composition. Review-169 RED then proved standalone +composition hit the Linux-only reader and three explicit blank/whitespace class cases seeded and +launched. ### Final local GREEN -- Exact-source focused gate: `6/6` files, `124/124` tests. -- Full exact-source Mosaic Vitest: `88/88` files, `1,573/1,573` tests. -- Helper + point-of-use coverage: `52/52`; 97.97% statements/lines, 88% branches, 100% functions. +- Exact-source focused gate: `6/6` files, `128/128` tests. +- Full exact-source Mosaic Vitest: `88/88` files, `1,577/1,577` tests. +- Helper + point-of-use coverage: `53/53`; 97.84% statements/lines, 91.66% branches, 100% functions. - Root preflight passed; typecheck `45/45`, lint `25/25`, build `25/25`. - Initial targeted shell gates passed: start-agent-session, install migration `21/21`, init-RCE. - Local aggregate framework shell stops at operator-global Pi `0.84.2` versus measured `0.84.1`. @@ -90,8 +97,8 @@ The full evidence and command boundaries are in ## Review / delivery notes -- Codex remediation review: approve, confidence `0.88`, no findings. -- Codex remediation security review: risk `none`, confidence `0.93`, no findings. +- Review-168 remediation Codex review: approve `0.88`; security risk `none` `0.93`. +- Review-169 remediation Codex review: approve `0.90`; security risk `none` `0.90`. - PR envelope reads `mos-dt-0`; the commit reads goals/goals. No goals Gitea principal exists on this host, so no other principal will be borrowed. Tracked in #1272. - PR #1270 is pushed, not merged. Do not represent `next` or #1268 CI as green until measured. diff --git a/packages/mosaic/src/commands/compose-contract.spec.ts b/packages/mosaic/src/commands/compose-contract.spec.ts index c8514f39..07d93c6b 100644 --- a/packages/mosaic/src/commands/compose-contract.spec.ts +++ b/packages/mosaic/src/commands/compose-contract.spec.ts @@ -334,7 +334,22 @@ describe('composeContract — overlay composer', () => { } }); - it('refuses a USER.md replacement symlink at the point of composition', () => { + it('refuses a fleet USER.md replacement symlink at the point of composition', () => { + mkdirSync(join(fixture.home, 'fleet'), { recursive: true }); + writeFileSync( + join(fixture.home, 'fleet', 'roster.yaml'), + [ + 'version: 1', + 'transport: tmux', + 'agents:', + ' - name: exact-user-seat', + ' runtime: pi', + ' class: worker', + '', + ].join('\n'), + ); + process.env['MOSAIC_AGENT_NAME'] = 'exact-user-seat'; + process.env['MOSAIC_AGENT_CLASS'] = 'worker'; writeFileSync(join(fixture.home, 'defaults', 'SOUL.md'), '# Generic soul\n'); writeFileSync(join(fixture.home, 'defaults', 'USER.md'), '# Generic user\n'); expect(seedFleetIdentityDefaults(fixture.home)).toEqual(['SOUL.md']); @@ -351,6 +366,17 @@ describe('composeContract — overlay composer', () => { expect(readFileSync(external, 'utf8')).toBe('UNSAFE-REPLACEMENT-USER-CONTENT\n'); }); + it('preserves tolerant standalone composition when optional USER.md is unreadable', () => { + const userPath = join(fixture.home, 'USER.md'); + rmSync(userPath); + mkdirSync(userPath); + + const out = composeContract('pi', fixture.home); + + expect(out).toContain(AGENTS); + expect(out).not.toContain('# User Profile'); + }); + it.each(['claude', 'codex', 'opencode', 'pi'] as const)( 'never injects installed TOOLS.md through a target symlink for %s', (runtime) => { diff --git a/packages/mosaic/src/commands/fleet-first-start-identity.ts b/packages/mosaic/src/commands/fleet-first-start-identity.ts index b9160db5..a9040936 100644 --- a/packages/mosaic/src/commands/fleet-first-start-identity.ts +++ b/packages/mosaic/src/commands/fleet-first-start-identity.ts @@ -51,12 +51,11 @@ function installedEntryExists(path: string): boolean { } } -/** Secure point-of-use read for a top-level identity contract. */ -export function readOptionalInstalledIdentityContract( +/** Secure fleet point-of-use read for a top-level identity contract. */ +export function readInstalledIdentityContractAtPointOfUse( mosaicHome: string, entry: (typeof FLEET_IDENTITY_DEFAULTS)[number], - required: boolean = false, -): Buffer | undefined { +): Buffer { const configuredPath = join(mosaicHome, entry); try { // Preserve the launcher's established support for a symlinked Mosaic home, @@ -68,7 +67,6 @@ export function readOptionalInstalledIdentityContract( maxBytes: MAX_IDENTITY_CONTRACT_BYTES, }).content; } catch (error: unknown) { - if (!required && isFilesystemError(error, 'ENOENT')) return undefined; throw unsafeIdentityError('installed', configuredPath, error); } } diff --git a/packages/mosaic/src/commands/launch-first-start.spec.ts b/packages/mosaic/src/commands/launch-first-start.spec.ts index 6a141b7b..755479b6 100644 --- a/packages/mosaic/src/commands/launch-first-start.spec.ts +++ b/packages/mosaic/src/commands/launch-first-start.spec.ts @@ -317,6 +317,23 @@ describe('fleet unattended first start (#1264)', () => { expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false); }); + it.each(['', ' ', '\t'])( + 'refuses explicit blank ambient fleet class %j before seeding', + (agentClass: string) => { + const fixture = createGreenfieldFixture(); + + const result = launchSync(fixture, { agentClass }); + const output = outputOf(result); + + expect(result.status, output).toBe(1); + expect(output).toContain('Refusing split identity authority'); + expect(output).not.toContain('Running setup wizard'); + expect(existsSync(fixture.capturePath)).toBe(false); + expect(existsSync(join(fixture.mosaicHome, 'SOUL.md'))).toBe(false); + expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false); + }, + ); + it.each([' unattended-seat', 'unattended-seat ', ''])( 'refuses non-exact ambient fleet name %j before seeding', (agentName: string) => { diff --git a/packages/mosaic/src/commands/launch.ts b/packages/mosaic/src/commands/launch.ts index 6c1275a2..d9f24224 100644 --- a/packages/mosaic/src/commands/launch.ts +++ b/packages/mosaic/src/commands/launch.ts @@ -31,7 +31,7 @@ import { readPersonaContractBlock } from '../fleet/persona-contract.js'; import { canonicalizeRoleClass } from './fleet-personas.js'; import { launchClaudex, type ClaudexHarnessAdapter } from './claudex.js'; import { - readOptionalInstalledIdentityContract, + readInstalledIdentityContractAtPointOfUse, seedFleetIdentityDefaults, } from './fleet-first-start-identity.js'; import { runLeaseEnforcementDoctorCheck } from './lease-doctor-check.js'; @@ -236,7 +236,7 @@ function checkRuntime(cmd: string): void { function assertAmbientFleetClassMatches(canonicalName: string, canonicalClass: string): void { const configuredClass = process.env['MOSAIC_AGENT_CLASS']; - if (!configuredClass?.trim()) return; + if (configuredClass === undefined) return; const ambientClass = canonicalizeRoleClass(configuredClass).canonicalClass; if (ambientClass !== canonicalClass) { @@ -583,20 +583,21 @@ For required push/merge/issue-close/release actions, execute without routine con parts.push(readFileSync(join(mosaicHome, 'AGENTS.md'), 'utf-8')); // USER.md (+ USER.local.md operator overlay, appended directly under the - // profile its base owns). + // profile its base owns). Fleet first start is Linux/systemd-owned and uses + // the no-follow reader at point of use. Standalone launches retain the + // portable tolerant path used on macOS and other supported hosts. + const fleetAgentName = process.env['MOSAIC_AGENT_NAME']; const user = - readOptionalInstalledIdentityContract( - mosaicHome, - 'USER.md', - process.env['MOSAIC_AGENT_NAME'] !== undefined, - )?.toString('utf8') ?? ''; + fleetAgentName === undefined + ? readOptional(join(mosaicHome, 'USER.md')) + : readInstalledIdentityContractAtPointOfUse(mosaicHome, 'USER.md').toString('utf8'); if (user) parts.push('\n\n# User Profile\n\n' + user); const userLocal = readOptional(join(mosaicHome, 'USER.local.md')); if (userLocal.trim()) { parts.push('\n\n## Operator Overlay (USER.local.md)\n\n' + userLocal); } - const fleetIdentity = resolveFleetIdentity(mosaicHome, process.env['MOSAIC_AGENT_NAME']); + const fleetIdentity = resolveFleetIdentity(mosaicHome, fleetAgentName); if (!fleetIdentity.ok) { throw new Error(`Fleet communications contract unavailable: ${fleetIdentity.error}`); }