Files
stack/docs/guides/installer-state-machine.md
T
be-coder-05 999b7e0862
ci/woodpecker/pr/greenfield-install Pipeline was successful
ci/woodpecker/pr/ci Pipeline failed
fix(ci): assert pinned greenfield expected red
2026-08-05 13:26:55 -05:00

8.6 KiB
Raw Blame History

Installer State Machine and Recovery

The unified installer uses a transactional P0P9 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 P0P8, 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

  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 C2C5 remain open. CI itself remains green only when the fixture's final P0P9 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 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.