100 lines
8.6 KiB
Markdown
100 lines
8.6 KiB
Markdown
# 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
|
||
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:
|
||
|
||
```text
|
||
${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
|
||
|
||
1. Read the named phase and remediation line from installer stderr.
|
||
2. Inspect `active.json`, then the referenced `journal.ndjson` and `commands.log`. Command output needed to diagnose a failure is preserved and surfaced; it is not redirected away.
|
||
3. For `rolled-back`, verify the target paths match their pre-install state before retrying.
|
||
4. For `failed-resumable`, repair the named phase owner requirement, then run `install.sh --check` before retrying the installer.
|
||
5. 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:
|
||
|
||
- `git` present: P1 and strict P3 pass; P4/P5/P6/P8 fail for their own reasons; P9 refuses success.
|
||
- `git` absent: P1 fails before target mutation and the installer emits no `Done.`.
|
||
|
||
The fixture is lane-parametric:
|
||
|
||
```bash
|
||
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.
|