8.6 KiB
Installer State Machine and Recovery
The unified installer uses a transactional P0–P9 model. It may report success only after P9 reasserts every applicable committed postcondition. Internal phases invoke the CLI by P3's absolute path; shell discovery is checked only at P8.
Canonical phases
| Phase | Responsibility | Failure disposition |
|---|---|---|
| P0 Resolve context | State target user, HOME, shell, privilege mode, architecture, libc, Node, and npm | Fail before mutation |
| P1 Preflight | Validate downstream tool closure (including git and python3), writable prefix, registry lane, disk/inodes, and exclusive lock |
Fail before target mutation |
| P2 Acquire artifacts | Resolve exact registry versions and an immutable framework commit; record lane and SHA-256 | Discard temporary work |
| P3 Install CLI | Install at the configured absolute prefix and require exact resolved version | Restore the prior prefix/npmrc snapshot |
| P4 Install framework + skills | Sync framework and consume a checkout-free, lane/versioned shipped-skill declaration | Restore prior framework/runtime trees |
| P5 Identity | Validate SOUL/USER content, owner, and mode; establish any credential capability requested downstream | Restore generated identity/credential binding |
| P6 Runtime linking / activation | Evaluate activation honestly; never treat dead enforcement hooks as active readiness | Restore runtime activation files |
| P7 Services | Provision only requested services/resources after any required P5 credential commits | Stop and restore requested services/resources |
| P8 Shell discoverability | Require fresh login and non-login shells of the actual target shell to resolve P3's path | Restore shell profiles |
| P9 Verify + commit | Re-run P0–P8, commit the manifest, and seal the journal | Leave an honestly reported resumable failure or restore the pre-install snapshot |
The phase numbers are a cross-workstream contract and must not be renumbered.
Side-effect-free check
bash tools/install.sh --check # stable/latest lane
bash tools/install.sh --check --next # prerelease lane
--check:
- emits exactly one
[P0]through[P8]PASS/FAIL row; - exits non-zero if any predicate fails;
- does not create the npm prefix, lock, journal, manifest, shell profile, or runtime file;
- uses temporary npm observation storage outside the target HOME and removes it before exit.
P4 currently fails as NOT-MEASURED / UNDECLARED until the installer publishes ~/.config/mosaic/.install-shipped-skills.json. C1 deliberately does not select among the conflicting candidate populations; C5 owns publishing and fulfilling that declaration. Once present, the P4 predicate requires the declaration's lane/version to match the resolved install and every named skill to remain contained under skills/<name>/SKILL.md with matching loadable frontmatter.
Durable journal
Each mutating run creates a private transaction directory:
${XDG_STATE_HOME:-~/.local/state}/mosaic/install/
active.json
<UTC-run-id>/
journal.ndjson
journal.ndjson.sha256 # committed runs only
commands.log
snapshot/
Before each mutation scope is touched, journal.ndjson records:
- phase and path;
- whether prior state existed and where its snapshot lives;
- the reversal action;
- the captured command-output location and command status.
Journal, action-status, manifest, or command-log write/sync failure is fatal. An unrecorded mutation is not allowed. Successful P9 runs append a seal event, write the SHA-256 sidecar, and make the journal and sidecar read-only. Required P4/P6 action failures are persisted in the manifest so a later --check cannot turn a failed action into a false pass.
Rollback roots must be non-overlapping, non-symlinked, target-user-owned strict descendants of canonical HOME; unsafe custom MOSAIC_HOME/MOSAIC_PREFIX values fail at P0. The same validation runs again immediately before recursive rollback. The OS lock is concurrency authority: if a process dies while active.json still says in-progress, a retry that acquires the free lock preserves the stale projection as prior-active.json and proceeds from the honestly retained partial state.
active.json is the current projection:
in-progress: incomplete/open transaction;rolled-back: a fault restored the snapshot;rollback-failed: restoration failed or refused a replaced/unsafe target and requires manual recovery;failed-resumable: named postconditions failed and the recorded partial state remains for remediation;committed: P9 passed and the journal is sealed.
Failure recovery
- Read the named phase and remediation line from installer stderr.
- Inspect
active.json, then the referencedjournal.ndjsonandcommands.log. Command output needed to diagnose a failure is preserved and surfaced; it is not redirected away. - For
rolled-back, verify the target paths match their pre-install state before retrying. - For
failed-resumable, repair the named phase owner requirement, then runinstall.sh --checkbefore retrying the installer. - Do not activate the #869 enforcement hooks merely to turn P6 green. A broker-less host with those hooks is a failed P6 state.
Greenfield CI gate
.woodpecker/greenfield-install.yml runs tools/e2e-install-test.sh from zero in Debian/glibc as a non-root uid with env -i. No host HOME, npm cache, credentials, or bind mount enters the target process. Checkout mode packages the complete current checkout into an archive, pins its SHA-256 through an internal fixture seam, and copies the self-contained fixture into the container; framework-installer changes in the PR are therefore exercised rather than fetched from an older remote branch.
The C1 fixture intentionally returns an attributable RED while C2–C5 remain open. CI itself remains green only when the fixture's final P0–P9 verdicts, required discriminator rows, and non-zero exit match the versioned contract in tools/fixtures/greenfield-expected-red.tsv. Any later remediation that changes an observed verdict makes CI red until the owning lane deliberately updates that manifest:
gitpresent: P1 and strict P3 pass; P4/P5/P6/P8 fail for their own reasons; P9 refuses success.gitabsent: P1 fails before target mutation and the installer emits noDone..
The fixture is lane-parametric:
bash tools/e2e-install-test.sh --lane next --git present
bash tools/e2e-install-test.sh --lane main --git present
CI exercises both lane parameters as expected-RED structural checks. Delivery targets main under the trunk-only merge rule; next remains a non-merging integration lane. The linked installer issue stays open after merge and closes only after Jarvis independently validates the greenfield behavior.
Source trust boundary
Remote source mode pins the resolved commit, records the archive SHA-256, limits compressed/expanded size and entry count, and rejects traversal, links, devices, and special files before extraction. This provides immutable run provenance and archive safety, not an independent authenticity root. Signed artifact metadata/provenance is explicitly deferred by the canonical greenfield PRD; C1 does not invent a signing system. The checkout CI seam does verify an expected digest supplied independently by the fixture.