This commit is contained in:
@@ -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 && \
|
||||||
|
pnpm --filter @mosaicstack/mosaic exec vitest run \
|
||||||
src/commands/launch-first-start.spec.ts
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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,139 +1,138 @@
|
|||||||
# #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/6–9 |
|
| 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 '
|
||||||
|
pnpm --filter @mosaicstack/mosaic... build &&
|
||||||
|
pnpm --filter @mosaicstack/mosaic exec vitest run \
|
||||||
src/commands/fleet-first-start-identity.spec.ts \
|
src/commands/fleet-first-start-identity.spec.ts \
|
||||||
src/commands/launch-first-start.spec.ts \
|
src/commands/launch-first-start.spec.ts \
|
||||||
src/commands/launch.spec.ts \
|
src/commands/launch.spec.ts \
|
||||||
src/commands/compose-contract.spec.ts \
|
src/commands/compose-contract.spec.ts \
|
||||||
src/config/file-adapter.test.ts \
|
src/config/file-adapter.test.ts \
|
||||||
src/cli-smoke.spec.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
|
|
||||||
Statements 100% | Branches 93.33% | Functions 100% | Lines 100%
|
|
||||||
9/9 tests passed; coverage command exit 0
|
|
||||||
```
|
|
||||||
|
|
||||||
Baseline commands:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
pnpm preflight exit 0
|
pnpm preflight exit 0
|
||||||
@@ -141,45 +140,62 @@ pnpm typecheck 45/45 tasks, exit 0
|
|||||||
pnpm lint 25/25 tasks, exit 0
|
pnpm lint 25/25 tasks, exit 0
|
||||||
pnpm build 25/25 tasks, exit 0
|
pnpm build 25/25 tasks, exit 0
|
||||||
pnpm format:check 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
|
git diff --check exit 0
|
||||||
```
|
```
|
||||||
|
|
||||||
The aggregate `test:framework-shell` command exited `1` at `invariant_r_unittest.py`: `5/6` tests
|
Pre-PR targeted shell runs on the unchanged shell surfaces also passed:
|
||||||
passed and the remaining test refused the operator-global Pi drift from measured `0.84.1` to
|
|
||||||
installed `0.84.2`. This is retained as an environment/version-coupling failure, not scored as a
|
|
||||||
#1264 code failure and not retried. The aggregate stopped there; later aggregate stages are
|
|
||||||
**UNTESTED** except for the three targeted shell suites listed above.
|
|
||||||
|
|
||||||
Root `pnpm test` is **UNTESTED** because it can execute the prohibited local PostgreSQL-dependent
|
```text
|
||||||
gateway isolation path. No PostgreSQL service, connection, migration, or initialization was used.
|
bash framework/tools/fleet/test-start-agent-session.sh exit 0 locally
|
||||||
CI is **UNTESTED — pending PR**.
|
bash framework/tools/quality/scripts/test-install-migration.sh 21 passed, 0 failed
|
||||||
|
bash framework/tools/_scripts/test-mosaic-init-rce.sh PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
The aggregate local `test:framework-shell` run stopped at `invariant_r_unittest.py`: installed
|
||||||
|
operator-global Pi is `0.84.2`, while the invariant is measured for `0.84.1`. Later aggregate stages
|
||||||
|
remain unmeasured except the targeted suites above. Root `pnpm test` remains locally **UNTESTED**
|
||||||
|
because this checkout prohibits the PostgreSQL-dependent gateway isolation path.
|
||||||
|
|
||||||
|
## Review and security evidence
|
||||||
|
|
||||||
|
- 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.
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user