fix(#1264): harden unattended identity bootstrap
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
goals
2026-08-16 18:33:39 -05:00
parent 43fa047787
commit 9dc90be7e1
14 changed files with 448 additions and 288 deletions
@@ -27,9 +27,11 @@ The fleet path never falls back to an interactive wizard. It exits nonzero befor
when: when:
- `MOSAIC_AGENT_NAME` is not an exact roster member; - `MOSAIC_AGENT_NAME` is not an exact roster member;
- an ambient `MOSAIC_AGENT_CLASS` disagrees with that member's canonical class;
- a missing destination has no safe regular default source; - a missing destination has no safe regular default source;
- a source or existing destination is a symlink, directory, unavailable, or over the bounded size; - 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. - the fleet communications helper/roster cannot be validated; or
- `USER.md` cannot be securely re-read at the point where its content is composed.
Diagnostics begin with: Diagnostics begin with:
@@ -45,14 +47,17 @@ not delete or replace an existing personalized `SOUL.md`/`USER.md` merely to cle
The source gate is: The source gate is:
```bash ```bash
pnpm --filter @mosaicstack/mosaic exec vitest run \ pnpm --filter @mosaicstack/mosaic... build && \
src/commands/launch-first-start.spec.ts pnpm --filter @mosaicstack/mosaic exec vitest run \
src/commands/launch-first-start.spec.ts
``` ```
It runs the real built CLI in subprocesses with piped stdin, temporary homes, a canonical fixture The build leg is load-bearing: `dist/` is ignored, so a direct Vitest invocation could otherwise run
roster, fake runtime/broker executables, and no provider call. It covers no-TTY launch, exact identity, absent or stale CLI output. The gate runs the exact-source built CLI in subprocesses with piped stdin,
private modes, no-clobber, missing/symlink defaults, unknown members, standalone wizard preservation, temporary homes, a canonical fixture roster, fake runtime/broker executables, and no provider call. It
and concurrent first start. covers no-TTY launch, exact identity,
private modes, no-clobber, missing/symlink defaults, unknown members, class mismatch,
standalone 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 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. been deployed. Those require separate environment-specific evidence.
@@ -14,17 +14,20 @@ fleet pane with no TTY, that child blocked or failed before the runtime boundary
The fix remains at `checkSoul()` and does not add flags to `yolo`, fleet commands, systemd units, or The fix remains at `checkSoul()` and does not add flags to `yolo`, fleet commands, systemd units, or
`start-agent-session.sh`: `start-agent-session.sh`:
1. A nonblank `MOSAIC_AGENT_NAME` selects the fleet path. 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 2. `resolveFleetIdentity()` must resolve that exact member through the existing roster/helper
boundary before any identity seed. boundary, and any ambient `MOSAIC_AGENT_CLASS` must canonicalize to the roster class, before any
3. Safe bounded snapshots are read from only the missing contracts under `defaults/`. identity seed.
4. Each snapshot is written to a random owner-private temporary file in `MOSAIC_HOME`. 3. `lstatSync()` preflights every destination directory entry without following links, so a dangling
5. `linkSync()` publishes the complete file without overwriting an existing path. `EEXIST` means a link is rejected before its counterpart can be published.
4. Safe bounded snapshots are read from only the missing contracts under `defaults/`.
5. Each snapshot is written to a random owner-private temporary file in `MOSAIC_HOME`.
6. `linkSync()` publishes the complete file without overwriting an existing path. `EEXIST` means a
concurrent seat or operator won; the existing path is preserved and revalidated. concurrent seat or operator won; the existing path is preserved and revalidated.
6. Temporary files are removed, and both installed contracts are re-opened through the no-symlink 7. Temporary files are removed, and both installed contracts are re-opened through the no-symlink
secure-file reader before launch continues. secure-file reader before launch continues.
7. `composeContract()` independently re-resolves the roster and injects exact member identity and 8. `composeContract()` independently re-resolves the roster, securely reads `USER.md` through a
communications data. 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 interactive wizard.
@@ -39,13 +42,14 @@ not the source of a fleet seat's identity. The canonical roster controls:
- tmux socket and helper target; and - tmux socket and helper target; and
- communications generation. - communications generation.
An unknown ambient name fails before any file is seeded. This avoids replacing the interactive wall An unknown/padded ambient name or mismatched ambient class fails before any file is seeded. This
with a fleet of indistinguishable or ambiently invented identities. avoids replacing the interactive wall with a fleet of indistinguishable or ambiently invented
identities.
## Concurrency and filesystem properties ## Concurrency and filesystem properties
- Sources and final destinations are bounded regular files beneath `MOSAIC_HOME`; symlinks are not - Sources and final destinations are bounded regular files beneath `MOSAIC_HOME`; target and dangling
followed. symlinks are not followed.
- New files have mode `0600`. - New files have mode `0600`.
- Hard-link publication is same-filesystem, atomic, and no-clobber. - Hard-link publication is same-filesystem, atomic, and no-clobber.
- A temporary path is removed only when this process successfully created it. - A temporary path is removed only when this process successfully created it.
@@ -56,9 +60,12 @@ with a fleet of indistinguishable or ambiently invented identities.
## Verification ## Verification
`src/commands/launch-first-start.spec.ts` uses the production-kind boundary: the real built CLI in a `src/commands/launch-first-start.spec.ts` uses the production-kind boundary: the real built CLI in a
no-TTY subprocess, not a direct wizard test. A fake lease launcher records whether execution reached no-TTY subprocess, not a direct wizard test. The package `test:vitest` gate builds Mosaic before
the runtime boundary and captures the composed prompt. Positive and negative cases prove the check Vitest, while the clean-checkout command builds its workspace dependencies first, so ignored
can both proceed and refuse. `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.
Real Pi authentication and provider task execution remain environment tests, not claims of this Real Pi authentication and provider task execution remain environment tests, not claims of this
fixture. fixture.
@@ -11,7 +11,8 @@ stop at the interactive identity wizard.
When `MOSAIC_AGENT_NAME` names an exact member of the installed fleet roster and top-level identity When `MOSAIC_AGENT_NAME` names an exact member of the installed fleet roster and top-level identity
contracts are absent, the launcher: contracts are absent, the launcher:
1. validates the exact roster member and installed fleet communications helper; 1. validates the exact roster member, its canonical class, and the installed fleet communications
helper;
2. reads the shipped generic contracts from 2. reads the shipped generic contracts from
`~/.config/mosaic/defaults/SOUL.md` and `defaults/USER.md`; `~/.config/mosaic/defaults/SOUL.md` and `defaults/USER.md`;
3. creates only the missing top-level `SOUL.md` and `USER.md` as owner-private files; 3. creates only the missing top-level `SOUL.md` and `USER.md` as owner-private files;
@@ -37,9 +38,10 @@ A roster-owned seat may be started without attaching to its pane:
mosaic fleet start <exact-roster-name> mosaic fleet start <exact-roster-name>
``` ```
Mosaic refuses before runtime execution if the requested member is absent, a required default is Mosaic refuses before runtime execution if the requested member is absent, its ambient class
missing or unsafe, or an existing identity contract is not a safe regular file. Repair the named conflicts with the roster, a required default is missing or unsafe, or an existing identity contract
path and retry the same exact roster member; do not copy another seat's personalized identity. 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.
## Separate prerequisites ## Separate prerequisites
+51 -32
View File
@@ -1,59 +1,78 @@
# Issue #1264 Independent Code and Security Review # Issue #1264 Code and Security Review
> Scope: uncommitted delivery delta for `fix/1264-fleet-unattended-first-start` against > Branch: `fix/1264-fleet-unattended-first-start` | Base:
> `origin/next@476db12b92971634b67fd2057b7577ee5894e449` | Reviewer: Codex CLI via Mosaic review tools > `origin/next@476db12b92971634b67fd2057b7577ee5894e449`
## Initial code review ## Initial automated review
Command: Codex reviewed the pre-PR uncommitted delta with:
```bash ```bash
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted \ ~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted \
-o /tmp/1264-codex-code-review.json -o /tmp/1264-codex-code-review.json
``` ```
Result: `request-changes`, confidence `0.93`, `20` files reviewed, `0` blockers, `1` should-fix. Result: `request-changes`, confidence `0.93`, 20 files, one should-fix. `checkSoul()` trimmed
`MOSAIC_AGENT_NAME` for pre-seed resolution while composition used the original value, so a padded
name could seed files before later refusal.
Finding: `checkSoul()` trimmed `MOSAIC_AGENT_NAME` for pre-seed roster resolution while later Remediation rejected blank/leading/trailing-whitespace values before roster lookup or writes and
composition used the original value. A padded exact name could therefore seed identity files and added three built-CLI no-side-effect regressions. Automated re-review approved that delta with no
then fail composition. findings (confidence `0.86`). Initial security review reported risk `none` (confidence `0.91`).
Remediation: ## Formal exact-head review
- treat any present blank or surrounding-whitespace value as an invalid fleet launch; Daphne reviewed PR #1268 at exact head `43fa0477877e0d0f110da8d11c3033b40ddeb191` and filed Gitea
- reject it before roster lookup or identity writes; and review ID 168 as `REQUEST_CHANGES`. The review was source/PR-only; the canary remained untouched.
- add three real-CLI no-side-effect regressions for leading padding, trailing padding, and empty
values.
## Code re-review Blocking groups:
Command: 1. class mismatch was validated after first-start mutation;
2. secure `USER.md` validation was discarded before ordinary path-following composition;
3. `existsSync()` treated a dangling destination symlink as missing, allowing counterpart partial
publication; and
4. the built-CLI/evidence chain allowed stale ignored `dist/`, cited an unshipped canary object, and
carried conflicting test totals/pane wording.
The diagnostic's defaults-only repair advice was also inaccurate for roster/class/destination
failures.
## Formal-review remediation
All four blocking groups received regressions before production changes. The RED run produced four
failures while 1,568 existing tests passed. Remediation then:
- validates canonical name and class before seeding;
- preflights destination directory entries with `lstatSync()` so target and dangling symlinks fail
before publication;
- securely reads `USER.md` through an `O_NOFOLLOW` descriptor at composition time;
- adds a Mosaic build before package Vitest and a dependency build in the clean-checkout command;
- replaces defaults-only advice with neutral named-component repair guidance; and
- reconciles shipping canary provenance, pane chronology, commands, and totals.
Remediation code review:
```bash ```bash
~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted \ ~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted \
-o /tmp/1264-codex-code-rereview.json -o /tmp/1264-remediation-code-review.json
``` ```
Result: `approve`, confidence `0.86`, `15` files reviewed, no findings. The review sandbox could run Result: `approve`, confidence `0.88`, 6 files, no findings. Summary: the fail-closed destination
package typecheck but could not run Vitest because its checkout was read-only and Vite attempted to checks, class-validation order, secure composition, and build-before-Vitest path are coherent.
create a timestamped config artifact (`EROFS`). This is not scored as test evidence; the executor's
writable worktree independently passed the focused and full suites recorded in the QA report.
## Security review Remediation security review:
Final command:
```bash ```bash
~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted \ ~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted \
-o /tmp/1264-codex-security-rereview.json -o /tmp/1264-remediation-security-review.json
``` ```
Result: risk `none`, confidence `0.91`, `20` files reviewed, `0` critical/high/medium/low findings. Result: risk `none`, confidence `0.93`, 9 files, no critical/high/medium/low findings. The sandbox
The review specifically confirmed roster validation before seeding, bounded no-symlink reads, could not run Vitest because Vite attempted to create a temporary config artifact on its read-only
no-clobber publication, unsafe/padded identity refusal, and fail-closed behavior before runtime. mount (`EROFS`); executor-owned focused and full results are recorded in the QA report.
## Independent PR review gate ## Remaining review gate
Automated review is complete. The PR still requires a formal reviewer who is neither the implementation Daphne must re-review the next exact pushed head. This report cannot record that future verdict
seat nor Fred, per the assignment. That review and CI status are recorded in the QA report when without changing the reviewed head, so the authoritative terminal verdict belongs to PR #1268's
available. Gitea review record. Fred and goals are excluded as reviewers.
@@ -4,7 +4,8 @@
- [x] `docs/PRD.md` updated with `FCM-REQ-12` and `AC-FCM-10`. - [x] `docs/PRD.md` updated with `FCM-REQ-12` and `AC-FCM-10`.
- [x] User workflow documents unattended fleet first start and separate prerequisites. - [x] User workflow documents unattended fleet first start and separate prerequisites.
- [x] Administrator operations page documents source/destination ownership, failure handling, and verification. - [x] Administrator operations page documents source/destination ownership, failure handling, and an
exact-source build-before-Vitest verification gate.
- [x] Developer architecture page documents control flow, identity authority, concurrency, and non-goals. - [x] Developer architecture page documents control flow, identity authority, concurrency, and non-goals.
- [x] `docs/SITEMAP.md` and book indexes updated. - [x] `docs/SITEMAP.md` and book indexes updated.
- [x] QA evidence is under `docs/reports/qa/`; working notes are under `docs/scratchpads/`. - [x] QA evidence is under `docs/reports/qa/`; working notes are under `docs/scratchpads/`.
@@ -22,6 +23,8 @@
## Review gate ## Review gate
- [x] Independent automated code/security review completed on the final uncommitted delta. - [x] Initial padded-name finding remediated and automated re-review approved.
- [x] Padded-name finding remediated and automated re-review approved with no findings. - [x] Daphne formal review ID 168 completed on exact first head `43fa0477` and requested changes.
- [ ] Formal PR review by a reviewer other than goals/Fred completed on the exact pushed head. - [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).
@@ -1,185 +1,201 @@
# #1264 Unattended Fleet First-Start Verification # #1264 Unattended Fleet First-Start Verification
> Status: **IN PROGRESS** | Executor: goals | Date: 2026-08-16 | Target: isolated local fixtures only > Status: **IN PROGRESS — review remediation complete locally; push/re-review pending** | Executor:
> goals | Date: 2026-08-16 | Target: isolated local fixtures only
## Objective ## Objective
Verify that a named fleet seat launched through a systemd-equivalent, no-TTY environment on a clean Verify that a named fleet seat launched through a systemd-equivalent, no-TTY environment on a clean
host reaches its runtime boundary without an interactive Mosaic identity wizard. Preserve standalone host reaches its runtime boundary without an interactive Mosaic identity wizard. Preserve standalone
wizard behavior and canonical roster ownership of exact seat identity. wizard behavior and canonical-roster ownership of exact seat identity.
## Source evidence accepted for local verification ## Source evidence accepted for local verification
Daphne's canary investigation was read from jarvis-brain commit Daphne's canary Run-7 report is reachable from jarvis-brain `origin/main` at
`6c0b6fc70ae6a179a1b7ff9dedfc54e9adccd19a`, report `8bf94afeb8c7d5df96cdd4a4508e75a1d2999710`,
`docs/reports/2026-08-16_sbx-canary-greenfield-e2e.md`. It measured: `docs/reports/2026-08-16_sbx-canary-greenfield-e2e.md`. The earlier local object
`6c0b6fc70ae6a179a1b7ff9dedfc54e9adccd19a` is not reachable from an origin ref and is not used as
shipping provenance. Run 7 measured:
```text ```text
systemd -> start-agent-session.sh -> mosaic yolo pi (PID 3726) systemd -> start-agent-session.sh -> mosaic yolo pi (PID 3726)
-> child mosaic wizard (PID 3762) -> child mosaic wizard (PID 3762)
``` ```
The canary pane remains preserved and was not accessed. Product behavior is independently tested here The pane was preserved when Run 7 was captured. Formal review ID 168 records that an authorized
with temporary roots and fake runtime executables; no canary or installed-host inference is scored as rollback occurred later. This task never accessed or altered the canary VM, pane, snapshot, or
local PASS evidence. rollback state. Product behavior is independently tested here with temporary roots and fake runtime
executables.
## Controls ## Controls
- Worktree base: `origin/next@476db12b92971634b67fd2057b7577ee5894e449`. - Original base: `origin/next@476db12b92971634b67fd2057b7577ee5894e449`.
- `DATABASE_URL` remains unset. - PR: #1268, first pushed head `43fa0477877e0d0f110da8d11c3033b40ddeb191`.
- No real credential, token, provider, VM, installed Mosaic tree, unit, timer, PATH profile, or live - `DATABASE_URL` remains unset for local tests.
tmux session is read or mutated. - No runtime/provider credential or token value, VM, installed Mosaic tree, unit, timer, PATH profile,
- Tiny's concurrent runtime-preflight and `start-agent-session.sh` PATH work are out of scope. or live tmux session is read or mutated. Standard Gitea/Woodpecker wrappers authenticate metadata
reads/writes without exposing credential values.
- Tiny's runtime-preflight and `start-agent-session.sh` PATH work remain out of scope.
- Held PR #1213 is not a dependency. - Held PR #1213 is not a dependency.
## Requirements-to-evidence map ## Requirements-to-evidence map
| Acceptance criterion | Method | Evidence | | Acceptance criterion | Method | Evidence |
| ---------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------ | | ---------------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------- |
| No-TTY fleet first start avoids wizard and reaches runtime | Real built-CLI subprocess with piped stdin | Focused GREEN, CLI test 1 | | 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 | Focused GREEN, CLI tests 1/11 | | 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 plus unknown/padded/blank-name refusals | Focused GREEN, CLI tests 1/69 | | Exact seat identity remains roster-owned | Captured argv; name/class mismatch no-side-effect refusals | CLI GREEN |
| Existing operator identity is never overwritten | Custom bytes/mode with defaults removed | Focused GREEN, CLI tests 2/3 | | 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 | Focused GREEN, CLI tests 2/11 | | Concurrent/repeated first start is safe | Four parallel CLIs plus repeated launch | CLI GREEN |
| Missing/unsafe defaults fail without prompting | Missing, symlink, oversized, and installed-link tests | Focused GREEN, unit/CLI tests | | Missing/unsafe defaults and destinations fail before partial mutation | Missing, target/dangling symlink, oversized, invalid-root cases | Filesystem/CLI GREEN |
| Standalone launch retains wizard | Same real CLI without fleet identity | Focused GREEN, CLI test 10 | | Validated `USER.md` cannot be replaced by an external symlink | Seed, replace, compose at point of use | Composition GREEN |
| Standalone launch retains wizard | Same built CLI without fleet identity | CLI GREEN |
| Built-CLI evidence cannot use stale ignored `dist/` | Build-with-dependencies gate before Vitest | Package script + command gate |
## Command evidence ## Initial RED
### Worktree helper refusal and sanctioned fallback Production source remained unchanged after adding the first reproducer. The CLI was built from
`origin/next@476db12` before the test.
Command:
```bash
~/bin/mosaic-worktree.sh new fix/1264-fleet-unattended-first-start --from origin/next
```
Exit: `1`. Stderr was retained; the helper refused because the derived worktree path was under
`/var/home/jason.woltje`, while `/src` does not exist on this host. Fred explicitly authorized the
plain-git fallback and path used for this task.
Command:
```bash
git -C /var/home/jason.woltje/src/stack worktree add \
/var/home/jason.woltje/agent-work/1264-unattended-first-start \
-b fix/1264-fleet-unattended-first-start origin/next
```
Exit: `0`; HEAD `476db12b92971634b67fd2057b7577ee5894e449`.
### RED
Production source remained unchanged after adding the reproducer. The built CLI represented
`origin/next@476db12` behavior.
Command:
```bash ```bash
env -u DATABASE_URL pnpm --filter @mosaicstack/mosaic exec vitest run \ env -u DATABASE_URL pnpm --filter @mosaicstack/mosaic exec vitest run \
src/commands/launch-first-start.spec.ts src/commands/launch-first-start.spec.ts
``` ```
Exit: `1`. Exit `1`; one file and one test failed. Output included:
```text ```text
Test Files 1 failed (1)
Tests 1 failed (1)
[mosaic] SOUL.md not found. Running setup wizard... [mosaic] SOUL.md not found. Running setup wizard...
◆ What would you like to do? ◆ What would you like to do?
[mosaic] Setup failed. Run: mosaic wizard [mosaic] Setup failed. Run: mosaic wizard
AssertionError: expected 1 to be +0 AssertionError: expected 1 to be +0
``` ```
The fixture used piped stdin (not a TTY), a temporary `HOME`/`MOSAIC_HOME`, shipped default bytes, The fake runtime-boundary capture was not created. Complete stdout/stderr was retained at
a canonical one-seat roster, a fake `pi`, and a fake lease-runtime boundary. The wizard rendered and `/tmp/1264-red.out` during that work session.
the runtime-boundary capture was never created. Complete combined stdout/stderr was retained at
`/tmp/1264-red.out` during execution.
### GREEN and baseline ## Formal-review remediation RED
After the production change, the original one-test command exited `0` with `1/1` passing. The final Daphne's exact-head review ID 168 requested changes at `43fa0477`. Before changing production code,
focused command was: new regressions were run against an exact-source build. Four tests failed while the existing 1,568
passed:
1. valid roster name plus mismatched ambient class seeded both files before refusal;
2. dangling `SOUL.md` allowed `USER.md` to be published before refusal;
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.
## Final GREEN
The production-kind command builds Mosaic and all workspace dependencies before invoking Vitest,
because `dist/` is ignored and may otherwise be absent or stale:
```bash ```bash
env -u DATABASE_URL pnpm --filter @mosaicstack/mosaic exec vitest run \ env -u DATABASE_URL sh -c '
src/commands/fleet-first-start-identity.spec.ts \ pnpm --filter @mosaicstack/mosaic... build &&
src/commands/launch-first-start.spec.ts \ pnpm --filter @mosaicstack/mosaic exec vitest run \
src/commands/launch.spec.ts \ src/commands/fleet-first-start-identity.spec.ts \
src/commands/compose-contract.spec.ts \ src/commands/launch-first-start.spec.ts \
src/config/file-adapter.test.ts \ src/commands/launch.spec.ts \
src/cli-smoke.spec.ts src/commands/compose-contract.spec.ts \
src/config/file-adapter.test.ts \
src/cli-smoke.spec.ts
'
``` ```
Exit: `0`; `6/6` files and `119/119` tests passed. The 11 production-kind CLI tests cover no-TTY Exit `0`: `6/6` files, `124/124` tests.
launch, captured exact roster name/class, byte-equal `0600` seeds, no-clobber/idempotence, partial
seed, missing/symlink defaults, unknown/padded/blank ambient members, standalone interactive control,
and four concurrent first starts with no temporary residue. Nine direct filesystem tests cover
successful/no-clobber hard-link publication, unexpected link errors, source prevalidation, existing
operator contracts, idempotence, symlink sources/destinations, and oversized input.
Full package Vitest: - 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.
- 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.
Full package gate (which rebuilds Mosaic itself after the clean-checkout dependency build):
```bash
env -u DATABASE_URL pnpm --filter @mosaicstack/mosaic run test:vitest
```
Exit `0`: `88/88` files, `1,573/1,573` tests.
Focused helper + point-of-use coverage:
```text ```text
Test Files 88 passed (88) 2 files, 52/52 tests
Tests 1568 passed (1568) Statements 97.97% | Branches 88% | Functions 100% | Lines 97.97%
Exit 0 Exit 0
``` ```
New helper coverage: Final repository gates after remediation:
```text ```text
Statements 100% | Branches 93.33% | Functions 100% | Lines 100% pnpm preflight exit 0
9/9 tests passed; coverage command exit 0 pnpm typecheck 45/45 tasks, exit 0
pnpm lint 25/25 tasks, exit 0
pnpm build 25/25 tasks, exit 0
pnpm format:check exit 0
git diff --check exit 0
``` ```
Baseline commands: Pre-PR targeted shell runs on the unchanged shell surfaces also passed:
```text ```text
pnpm preflight exit 0 bash framework/tools/fleet/test-start-agent-session.sh exit 0 locally
pnpm typecheck 45/45 tasks, exit 0 bash framework/tools/quality/scripts/test-install-migration.sh 21 passed, 0 failed
pnpm lint 25/25 tasks, exit 0 bash framework/tools/_scripts/test-mosaic-init-rce.sh PASS
pnpm build 25/25 tasks, exit 0
pnpm format:check exit 0
pnpm --filter @mosaicstack/mosaic build exit 0
bash framework/tools/fleet/test-start-agent-session.sh
exit 0; retained expected fixture LD_PRELOAD warning
bash framework/tools/quality/scripts/test-install-migration.sh
21 passed, 0 failed, exit 0
bash framework/tools/_scripts/test-mosaic-init-rce.sh
PASS, exit 0
git diff --check exit 0
``` ```
The aggregate `test:framework-shell` command exited `1` at `invariant_r_unittest.py`: `5/6` tests The aggregate local `test:framework-shell` run stopped at `invariant_r_unittest.py`: installed
passed and the remaining test refused the operator-global Pi drift from measured `0.84.1` to operator-global Pi is `0.84.2`, while the invariant is measured for `0.84.1`. Later aggregate stages
installed `0.84.2`. This is retained as an environment/version-coupling failure, not scored as a remain unmeasured except the targeted suites above. Root `pnpm test` remains locally **UNTESTED**
#1264 code failure and not retried. The aggregate stopped there; later aggregate stages are because this checkout prohibits the PostgreSQL-dependent gateway isolation path.
**UNTESTED** except for the three targeted shell suites listed above.
Root `pnpm test` is **UNTESTED** because it can execute the prohibited local PostgreSQL-dependent ## Review and security evidence
gateway isolation path. No PostgreSQL service, connection, migration, or initialization was used.
CI is **UNTESTED — pending PR**. - Initial Codex review found padded-name mutation-before-refusal; it was fixed with three
no-side-effect regressions.
- Codex review of the formal-review remediation: `approve`, confidence `0.88`, 6 files, no findings.
- 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.
## CI evidence and external blocker
Pipeline 2445 ran against exact first head `43fa0477`:
- install, sanitization, upgrade guard, typecheck, lint, and format passed;
- Mosaic Vitest passed `88/88`, `1,568/1,568`; and
- the test step emitted exactly one `FAIL:` line:
```text
FAIL: host provides 'pi' in the system path; missing-binary cases are not measurable here
```
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.
PR #1268's envelope was read back as `user.login=mos-dt-0`; its commit is explicitly authored and
committed by `goals <[email protected]>`. No goals Gitea login exists on this host, and no
other principal was borrowed. The cross-wrapper principal defect is tracked in #1272.
## Explicitly untested ## Explicitly untested
- Canary VM remediation or restart: **UNTESTED and prohibited for this task**. - Canary VM remediation/restart: **UNTESTED and prohibited**.
- Real Pi authentication/provider prompt and task execution: **UNTESTED**. - Real Pi authentication/provider prompt and task execution: **UNTESTED**.
- PR #1213 composition layer: **UNTESTED and not required**. - PR #1213 composition layer: **UNTESTED and not required**.
- Deployment/published npm package behavior: **UNTESTED until CI/release; deployment is not this PR's scope**. - Deployment/published npm behavior: **UNTESTED until merge/release**.
- Local PostgreSQL execution/migration: **UNTESTED and prohibited**.
## Review and residual risks The local gate proves Mosaic crosses its identity boundary and reaches a fake lease-runtime boundary;
it does not claim provider readiness, deployment, or a currently running canary seat.
Initial independent Codex code review returned `request-changes` with one should-fix: a padded
`MOSAIC_AGENT_NAME` could be trimmed for pre-seed validation and later rejected in composition,
leaving seeds behind. The implementation now rejects blank or padded values before roster lookup or
writes; three no-side-effect regression cases pass. Codex re-review approved the remediated delta
with no findings (`confidence=0.86`). Its read-only sandbox could not execute Vitest because Vite
needed a temporary config artifact; executor-owned focused/full results above are the test evidence.
Final Codex security review found no confirmed vulnerabilities (`risk=none`, confidence `0.91`).
Formal PR review by a reviewer other than goals/Fred and CI remain pending.
Real Pi authentication/provider prompt and task execution remain unmeasured. The local gate proves
that Mosaic crosses its identity boundary and reaches the fake lease-runtime boundary; it does not
claim provider readiness or deployment.
+67 -84
View File
@@ -3,112 +3,95 @@
## Tracking ## Tracking
- Issue: `mosaicstack/stack#1264` - Issue: `mosaicstack/stack#1264`
- PR: `mosaicstack/stack#1268`
- Branch: `fix/1264-fleet-unattended-first-start` - Branch: `fix/1264-fleet-unattended-first-start`
- Base: `origin/next@476db12b92971634b67fd2057b7577ee5894e449` - Base: `origin/next@476db12b92971634b67fd2057b7577ee5894e449`
- Worktree: `/var/home/jason.woltje/agent-work/1264-unattended-first-start` - First pushed head: `43fa0477877e0d0f110da8d11c3033b40ddeb191`
- Current remediation worktree: `/var/home/jason.woltje/agent-work/1264-review-remediation`
- Coordinator: Fred; reviewer must be neither Fred nor this implementation seat. - Coordinator: Fred; reviewer must be neither Fred nor this implementation seat.
- `docs/TASKS.md` is orchestrator-owned and is not modified by this worker. - `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.
## Objective ## Objective
A roster-owned fleet seat launched from systemd on a clean host must cross Mosaic's first-run identity A roster-owned fleet seat launched from systemd on a clean host must cross Mosaic's first-run identity
gate without a human or TTY, while retaining an exact seat identity from the canonical roster and gate without a human or TTY, while retaining exact name/class from the canonical roster and
preserving the interactive wizard for standalone launches. preserving the standalone interactive wizard.
## Intake and boundaries ## Intake and boundaries
- Read Daphne's source report at jarvis-brain commit - Shipping canary provenance is jarvis-brain `origin/main` commit
`6c0b6fc70ae6a179a1b7ff9dedfc54e9adccd19a` before implementation. `8bf94afeb8c7d5df96cdd4a4508e75a1d2999710`. The earlier local `6c0b6fc...` object is not used.
- Read Tiny's concurrent `PREFLIGHT-STATE.md`; do not edit - The Run-7 pane was preserved when evidence was captured; formal review records a later authorized
`start-agent-session.sh`, its PATH builder, runtime preflight, or #1258's Node candidate. rollback. This task never accessed or altered the canary.
- Do not touch the canary VM or this host's `~/.config/mosaic`. - Tiny's concurrent runtime-preflight, `start-agent-session.sh`, and #1258 PATH seam remain untouched.
- Do not depend on held PR #1213 or introduce the proposed `~/.mosaic` composition layer. - Held PR #1213 is not a dependency.
- No real credentials/provider calls. Tests use temporary roots and fake executables only. - No runtime/provider credential values or provider calls, installed-host changes, PostgreSQL, unit,
- Report exact commands, exit codes, and retained stderr in timer, or profile mutation. Tests use temporary roots and fake executables only; Gitea/Woodpecker
`docs/reports/qa/2026-08-16-1264-unattended-first-start.md`. metadata operations use standard wrappers without exposing credentials.
## Requirements and design assumptions ## Requirements and design
- PRD IDs: `FCM-REQ-12`, `AC-FCM-10`; `FCM-REQ-11` is reserved by concurrent #1256. - PRD IDs: `FCM-REQ-12`, `AC-FCM-10`; `FCM-REQ-11` is reserved by #1256.
- `ASSUMPTION:` generated `MOSAIC_AGENT_NAME` distinguishes fleet launches; the existing runtime - A present fleet name must be nonblank, whitespace-exact, and resolve through the canonical roster.
composer still validates the exact member against the canonical roster. - Any ambient class must canonicalize to the roster class before mutation.
- Prefer the shipped `defaults/SOUL.md` and `defaults/USER.md` over threading wizard flags through - Preflight all destination directory entries with no-follow existence semantics so dangling links
every launcher. Seed only missing top-level files, never overwrite existing operator content. fail before counterpart publication.
- Generic defaults are a base behavior contract, not the seat identity. The roster-resolved injected - Seed only missing top-level files from bounded regular defaults with owner-private, atomic,
block supplies exact agent/session name and class. no-clobber hard links.
- Fleet missing/unsafe defaults must fail closed without attempting an interactive wizard. - Generic defaults are behavior, not identity or authority.
- Standalone missing identity retains today's wizard behavior. - Securely consume `USER.md` through a descriptor at composition time.
- Standalone missing identity retains the wizard.
## Plan
1. Add a no-TTY, systemd-equivalent failing reproducer before production changes; record RED.
2. Add narrow first-start bootstrap logic at the existing `checkSoul()` seam only.
3. Prove generic default bytes, private modes, exact roster identity, no clobber, idempotence/race,
invalid-default refusal, and standalone wizard preservation.
4. Run focused, package, shell/framework, typecheck, lint, format, build, and greenfield fixture gates.
5. Update user/developer/admin documentation, sitemap, QA report, and documentation checklist.
6. Obtain independent code review, remediate, commit with explicit `goals` identity, queue-guard,
push, open PR to `next`, and request a reviewer other than Fred/goals.
7. After branch is pushed and worktree is clean, remove this worktree as Fred explicitly required.
## Budget
- No user-specified token cap.
- Working estimate: 25K tokens for source/test/docs/review/PR lifecycle.
- Reduce scope before expanding launcher surfaces; stop and report if the fix requires #1213 or the
contested pane-PATH function.
## Progress ## Progress
- [x] Issue #1264 identified and read. - [x] Issue, canary report, Tiny collision state, and PRD read/amended.
- [x] Daphne's report and Tiny's state read. - [x] Initial production-kind RED captured with a real built CLI and no TTY.
- [x] Fred authorized plain-git worktree placement after the mandated helper failed on this host. - [x] Implementation, tests, user/admin/developer docs, QA, and indexes delivered.
- [x] PRD amended before coding. - [x] Initial automated review finding (padded name before write) remediated.
- [x] RED test captured: focused Vitest `1 failed`, command exit `1`; real built CLI entered the - [x] Commit `43fa0477` pushed; PR #1268 opened against `next`; original worktree removed cleanly.
identity wizard under piped stdin and never reached the fake runtime boundary. - [x] Daphne formal review ID 168 completed on exact first head: `REQUEST_CHANGES` with four groups.
- [x] Implementation and canonical documentation complete. - [x] All four groups reproduced red before remediation and now pass locally.
- [x] Applicable local baseline and situational gates complete; aggregate shell has one scoped - [x] Remediation Codex review approved; remediation security review risk `none`.
environment refusal and root DB-backed test remains prohibited/unrun. - [ ] Commit/push remediation with explicit goals author/committer; verify remote object/content.
- [x] Independent automated review complete: one padded-name finding fixed; clean code/security - [ ] Daphne exact-new-head re-review.
re-review. Formal non-goals/non-Fred PR reviewer pending. - [ ] Terminal #1268 CI. Pipeline 2445's only `FAIL:` was the inherited Pi-PATH CI-fit guard; PR
- [ ] PR lifecycle complete. #1270's pipeline 2448 is green, but #1270 is not merged.
- [ ] Worktree removed. - [ ] Remove the clean remediation worktree after push.
## Test evidence ## Test evidence
### RED — 2026-08-16 ### Initial RED
```bash The built `origin/next` CLI entered `mosaic wizard`, rendered `What would you like to do?`, exited 1,
env -u DATABASE_URL pnpm --filter @mosaicstack/mosaic exec vitest run \ and never created the fake runtime-boundary capture.
src/commands/launch-first-start.spec.ts
```
Exit `1`; `1` file failed, `1` test failed. The child emitted ### Formal-review RED
`[mosaic] SOUL.md not found. Running setup wizard...`, rendered
`What would you like to do?`, then emitted `[mosaic] Setup failed. Run: mosaic wizard`.
The assertion expected runtime exit `0` and received `1`; the fake runtime-boundary capture was not
created. Full output is retained at `/tmp/1264-red.out` for this work session.
### GREEN — current delta 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.
- Original no-TTY subprocess test: `1/1` passed, command exit `0`. ### Final local GREEN
- Final focused set: `6/6` files, `119/119` tests passed.
- Full Mosaic Vitest: `88/88` files, `1568/1568` tests passed.
- New helper coverage: 100% statements/functions/lines, 93.33% branches.
- Root preflight/format/diff checks passed; typecheck 45/45, lint 25/25, build 25/25.
- Targeted start-agent-session, install migration (21/21), and init-RCE shell tests passed.
- Aggregate framework shell stopped at the known environment refusal: global Pi is 0.84.2 while
Invariant R is measured for 0.84.1. It was not retried or scored as a #1264 failure.
- Root `pnpm test` remains **UNTESTED** because it can execute prohibited PostgreSQL-dependent tests.
- CI remains **UNTESTED** until the PR is pushed.
## Risks / blockers - 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.
- 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`.
- Local root `pnpm test` remains unrun because the checkout prohibits its PostgreSQL-dependent path.
- The shipped defaults are intentionally generic; exact fleet identity must remain visibly The full evidence and command boundaries are in
roster-derived to avoid making every seat indistinguishable. `docs/reports/qa/2026-08-16-1264-unattended-first-start.md`.
- First-start writes are a concurrent boundary when several systemd seats launch together; creation
must be no-clobber and idempotent. ## Review / delivery notes
- The test intentionally drives the real built CLI rather than exporting private launch helpers; this
keeps the systemd/no-TTY execution boundary under test. - Codex remediation review: approve, confidence `0.88`, no findings.
- Real Pi authentication and provider task execution remain an environment-level residual and are - Codex remediation security review: risk `none`, confidence `0.93`, no findings.
explicitly untested in this local fixture. - 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.
+1 -1
View File
@@ -102,7 +102,7 @@ mosaic yolo pi # Launch Pi in yolo mode
The launcher: The launcher:
1. Verifies `~/.config/mosaic` exists 1. Verifies `~/.config/mosaic` exists
2. Resolves identity: standalone launches auto-run `mosaic init` when `SOUL.md` is missing; exact roster-owned fleet launches atomically seed only missing `SOUL.md`/`USER.md` from generic `defaults/` and never prompt 2. Resolves identity: standalone launches auto-run `mosaic init` when `SOUL.md` is missing; exact roster-owned fleet launches validate name/class, atomically seed only missing `SOUL.md`/`USER.md` from generic `defaults/`, securely consume `USER.md`, and never prompt
3. Injects `AGENTS.md` into the runtime 3. Injects `AGENTS.md` into the runtime
4. Forwards all arguments to the runtime CLI 4. Forwards all arguments to the runtime CLI
+2 -1
View File
@@ -24,7 +24,8 @@
"build": "tsc", "build": "tsc",
"lint": "eslint src", "lint": "eslint src",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell", "test": "pnpm run test:vitest && pnpm run test:framework-shell",
"test:vitest": "pnpm run build && vitest run --passWithNoTests",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/fleet/test-start-agent-session.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh" "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/fleet/test-start-agent-session.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh"
}, },
"dependencies": { "dependencies": {
@@ -15,6 +15,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { FileConfigAdapter } from '../config/file-adapter.js'; import { FileConfigAdapter } from '../config/file-adapter.js';
import { seedFleetIdentityDefaults } from './fleet-first-start-identity.js';
import { composeContract } from './launch.js'; import { composeContract } from './launch.js';
/** /**
@@ -319,6 +320,7 @@ describe('composeContract — overlay composer', () => {
].join('\n'), ].join('\n'),
); );
process.env['MOSAIC_AGENT_NAME'] = 'exact-self'; process.env['MOSAIC_AGENT_NAME'] = 'exact-self';
expect(seedFleetIdentityDefaults(installedHome)).toEqual(['SOUL.md', 'USER.md']);
const composed = composeContract('pi', installedHome); const composed = composeContract('pi', installedHome);
expect(composed).toContain(sourceTools); expect(composed).toContain(sourceTools);
@@ -332,6 +334,23 @@ describe('composeContract — overlay composer', () => {
} }
}); });
it('refuses a USER.md replacement symlink at the point of composition', () => {
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']);
const userPath = join(fixture.home, 'USER.md');
const external = join(fixture.root, 'attacker-user.md');
writeFileSync(external, 'UNSAFE-REPLACEMENT-USER-CONTENT\n');
rmSync(userPath);
symlinkSync(external, userPath);
expect(() => composeContract('pi', fixture.home)).toThrow(
`fleet identity installed is unavailable or unsafe: ${userPath}`,
);
expect(readFileSync(external, 'utf8')).toBe('UNSAFE-REPLACEMENT-USER-CONTENT\n');
});
it.each(['claude', 'codex', 'opencode', 'pi'] as const)( it.each(['claude', 'codex', 'opencode', 'pi'] as const)(
'never injects installed TOOLS.md through a target symlink for %s', 'never injects installed TOOLS.md through a target symlink for %s',
(runtime) => { (runtime) => {
@@ -115,6 +115,17 @@ describe('seedFleetIdentityDefaults', () => {
expect(existsSync(join(mosaicHome, 'USER.md'))).toBe(false); expect(existsSync(join(mosaicHome, 'USER.md'))).toBe(false);
}); });
it('fails closed when the configured Mosaic home is not a directory', () => {
const root = mkdtempSync(join(tmpdir(), 'mosaic-identity-invalid-home-'));
roots.push(root);
const mosaicHome = join(root, 'mosaic-home');
writeFixture(mosaicHome, 'not a directory\n');
expect(() => seedFleetIdentityDefaults(mosaicHome)).toThrow(
`fleet identity installed is unavailable or unsafe: ${join(mosaicHome, 'SOUL.md')}`,
);
});
it('refuses a symlinked default instead of following it', () => { it('refuses a symlinked default instead of following it', () => {
const mosaicHome = createMosaicHome(); const mosaicHome = createMosaicHome();
const source = join(mosaicHome, 'defaults', 'SOUL.md'); const source = join(mosaicHome, 'defaults', 'SOUL.md');
@@ -139,6 +150,24 @@ describe('seedFleetIdentityDefaults', () => {
expect(existsSync(join(mosaicHome, 'USER.md'))).toBe(false); expect(existsSync(join(mosaicHome, 'USER.md'))).toBe(false);
}); });
it.each(['SOUL.md', 'USER.md'] as const)(
'rejects a dangling %s destination before publishing its counterpart',
(entry) => {
const mosaicHome = createMosaicHome();
const destination = join(mosaicHome, entry);
const counterpart = join(mosaicHome, entry === 'SOUL.md' ? 'USER.md' : 'SOUL.md');
symlinkSync(join(mosaicHome, 'missing-identity-target'), destination);
expect(existsSync(destination)).toBe(false);
expect(lstatSync(destination).isSymbolicLink()).toBe(true);
expect(() => seedFleetIdentityDefaults(mosaicHome)).toThrow(
`fleet identity installed is unavailable or unsafe: ${destination}`,
);
expect(lstatSync(destination).isSymbolicLink()).toBe(true);
expect(existsSync(counterpart)).toBe(false);
},
);
it('rejects an oversized source before publishing a partial identity', () => { it('rejects an oversized source before publishing a partial identity', () => {
const mosaicHome = createMosaicHome(); const mosaicHome = createMosaicHome();
const source = join(mosaicHome, 'defaults', 'USER.md'); const source = join(mosaicHome, 'defaults', 'USER.md');
@@ -1,13 +1,13 @@
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { existsSync, linkSync, rmSync, writeFileSync } from 'node:fs'; import { linkSync, lstatSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { readRegularFileSecure } from '../fleet/secure-file.js'; import { readRegularFileSecure } from '../fleet/secure-file.js';
const MAX_IDENTITY_CONTRACT_BYTES = 256 * 1024; const MAX_IDENTITY_CONTRACT_BYTES = 256 * 1024;
export const FLEET_IDENTITY_DEFAULTS = ['SOUL.md', 'USER.md'] as const; export const FLEET_IDENTITY_DEFAULTS = ['SOUL.md', 'USER.md'] as const;
function isAlreadyExistsError(error: unknown): boolean { function isFilesystemError(error: unknown, code: string): boolean {
return error instanceof Error && 'code' in error && error.code === 'EEXIST'; return error instanceof Error && 'code' in error && error.code === code;
} }
/** @internal Publish a complete temporary file without replacing any path. */ /** @internal Publish a complete temporary file without replacing any path. */
@@ -16,11 +16,16 @@ export function linkIdentityContractNoClobber(source: string, destination: strin
linkSync(source, destination); linkSync(source, destination);
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {
if (isAlreadyExistsError(error)) return false; if (isFilesystemError(error, 'EEXIST')) return false;
throw error; throw error;
} }
} }
function unsafeIdentityError(kind: 'default' | 'installed', path: string, error: unknown): Error {
const reason = error instanceof Error ? error.message : String(error);
return new Error(`fleet identity ${kind} is unavailable or unsafe: ${path} (${reason})`);
}
function readIdentityContract( function readIdentityContract(
mosaicHome: string, mosaicHome: string,
path: string, path: string,
@@ -32,8 +37,39 @@ function readIdentityContract(
maxBytes: MAX_IDENTITY_CONTRACT_BYTES, maxBytes: MAX_IDENTITY_CONTRACT_BYTES,
}).content; }).content;
} catch (error: unknown) { } catch (error: unknown) {
const reason = error instanceof Error ? error.message : String(error); throw unsafeIdentityError(kind, path, error);
throw new Error(`fleet identity ${kind} is unavailable or unsafe: ${path} (${reason})`); }
}
function installedEntryExists(path: string): boolean {
try {
lstatSync(path);
return true;
} catch (error: unknown) {
if (isFilesystemError(error, 'ENOENT')) return false;
throw unsafeIdentityError('installed', path, error);
}
}
/** Secure point-of-use read for a top-level identity contract. */
export function readOptionalInstalledIdentityContract(
mosaicHome: string,
entry: (typeof FLEET_IDENTITY_DEFAULTS)[number],
required: boolean = false,
): Buffer | undefined {
const configuredPath = join(mosaicHome, entry);
try {
// Preserve the launcher's established support for a symlinked Mosaic home,
// while pinning this read to the resolved directory. O_NOFOLLOW still
// rejects replacement of the identity file itself (or any child ancestor).
const canonicalHome = realpathSync(mosaicHome);
return readRegularFileSecure(join(canonicalHome, entry), {
root: canonicalHome,
maxBytes: MAX_IDENTITY_CONTRACT_BYTES,
}).content;
} catch (error: unknown) {
if (!required && isFilesystemError(error, 'ENOENT')) return undefined;
throw unsafeIdentityError('installed', configuredPath, error);
} }
} }
@@ -50,7 +86,7 @@ export function seedFleetIdentityDefaults(mosaicHome: string): string[] {
for (const entry of FLEET_IDENTITY_DEFAULTS) { for (const entry of FLEET_IDENTITY_DEFAULTS) {
const destination = join(mosaicHome, entry); const destination = join(mosaicHome, entry);
if (existsSync(destination)) { if (installedEntryExists(destination)) {
readIdentityContract(mosaicHome, destination, 'installed'); readIdentityContract(mosaicHome, destination, 'installed');
continue; continue;
} }
@@ -69,7 +105,11 @@ export function seedFleetIdentityDefaults(mosaicHome: string): string[] {
try { try {
writeFileSync(temporary, content, { flag: 'wx', mode: 0o600 }); writeFileSync(temporary, content, { flag: 'wx', mode: 0o600 });
temporaryCreated = true; temporaryCreated = true;
if (linkIdentityContractNoClobber(temporary, destination)) seeded.push(entry); if (linkIdentityContractNoClobber(temporary, destination)) {
seeded.push(entry);
} else {
readIdentityContract(mosaicHome, destination, 'installed');
}
} finally { } finally {
if (temporaryCreated) rmSync(temporary, { force: true }); if (temporaryCreated) rmSync(temporary, { force: true });
} }
@@ -108,6 +108,7 @@ function launchEnvironment(
capturePath: string, capturePath: string,
fleet: boolean = true, fleet: boolean = true,
agentName: string = 'unattended-seat', agentName: string = 'unattended-seat',
agentClass: string = 'worker',
): NodeJS.ProcessEnv { ): NodeJS.ProcessEnv {
return { return {
HOME: fixture.home, HOME: fixture.home,
@@ -115,7 +116,7 @@ function launchEnvironment(
...(fleet ...(fleet
? { ? {
MOSAIC_AGENT_NAME: agentName, MOSAIC_AGENT_NAME: agentName,
MOSAIC_AGENT_CLASS: 'worker', MOSAIC_AGENT_CLASS: agentClass,
} }
: {}), : {}),
MOSAIC_TEST_RUNTIME_CAPTURE: capturePath, MOSAIC_TEST_RUNTIME_CAPTURE: capturePath,
@@ -129,6 +130,7 @@ function launchSync(
readonly capturePath?: string; readonly capturePath?: string;
readonly fleet?: boolean; readonly fleet?: boolean;
readonly agentName?: string; readonly agentName?: string;
readonly agentClass?: string;
} = {}, } = {},
): SpawnSyncReturns<string> { ): SpawnSyncReturns<string> {
const capturePath = options.capturePath ?? fixture.capturePath; const capturePath = options.capturePath ?? fixture.capturePath;
@@ -142,6 +144,7 @@ function launchSync(
capturePath, capturePath,
options.fleet ?? true, options.fleet ?? true,
options.agentName ?? 'unattended-seat', options.agentName ?? 'unattended-seat',
options.agentClass ?? 'worker',
), ),
}); });
} }
@@ -300,6 +303,20 @@ describe('fleet unattended first start (#1264)', () => {
expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false); expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false);
}); });
it('refuses a mismatched ambient fleet class before seeding or prompting', () => {
const fixture = createGreenfieldFixture();
const result = launchSync(fixture, { agentClass: 'reviewer' });
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 ', ''])( it.each([' unattended-seat', 'unattended-seat ', ''])(
'refuses non-exact ambient fleet name %j before seeding', 'refuses non-exact ambient fleet name %j before seeding',
(agentName: string) => { (agentName: string) => {
+29 -10
View File
@@ -30,7 +30,10 @@ import { readRegularFileSecure } from '../fleet/secure-file.js';
import { readPersonaContractBlock } from '../fleet/persona-contract.js'; import { readPersonaContractBlock } from '../fleet/persona-contract.js';
import { canonicalizeRoleClass } from './fleet-personas.js'; import { canonicalizeRoleClass } from './fleet-personas.js';
import { launchClaudex, type ClaudexHarnessAdapter } from './claudex.js'; import { launchClaudex, type ClaudexHarnessAdapter } from './claudex.js';
import { seedFleetIdentityDefaults } from './fleet-first-start-identity.js'; import {
readOptionalInstalledIdentityContract,
seedFleetIdentityDefaults,
} from './fleet-first-start-identity.js';
import { runLeaseEnforcementDoctorCheck } from './lease-doctor-check.js'; import { runLeaseEnforcementDoctorCheck } from './lease-doctor-check.js';
const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic'); const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
@@ -231,6 +234,18 @@ function checkRuntime(cmd: string): void {
} }
} }
function assertAmbientFleetClassMatches(canonicalName: string, canonicalClass: string): void {
const configuredClass = process.env['MOSAIC_AGENT_CLASS'];
if (!configuredClass?.trim()) return;
const ambientClass = canonicalizeRoleClass(configuredClass).canonicalClass;
if (ambientClass !== canonicalClass) {
throw new Error(
`Ambient MOSAIC_AGENT_CLASS resolves to "${ambientClass}" but canonical roster member "${canonicalName}" resolves to "${canonicalClass}". Refusing split identity authority.`,
);
}
}
function checkSoul(): void { function checkSoul(): void {
const soulPath = join(MOSAIC_HOME, 'SOUL.md'); const soulPath = join(MOSAIC_HOME, 'SOUL.md');
const fleetAgentName = process.env['MOSAIC_AGENT_NAME']; const fleetAgentName = process.env['MOSAIC_AGENT_NAME'];
@@ -247,6 +262,10 @@ function checkSoul(): void {
`canonical fleet identity is unavailable: ${fleetIdentity.error ?? 'exact roster member was not resolved'}`, `canonical fleet identity is unavailable: ${fleetIdentity.error ?? 'exact roster member was not resolved'}`,
); );
} }
assertAmbientFleetClassMatches(
fleetIdentity.identity.member.name,
fleetIdentity.identity.member.className,
);
const seeded = seedFleetIdentityDefaults(MOSAIC_HOME); const seeded = seedFleetIdentityDefaults(MOSAIC_HOME);
if (seeded.length > 0) { if (seeded.length > 0) {
console.log( console.log(
@@ -258,7 +277,7 @@ function checkSoul(): void {
const reason = error instanceof Error ? error.message : String(error); const reason = error instanceof Error ? error.message : String(error);
console.error(`[mosaic] ERROR: unattended fleet identity initialization failed: ${reason}`); console.error(`[mosaic] ERROR: unattended fleet identity initialization failed: ${reason}`);
console.error( console.error(
`[mosaic] Repair the shipped identity defaults under ${join(MOSAIC_HOME, 'defaults')} and retry this exact roster member.`, '[mosaic] Repair the named fleet roster, launch identity, installed contract, or shipped default, then retry.',
); );
process.exit(1); process.exit(1);
} }
@@ -565,7 +584,12 @@ For required push/merge/issue-close/release actions, execute without routine con
// USER.md (+ USER.local.md operator overlay, appended directly under the // USER.md (+ USER.local.md operator overlay, appended directly under the
// profile its base owns). // profile its base owns).
const user = readOptional(join(mosaicHome, 'USER.md')); const user =
readOptionalInstalledIdentityContract(
mosaicHome,
'USER.md',
process.env['MOSAIC_AGENT_NAME'] !== undefined,
)?.toString('utf8') ?? '';
if (user) parts.push('\n\n# User Profile\n\n' + user); if (user) parts.push('\n\n# User Profile\n\n' + user);
const userLocal = readOptional(join(mosaicHome, 'USER.local.md')); const userLocal = readOptional(join(mosaicHome, 'USER.local.md'));
if (userLocal.trim()) { if (userLocal.trim()) {
@@ -577,13 +601,8 @@ For required push/merge/issue-close/release actions, execute without routine con
throw new Error(`Fleet communications contract unavailable: ${fleetIdentity.error}`); throw new Error(`Fleet communications contract unavailable: ${fleetIdentity.error}`);
} }
const canonicalMember = fleetIdentity.identity?.member; const canonicalMember = fleetIdentity.identity?.member;
if (canonicalMember && process.env['MOSAIC_AGENT_CLASS']?.trim()) { if (canonicalMember) {
const ambientClass = canonicalizeRoleClass(process.env['MOSAIC_AGENT_CLASS']).canonicalClass; assertAmbientFleetClassMatches(canonicalMember.name, canonicalMember.className);
if (ambientClass !== canonicalMember.className) {
throw new Error(
`Ambient MOSAIC_AGENT_CLASS resolves to "${ambientClass}" but canonical roster member "${canonicalMember.name}" resolves to "${canonicalMember.className}". Refusing split identity authority.`,
);
}
} }
// TOOLS.md // TOOLS.md