fix(installer): harden greenfield detector contracts

This commit is contained in:
2026-08-05 17:46:58 -05:00
parent 99e28d4100
commit 3edde464b3
18 changed files with 924 additions and 190 deletions
+19
View File
@@ -34,6 +34,25 @@ steps:
bash tools/verify-greenfield-expected-red.sh \ bash tools/verify-greenfield-expected-red.sh \
main-git-present /tmp/greenfield-main-git-present.log "$fixture_status" main-git-present /tmp/greenfield-main-git-present.log "$fixture_status"
greenfield-remote-installer-contract:
image: node:22-bookworm-slim
commands:
- |
expected="$(awk 'NF {print $1; exit}' tools/install.sh.sha256)"
actual="$(sha256sum tools/install.sh | awk '{print $1}')"
test "$actual" = "$expected"
set +e
MOSAIC_GREENFIELD_CONTAINER=1 \
MOSAIC_FIXTURE_INSTALLER_URL="https://git.mosaicstack.dev/mosaicstack/stack/raw/commit/${CI_COMMIT_SHA}/tools/install.sh" \
MOSAIC_FIXTURE_INSTALLER_SHA256="$expected" \
bash tools/e2e-install-test.sh --lane next --source remote --git present \
> /tmp/greenfield-remote.log 2>&1
fixture_status=$?
set -e
cat /tmp/greenfield-remote.log
bash tools/verify-greenfield-expected-red.sh \
next-git-present /tmp/greenfield-remote.log "$fixture_status"
greenfield-git-absent: greenfield-git-absent:
image: node:22-bookworm-slim image: node:22-bookworm-slim
commands: commands:
+11 -16
View File
@@ -7,20 +7,21 @@ Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi —
## Quick Install ## Quick Install
```bash ```bash
bash -o pipefail -c 'curl -fsSL https://mosaicstack.dev/install.sh | bash' d="$(mktemp -d)" && trap 'rm -rf "$d"' EXIT && curl -fsSL -o "$d/install.sh" https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh -o "$d/install.sh.sha256" https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh.sha256 && (cd "$d" && test -s install.sh && sha256sum -c install.sh.sha256 && bash install.sh)
``` ```
Or use the direct URL: The published installer body must be non-empty and match its versioned SHA-256
sidecar before it executes. A failed fetch, HTTP-200 empty body, or digest
```bash mismatch is fatal. Because both files come from the same repository and trust
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh) domain, this detects corruption or inconsistent publication—not repository or
``` server compromise. Independently signed release provenance is explicitly
deferred by the greenfield-install PRD.
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use: The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
```bash ```bash
bash <(curl -fsSL …) --yes # Accept all defaults (cd "$d" && bash install.sh --yes) # Accept all defaults
bash <(curl -fsSL …) --yes --no-auto-launch # Install only, skip wizard (cd "$d" && bash install.sh --yes --no-auto-launch) # Install only, skip wizard
``` ```
This installs both components: This installs both components:
@@ -348,16 +349,10 @@ Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding),
## Upgrading ## Upgrading
Run the installer again — it handles upgrades automatically: Run the same verified installer flow again — it handles upgrades automatically:
```bash ```bash
bash -o pipefail -c 'curl -fsSL https://mosaicstack.dev/install.sh | bash' d="$(mktemp -d)" && trap 'rm -rf "$d"' EXIT && curl -fsSL -o "$d/install.sh" https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh -o "$d/install.sh.sha256" https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh.sha256 && (cd "$d" && test -s install.sh && sha256sum -c install.sh.sha256 && bash install.sh)
```
Or use the direct URL:
```bash
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
``` ```
Or use the CLI: Or use the CLI:
+8 -2
View File
@@ -1387,14 +1387,20 @@ A from-zero install can report success while leaving the target host unusable be
6. The from-zero fixture SHALL be lane-parametric, use Debian/glibc, run the documented install command as a non-root target user with an isolated HOME, and inherit no host credentials, npm cache, home directory, or runtime configuration. 6. The from-zero fixture SHALL be lane-parametric, use Debian/glibc, run the documented install command as a non-root target user with an isolated HOME, and inherit no host credentials, npm cache, home directory, or runtime configuration.
7. The fixture SHALL select `next` with `--next` or `MOSAIC_NEXT=1` and assert the resolved lane version. Internal predicates use P3's absolute CLI path; shell discoverability is tested only at P8. 7. The fixture SHALL select `next` with `--next` or `MOSAIC_NEXT=1` and assert the resolved lane version. Internal predicates use P3's absolute CLI path; shell discoverability is tested only at P8.
8. Fault injection after each P2P8 phase SHALL prove either clean rollback or a durable, honestly reported resumable partial state, with no journal incorrectly left in progress. 8. Fault injection after each P2P8 phase SHALL prove either clean rollback or a durable, honestly reported resumable partial state, with no journal incorrectly left in progress.
9. Unsupported musl/Alpine and unavailable Docker SHALL fail loudly rather than skip as pass. 9. Unsupported musl/Alpine and unavailable Docker SHALL fail loudly rather than skip as pass. The repository's installer tests SHALL nevertheless run in the canonical Alpine CI image by explicitly modeling a supported non-root/glibc target and using portable filesystem enumeration.
10. P0 SHALL bind the effective uid and username to the authoritative passwd HOME and shell and state/reject unsafe root or sudo-with-inherited-HOME privilege contexts.
11. Created paths SHALL satisfy phase-specific target owner/group and mode policy: P3 executables are not group/world writable, framework/runtime trees are not group/world writable, and identity/credential material is private.
12. The expected-RED comparator SHALL validate the complete manifest before selecting a case: exact case population, one exit and P0P9 disposition per case, pinned require/forbid classes, and no malformed, duplicate, or unknown rows.
13. The published installer contract SHALL reject failed fetches, HTTP-success empty bodies, and digest mismatch, then execute the exact digest-verified body. The remote CI arm SHALL bind that body to the immutable CI commit.
14. Phase diagnostics SHALL be redacted before terminal or durable-log output. A seeded positive-control canary SHALL remain absent from observed argv, output, command logs, npm configuration, generated files, and shell history.
### C1 acceptance criteria ### C1 acceptance criteria
1. The pre-C1 from-zero matrix records both discriminating controls: with `git` absent, the legacy installer still exits zero while P1 fails and skill sync degrades; with `git` present, P1 passes and the observed sync store/runtime links are 101/101. The C1 installer must fail at P1 before mutation when `git` is absent. 1. The pre-C1 from-zero matrix records both discriminating controls: with `git` absent, the legacy installer still exits zero while P1 fails and skill sync degrades; with `git` present, P1 passes and the observed sync store/runtime links are 101/101. The C1 installer must fail at P1 before mutation when `git` is absent.
2. The discriminating P3 row passes: the binary exists at the expected absolute path and reports exactly the resolved `next` lane version, while P4, P5, and P8 fail. 2. The discriminating P3 row passes: the binary exists at the expected absolute path and reports exactly the resolved `next` lane version, while P4, P5, and P8 fail.
3. The `--check` mutation negative control proves host fingerprints are byte-identical before and after observation. 3. The `--check` mutation negative control proves host fingerprints are byte-identical before and after observation.
4. Woodpecker executes and validates the expected RED fixture; C1 does not repair P4/P5/P8 or activate #869. 4. Woodpecker executes and validates the expected RED fixture plus the immutable remote-installer contract; C1 does not repair P4/P5/P8 or activate #869.
5. Negative controls prove manifest shrink/duplicates/unknown rows fail, unsafe P0/P3/P4/P5 contexts fail, the P2P8 fault seam enters real actions rather than synthetic writes, empty/mismatched fetched bodies fail, and a deliberately emitted secret canary is redacted from every persisted/output population.
### Explicit exclusions and dependencies ### Explicit exclusions and dependencies
+7 -7
View File
@@ -6,12 +6,12 @@ The unified installer uses a transactional P0P9 model. It may report success
| Phase | Responsibility | Failure disposition | | Phase | Responsibility | Failure disposition |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| P0 Resolve context | State target user, HOME, shell, privilege mode, architecture, libc, Node, and npm | Fail before mutation | | P0 Resolve context | Bind uid/username to the authoritative passwd HOME/shell, state 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 | | 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 | | 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 | | P3 Install CLI | Install at the configured absolute prefix; require exact version plus target owner/group and non-writable executable mode | 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 | | 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 | | P5 Identity | Validate SOUL/USER content and private modes; require private credential storage and target owner/group | Restore generated identity/credential binding |
| P6 Runtime linking / activation | Evaluate activation honestly; never treat dead enforcement hooks as active readiness | Restore runtime activation files | | 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 | | 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 | | P8 Shell discoverability | Require fresh login and non-login shells of the actual target shell to resolve P3's path | Restore shell profiles |
@@ -56,7 +56,7 @@ Before each mutation scope is touched, `journal.ndjson` records:
- the reversal action; - the reversal action;
- the captured command-output location and command status. - 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. Journal, action-status, manifest, or command-log write/sync failure is fatal. An unrecorded mutation is not allowed. Command diagnostics are redacted before terminal output or durable logging; credential-shaped environment values, bearer values, auth tokens, and credentialed URLs are never deliberately persisted. 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. 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.
@@ -80,7 +80,7 @@ Rollback roots must be non-overlapping, non-symlinked, target-user-owned strict
`.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. `.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: 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, seeded secret-canary scan, and non-zero exit match the versioned contract in `tools/fixtures/greenfield-expected-red.tsv`. The comparator validates the complete three-case schema before selecting a case: exactly one exit and P0P9 disposition per case, pinned require/forbid populations, and no duplicate or unknown rows. 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` 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.`. - `git` absent: P1 fails before target mutation and the installer emits no `Done.`.
@@ -92,8 +92,8 @@ bash tools/e2e-install-test.sh --lane next --git present
bash tools/e2e-install-test.sh --lane main --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. CI exercises both lane parameters as expected-RED structural checks. A separate remote-contract arm fetches the installer at the immutable CI commit, rejects failed or empty HTTP-success bodies, compares it to the reviewed `tools/install.sh.sha256`, and executes that exact fetched artifact. The P2P8 fault matrix runs the real phase actions (including the P3 npm path, P4 framework path, and wizard path) rather than synthetic representative writes, then compares the complete target tree to its pre-install fingerprint. 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 ## 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. Remote installer mode requires a non-empty body and an expected SHA-256 before execution. Remote source-archive mode separately 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. These controls provide immutable run provenance and archive safety, not an independent signing root. Signed artifact metadata/provenance is explicitly deferred by the canonical greenfield PRD; C1 does not invent a signing system. The checkout and remote CI seams verify reviewed digests before executing their artifacts.
@@ -50,7 +50,7 @@ Implement C1 from the canonical greenfield-install PRD v2: a transactional P0
- [x] Corrected RED transcript captured and reported, including the git-present/absent controls and strict P3 PASS. - [x] Corrected RED transcript captured and reported, including the git-present/absent controls and strict P3 PASS.
- [x] State-machine implementation complete: private pre-mutation journal/snapshot, P0P8 `--check`, P2P8 fault seam, rollback, durable manifest/journal seal, action-status persistence, safe rollback roots, and stale-projection recovery. - [x] State-machine implementation complete: private pre-mutation journal/snapshot, P0P8 `--check`, P2P8 fault seam, rollback, durable manifest/journal seal, action-status persistence, safe rollback roots, and stale-projection recovery.
- [x] Debian/glibc checkout fixture now packages the complete current checkout, verifies its digest in-container, and reaches the expected attributable RED without host inheritance. CI compares its exact final phase map/reasons to `tools/fixtures/greenfield-expected-red.tsv`; the fixture remains red while the detector job is green only on an exact match. - [x] Debian/glibc checkout fixture now packages the complete current checkout, verifies its digest in-container, and reaches the expected attributable RED without host inheritance. CI compares its exact final phase map/reasons to `tools/fixtures/greenfield-expected-red.tsv`; the fixture remains red while the detector job is green only on an exact match.
- [ ] Reviews complete. Automated review defects around Bash conditional errexit, explicit exits, P4/P6 persisted action status, dev/offline source resolution, stale locks, checkout coverage, and rollback path safety were remediated. Remaining automated objections are the charter-mandated expected RED/C5 boundary and signed provenance, which the canonical PRD explicitly defers; independent informed review is still required. - [ ] Reviews complete. Reviews 80 (`rev-security-02`) and 81 (`rev-974`) requested changes at `3934e03f`; their eight non-overlapping detector findings are being remediated red-first. Current remediation adds canonical-image portability, absolute P3 CLI propagation, exact expected-RED schema/cardinality, passwd-HOME binding, created-path owner/mode policy, real-action P2P8 fault injection, verified non-empty remote installer execution, and seeded secret-canary/redacted diagnostics. Both old verdicts become void when the remediation head moves and require fresh independent review.
## Risks / blockers ## Risks / blockers
@@ -59,10 +59,25 @@ Implement C1 from the canonical greenfield-install PRD v2: a transactional P0
- #869 must remain staged and inactive. - #869 must remain staged and inactive.
- Late sequencing input MB-BRAIN-01 is accommodated without implementation or renumbering: P2 covers installer distribution only; P5 owns requested credential capability; P7 leaves an ordered seam for credential-dependent resource provisioning after P5. - Late sequencing input MB-BRAIN-01 is accommodated without implementation or renumbering: P2 covers installer distribution only; P5 owns requested credential capability; P7 leaves an ordered seam for credential-dependent resource provisioning after P5.
## Remediation review controls
- B1 RED: the next-lane harness failed immediately under `ci-base:latest` as root/musl; it now models uid 1001/glibc explicitly and uses Python tree fingerprints instead of GNU `find -printf`.
- B2 RED: framework/runtime linking consumed bare `mosaic` from PATH after P3 had committed an absolute path. The unified installer now exports/passes `MOSAIC_CLI_PATH`; the linker invokes that absolute artifact, and wizard auto-launch has no stale-PATH fallback.
- B3 RED: a one-row manifest (`exit=1`) certified any exit-1 log. Full-manifest validation now requires the exact three cases, one exit and P0P9 row each, pinned require/forbid populations, and rejects malformed/duplicate/unknown rows; shrink is a negative control.
- B4 RED: uid 1001 with a passwd HOME different from ambient HOME produced P0 PASS. P0 now binds uid, username, passwd HOME and shell and explicitly rejects root and sudo-with-inherited-HOME controls.
- B5 RED: mode-0777 CLI, mode-0644 identity, and mode-0755 credential storage passed. P3/P4/P5 now apply target owner/group plus executable/shared/private policies; framework credential storage is created 0700.
- B6 RED: fault injection only wrote `.selftest-*` files. The synthetic path was removed; the P2P8 matrix enters the normal action flow, proves an action observation occurred, injects after each real phase, and fingerprints rollback.
- B7 RED: an HTTP-200 empty body exits zero when piped to Bash. The fetched installer must now be non-empty, digest-equal to `tools/install.sh.sha256`, and that exact file is executed; failed/empty/mismatch controls are blocking and CI has a remote immutable-commit arm.
- B8 RED: raw combined command output was duplicated to terminal and `commands.log`. Both capture layers now redact before output/persistence; a seeded canary is positively emitted by the fake credential-capable registry and must remain absent from terminal, command log, npmrc, generated files and observed argv. The real greenfield fixture also scans those populations.
- Advisory code review findings are fixed: URL userinfo redaction now handles raw `@`, repeated `:`, percent encoding and multiple URLs in both capture layers; the real greenfield path positively emits its canary through `state_run_captured`; and verified-fetch removes its temporary body after successful execution.
- Advisory security review's independent trust-root finding is **DEFERRED by canonical PRD v2 §3**, which explicitly excludes signed provenance. README now states precisely that the same-origin sidecar detects empty/corrupt/inconsistent publication but cannot authenticate against repository/server compromise; no stronger claim remains.
- The web1 no-manifest representativeness observation is recorded but intentionally not acted on: it is explicitly outside these eight blockers. This remediation does not weaken or otherwise change P9's manifest-presence assertion.
## Verification log ## Verification log
- `bash -n` and ShellCheck pass for all changed shell surfaces; `git diff --check` passes. - `bash -n` and ShellCheck pass for all changed shell surfaces; `git diff --check` passes.
- `bash tools/install-state-machine.test.sh` passes, including exact P0P8 rows, good/bad discrimination, persisted P4/P6 action failures, P2P8 rollback, unsafe/overlapping/symlink roots, stale `active.json`, and fatal journal initialization. - `bash tools/install-state-machine.test.sh` passes, including exact P0P8 rows, passwd-HOME/privilege discrimination, owner/group/mode attacks, persisted P4/P6 action failures, no synthetic fault implementation, unsafe/overlapping/symlink roots, and fatal journal initialization.
- `bash tools/install-next-lane.test.sh` passes, including exact `@next` versions, immutable source fallback, source-build/archive-failure rollback, offline `--dev`, explicit refs, and prerelease suffix mismatch. - `bash tools/install-next-lane.test.sh` passes inside `ci-base:latest`, including exact `@next` versions, immutable source fallback, source-build/archive-failure rollback, offline `--dev`, explicit refs, prerelease suffix mismatch, absolute P3 CLI propagation, secret redaction, real-action P2P8 rollback, and stale projection recovery.
- Comparator controls pass for verdict drift, unexpected exit, manifest shrink, missing phases, duplicate rows, unknown cases, and unknown kinds. Verified-fetch controls pass for successful execution and failed/empty/digest-mismatch rejection.
- `bash tools/e2e-install-test.sh --lane next --source checkout --git present` returns the required expected RED in clean Debian/glibc as uid 1001: installer P0/P1/P2/P3/P7 PASS; P4/P5/P6/P8 and P9 blocking; no `Done.` claim; checkout archive digest pinned and current framework installer exercised. `tools/verify-greenfield-expected-red.sh` converts that expected detector result into a green CI assertion and fails on any unreviewed verdict drift. - `bash tools/e2e-install-test.sh --lane next --source checkout --git present` returns the required expected RED in clean Debian/glibc as uid 1001: installer P0/P1/P2/P3/P7 PASS; P4/P5/P6/P8 and P9 blocking; no `Done.` claim; checkout archive digest pinned and current framework installer exercised. `tools/verify-greenfield-expected-red.sh` converts that expected detector result into a green CI assertion and fails on any unreviewed verdict drift.
- Earlier repository gates passed: `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, `pnpm test:installer`, upgrade manifest/rollback/durable-snapshot/migration suites, and focused `@mosaicstack/mosaic` tests with an isolated npm prefix. Full rerun is required after final edits. - Earlier repository gates passed: `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, upgrade manifest/rollback/durable-snapshot/migration suites, and focused `@mosaicstack/mosaic` tests with an isolated npm prefix. Full exact-remediation rerun is required before push.
+1 -1
View File
@@ -11,7 +11,7 @@
"typecheck": "pnpm preflight && turbo run typecheck", "typecheck": "pnpm preflight && turbo run typecheck",
"test:checkout": "node --test scripts/*.test.mjs", "test:checkout": "node --test scripts/*.test.mjs",
"test": "pnpm test:checkout && turbo run test && pnpm run test:installer", "test": "pnpm test:checkout && turbo run test && pnpm run test:installer",
"test:installer": "bash tools/install-state-machine.test.sh && bash tools/install-next-lane.test.sh && bash tools/verify-greenfield-expected-red.test.sh", "test:installer": "bash tools/install-state-machine.test.sh && bash tools/install-next-lane.test.sh && bash tools/verify-greenfield-expected-red.test.sh && bash tools/verified-installer-fetch.test.sh",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
"prepare": "node scripts/install-hooks.mjs" "prepare": "node scripts/install-hooks.mjs"
+63 -9
View File
@@ -710,9 +710,11 @@ trap 'restore_snapshot; exit 1' ERR INT TERM
sync_framework sync_framework
# Ensure persistent directories exist # Ensure persistent directories exist. Credentials are private material and
# must never inherit a permissive umask/default mode.
mkdir -p "$TARGET_DIR/memory" mkdir -p "$TARGET_DIR/memory"
mkdir -p "$TARGET_DIR/credentials" mkdir -p "$TARGET_DIR/credentials"
chmod 0700 "$TARGET_DIR/credentials"
# Reconcile contract files from defaults/ into the framework root: framework-owned # Reconcile contract files from defaults/ into the framework root: framework-owned
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent # files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
@@ -781,22 +783,74 @@ record_phase_outcome() {
fi fi
} }
redact_install_stream() {
# Keep this bootstrap copy behaviorally identical to tools/install.sh's
# state_redact_stream; neither installer can assume the other is installed.
python3 /dev/fd/3 3<<'PY'
import os, re, sys
text = sys.stdin.read()
secret_name = re.compile(r"(?:TOKEN|PASSWORD|PASSWD|SECRET|API_KEY|AUTH|CREDENTIAL|CANARY)", re.I)
secrets = {value for name, value in os.environ.items() if secret_name.search(name) and len(value) >= 4}
for value in sorted(secrets, key=len, reverse=True):
text = text.replace(value, "[REDACTED]")
patterns = (
(re.compile(r"(?im)^(\s*(?:proxy-)?authorization\s*:\s*)[^\r\n]+"), r"\1[REDACTED]"),
(re.compile(r"(?im)^(\s*(?:set-)?cookie\s*:\s*)[^\r\n]+"), r"\1[REDACTED]"),
(re.compile(r"(?i)(Bearer\s+)[^\s'\"]+"), r"\1[REDACTED]"),
(re.compile(r"(?i)((?:[_-]?auth(?:Token)?|token|password|passwd|secret|api[_-]?key)\s*[=:]\s*)[^\s'\"]+"), r"\1[REDACTED]"),
)
for pattern, replacement in patterns:
text = pattern.sub(replacement, text)
url_pattern = re.compile(r"https?://[^\s'\"<>]+", re.I)
def redact_url(match):
url = match.group(0)
scheme_end = url.find("://") + 3
authority_end = len(url)
for separator in "/?#":
position = url.find(separator, scheme_end)
if position != -1:
authority_end = min(authority_end, position)
authority = url[scheme_end:authority_end]
at = authority.rfind("@")
if at != -1:
return url[:scheme_end] + "[REDACTED]@" + authority[at + 1:] + url[authority_end:]
return url
sys.stdout.write(url_pattern.sub(redact_url, text))
PY
}
run_captured() { run_captured() {
local label="$1" output status=0 local label="$1" redacted redactor_pid capture_fd status=0 redact_status=0
shift shift
output="$(mktemp "${TMPDIR:-/tmp}/mosaic-post-install.XXXXXX.log")" redacted="$(mktemp "${TMPDIR:-/tmp}/mosaic-post-redacted.XXXXXX")"
if "$@" >"$output" 2>&1; then status=0; else status=$?; fi chmod 0600 "$redacted" || { rm -f "$redacted"; exit 1; }
# Preserve in-shell command behavior without ever staging plaintext output on
# disk. Process substitution carries raw bytes only through a pipe.
exec {capture_fd}> >(redact_install_stream > "$redacted")
redactor_pid=$!
set +e
"$@" >&"$capture_fd" 2>&1
status=$?
exec {capture_fd}>&-
wait "$redactor_pid"
redact_status=$?
set -e
if [[ "$redact_status" -ne 0 ]]; then
rm -f "$redacted"
fail "Could not redact '$label' diagnostics; refusing to expose or persist raw output."
exit 1
fi
if [[ -n "${MOSAIC_INSTALL_COMMAND_LOG:-}" ]]; then if [[ -n "${MOSAIC_INSTALL_COMMAND_LOG:-}" ]]; then
if ! { printf '\n=== %s (exit=%s) ===\n' "$label" "$status"; cat "$output"; } >> "$MOSAIC_INSTALL_COMMAND_LOG" \ if ! { printf '\n=== %s (exit=%s) ===\n' "$label" "$status"; cat "$redacted"; } >> "$MOSAIC_INSTALL_COMMAND_LOG" \
|| ! sync "$MOSAIC_INSTALL_COMMAND_LOG"; then || ! sync "$MOSAIC_INSTALL_COMMAND_LOG"; then
cat "$output" >&2 cat "$redacted" >&2
rm -f "$output" rm -f "$redacted"
fail "Could not durably append '$label' diagnostics to the install command log." fail "Could not durably append '$label' diagnostics to the install command log."
exit 1 exit 1
fi fi
fi fi
if [[ "$status" -ne 0 ]]; then cat "$output" >&2; fi if [[ "$status" -ne 0 ]]; then cat "$redacted" >&2; fi
rm -f "$output" rm -f "$redacted"
return "$status" return "$status"
} }
@@ -68,8 +68,15 @@ copy_claude_settings_guarded() {
guard_args+=(--allow-inactive-enforcement) guard_args+=(--allow-inactive-enforcement)
fi fi
if command -v mosaic >/dev/null 2>&1; then local mosaic_cli="${MOSAIC_CLI_PATH:-}"
if mosaic "${guard_args[@]}"; then # Unified install passes P3's committed absolute artifact. Standalone
# framework installs may resolve PATH once, but still invoke the resulting
# absolute path rather than a bare command.
if [[ -z "$mosaic_cli" ]]; then
mosaic_cli="$(command -v mosaic 2>/dev/null || true)"
fi
if [[ "$mosaic_cli" == /* && -x "$mosaic_cli" ]]; then
if "$mosaic_cli" "${guard_args[@]}"; then
return 0 return 0
fi fi
echo "[mosaic-link] Enforcement hooks were NOT wired into $dst (see message above)." >&2 echo "[mosaic-link] Enforcement hooks were NOT wired into $dst (see message above)." >&2
@@ -77,7 +84,7 @@ copy_claude_settings_guarded() {
return 0 return 0
fi fi
echo "[mosaic-link] ERROR: 'mosaic' CLI not found on PATH — cannot confirm lease-enforcement" >&2 echo "[mosaic-link] ERROR: P3 absolute mosaic CLI unavailable — cannot confirm lease-enforcement" >&2
echo "[mosaic-link] activation capability. enforcement requested but activation half absent —" >&2 echo "[mosaic-link] activation capability. enforcement requested but activation half absent —" >&2
echo "[mosaic-link] needs a published CLI carrying launch-runtime activation + a broker" >&2 echo "[mosaic-link] needs a published CLI carrying launch-runtime activation + a broker" >&2
echo "[mosaic-link] supervisor; refusing to wire a dead gate (see #869)." >&2 echo "[mosaic-link] supervisor; refusing to wire a dead gate (see #869)." >&2
+48 -5
View File
@@ -14,6 +14,8 @@ SOURCE="${MOSAIC_INSTALL_SOURCE:-checkout}"
IMAGE="${MOSAIC_INSTALL_IMAGE:-node:22-bookworm-slim}" IMAGE="${MOSAIC_INSTALL_IMAGE:-node:22-bookworm-slim}"
GIT_MODE="${MOSAIC_INSTALL_GIT_MODE:-present}" GIT_MODE="${MOSAIC_INSTALL_GIT_MODE:-present}"
INSTALLER_FILE="${MOSAIC_FIXTURE_INSTALLER_FILE:-$ROOT/tools/install.sh}" INSTALLER_FILE="${MOSAIC_FIXTURE_INSTALLER_FILE:-$ROOT/tools/install.sh}"
INSTALLER_URL="${MOSAIC_FIXTURE_INSTALLER_URL:-}"
INSTALLER_SHA256="${MOSAIC_FIXTURE_INSTALLER_SHA256:-}"
IN_CLEAN_CONTAINER="${MOSAIC_GREENFIELD_CONTAINER:-0}" IN_CLEAN_CONTAINER="${MOSAIC_GREENFIELD_CONTAINER:-0}"
usage() { usage() {
@@ -39,6 +41,11 @@ done
case "$LANE" in next|main) ;; *) echo "[fixture] unsupported lane '$LANE' (expected next|main)" >&2; exit 2 ;; esac case "$LANE" in next|main) ;; *) echo "[fixture] unsupported lane '$LANE' (expected next|main)" >&2; exit 2 ;; esac
case "$SOURCE" in checkout|remote) ;; *) echo "[fixture] unsupported source '$SOURCE' (expected checkout|remote)" >&2; exit 2 ;; esac case "$SOURCE" in checkout|remote) ;; *) echo "[fixture] unsupported source '$SOURCE' (expected checkout|remote)" >&2; exit 2 ;; esac
case "$GIT_MODE" in present|absent) ;; *) echo "[fixture] unsupported git mode '$GIT_MODE' (expected present|absent)" >&2; exit 2 ;; esac case "$GIT_MODE" in present|absent) ;; *) echo "[fixture] unsupported git mode '$GIT_MODE' (expected present|absent)" >&2; exit 2 ;; esac
if [[ "$SOURCE" == remote ]]; then
[[ -n "$INSTALLER_URL" ]] || INSTALLER_URL="https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/${LANE}/tools/install.sh"
[[ "$INSTALLER_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|| { echo '[fixture] remote source requires MOSAIC_FIXTURE_INSTALLER_SHA256=64hex' >&2; exit 2; }
fi
if [[ "$IN_CLEAN_CONTAINER" != "1" ]]; then if [[ "$IN_CLEAN_CONTAINER" != "1" ]]; then
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
@@ -63,7 +70,7 @@ if [[ "$SOURCE" == "checkout" ]]; then
&& framework_payload_count="$(find "$ROOT/packages/mosaic/framework/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')" && framework_payload_count="$(find "$ROOT/packages/mosaic/framework/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
[[ -d "$ROOT/skills" ]] \ [[ -d "$ROOT/skills" ]] \
&& repo_root_count="$(find "$ROOT/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')" && repo_root_count="$(find "$ROOT/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
checkout_archive="$(mktemp "${TMPDIR:-/tmp}/mosaic-greenfield-checkout.XXXXXX.tar.gz")" checkout_archive="$(mktemp "${TMPDIR:-/tmp}/mosaic-greenfield-checkout.XXXXXX")"
repo_parent="$(dirname "$ROOT")" repo_parent="$(dirname "$ROOT")"
repo_name="$(basename "$ROOT")" repo_name="$(basename "$ROOT")"
tar -C "$repo_parent" \ tar -C "$repo_parent" \
@@ -75,7 +82,7 @@ if [[ "$SOURCE" == "checkout" ]]; then
checkout_content_id="${checkout_digest:0:40}" checkout_content_id="${checkout_digest:0:40}"
fi fi
inner="$(mktemp "${TMPDIR:-/tmp}/mosaic-greenfield-inner.XXXXXX.sh")" inner="$(mktemp "${TMPDIR:-/tmp}/mosaic-greenfield-inner.XXXXXX")"
trap 'rm -f "$inner" "$checkout_archive"' EXIT trap 'rm -f "$inner" "$checkout_archive"' EXIT
cat > "$inner" <<'INNER' cat > "$inner" <<'INNER'
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -104,7 +111,13 @@ case "$FIXTURE_SOURCE" in
printf '%s' "$FIXTURE_INSTALLER_B64" | base64 -d > /tmp/install.sh printf '%s' "$FIXTURE_INSTALLER_B64" | base64 -d > /tmp/install.sh
;; ;;
remote) remote)
curl -fsSL "https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/${FIXTURE_LANE}/tools/install.sh" > /tmp/install.sh curl -fsSL "$FIXTURE_INSTALLER_URL" -o /tmp/install.sh
[[ -s /tmp/install.sh ]] || { echo '[fixture] remote installer returned an empty HTTP-success body' >&2; exit 1; }
actual_installer_sha256="$(sha256sum /tmp/install.sh | awk '{print $1}')"
[[ "$actual_installer_sha256" == "$FIXTURE_INSTALLER_SHA256" ]] || {
echo "[fixture] remote installer digest mismatch got=$actual_installer_sha256 expected=$FIXTURE_INSTALLER_SHA256" >&2
exit 1
}
;; ;;
esac esac
chmod 0755 /tmp/install.sh chmod 0755 /tmp/install.sh
@@ -134,12 +147,38 @@ fi
resolved_version="$(npm view "$resolved_spec" version --registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ 2>/dev/null || true)" resolved_version="$(npm view "$resolved_spec" version --registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ 2>/dev/null || true)"
printf '[fixture] resolved lane=%s package=%s version=%s\n' "$lane" "$resolved_spec" "${resolved_version:-UNRESOLVED}" printf '[fixture] resolved lane=%s package=%s version=%s\n' "$lane" "$resolved_spec" "${resolved_version:-UNRESOLVED}"
secret_canary='MOSAIC_C1_CANARY_6f3c91e2'
argv_capture=/tmp/mosaic-installer-argv.log
: > "$argv_capture"
set +e set +e
MOSAIC_NO_COLOR=1 MOSAIC_ASSUME_YES=1 \ MOSAIC_NO_COLOR=1 MOSAIC_ASSUME_YES=1 MOSAIC_INSTALL_SECRET_CANARY="$secret_canary" \
bash /tmp/install.sh "${lane_args[@]}" --yes --no-auto-launch >"$install_log" 2>&1 MOSAIC_INSTALL_REDACTION_PROBE=1 \
bash /tmp/install.sh "${lane_args[@]}" --yes --no-auto-launch >"$install_log" 2>&1 &
installer_pid=$!
while kill -0 "$installer_pid" 2>/dev/null; do
for cmdline in /proc/[0-9]*/cmdline; do
[[ -r "$cmdline" ]] || continue
tr '\0' ' ' < "$cmdline" >> "$argv_capture" 2>/dev/null || true
printf '\n' >> "$argv_capture"
done
sleep 0.02
done
wait "$installer_pid"
install_status=$? install_status=$?
set -e set -e
cat "$install_log" cat "$install_log"
probe_ok=true
if [[ "$FIXTURE_GIT_MODE" == present ]] \
&& ! grep -q '^\[REDACTION-PROBE\] emitted=\[REDACTED\]$' "$install_log"; then
probe_ok=false
fi
if [[ "$probe_ok" != true ]] \
|| grep -F "$secret_canary" "$argv_capture" >/dev/null \
|| grep -R -F "$secret_canary" "$home" >/dev/null 2>&1; then
phase_fail P0 'seeded credential probe missing or canary leaked to argv, output, command log, npmrc, generated files, or shell history'
else
printf '[SECRET-CONTROL] PASS: seeded captured-command canary was redacted and absent from argv/output/commands.log/npmrc/generated/history populations\n'
fi
printf '[fixture] installer_exit=%d done_claims=%s\n' \ printf '[fixture] installer_exit=%d done_claims=%s\n' \
"$install_status" "$(grep -cF 'Done.' "$install_log" || true)" "$install_status" "$(grep -cF 'Done.' "$install_log" || true)"
@@ -329,6 +368,8 @@ if [[ "$IN_CLEAN_CONTAINER" == "1" ]]; then
FIXTURE_SOURCE="$SOURCE" \ FIXTURE_SOURCE="$SOURCE" \
FIXTURE_GIT_MODE="$GIT_MODE" \ FIXTURE_GIT_MODE="$GIT_MODE" \
FIXTURE_INSTALLER_B64="$installer_b64" \ FIXTURE_INSTALLER_B64="$installer_b64" \
FIXTURE_INSTALLER_URL="$INSTALLER_URL" \
FIXTURE_INSTALLER_SHA256="$INSTALLER_SHA256" \
FIXTURE_CHECKOUT_SHA256="$checkout_digest" \ FIXTURE_CHECKOUT_SHA256="$checkout_digest" \
FIXTURE_CHECKOUT_CONTENT_ID="$checkout_content_id" \ FIXTURE_CHECKOUT_CONTENT_ID="$checkout_content_id" \
FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \ FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \
@@ -345,6 +386,8 @@ else
--env FIXTURE_SOURCE="$SOURCE" \ --env FIXTURE_SOURCE="$SOURCE" \
--env FIXTURE_GIT_MODE="$GIT_MODE" \ --env FIXTURE_GIT_MODE="$GIT_MODE" \
--env FIXTURE_INSTALLER_B64="$installer_b64" \ --env FIXTURE_INSTALLER_B64="$installer_b64" \
--env FIXTURE_INSTALLER_URL="$INSTALLER_URL" \
--env FIXTURE_INSTALLER_SHA256="$INSTALLER_SHA256" \
--env FIXTURE_CHECKOUT_SHA256="$checkout_digest" \ --env FIXTURE_CHECKOUT_SHA256="$checkout_digest" \
--env FIXTURE_CHECKOUT_CONTENT_ID="$checkout_content_id" \ --env FIXTURE_CHECKOUT_CONTENT_ID="$checkout_content_id" \
--env FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \ --env FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \
+8 -5
View File
@@ -13,10 +13,11 @@ next-git-present phase P8=FAIL
next-git-present phase P9=FAIL next-git-present phase P9=FAIL
next-git-present require ^\[fixture\] resolved lane=next .*version=[0-9]+\.[0-9]+\.[0-9]+-next\. next-git-present require ^\[fixture\] resolved lane=next .*version=[0-9]+\.[0-9]+\.[0-9]+-next\.
next-git-present require ^\[fixture\] installer_exit=1 done_claims=0$ next-git-present require ^\[fixture\] installer_exit=1 done_claims=0$
next-git-present require ^\[SECRET-CONTROL\] PASS:
next-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$ next-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$
next-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED: next-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:
next-git-present require ^\[P6\] FAIL: next-git-present require ^\[P6\] FAIL: broker absent but dead enforcement hooks are active
next-git-present forbid Done\. next-git-present forbid Done\.|MOSAIC_C1_CANARY_|CLI not found on PATH
main-git-present exit 1 main-git-present exit 1
main-git-present phase P0=PASS main-git-present phase P0=PASS
main-git-present phase P1=PASS main-git-present phase P1=PASS
@@ -30,10 +31,11 @@ main-git-present phase P8=FAIL
main-git-present phase P9=FAIL main-git-present phase P9=FAIL
main-git-present require ^\[fixture\] resolved lane=main .*version=[0-9]+\.[0-9]+\.[0-9]+$ main-git-present require ^\[fixture\] resolved lane=main .*version=[0-9]+\.[0-9]+\.[0-9]+$
main-git-present require ^\[fixture\] installer_exit=1 done_claims=0$ main-git-present require ^\[fixture\] installer_exit=1 done_claims=0$
main-git-present require ^\[SECRET-CONTROL\] PASS:
main-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$ main-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$
main-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED: main-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:
main-git-present require ^\[P6\] FAIL: main-git-present require ^\[P6\] FAIL: runtime linking/activation action reported a required failure
main-git-present forbid Done\. main-git-present forbid Done\.|MOSAIC_C1_CANARY_|CLI not found on PATH
next-git-absent exit 1 next-git-absent exit 1
next-git-absent phase P0=PASS next-git-absent phase P0=PASS
next-git-absent phase P1=FAIL next-git-absent phase P1=FAIL
@@ -46,6 +48,7 @@ next-git-absent phase P7=PASS
next-git-absent phase P8=FAIL next-git-absent phase P8=FAIL
next-git-absent phase P9=FAIL next-git-absent phase P9=FAIL
next-git-absent require ^\[fixture\] installer_exit=1 done_claims=0$ next-git-absent require ^\[fixture\] installer_exit=1 done_claims=0$
next-git-absent require ^\[SECRET-CONTROL\] PASS:
next-git-absent require ^\[P1\] FAIL: undeclared/missing prerequisite\(s\)=git; next-git-absent require ^\[P1\] FAIL: undeclared/missing prerequisite\(s\)=git;
next-git-absent require ^\[P3\] FAIL: .*executable=no next-git-absent require ^\[P3\] FAIL: .*executable=no
next-git-absent forbid Done\. next-git-absent forbid Done\.|MOSAIC_C1_CANARY_
1 # Pinned C1 expected-RED contract. Updating a verdict/reason requires review by the owning remediation lane.
13 next-git-present
14 next-git-present
15 next-git-present
16 next-git-present
17 next-git-present
18 next-git-present
19 next-git-present
20 next-git-present
21 main-git-present
22 main-git-present
23 main-git-present
31 main-git-present
32 main-git-present
33 main-git-present
34 main-git-present
35 main-git-present
36 main-git-present
37 main-git-present
38 main-git-present
39 next-git-absent
40 next-git-absent
41 next-git-absent
48 next-git-absent
49 next-git-absent
50 next-git-absent
51 next-git-absent
52 next-git-absent
53 next-git-absent
54 next-git-absent
+221 -8
View File
@@ -4,6 +4,8 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-next-install-test-XXXXXX")" TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-next-install-test-XXXXXX")"
trap 'rm -rf "$TMP"' EXIT trap 'rm -rf "$TMP"' EXIT
export TMPDIR="$TMP/runtime-tmp"
mkdir -p "$TMPDIR"
FAKE_BIN="$TMP/bin" FAKE_BIN="$TMP/bin"
HOME_DIR="$TMP/home" HOME_DIR="$TMP/home"
@@ -13,6 +15,48 @@ STATE="$TMP/state"
LOG="$TMP/npm.log" LOG="$TMP/npm.log"
mkdir -p "$FAKE_BIN" "$HOME_DIR" "$STATE" mkdir -p "$FAKE_BIN" "$HOME_DIR" "$STATE"
# Model the supported non-root/glibc target explicitly even when this harness
# itself runs as root in Alpine/BusyBox CI.
cat > "$FAKE_BIN/id" <<'FAKE_ID'
#!/usr/bin/env bash
case "${1:-}" in
-u) echo 1001 ;;
-g) echo 1001 ;;
-un) echo fixture-user ;;
*) exec /bin/id "$@" ;;
esac
FAKE_ID
cat > "$FAKE_BIN/getent" <<FAKE_GETENT
#!/usr/bin/env bash
printf 'fixture-user:x:1001:1001::%s:/bin/bash\n' '$HOME_DIR'
FAKE_GETENT
cat > "$FAKE_BIN/ldd" <<'FAKE_LDD'
#!/usr/bin/env bash
printf 'ldd (GNU libc) 2.36\n'
FAKE_LDD
cat > "$FAKE_BIN/stat" <<'FAKE_STAT'
#!/usr/bin/env bash
if [[ "${1:-} ${2:-}" == '-c %u' ]]; then
[[ "${3:-}" == "${MOSAIC_TEST_WRONG_OWNER_PATH:-__none__}" ]] && echo 9999 || echo 1001
exit 0
fi
if [[ "${1:-} ${2:-}" == '-c %g' ]]; then
[[ "${3:-}" == "${MOSAIC_TEST_WRONG_GROUP_PATH:-__none__}" ]] && echo 9999 || echo 1001
exit 0
fi
exec /bin/stat "$@"
FAKE_STAT
cat > "$FAKE_BIN/realpath" <<'FAKE_REALPATH'
#!/usr/bin/env python3
import os, sys
args=sys.argv[1:]
mode=args.pop(0) if args and args[0] in ('-e','-m') else '-m'
if args and args[0]=='--': args.pop(0)
if len(args)!=1 or (mode=='-e' and not os.path.exists(args[0])): raise SystemExit(1)
print(os.path.realpath(args[0]))
FAKE_REALPATH
chmod 0755 "$FAKE_BIN/id" "$FAKE_BIN/getent" "$FAKE_BIN/ldd" "$FAKE_BIN/stat" "$FAKE_BIN/realpath"
cat > "$FAKE_BIN/npm" <<'FAKE_NPM' cat > "$FAKE_BIN/npm" <<'FAKE_NPM'
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
@@ -31,6 +75,15 @@ install_cli() {
mkdir -p "${MOSAIC_PREFIX:?}/bin" mkdir -p "${MOSAIC_PREFIX:?}/bin"
cat > "$MOSAIC_PREFIX/bin/mosaic" <<CLI cat > "$MOSAIC_PREFIX/bin/mosaic" <<CLI
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" == "wizard" ]]; then
printf 'wizard\n' >> "\${MOSAIC_TEST_NPM_LOG:?}"
mkdir -p "\${MOSAIC_HOME:?}" "\${HOME:?}/.config/mosaic-gateway"
printf '# Soul\\n\\nConfigured.\\n' > "\$MOSAIC_HOME/SOUL.md"
printf '# User\\n\\nConfigured.\\n' > "\$MOSAIC_HOME/USER.md"
chmod 0600 "\$MOSAIC_HOME/SOUL.md" "\$MOSAIC_HOME/USER.md"
exit 0
fi
printf '%s\\n' '$version' printf '%s\\n' '$version'
CLI CLI
chmod +x "$MOSAIC_PREFIX/bin/mosaic" chmod +x "$MOSAIC_PREFIX/bin/mosaic"
@@ -51,6 +104,12 @@ if [[ "$1" == "view" ]]; then
fi fi
if [[ "$1" == "install" ]]; then if [[ "$1" == "install" ]]; then
if [[ -n "${MOSAIC_INSTALL_SECRET_CANARY:-}" ]]; then
printf 'registry diagnostic authToken=%s\n' "$MOSAIC_INSTALL_SECRET_CANARY"
printf 'urls=https://alice:p@[email protected]/a https://bob:pa:[email protected]/b https://carol:p%%[email protected]/c https://[email protected]/d https://user%%[email protected]/e\n'
printf 'Authorization: Basic QWxhZGRpbjpvcGVu\n//registry/:_auth=Ym9iOnNlY3JldA==\nCookie: session=abc123\nSet-Cookie: sid=xyz789\n'
printf '%s\n' "$MOSAIC_INSTALL_SECRET_CANARY" > "${MOSAIC_TEST_CANARY_OBSERVATION:?}"
fi
case "$*" in case "$*" in
*"@mosaicstack/[email protected]"*) *"@mosaicstack/[email protected]"*)
install_cli "0.0.49-next.999" install_cli "0.0.49-next.999"
@@ -141,7 +200,21 @@ if [[ -z "$dest" ]]; then
echo "fake tar missing -C destination" >&2 echo "fake tar missing -C destination" >&2
exit 1 exit 1
fi fi
mkdir -p "$dest/stack/packages/mosaic" "$dest/stack/apps/gateway" mkdir -p "$dest/stack/packages/mosaic/framework" "$dest/stack/apps/gateway"
cat > "$dest/stack/packages/mosaic/framework/install.sh" <<'FRAMEWORK'
#!/usr/bin/env bash
set -euo pipefail
expected="${MOSAIC_PREFIX:?}/bin/mosaic"
[[ "${MOSAIC_CLI_PATH:-}" == "$expected" && -x "$MOSAIC_CLI_PATH" ]] || {
echo "framework did not receive P3 absolute CLI (got=${MOSAIC_CLI_PATH:-unset} expected=$expected)" >&2
exit 61
}
printf 'framework-cli=%s version=%s\n' "$MOSAIC_CLI_PATH" "$($MOSAIC_CLI_PATH --version)" >> "${MOSAIC_TEST_NPM_LOG:?}"
mkdir -p "${MOSAIC_HOME:?}/credentials"
chmod 0700 "$MOSAIC_HOME/credentials"
printf '# framework fixture\n' > "$MOSAIC_HOME/AGENTS.md"
FRAMEWORK
chmod 0755 "$dest/stack/packages/mosaic/framework/install.sh"
FAKE_TAR FAKE_TAR
chmod +x "$FAKE_BIN/tar" chmod +x "$FAKE_BIN/tar"
@@ -187,15 +260,28 @@ reset_state() {
rm -f "$STATE"/* rm -f "$STATE"/*
} }
prefix_fingerprint() { tree_fingerprint() {
if [[ ! -d "$PREFIX" ]]; then printf 'ABSENT\n'; return; fi local root="$1"
( if [[ ! -d "$root" ]]; then printf 'ABSENT\n'; return; fi
cd "$PREFIX" python3 - "$root" <<'PY'
find . -mindepth 1 -printf '%P|%y|%m|%l\n' | LC_ALL=C sort import hashlib, os, stat, sys
find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum root=os.path.abspath(sys.argv[1]); rows=[]
) | sha256sum | awk '{print $1}' for current, dirs, files in os.walk(root, topdown=True, followlinks=False):
for name in dirs + files:
path=os.path.join(current,name); meta=os.lstat(path)
rel=os.path.relpath(path,root)
target=os.readlink(path) if stat.S_ISLNK(meta.st_mode) else ''
digest=''
if stat.S_ISREG(meta.st_mode):
with open(path,'rb') as handle: digest=hashlib.sha256(handle.read()).hexdigest()
rows.append((rel,stat.S_IFMT(meta.st_mode),stat.S_IMODE(meta.st_mode),target,digest))
payload='\n'.join('|'.join(map(str,row)) for row in sorted(rows)).encode()
print(hashlib.sha256(payload).hexdigest())
PY
} }
prefix_fingerprint() { tree_fingerprint "$PREFIX"; }
reset_state reset_state
echo "[test] --next fast path pins resolved package versions" echo "[test] --next fast path pins resolved package versions"
OUTPUT="$( OUTPUT="$(
@@ -356,4 +442,131 @@ set -e
[[ "$CHECK_STATUS" -ne 0 ]] [[ "$CHECK_STATUS" -ne 0 ]]
grep -q '^\[P2\] FAIL: resolved_version=unavailable' <<<"$OUTPUT" grep -q '^\[P2\] FAIL: resolved_version=unavailable' <<<"$OUTPUT"
printf '[test] full framework path receives P3 absolute CLI without relying on PATH\n'
rm -rf "$HOME_DIR" "$STATE"; mkdir -p "$HOME_DIR" "$STATE"; reset_state
set +e
OUTPUT="$(
HOME="$HOME_DIR" MOSAIC_HOME="$MOSAIC_HOME" MOSAIC_PREFIX="$PREFIX" \
MOSAIC_INSTALL_STATE_DIR="$TMP/full-state" MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --next --yes --no-auto-launch 2>&1
)"
FULL_STATUS=$?
set -e
[[ "$FULL_STATUS" -ne 0 ]] # P4 remains intentionally undeclared until C5.
grep -qF "framework-cli=$PREFIX/bin/mosaic version=0.0.49-next.999" "$LOG"
if grep -q "CLI not found on PATH\|did not receive P3 absolute CLI" <<<"$OUTPUT"; then
echo "internal framework phase depended on PATH instead of P3 absolute CLI" >&2
exit 1
fi
printf '[test] captured diagnostics redact seeded credential canary everywhere\n'
rm -rf "$HOME_DIR" "$STATE"; mkdir -p "$HOME_DIR" "$STATE"; reset_state
canary='C1_SECRET_CANARY_7df4c2'
OUTPUT="$(
HOME="$HOME_DIR" MOSAIC_HOME="$MOSAIC_HOME" MOSAIC_PREFIX="$PREFIX" \
MOSAIC_INSTALL_STATE_DIR="$TMP/secret-state" MOSAIC_NO_COLOR=1 \
MOSAIC_INSTALL_SECRET_CANARY="$canary" MOSAIC_TEST_CANARY_OBSERVATION="$TMP/canary-observed" \
MOSAIC_TEST_NPM_LOG="$LOG" MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
)"
if grep -qF "$canary" <<<"$OUTPUT"; then echo 'credential canary leaked to terminal output' >&2; exit 1; fi
if grep -Eq 'alice:p@ss|bob:pa:ss|carol:p%40ss|token@example|user%3Apass|QWxhZGRpbjpvcGVu|Ym9iOnNlY3JldA|session=abc123|sid=xyz789' <<<"$OUTPUT"; then
echo 'credentialed URL userinfo leaked to terminal output' >&2; exit 1
fi
[[ "$(grep -oF '[REDACTED]@' <<<"$OUTPUT" | wc -l | tr -d ' ')" -ge 5 ]] \
|| { echo 'credentialed URL redaction controls were not all exercised' >&2; exit 1; }
secret_active="$TMP/secret-state/active.json"
secret_journal="$(node -p "require('$secret_active').journal")"
secret_command_log="$(dirname "$secret_journal")/commands.log"
if grep -R -F "$canary" "$secret_command_log" "$HOME_DIR" 2>/dev/null; then
echo 'credential canary leaked to persistent installer output' >&2; exit 1
fi
if grep -E 'alice:p@ss|bob:pa:ss|carol:p%40ss|token@example|user%3Apass|QWxhZGRpbjpvcGVu|Ym9iOnNlY3JldA|session=abc123|sid=xyz789' "$secret_command_log" >/dev/null; then
echo 'credentialed URL userinfo leaked to persistent installer output' >&2; exit 1
fi
if [[ "$(cat "$TMP/canary-observed" 2>/dev/null || true)" != "$canary" ]]; then
echo 'credential canary positive control was not exercised' >&2; exit 1
fi
if find "$TMPDIR" -maxdepth 1 -type f \( -name 'mosaic-phase-redacted.*' -o -name 'mosaic-post-redacted.*' \) -print -quit | grep -q .; then
echo 'redacted diagnostic staging file survived normal completion' >&2; exit 1
fi
printf '[test] framework nested capture redacts the same canary and URL variants\n'
framework_test_home="$TMP/framework-redact-home"
framework_target="$framework_test_home/.config/mosaic"
framework_cli="$TMP/framework-redact-cli"
framework_log="$TMP/framework-redact-commands.log"
framework_status="$TMP/framework-redact-status.tsv"
mkdir -p "$framework_test_home"; : > "$framework_log"; : > "$framework_status"
cat > "$framework_cli" <<'FRAMEWORK_CLI'
#!/usr/bin/env bash
printf 'nested authToken=%s\n' "${MOSAIC_INSTALL_SECRET_CANARY:?}"
printf 'nested=https://alice:p@[email protected]/a https://bob:pa:[email protected]/b https://carol:p%%[email protected]/c https://[email protected]/d https://user%%[email protected]/e\n'
printf 'Authorization: Basic QWxhZGRpbjpvcGVu\n//registry/:_auth=Ym9iOnNlY3JldA==\nCookie: session=abc123\nSet-Cookie: sid=xyz789\n'
exit 1
FRAMEWORK_CLI
chmod 0755 "$framework_cli"
set +e
FRAMEWORK_OUTPUT="$(
HOME="$framework_test_home" MOSAIC_HOME="$framework_target" MOSAIC_INSTALL_MODE=overwrite \
MOSAIC_CLI_PATH="$framework_cli" MOSAIC_INSTALL_SECRET_CANARY="$canary" \
MOSAIC_INSTALL_COMMAND_LOG="$framework_log" MOSAIC_INSTALL_PHASE_STATUS_FILE="$framework_status" \
MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 MOSAIC_SKIP_SKILLS_SYNC=1 \
bash "$ROOT/packages/mosaic/framework/install.sh" 2>&1
)"
framework_install_status=$?
set -e
[[ "$framework_install_status" -eq 0 ]]
if grep -Eq "$canary|alice:p@ss|bob:pa:ss|carol:p%40ss|token@example|user%3Apass|QWxhZGRpbjpvcGVu|Ym9iOnNlY3JldA|session=abc123|sid=xyz789" <<<"$FRAMEWORK_OUTPUT" \
|| grep -Eq "$canary|alice:p@ss|bob:pa:ss|carol:p%40ss|token@example|user%3Apass|QWxhZGRpbjpvcGVu|Ym9iOnNlY3JldA|session=abc123|sid=xyz789" "$framework_log"; then
echo 'framework nested capture leaked credential diagnostics' >&2; exit 1
fi
[[ "$(grep -oF '[REDACTED]@' "$framework_log" | wc -l | tr -d ' ')" -ge 5 ]] \
|| { echo 'framework URL redaction controls were not exercised' >&2; exit 1; }
printf '[test] real P2-P8 actions run under fault injection and restore actual surfaces\n'
for phase in P2 P3 P4 P5 P6 P7 P8; do
rm -rf "$HOME_DIR" "$STATE" "$TMP/fault-$phase"; mkdir -p "$HOME_DIR" "$STATE" "$TMP/fault-$phase"
printf 'operator-sentinel\n' > "$HOME_DIR/operator.txt"
reset_state
before="$(tree_fingerprint "$HOME_DIR")"
set +e
HOME="$HOME_DIR" MOSAIC_HOME="$MOSAIC_HOME" MOSAIC_PREFIX="$PREFIX" \
MOSAIC_INSTALL_STATE_DIR="$TMP/fault-$phase" MOSAIC_INSTALL_FAULT_AFTER="$phase" \
MOSAIC_INSTALL_SELF_TEST_ALLOW=1 MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --state-machine-self-test --next --yes \
>"$TMP/fault-$phase.log" 2>&1
status=$?
set -e
[[ "$status" -eq 97 ]] || { echo "$phase real fault expected 97, got $status" >&2; exit 1; }
[[ -s "$LOG" ]] || { echo "$phase fault never entered the real action path" >&2; exit 1; }
[[ "$(tree_fingerprint "$HOME_DIR")" == "$before" ]] || { echo "$phase real rollback mismatch" >&2; exit 1; }
grep -q "phase=$phase" "$TMP/fault-$phase.log"
if find "$TMP/fault-$phase" -type f -exec grep -l '"status"[[:space:]]*:[[:space:]]*"in-progress"' {} + 2>/dev/null | grep -q .; then
echo "$phase left an in-progress transaction" >&2; exit 1
fi
done
printf '[test] stale projection is preserved while the real fault path acquires a free OS lock\n'
rm -rf "$HOME_DIR" "$STATE" "$TMP/stale-state"; mkdir -p "$HOME_DIR" "$STATE" "$TMP/stale-state"
printf '{"status":"in-progress","journal":"%s"}\n' "$TMP/stale-state/dead-run/journal.ndjson" > "$TMP/stale-state/active.json"
reset_state
set +e
HOME="$HOME_DIR" MOSAIC_HOME="$MOSAIC_HOME" MOSAIC_PREFIX="$PREFIX" \
MOSAIC_INSTALL_STATE_DIR="$TMP/stale-state" MOSAIC_INSTALL_FAULT_AFTER=P2 \
MOSAIC_INSTALL_SELF_TEST_ALLOW=1 MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --state-machine-self-test --next --yes >"$TMP/stale.log" 2>&1
stale_status=$?
set -e
[[ "$stale_status" -eq 97 ]]
find "$TMP/stale-state" -name prior-active.json -type f -print -quit | grep -q .
[[ "$(node -p "require('$TMP/stale-state/active.json').status")" == rolled-back ]]
echo "[test] installer next lane tests passed" echo "[test] installer next lane tests passed"
+121 -59
View File
@@ -133,16 +133,26 @@ make_fake_npm "$good_bin"
cp "$COMPAT_BIN/realpath" "$good_bin/realpath" cp "$COMPAT_BIN/realpath" "$good_bin/realpath"
cat > "$good_bin/id" <<'ID' cat > "$good_bin/id" <<'ID'
#!/bin/bash #!/bin/bash
uid="${MOSAIC_TEST_UID:-1001}"
gid="${MOSAIC_TEST_GID:-1001}"
user="${MOSAIC_TEST_USER:-fixture-user}"
case "${1:-}" in case "${1:-}" in
-u) echo 1001 ;; -u) echo "$uid" ;;
-g) echo 1001 ;; -g) echo "$gid" ;;
-un) echo fixture-user ;; -un) echo "$user" ;;
*) exec /bin/id "$@" ;; *) exec /bin/id "$@" ;;
esac esac
ID ID
cat > "$good_bin/stat" <<'STAT' cat > "$good_bin/stat" <<'STAT'
#!/bin/bash #!/bin/bash
if [[ "${1:-} ${2:-}" == '-c %u' ]]; then echo 1001; exit 0; fi if [[ "${1:-} ${2:-}" == '-c %u' ]]; then
[[ "${3:-}" == "${MOSAIC_TEST_WRONG_OWNER_PATH:-__none__}" ]] && echo 9999 || echo "${MOSAIC_TEST_UID:-1001}"
exit 0
fi
if [[ "${1:-} ${2:-}" == '-c %g' ]]; then
[[ "${3:-}" == "${MOSAIC_TEST_WRONG_GROUP_PATH:-__none__}" ]] && echo 9999 || echo "${MOSAIC_TEST_GID:-1001}"
exit 0
fi
exec /bin/stat "$@" exec /bin/stat "$@"
STAT STAT
cat > "$good_bin/curl" <<'CURL' cat > "$good_bin/curl" <<'CURL'
@@ -161,7 +171,7 @@ CLI
chmod 0755 "$good_prefix/bin/mosaic" chmod 0755 "$good_prefix/bin/mosaic"
cat > "$good_bin/getent" <<GETENT cat > "$good_bin/getent" <<GETENT
#!/bin/bash #!/bin/bash
printf '%s:x:%s:%s::%s:%s\\n' '$(id -un)' '$(id -u)' '$(id -g)' '$good_home' '$good_bin/bash' printf '%s:x:%s:%s::%s:%s\\n' "\${MOSAIC_TEST_USER:-fixture-user}" "\${MOSAIC_TEST_UID:-1001}" "\${MOSAIC_TEST_GID:-1001}" "\${MOSAIC_TEST_PASSWD_HOME:-$good_home}" '$good_bin/bash'
GETENT GETENT
cat > "$good_bin/bash" <<SHELL cat > "$good_bin/bash" <<SHELL
#!/bin/bash #!/bin/bash
@@ -199,6 +209,101 @@ good_rows="$(grep -Ec '^\[P[0-8]\] PASS:' "$TMP/good-check.log" || true)"
[[ "$good_rows" -eq 9 ]] && pass_case 'good-host --check emitted nine PASS rows' \ [[ "$good_rows" -eq 9 ]] && pass_case 'good-host --check emitted nine PASS rows' \
|| { cat "$TMP/good-check.log" >&2; fail_case "good-host --check emitted $good_rows PASS rows"; } || { cat "$TMP/good-check.log" >&2; fail_case "good-host --check emitted $good_rows PASS rows"; }
printf '[test] case: P0 binds uid, username, passwd HOME, shell, and privilege mode\n'
passwd_home="$TMP/passwd-authoritative-home"
mkdir -p "$passwd_home"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" \
MOSAIC_TEST_PASSWD_HOME="$passwd_home" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/wrong-home.log" 2>&1
wrong_home_status=$?
set -e
[[ "$wrong_home_status" -ne 0 ]] || fail_case 'P0 accepted ambient HOME that disagrees with passwd HOME'
grep -q '^\[P0\] FAIL:.*HOME mismatch' "$TMP/wrong-home.log" \
&& pass_case 'P0 rejects ambient HOME that disagrees with passwd HOME' \
|| fail_case 'P0 did not attribute the passwd HOME mismatch'
for privilege_case in root-with-home sudo-with-inherited-home; do
extra_env=()
[[ "$privilege_case" == sudo-with-inherited-home ]] && extra_env+=(SUDO_USER=fixture-user SUDO_UID=1001)
set +e
env HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" \
MOSAIC_TEST_UID=0 MOSAIC_TEST_GID=0 MOSAIC_TEST_USER=root MOSAIC_TEST_PASSWD_HOME=/root \
MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" "${extra_env[@]}" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/$privilege_case.log" 2>&1
privilege_status=$?
set -e
[[ "$privilege_status" -ne 0 ]] || fail_case "P0 accepted unsafe $privilege_case context"
grep -q '^\[P0\] FAIL:.*privilege=' "$TMP/$privilege_case.log" \
&& pass_case "P0 states and rejects $privilege_case privilege context" \
|| fail_case "P0 did not state $privilege_case privilege mode"
done
printf '[test] case: P3/P5 reject unsafe owner, group, and mode\n'
chmod 0777 "$good_prefix/bin/mosaic"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p3-mode.log" 2>&1
p3_mode_status=$?
set -e
[[ "$p3_mode_status" -ne 0 ]] || fail_case 'P3 accepted mode-0777 CLI'
grep -q '^\[P3\] FAIL:.*unsafe owner/group/mode' "$TMP/p3-mode.log" \
&& pass_case 'P3 rejects group/world-writable CLI' || fail_case 'P3 did not attribute unsafe CLI mode'
chmod 0755 "$good_prefix/bin/mosaic"
for ownership_case in owner group; do
wrong_env=()
[[ "$ownership_case" == owner ]] && wrong_env+=(MOSAIC_TEST_WRONG_OWNER_PATH="$good_prefix/bin/mosaic")
[[ "$ownership_case" == group ]] && wrong_env+=(MOSAIC_TEST_WRONG_GROUP_PATH="$good_prefix/bin/mosaic")
set +e
env HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" "${wrong_env[@]}" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/p3-$ownership_case.log" 2>&1
owner_status=$?
set -e
[[ "$owner_status" -ne 0 ]] || fail_case "P3 accepted wrong CLI $ownership_case"
grep -q '^\[P3\] FAIL:.*unsafe owner/group/mode' "$TMP/p3-$ownership_case.log" \
&& pass_case "P3 rejects wrong CLI $ownership_case" || fail_case "P3 did not attribute wrong CLI $ownership_case"
done
chmod 0644 "$good_mosaic/SOUL.md" "$good_mosaic/USER.md"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p5-mode.log" 2>&1
p5_mode_status=$?
set -e
[[ "$p5_mode_status" -ne 0 ]] || fail_case 'P5 accepted world-readable identity files'
grep -q '^\[P5\] FAIL:' "$TMP/p5-mode.log" \
&& pass_case 'P5 rejects world-readable identity files' || fail_case 'P5 did not reject identity mode 0644'
chmod 0600 "$good_mosaic/SOUL.md" "$good_mosaic/USER.md"
mkdir -p "$good_mosaic/credentials"
chmod 0755 "$good_mosaic/credentials"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p5-credentials.log" 2>&1
credential_status=$?
set -e
[[ "$credential_status" -ne 0 ]] || fail_case 'P5 accepted mode-0755 credentials directory'
grep -q '^\[P5\] FAIL:.*credentials' "$TMP/p5-credentials.log" \
&& pass_case 'P5 rejects group/world-readable credential storage' \
|| fail_case 'P5 did not attribute unsafe credential directory mode'
chmod 0700 "$good_mosaic/credentials"
printf '# framework\n' > "$good_mosaic/AGENTS.md"
chmod 0666 "$good_mosaic/AGENTS.md"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p4-tree-mode.log" 2>&1
framework_mode_status=$?
set -e
[[ "$framework_mode_status" -ne 0 ]] || fail_case 'P4 accepted group/world-writable framework path'
grep -q '^\[P4\] FAIL:.*owner/mode policy' "$TMP/p4-tree-mode.log" \
&& pass_case 'P4 inventories and rejects unsafe created framework paths' \
|| fail_case 'P4 did not attribute unsafe created-path mode'
chmod 0644 "$good_mosaic/AGENTS.md"
printf '[test] case: persisted required-action failures remain blocking\n' printf '[test] case: persisted required-action failures remain blocking\n'
for blocked_phase in P4 P6; do for blocked_phase in P4 P6; do
node -e ' node -e '
@@ -219,36 +324,12 @@ for blocked_phase in P4 P6; do
done done
printf '{\n "lane": "next",\n "cliVersion": "0.0.50-next.999",\n "phaseOutcomes": {"P4":"committed","P6":"committed"}\n}\n' > "$good_mosaic/.install-manifest.json" printf '{\n "lane": "next",\n "cliVersion": "0.0.50-next.999",\n "phaseOutcomes": {"P4":"committed","P6":"committed"}\n}\n' > "$good_mosaic/.install-manifest.json"
printf '[test] case: per-phase P2-P8 fault injection restores representative host mutations\n' printf '[test] case: fault injection has no synthetic mutation implementation\n'
for phase in P2 P3 P4 P5 P6 P7 P8; do if grep -q '\.selftest-' "$ROOT/tools/install.sh"; then
home="$TMP/fault-$phase/home" fail_case 'synthetic .selftest mutation path remains in the production fault seam'
state="$TMP/fault-$phase/state" else
mkdir -p "$home/.config/mosaic" "$home/.npm-global/bin" "$home/.claude" "$state" pass_case 'fault seam is attached only to real P2-P8 action flow (exercised by install-next-lane.test.sh)'
printf 'operator-framework-sentinel\n' > "$home/.config/mosaic/operator.txt" fi
printf '@scope:registry=https://pre.example.invalid/\n' > "$home/.npmrc"
printf 'old-cli\n' > "$home/.npm-global/bin/mosaic"
printf '{"hooks":{"safe":true}}\n' > "$home/.claude/settings.json"
before="$(fingerprint "$home")"
set +e
HOME="$home" MOSAIC_HOME="$home/.config/mosaic" MOSAIC_PREFIX="$home/.npm-global" \
MOSAIC_INSTALL_STATE_DIR="$state" MOSAIC_INSTALL_FAULT_AFTER="$phase" \
MOSAIC_NO_COLOR=1 PATH="$COMPAT_BIN:$PATH" bash "$ROOT/tools/install.sh" --state-machine-self-test \
>"$TMP/fault-$phase.log" 2>&1
status=$?
set -e
after="$(fingerprint "$home")"
[[ "$status" -ne 0 ]] || fail_case "$phase injected fault returned zero"
grep -q "phase=$phase" "$TMP/fault-$phase.log" \
|| fail_case "$phase fault transcript did not name the injected phase"
[[ "$before" == "$after" ]] \
&& pass_case "$phase rollback restored framework/npmrc/prefix/runtime representative state" \
|| fail_case "$phase rollback mismatch (before=$before after=$after)"
if find "$state" -type f -exec grep -l '"status"[[:space:]]*:[[:space:]]*"in-progress"' {} + 2>/dev/null | grep -q .; then
fail_case "$phase left a journal in-progress"
else
pass_case "$phase left no journal falsely in-progress"
fi
done
printf '[test] case: unsafe and overlapping rollback roots fail before mutation\n' printf '[test] case: unsafe and overlapping rollback roots fail before mutation\n'
unsafe_home="$TMP/unsafe-home" unsafe_home="$TMP/unsafe-home"
@@ -262,7 +343,7 @@ for case_name in root-target home-target overlap-target; do
before="$(fingerprint "$unsafe_home")" before="$(fingerprint "$unsafe_home")"
set +e set +e
HOME="$unsafe_home" MOSAIC_HOME="$unsafe_mosaic" MOSAIC_PREFIX="$unsafe_prefix" \ HOME="$unsafe_home" MOSAIC_HOME="$unsafe_mosaic" MOSAIC_PREFIX="$unsafe_prefix" \
MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \ MOSAIC_TEST_PASSWD_HOME="$unsafe_home" MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/$case_name.log" 2>&1 bash "$ROOT/tools/install.sh" --check --next >"$TMP/$case_name.log" 2>&1
status=$? status=$?
set -e set -e
@@ -279,7 +360,7 @@ mkdir -p "$symlink_home" "$symlink_outside"
ln -s "$symlink_outside" "$symlink_home/.config" ln -s "$symlink_outside" "$symlink_home/.config"
set +e set +e
HOME="$symlink_home" MOSAIC_HOME="$symlink_home/.config/mosaic" MOSAIC_PREFIX="$symlink_home/.npm-global" \ HOME="$symlink_home" MOSAIC_HOME="$symlink_home/.config/mosaic" MOSAIC_PREFIX="$symlink_home/.npm-global" \
MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \ MOSAIC_TEST_PASSWD_HOME="$symlink_home" MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/symlink-target.log" 2>&1 bash "$ROOT/tools/install.sh" --check --next >"$TMP/symlink-target.log" 2>&1
status=$? status=$?
set -e set -e
@@ -289,26 +370,6 @@ grep -q '^\[P0\] FAIL:.*unsafe context' "$TMP/symlink-target.log" \
|| fail_case 'symlinked rollback parent lacked an attributable P0 failure' || fail_case 'symlinked rollback parent lacked an attributable P0 failure'
[[ -z "$(find "$symlink_outside" -mindepth 1 -print -quit)" ]] || fail_case 'symlink target was mutated' [[ -z "$(find "$symlink_outside" -mindepth 1 -print -quit)" ]] || fail_case 'symlink target was mutated'
printf '[test] case: stale in-progress projection does not impersonate a live OS lock\n'
stale_home="$TMP/stale/home"
stale_state="$TMP/stale/state"
mkdir -p "$stale_home/.config/mosaic" "$stale_state"
printf '{"status":"in-progress","journal":"%s"}\n' "$stale_state/dead-run/journal.ndjson" > "$stale_state/active.json"
set +e
HOME="$stale_home" MOSAIC_HOME="$stale_home/.config/mosaic" MOSAIC_PREFIX="$stale_home/.npm-global" \
MOSAIC_INSTALL_STATE_DIR="$stale_state" MOSAIC_INSTALL_FAULT_AFTER=P2 MOSAIC_NO_COLOR=1 \
PATH="$COMPAT_BIN:$PATH" bash "$ROOT/tools/install.sh" --state-machine-self-test >"$TMP/stale.log" 2>&1
status=$?
set -e
[[ "$status" -eq 97 ]] || fail_case "stale projection recovery expected injected status 97, got $status"
if find "$stale_state" -name prior-active.json -type f -print -quit | grep -q .; then
pass_case 'stale projection was preserved and superseded after the free OS lock was acquired'
else
fail_case 'stale projection was not preserved for recovery evidence'
fi
[[ "$(node -p "require('$stale_state/active.json').status")" == "rolled-back" ]] \
|| fail_case 'stale retry did not reach an honest rolled-back terminal state'
printf '[test] case: journal initialization failure is fatal before mutation\n' printf '[test] case: journal initialization failure is fatal before mutation\n'
journal_home="$TMP/journal-failure/home" journal_home="$TMP/journal-failure/home"
mkdir -p "$journal_home/.config/mosaic" mkdir -p "$journal_home/.config/mosaic"
@@ -316,8 +377,9 @@ printf 'journal-sentinel\n' > "$journal_home/.config/mosaic/operator.txt"
before="$(fingerprint "$journal_home")" before="$(fingerprint "$journal_home")"
set +e set +e
HOME="$journal_home" MOSAIC_HOME="$journal_home/.config/mosaic" MOSAIC_PREFIX="$journal_home/.npm-global" \ HOME="$journal_home" MOSAIC_HOME="$journal_home/.config/mosaic" MOSAIC_PREFIX="$journal_home/.npm-global" \
MOSAIC_INSTALL_STATE_DIR="/proc/mosaic-journal-denied-$$" MOSAIC_INSTALL_FAULT_AFTER=P2 \ MOSAIC_TEST_PASSWD_HOME="$journal_home" MOSAIC_INSTALL_STATE_DIR="/proc/mosaic-journal-denied-$$" \
MOSAIC_NO_COLOR=1 bash "$ROOT/tools/install.sh" --state-machine-self-test \ MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch \
>"$TMP/journal-failure.log" 2>&1 >"$TMP/journal-failure.log" 2>&1
status=$? status=$?
set -e set -e
+198 -69
View File
@@ -510,27 +510,77 @@ state_action_failed() {
fi fi
} }
state_redact_stream() {
python3 /dev/fd/3 3<<'PY'
import os, re, sys
text = sys.stdin.read()
secret_name = re.compile(r"(?:TOKEN|PASSWORD|PASSWD|SECRET|API_KEY|AUTH|CREDENTIAL|CANARY)", re.I)
secrets = {value for name, value in os.environ.items() if secret_name.search(name) and len(value) >= 4}
for value in sorted(secrets, key=len, reverse=True):
text = text.replace(value, "[REDACTED]")
patterns = (
(re.compile(r"(?im)^(\s*(?:proxy-)?authorization\s*:\s*)[^\r\n]+"), r"\1[REDACTED]"),
(re.compile(r"(?im)^(\s*(?:set-)?cookie\s*:\s*)[^\r\n]+"), r"\1[REDACTED]"),
(re.compile(r"(?i)(Bearer\s+)[^\s'\"]+"), r"\1[REDACTED]"),
(re.compile(r"(?i)((?:[_-]?auth(?:Token)?|token|password|passwd|secret|api[_-]?key)\s*[=:]\s*)[^\s'\"]+"), r"\1[REDACTED]"),
)
for pattern, replacement in patterns:
text = pattern.sub(replacement, text)
url_pattern = re.compile(r"https?://[^\s'\"<>]+", re.I)
def redact_url(match):
url = match.group(0)
scheme_end = url.find("://") + 3
authority_end = len(url)
for separator in "/?#":
position = url.find(separator, scheme_end)
if position != -1:
authority_end = min(authority_end, position)
authority = url[scheme_end:authority_end]
at = authority.rfind("@")
if at != -1:
return url[:scheme_end] + "[REDACTED]@" + authority[at + 1:] + url[authority_end:]
return url
sys.stdout.write(url_pattern.sub(redact_url, text))
PY
}
state_redaction_probe() {
printf '[REDACTION-PROBE] emitted=%s\n' "${MOSAIC_INSTALL_SECRET_CANARY:?redaction probe requires canary}"
}
state_run_captured() { state_run_captured() {
local label="$1" output status=0 local label="$1" redacted redactor_pid capture_fd status=0 redact_status=0
shift shift
output="$(mktemp "${TMPDIR:-/tmp}/mosaic-phase-command.XXXXXX.log")" || return redacted="$(mktemp "${TMPDIR:-/tmp}/mosaic-phase-redacted.XXXXXX")" || return
# The command is deliberately called in a conditional so its status can be chmod 0600 "$redacted" || { rm -f "$redacted"; return 1; }
# journaled before the caller's ERR trap rolls back. Bash disables errexit in # Process substitution preserves in-shell phase side effects while ensuring
# functions invoked this way, so every multi-command phase helper below must # plaintext diagnostics exist only in a pipe, never in a filesystem body.
# explicitly return on each required command failure. exec {capture_fd}> >(state_redact_stream > "$redacted")
if "$@" >"$output" 2>&1; then status=0; else status=$?; fi redactor_pid=$!
cat "$output" || { rm -f "$output"; return 1; } set +e
if ! { printf '\n=== %s (exit=%s) ===\n' "$label" "$status"; cat "$output"; } >> "$STATE_COMMAND_LOG"; then "$@" >&"$capture_fd" 2>&1
rm -f "$output" status=$?
exec {capture_fd}>&-
wait "$redactor_pid"
redact_status=$?
set -e
if [[ "$redact_status" -ne 0 ]]; then
rm -f "$redacted"
fail "Could not redact '$label' diagnostics; refusing to expose or persist raw command output."
return 1
fi
cat "$redacted" || { rm -f "$redacted"; return 1; }
if ! { printf '\n=== %s (exit=%s) ===\n' "$label" "$status"; cat "$redacted"; } >> "$STATE_COMMAND_LOG"; then
rm -f "$redacted"
fail "Could not append '$label' output to $STATE_COMMAND_LOG; refusing to continue." fail "Could not append '$label' output to $STATE_COMMAND_LOG; refusing to continue."
return 1 return 1
fi fi
if ! sync "$STATE_COMMAND_LOG"; then if ! sync "$STATE_COMMAND_LOG"; then
rm -f "$output" rm -f "$redacted"
fail "Could not sync '$label' output in $STATE_COMMAND_LOG; refusing to continue." fail "Could not sync '$label' output in $STATE_COMMAND_LOG; refusing to continue."
return 1 return 1
fi fi
rm -f "$output" rm -f "$redacted"
state_json_line command "$STATE_CURRENT_PHASE" "$([[ "$status" -eq 0 ]] && echo committed || echo failed)" "label=$label output_log=$STATE_COMMAND_LOG exit=$status" state_json_line command "$STATE_CURRENT_PHASE" "$([[ "$status" -eq 0 ]] && echo committed || echo failed)" "label=$label output_log=$STATE_COMMAND_LOG exit=$status"
return "$status" return "$status"
} }
@@ -569,6 +619,55 @@ state_target_shell() {
printf '%s' "${shell:-${SHELL:-}}" printf '%s' "${shell:-${SHELL:-}}"
} }
STATE_POLICY_REASON=""
state_path_owner_mode_ok() {
local path="$1" policy="${2:-shared-read}" uid gid mode mode_value original resolved
original="$path"
[[ -e "$path" ]] || { STATE_POLICY_REASON="$path missing"; return 1; }
if [[ -L "$path" ]]; then
resolved="$(realpath -e -- "$path" 2>/dev/null)" \
|| { STATE_POLICY_REASON="$path has an unresolved symlink target"; return 1; }
if [[ "$resolved" != "$HOME"/* && "$resolved" != "$PREFIX"/* ]]; then
STATE_POLICY_REASON="$path escapes target-owned roots via symlink to $resolved"
return 1
fi
path="$resolved"
fi
uid="$(stat -c '%u' "$path" 2>/dev/null)" || { STATE_POLICY_REASON="$original owner unreadable"; return 1; }
gid="$(stat -c '%g' "$path" 2>/dev/null)" || { STATE_POLICY_REASON="$path group unreadable"; return 1; }
mode="$(stat -c '%a' "$path" 2>/dev/null)" || { STATE_POLICY_REASON="$path mode unreadable"; return 1; }
[[ "$uid" == "$(id -u)" && "$gid" == "$(id -g)" && "$mode" =~ ^[0-7]{3,4}$ ]] \
|| { STATE_POLICY_REASON="$original owner=$uid group=$gid mode=$mode expected=$(id -u):$(id -g)"; return 1; }
mode_value=$((8#$mode))
case "$policy" in
private)
(( (mode_value & 077) == 0 )) \
|| { STATE_POLICY_REASON="$original mode=$mode exposes private material to group/other"; return 1; }
;;
executable)
(( (mode_value & 0111) != 0 && (mode_value & 022) == 0 )) \
|| { STATE_POLICY_REASON="$original mode=$mode is not executable or is group/world-writable"; return 1; }
;;
shared-read)
(( (mode_value & 022) == 0 )) \
|| { STATE_POLICY_REASON="$original mode=$mode is group/world-writable"; return 1; }
;;
*) STATE_POLICY_REASON="unknown owner/mode policy=$policy for $path"; return 1 ;;
esac
}
state_tree_owner_mode_ok() {
local root="$1" path policy
[[ -e "$root" ]] || return 0
while IFS= read -r -d '' path; do
policy=shared-read
case "$path" in
"$MOSAIC_HOME/credentials"|"$MOSAIC_HOME/credentials"/*|"$MOSAIC_HOME/SOUL.md"|"$MOSAIC_HOME/USER.md") policy=private ;;
esac
state_path_owner_mode_ok "$path" "$policy" || return
done < <(find "$root" -xdev -print0)
}
state_resolved_version() { state_resolved_version() {
local cli gateway local cli gateway
if [[ "$FLAG_DEV" == "true" ]]; then if [[ "$FLAG_DEV" == "true" ]]; then
@@ -598,21 +697,37 @@ state_predicate() {
local phase="$1" shell node_major installed expected local phase="$1" shell node_major installed expected
local missing=() login_path nonlogin_path broker=false dead_hooks=0 local missing=() login_path nonlogin_path broker=false dead_hooks=0
local prefix_parent disk_kb inode_count min_disk_kb min_inodes npm_major privilege_mode local prefix_parent disk_kb inode_count min_disk_kb min_inodes npm_major privilege_mode
local passwd_row passwd_user passwd_uid passwd_home passwd_shell actual_user actual_uid
STATE_REASON="" STATE_REASON=""
case "$phase" in case "$phase" in
P0) P0)
shell="$(state_target_shell)" actual_uid="$(id -u 2>/dev/null || true)"
actual_user="$(id -un 2>/dev/null || true)"
passwd_row="$(getent passwd "$actual_uid" 2>/dev/null || true)"
IFS=: read -r passwd_user _ passwd_uid _ _ passwd_home passwd_shell <<<"$passwd_row"
shell="$passwd_shell"
node_major="$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)" node_major="$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)"
npm_major="$(npm --version 2>/dev/null | cut -d. -f1 || echo 0)" npm_major="$(npm --version 2>/dev/null | cut -d. -f1 || echo 0)"
privilege_mode="$([[ "$(id -u)" -eq 0 ]] && echo root-without-explicit-target || echo user)" if [[ "$actual_uid" == 0 && -n "${SUDO_USER:-}" ]]; then
if [[ -n "$HOME" && -n "$shell" && "$privilege_mode" == "user" && "$(uname -s)" == "Linux" ]] \ privilege_mode="sudo-with-inherited-home"
elif [[ "$actual_uid" == 0 ]]; then
privilege_mode="root-without-explicit-target"
else
privilege_mode="user"
fi
if [[ -z "$passwd_row" || "$actual_uid" != "$passwd_uid" || "$actual_user" != "$passwd_user" \
|| -z "$passwd_home" || "$HOME" != "$passwd_home" ]]; then
STATE_REASON="unsupported or unresolved target account: HOME mismatch or passwd identity mismatch (target=${actual_user:-unknown} uid=${actual_uid:-unknown} HOME=${HOME:-unset} passwd_user=${passwd_user:-unset} passwd_uid=${passwd_uid:-unset} passwd_HOME=${passwd_home:-unset} shell=${passwd_shell:-unset} privilege=$privilege_mode)"
return 1
fi
if [[ -n "$shell" && "$privilege_mode" == "user" && "$(uname -s)" == "Linux" ]] \
&& ldd --version 2>&1 | grep -i 'glibc\|gnu libc' >/dev/null \ && ldd --version 2>&1 | grep -i 'glibc\|gnu libc' >/dev/null \
&& [[ "$(uname -m)" == "x86_64" ]] && [[ "$node_major" -ge 20 ]] && [[ "$npm_major" -ge 9 ]] \ && [[ "$(uname -m)" == "x86_64" ]] && [[ "$node_major" -ge 20 ]] && [[ "$npm_major" -ge 9 ]] \
&& state_validate_target_paths; then && state_validate_target_paths; then
STATE_REASON="target=$(id -un) uid=$(id -u) HOME=$HOME shell=$shell privilege=$privilege_mode arch=x86_64 libc=glibc node=$(node --version) npm=$(npm --version)" STATE_REASON="target=$actual_user uid=$actual_uid HOME=$HOME passwd_HOME=$passwd_home shell=$shell privilege=$privilege_mode arch=x86_64 libc=glibc node=$(node --version) npm=$(npm --version)"
return 0 return 0
fi fi
STATE_REASON="unsupported, unresolved, or unsafe context (target=$(id -un 2>/dev/null || echo unknown) uid=$(id -u) HOME=${HOME:-unset} shell=${shell:-unset} privilege=$privilege_mode arch=$(uname -m 2>/dev/null || echo unknown) node_major=$node_major npm_major=$npm_major path_check=${STATE_PATH_REASON:-not-reached})" STATE_REASON="unsupported, unresolved, or unsafe context (target=${actual_user:-unknown} uid=${actual_uid:-unknown} HOME=${HOME:-unset} passwd_HOME=${passwd_home:-unset} shell=${shell:-unset} privilege=$privilege_mode arch=$(uname -m 2>/dev/null || echo unknown) node_major=$node_major npm_major=$npm_major path_check=${STATE_PATH_REASON:-not-reached})"
return 1 return 1
;; ;;
P1) P1)
@@ -684,11 +799,12 @@ state_predicate() {
expected="$(state_expected_cli_version)" expected="$(state_expected_cli_version)"
installed="" installed=""
[[ -x "$PREFIX/bin/mosaic" ]] && installed="$("$PREFIX/bin/mosaic" --version 2>&1 | tail -n 1 | tr -d '\r' || true)" [[ -x "$PREFIX/bin/mosaic" ]] && installed="$("$PREFIX/bin/mosaic" --version 2>&1 | tail -n 1 | tr -d '\r' || true)"
if [[ -n "$expected" && -x "$PREFIX/bin/mosaic" && "$installed" == "$expected" ]]; then if [[ -n "$expected" && -x "$PREFIX/bin/mosaic" && "$installed" == "$expected" ]] \
STATE_REASON="absolute_path=$PREFIX/bin/mosaic version=$installed equals resolved lane version" && state_path_owner_mode_ok "$PREFIX/bin/mosaic" executable; then
STATE_REASON="absolute_path=$PREFIX/bin/mosaic version=$installed equals resolved lane version; owner/mode policy satisfied"
return 0 return 0
fi fi
STATE_REASON="absolute_path=$PREFIX/bin/mosaic executable=$([[ -x "$PREFIX/bin/mosaic" ]] && echo yes || echo no) got=${installed:-missing} expected=${expected:-unresolved}" STATE_REASON="absolute_path=$PREFIX/bin/mosaic executable=$([[ -x "$PREFIX/bin/mosaic" ]] && echo yes || echo no) got=${installed:-missing} expected=${expected:-unresolved}; unsafe owner/group/mode=${STATE_POLICY_REASON:-not-evaluated}"
return 1 return 1
;; ;;
P4) P4)
@@ -699,6 +815,10 @@ state_predicate() {
local expected_lane expected_version local expected_lane expected_version
expected_lane="$([[ "$FLAG_NEXT" == true ]] && echo next || echo latest)" expected_lane="$([[ "$FLAG_NEXT" == true ]] && echo next || echo latest)"
expected_version="$(state_expected_cli_version)" expected_version="$(state_expected_cli_version)"
if [[ -e "$MOSAIC_HOME" ]] && ! state_tree_owner_mode_ok "$MOSAIC_HOME"; then
STATE_REASON="framework created-path owner/mode policy failed: $STATE_POLICY_REASON"
return 1
fi
if [[ ! -s "$declared_set" ]]; then if [[ ! -s "$declared_set" ]]; then
STATE_REASON="NOT-MEASURED / UNDECLARED: installer published no checkout-free, lane/versioned shipped-set artifact at $declared_set" STATE_REASON="NOT-MEASURED / UNDECLARED: installer published no checkout-free, lane/versioned shipped-set artifact at $declared_set"
return 1 return 1
@@ -738,12 +858,14 @@ NODE
for skill in SOUL.md USER.md; do for skill in SOUL.md USER.md; do
local path="$MOSAIC_HOME/$skill" local path="$MOSAIC_HOME/$skill"
if [[ ! -s "$path" ]] || ! grep -q '^# ' "$path" 2>/dev/null \ if [[ ! -s "$path" ]] || ! grep -q '^# ' "$path" 2>/dev/null \
|| [[ "$(stat -c '%u' "$path" 2>/dev/null || echo -1)" != "$(id -u)" ]] \ || ! state_path_owner_mode_ok "$path" private; then
|| [[ "$(stat -c '%a' "$path" 2>/dev/null || echo 777)" =~ [2367]$ ]]; then
missing+=("$skill") missing+=("$skill")
fi fi
done done
if [[ "${#missing[@]}" -eq 0 ]]; then STATE_REASON="SOUL.md and USER.md parse and have target owner/mode"; return 0; fi if [[ -e "$MOSAIC_HOME/credentials" ]] && ! state_tree_owner_mode_ok "$MOSAIC_HOME/credentials"; then
missing+=("credentials(owner/mode=$STATE_POLICY_REASON)")
fi
if [[ "${#missing[@]}" -eq 0 ]]; then STATE_REASON="SOUL.md and USER.md parse and have private target owner/mode; credential paths are private"; return 0; fi
STATE_REASON="identity missing, empty, malformed, wrong-owner, or unsafe-mode: ${missing[*]}" STATE_REASON="identity missing, empty, malformed, wrong-owner, or unsafe-mode: ${missing[*]}"
return 1 return 1
;; ;;
@@ -863,7 +985,8 @@ state_validate_target_paths() {
} }
state_snapshot_create() { state_snapshot_create() {
local dst list path key index=0 local dst list path key index=0 parent parent_list parent_status
local -A recorded_parents=()
if ! state_validate_target_paths; then if ! state_validate_target_paths; then
fail "P1 Preflight refused snapshot creation: $STATE_PATH_REASON" fail "P1 Preflight refused snapshot creation: $STATE_PATH_REASON"
return 1 return 1
@@ -871,7 +994,9 @@ state_snapshot_create() {
STATE_SNAPSHOT_DIR="$STATE_RUN_DIR/snapshot" STATE_SNAPSHOT_DIR="$STATE_RUN_DIR/snapshot"
mkdir -p "$STATE_SNAPSHOT_DIR/data" mkdir -p "$STATE_SNAPSHOT_DIR/data"
list="$STATE_SNAPSHOT_DIR/paths.tsv" list="$STATE_SNAPSHOT_DIR/paths.tsv"
parent_list="$STATE_SNAPSHOT_DIR/parents.tsv"
: > "$list" : > "$list"
: > "$parent_list"
for path in "$MOSAIC_HOME" "$PREFIX" "$HOME/.npmrc" "$HOME/.bashrc" "$HOME/.bash_profile" \ for path in "$MOSAIC_HOME" "$PREFIX" "$HOME/.npmrc" "$HOME/.bashrc" "$HOME/.bash_profile" \
"$HOME/.profile" "$HOME/.zshrc" "$HOME/.config/fish/config.fish" "$HOME/.claude" \ "$HOME/.profile" "$HOME/.zshrc" "$HOME/.config/fish/config.fish" "$HOME/.claude" \
"$HOME/.pi" "$HOME/.codex" "$HOME/.config/opencode" "$HOME/.config/mosaic-gateway" \ "$HOME/.pi" "$HOME/.codex" "$HOME/.config/opencode" "$HOME/.config/mosaic-gateway" \
@@ -886,12 +1011,22 @@ state_snapshot_create() {
else else
printf 'absent\t%s\t%s\n' "$path" "$key" >> "$list" printf 'absent\t%s\t%s\n' "$path" "$key" >> "$list"
fi fi
parent="$(dirname "$path")"
while [[ "$parent" != "$HOME" && "$parent" == "$HOME"/* ]]; do
if [[ -z "${recorded_parents[$parent]:-}" ]]; then
recorded_parents[$parent]=1
parent_status=absent
[[ -d "$parent" ]] && parent_status=present
printf '%s\t%s\n' "$parent_status" "$parent" >> "$parent_list"
fi
parent="$(dirname "$parent")"
done
done done
state_json_line snapshot P1 committed "pre-install snapshot=$STATE_SNAPSHOT_DIR" state_json_line snapshot P1 committed "pre-install snapshot=$STATE_SNAPSHOT_DIR"
} }
state_snapshot_restore() { state_snapshot_restore() {
local status target key saved local status target key saved parent
[[ -s "$STATE_SNAPSHOT_DIR/paths.tsv" ]] || return 1 [[ -s "$STATE_SNAPSHOT_DIR/paths.tsv" ]] || return 1
while IFS=$'\t' read -r status target key; do while IFS=$'\t' read -r status target key; do
[[ -n "$target" ]] || continue [[ -n "$target" ]] || continue
@@ -906,6 +1041,19 @@ state_snapshot_restore() {
cp -a "$saved" "$target" || return cp -a "$saved" "$target" || return
fi fi
done < "$STATE_SNAPSHOT_DIR/paths.tsv" done < "$STATE_SNAPSHOT_DIR/paths.tsv"
# Mutating a previously absent nested target can leave empty parents behind
# after the target itself is restored. Remove only parents proven absent in
# the pre-install snapshot; repeated passes handle arbitrary nesting without
# depending on GNU tac/sort behavior.
if [[ -s "$STATE_SNAPSHOT_DIR/parents.tsv" ]]; then
for _ in {1..16}; do
while IFS=$'\t' read -r status parent; do
[[ "$status" == absent ]] || continue
[[ "$parent" != "$HOME" && "$parent" == "$HOME"/* ]] || return 1
rmdir "$parent" 2>/dev/null || true
done < "$STATE_SNAPSHOT_DIR/parents.tsv"
done
fi
} }
state_begin_install() { state_begin_install() {
@@ -980,44 +1128,19 @@ state_mark_resumable_failure() {
echo " Remediation: fix each named phase, then run this installer with --check; journal: $STATE_JOURNAL" >&2 echo " Remediation: fix each named phase, then run this installer with --check; journal: $STATE_JOURNAL" >&2
} }
state_self_test() { state_maybe_inject_fault() {
local phase path local phase="$1"
state_begin_install [[ "${MOSAIC_INSTALL_FAULT_AFTER:-}" == "$phase" ]] || return 0
state_snapshot_create state_json_line fault "$phase" injected "phase=$phase after real phase action"
trap 'state_handle_unexpected_failure "$?" "$STATE_CURRENT_PHASE"' ERR INT TERM echo "Injected installer fault after real action: phase=$phase" >&2
for phase in P2 P3 P4 P5 P6 P7 P8; do state_handle_unexpected_failure 97 "$phase"
state_phase_begin "$phase"
case "$phase" in
P2) path="$MOSAIC_HOME/.selftest-artifact" ;;
P3) path="$PREFIX/bin/mosaic" ;;
P4) path="$MOSAIC_HOME/.selftest-framework" ;;
P5) path="$MOSAIC_HOME/SOUL.md" ;;
P6) path="$HOME/.claude/settings.json" ;;
P7) path="$MOSAIC_HOME/.selftest-service" ;;
P8) path="$HOME/.bashrc" ;;
esac
state_record_mutation "$phase" "$path" "restore representative path from $STATE_SNAPSHOT_DIR"
mkdir -p "$(dirname "$path")"
printf 'mutated-by-%s\n' "$phase" > "$path"
state_phase_finish "$phase" committed "representative mutation committed"
if [[ "${MOSAIC_INSTALL_FAULT_AFTER:-}" == "$phase" ]]; then
state_json_line fault "$phase" injected "phase=$phase"
echo "Injected installer fault: phase=$phase" >&2
state_snapshot_restore
state_json_line install "$phase" rolled-back "fault injection restored pre-install snapshot"
state_write_active "$(printf '{\"status\":\"rolled-back\",\"phase\":\"%s\",\"journal\":\"%s\"}' "$phase" "$STATE_JOURNAL")"
exit 97
fi
done
fail "self-test requires MOSAIC_INSTALL_FAULT_AFTER=P2..P8"
exit 2
} }
resolve_source_commit() { resolve_source_commit() {
local encoded_ref body headers content_type local encoded_ref body headers content_type
encoded_ref="$(node -p 'encodeURIComponent(process.argv[1])' "$GIT_REF")" encoded_ref="$(node -p 'encodeURIComponent(process.argv[1])' "$GIT_REF")"
body="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref.XXXXXX.json")" || return body="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref-body.XXXXXX")" || return
headers="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref.XXXXXX.headers")" || { rm -f "$body"; return 1; } headers="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref-headers.XXXXXX")" || { rm -f "$body"; return 1; }
if ! curl -fsSL -D "$headers" -o "$body" \ if ! curl -fsSL -D "$headers" -o "$body" \
"https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/commits?sha=${encoded_ref}&limit=1"; then "https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/commits?sha=${encoded_ref}&limit=1"; then
rm -f "$body" "$headers" rm -f "$body" "$headers"
@@ -1252,9 +1375,10 @@ install_next_cli_from_registry() {
# ─── preflight / state-machine dispatch ────────────────────────────────────── # ─── preflight / state-machine dispatch ──────────────────────────────────────
if [[ "$FLAG_STATE_SELF_TEST" == "true" ]]; then if [[ "$FLAG_STATE_SELF_TEST" == "true" ]]; then
require_cmd node if [[ "${MOSAIC_INSTALL_SELF_TEST_ALLOW:-0}" != 1 || ! "${MOSAIC_INSTALL_FAULT_AFTER:-}" =~ ^P[2-8]$ ]]; then
require_cmd flock fail "state-machine self-test requires MOSAIC_INSTALL_SELF_TEST_ALLOW=1 and MOSAIC_INSTALL_FAULT_AFTER=P2..P8"
state_self_test exit 2
fi
fi fi
# `--check` exits before mkdir, npm-prefix setup, locks, snapshots, downloads, or # `--check` exits before mkdir, npm-prefix setup, locks, snapshots, downloads, or
@@ -1306,6 +1430,9 @@ state_phase_finish P0 committed "$P0_REASON"
state_phase_finish P1 committed "$P1_REASON; exclusive lock acquired; journal opened" state_phase_finish P1 committed "$P1_REASON; exclusive lock acquired; journal opened"
state_snapshot_create state_snapshot_create
trap 'state_handle_unexpected_failure "$?" "$STATE_CURRENT_PHASE"' ERR INT TERM trap 'state_handle_unexpected_failure "$?" "$STATE_CURRENT_PHASE"' ERR INT TERM
if [[ "${MOSAIC_INSTALL_REDACTION_PROBE:-0}" == 1 ]]; then
state_run_captured "credential redaction acceptance probe" state_redaction_probe
fi
state_phase_begin P2 state_phase_begin P2
state_record_mutation P2 "$STATE_RUN_DIR/work" "discard acquired temporary artifacts" state_record_mutation P2 "$STATE_RUN_DIR/work" "discard acquired temporary artifacts"
@@ -1322,6 +1449,7 @@ if [[ "$FLAG_FRAMEWORK" == "true" || "$FLAG_DEV" == "true" ]]; then
state_run_captured "P2 acquire pinned source archive" ensure_monorepo state_run_captured "P2 acquire pinned source archive" ensure_monorepo
fi fi
state_phase_finish P2 committed "lane=$([[ "$FLAG_NEXT" == true ]] && echo next || echo latest) cli_version=${RESOLVED_CLI_VERSION:-pending-source-package-build} source_commit=${RESOLVED_SOURCE_COMMIT:-deferred-until-source-fallback} sha256=${RESOLVED_SOURCE_DIGEST:-deferred-until-source-fallback}" state_phase_finish P2 committed "lane=$([[ "$FLAG_NEXT" == true ]] && echo next || echo latest) cli_version=${RESOLVED_CLI_VERSION:-pending-source-package-build} source_commit=${RESOLVED_SOURCE_COMMIT:-deferred-until-source-fallback} sha256=${RESOLVED_SOURCE_DIGEST:-deferred-until-source-fallback}"
state_maybe_inject_fault P2
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
# PART 1: Framework (bash launcher + guides + runtime configs + tools) # PART 1: Framework (bash launcher + guides + runtime configs + tools)
@@ -1366,6 +1494,7 @@ if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
# Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.) # Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.)
info "Installing framework to ${MOSAIC_HOME}" info "Installing framework to ${MOSAIC_HOME}"
MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \ MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \
MOSAIC_CLI_PATH="$PREFIX/bin/mosaic" \
MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \ MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \
MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \ MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \
bash "$FRAMEWORK_SRC/install.sh" || return bash "$FRAMEWORK_SRC/install.sh" || return
@@ -1539,6 +1668,7 @@ else
fail "P3 Install CLI failed: $STATE_REASON" fail "P3 Install CLI failed: $STATE_REASON"
false false
fi fi
state_maybe_inject_fault P3
state_phase_begin P4 state_phase_begin P4
if [[ "$FLAG_FRAMEWORK" == "true" ]]; then if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
@@ -1550,6 +1680,7 @@ if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
state_record_mutation P4 "$HOME/.local/state/mosaic/backups" "restore framework backup state from $STATE_SNAPSHOT_DIR" state_record_mutation P4 "$HOME/.local/state/mosaic/backups" "restore framework backup state from $STATE_SNAPSHOT_DIR"
state_record_mutation P6 "$HOME/.claude/settings.json" "restore activation settings from $STATE_SNAPSHOT_DIR" state_record_mutation P6 "$HOME/.claude/settings.json" "restore activation settings from $STATE_SNAPSHOT_DIR"
fi fi
export MOSAIC_CLI_PATH="$PREFIX/bin/mosaic"
state_run_captured "P4 install framework and skills; P6 evaluate activation" install_phase_p4_action state_run_captured "P4 install framework and skills; P6 evaluate activation" install_phase_p4_action
if [[ "$FLAG_FRAMEWORK" == "false" ]]; then if [[ "$FLAG_FRAMEWORK" == "false" ]]; then
state_phase_finish P4 not-requested "framework component excluded by --cli" state_phase_finish P4 not-requested "framework component excluded by --cli"
@@ -1560,6 +1691,7 @@ else
# absent. Keep the partial state for P5-P8 diagnostics; P9 fails non-zero. # absent. Keep the partial state for P5-P8 diagnostics; P9 fails non-zero.
state_phase_finish P4 failed-resumable "$STATE_REASON" state_phase_finish P4 failed-resumable "$STATE_REASON"
fi fi
state_maybe_inject_fault P4
# P5/P7 actions (wizard/service requests) live in the summary flow below and # P5/P7 actions (wizard/service requests) live in the summary flow below and
# bind their mutation records immediately before the wizard executes. P8 is # bind their mutation records immediately before the wizard executes. P8 is
@@ -1579,7 +1711,7 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
# First install guidance / auto-launch # First install guidance / auto-launch
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
echo "" echo ""
if [[ "$FLAG_NO_AUTO_LAUNCH" == "false" ]] && [[ -t 0 ]] && [[ -t 1 ]]; then if [[ "$FLAG_NO_AUTO_LAUNCH" == "false" ]] && { { [[ -t 0 ]] && [[ -t 1 ]]; } || [[ "$FLAG_STATE_SELF_TEST" == true ]]; }; then
# Interactive TTY and auto-launch not suppressed: run the unified wizard. # Interactive TTY and auto-launch not suppressed: run the unified wizard.
# `mosaic wizard` now runs the full first-run flow end-to-end: identity # `mosaic wizard` now runs the full first-run flow end-to-end: identity
# setup → runtimes → hooks preview → skills → finalize → gateway # setup → runtimes → hooks preview → skills → finalize → gateway
@@ -1589,15 +1721,11 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
MOSAIC_BIN="$PREFIX/bin/mosaic" MOSAIC_BIN="$PREFIX/bin/mosaic"
if ! command -v "$MOSAIC_BIN" &>/dev/null && ! command -v mosaic &>/dev/null; then if [[ ! -x "$MOSAIC_BIN" ]]; then
warn "mosaic binary not found on PATH — skipping auto-launch." warn "P3 absolute mosaic binary is unavailable — skipping auto-launch."
warn "Add $PREFIX/bin to PATH and run: mosaic wizard" warn "Repair $MOSAIC_BIN and run it with: $MOSAIC_BIN wizard"
else else
# Prefer the absolute path from the prefix we just installed to
MOSAIC_CMD="mosaic"
if [[ -x "$MOSAIC_BIN" ]]; then
MOSAIC_CMD="$MOSAIC_BIN" MOSAIC_CMD="$MOSAIC_BIN"
fi
state_record_mutation P5 "$MOSAIC_HOME/SOUL.md" "restore identity from $STATE_SNAPSHOT_DIR" state_record_mutation P5 "$MOSAIC_HOME/SOUL.md" "restore identity from $STATE_SNAPSHOT_DIR"
state_record_mutation P5 "$MOSAIC_HOME/USER.md" "restore identity from $STATE_SNAPSHOT_DIR" state_record_mutation P5 "$MOSAIC_HOME/USER.md" "restore identity from $STATE_SNAPSHOT_DIR"
@@ -1741,6 +1869,7 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
else else
state_phase_finish "$phase" failed-resumable "$STATE_REASON" state_phase_finish "$phase" failed-resumable "$STATE_REASON"
fi fi
state_maybe_inject_fault "$phase"
done done
echo "" echo ""
+1
View File
@@ -0,0 +1 @@
4cd391b0974d3cce6c2a98455420d45bc2a04cb624e3c4bf43a813b8e28693e6 install.sh
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Fetch, authenticate, and execute the exact downloaded installer body.
set -euo pipefail
url="${1:?usage: verified-installer-fetch.sh <url> <sha256> [-- installer-args...]}"
expected="${2:?usage: verified-installer-fetch.sh <url> <sha256> [-- installer-args...]}"
shift 2
[[ "${1:-}" != -- ]] || shift
[[ "$expected" =~ ^[0-9a-f]{64}$ ]] || { echo 'installer expected SHA-256 must be 64 lowercase hex characters' >&2; exit 2; }
tmp="$(mktemp "${TMPDIR:-/tmp}/mosaic-installer-body.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
chmod 0600 "$tmp"
curl -fsSL "$url" -o "$tmp"
[[ -s "$tmp" ]] || { echo 'installer fetch returned an empty HTTP-success body' >&2; exit 1; }
actual="$(sha256sum "$tmp" | awk '{print $1}')"
[[ "$actual" == "$expected" ]] || { echo "installer SHA-256 mismatch (got=$actual expected=$expected)" >&2; exit 1; }
status=0
bash "$tmp" "$@" || status=$?
rm -f "$tmp"
trap - EXIT
exit "$status"
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-fetch-contract.XXXXXX")"
trap 'rm -rf "$TMP"' EXIT
FAKE_BIN="$TMP/bin"; mkdir -p "$FAKE_BIN"
cat > "$FAKE_BIN/curl" <<'CURL'
#!/usr/bin/env bash
set -euo pipefail
url=""; output=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o) output="$2"; shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
emit() { if [[ -n "$output" ]]; then cat > "$output"; else cat; fi; }
case "$url" in
fixture://ok)
emit <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
printf 'executed:%s\n' "${1:-missing}"
SCRIPT
;;
fixture://empty) : > "$output" ;;
fixture://failed) exit 22 ;;
*) exit 2 ;;
esac
CURL
chmod 0755 "$FAKE_BIN/curl"
cat > "$TMP/ok.sh" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
printf 'executed:%s\n' "${1:-missing}"
SCRIPT
ok_sha="$(sha256sum "$TMP/ok.sh" | awk '{print $1}')"
empty_sha="$(printf '' | sha256sum | awk '{print $1}')"
mkdir -p "$TMP/downloads"
output="$(TMPDIR="$TMP/downloads" PATH="$FAKE_BIN:$PATH" bash "$ROOT/tools/verified-installer-fetch.sh" fixture://ok "$ok_sha" -- marker)"
[[ "$output" == 'executed:marker' ]]
[[ -z "$(find "$TMP/downloads" -mindepth 1 -print -quit)" ]]
printf '[test] PASS: digest-pinned fetched artifact executes and its temporary body is removed\n'
for row in 'fixture://empty empty-body' 'fixture://failed failed-fetch'; do
url="${row%% *}"; name="${row#* }"
set +e
PATH="$FAKE_BIN:$PATH" bash "$ROOT/tools/verified-installer-fetch.sh" "$url" "$empty_sha" -- marker \
>"$TMP/$name.log" 2>&1
status=$?
set -e
[[ "$status" -ne 0 ]] || { echo "[test] FAIL: $name certified success" >&2; exit 1; }
done
printf '[test] PASS: failed fetch and HTTP-200 empty body are both rejected\n'
set +e
PATH="$FAKE_BIN:$PATH" bash "$ROOT/tools/verified-installer-fetch.sh" fixture://ok "${ok_sha/0/1}" -- marker \
>"$TMP/mismatch.log" 2>&1
status=$?
set -e
[[ "$status" -ne 0 ]] || { echo '[test] FAIL: digest mismatch was accepted' >&2; exit 1; }
printf '[test] PASS: fetched installer digest mismatch is blocking\n'
+62
View File
@@ -13,6 +13,68 @@ FIXTURE_EXIT="${3:?usage: verify-greenfield-expected-red.sh <case> <log> <fixtur
[[ -r "$LOG" ]] || { echo "fixture log is unreadable: $LOG" >&2; exit 2; } [[ -r "$LOG" ]] || { echo "fixture log is unreadable: $LOG" >&2; exit 2; }
[[ "$FIXTURE_EXIT" =~ ^[0-9]+$ ]] || { echo "fixture exit is not numeric: $FIXTURE_EXIT" >&2; exit 2; } [[ "$FIXTURE_EXIT" =~ ^[0-9]+$ ]] || { echo "fixture exit is not numeric: $FIXTURE_EXIT" >&2; exit 2; }
# Validate the entire pinned contract before selecting one case. Otherwise a
# deleted case/phase silently disappears from the gate and a one-row manifest
# can certify any exit-1 transcript.
expected_cases=(next-git-present main-git-present next-git-absent)
declare -A allowed_case=(
[next-git-present]=1 [main-git-present]=1 [next-git-absent]=1
)
declare -A expected_requires=(
[next-git-present]=6 [main-git-present]=6 [next-git-absent]=4
)
declare -A row_count=() exit_count=() require_count=() forbid_count=() phase_count=() unique_rows=()
while IFS= read -r raw; do
[[ -n "$raw" && "${raw:0:1}" != "#" ]] || continue
field_count="$(awk -F '\t' '{print NF}' <<<"$raw")"
[[ "$field_count" -eq 3 ]] || { echo "invalid expected-RED manifest row (expected exactly 3 tab fields): $raw" >&2; exit 2; }
IFS=$'\t' read -r case_name kind expectation <<<"$raw"
[[ -n "${allowed_case[$case_name]:-}" ]] || { echo "invalid expected-RED manifest case: $case_name" >&2; exit 2; }
unique_key="$case_name|$kind|$expectation"
[[ -z "${unique_rows[$unique_key]:-}" ]] || { echo "duplicate expected-RED manifest row: $raw" >&2; exit 2; }
unique_rows[$unique_key]=1
row_count[$case_name]=$((${row_count[$case_name]:-0} + 1))
case "$kind" in
exit)
[[ "$expectation" == 1 ]] || { echo "invalid expected-RED exit contract: case=$case_name expected=$expectation" >&2; exit 2; }
exit_count[$case_name]=$((${exit_count[$case_name]:-0} + 1))
;;
phase)
[[ "$expectation" =~ ^(P[0-9])=(PASS|FAIL)$ ]] \
|| { echo "invalid expected-RED phase disposition: case=$case_name value=$expectation" >&2; exit 2; }
phase="${BASH_REMATCH[1]}"
phase_key="$case_name|$phase"
phase_count[$phase_key]=$((${phase_count[$phase_key]:-0} + 1))
;;
require)
[[ -n "$expectation" ]] || { echo "empty expected-RED require row: case=$case_name" >&2; exit 2; }
require_count[$case_name]=$((${require_count[$case_name]:-0} + 1))
;;
forbid)
[[ -n "$expectation" ]] || { echo "empty expected-RED forbid row: case=$case_name" >&2; exit 2; }
forbid_count[$case_name]=$((${forbid_count[$case_name]:-0} + 1))
;;
*) echo "invalid expected-RED manifest kind: case=$case_name kind=$kind" >&2; exit 2 ;;
esac
done < "$MANIFEST"
for case_name in "${expected_cases[@]}"; do
[[ "${exit_count[$case_name]:-0}" -eq 1 ]] \
|| { echo "expected-RED manifest requires exactly one exit row for case=$case_name" >&2; exit 2; }
for phase in P0 P1 P2 P3 P4 P5 P6 P7 P8 P9; do
[[ "${phase_count[$case_name|$phase]:-0}" -eq 1 ]] \
|| { echo "expected-RED manifest requires exactly one $phase disposition for case=$case_name" >&2; exit 2; }
done
[[ "${require_count[$case_name]:-0}" -eq "${expected_requires[$case_name]}" ]] \
|| { echo "expected-RED manifest require-row population changed for case=$case_name" >&2; exit 2; }
[[ "${forbid_count[$case_name]:-0}" -eq 1 ]] \
|| { echo "expected-RED manifest requires exactly one forbid row for case=$case_name" >&2; exit 2; }
expected_total=$((1 + 10 + expected_requires[$case_name] + 1))
[[ "${row_count[$case_name]:-0}" -eq "$expected_total" ]] \
|| { echo "expected-RED manifest row population changed for case=$case_name" >&2; exit 2; }
done
[[ -n "${allowed_case[$CASE]:-}" ]] || { echo "unknown expected-RED verification case: $CASE" >&2; exit 2; }
checks=0 checks=0
failures=0 failures=0
while IFS=$'\t' read -r case_name kind expectation; do while IFS=$'\t' read -r case_name kind expectation; do
+42 -1
View File
@@ -7,13 +7,14 @@ trap 'rm -rf "$TMP"' EXIT
cat > "$TMP/match.log" <<'LOG' cat > "$TMP/match.log" <<'LOG'
[fixture] resolved lane=next package=@mosaicstack/mosaic@next version=0.0.50-next.999 [fixture] resolved lane=next package=@mosaicstack/mosaic@next version=0.0.50-next.999
[fixture] installer_exit=1 done_claims=0 [fixture] installer_exit=1 done_claims=0
[SECRET-CONTROL] PASS: seeded canary absent from complete scan population
[P0] PASS: supported context [P0] PASS: supported context
[P1] PASS: preflight complete [P1] PASS: preflight complete
[P2] PASS: pinned artifact [P2] PASS: pinned artifact
[P3] PASS: absolute_path=/home/test/.npm-global/bin/mosaic version=0.0.50-next.999 equals resolved lane version [P3] PASS: absolute_path=/home/test/.npm-global/bin/mosaic version=0.0.50-next.999 equals resolved lane version
[P4] FAIL: NOT-MEASURED / UNDECLARED: declaration absent [P4] FAIL: NOT-MEASURED / UNDECLARED: declaration absent
[P5] FAIL: identity absent [P5] FAIL: identity absent
[P6] FAIL: activation unavailable [P6] FAIL: broker absent but dead enforcement hooks are active
[P7] PASS: no services requested [P7] PASS: no services requested
[P8] FAIL: shell path absent [P8] FAIL: shell path absent
[P9] FAIL: aggregate refusal [P9] FAIL: aggregate refusal
@@ -34,3 +35,43 @@ if bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/m
exit 1 exit 1
fi fi
printf '[test] PASS: unexpected fixture exit remains blocking\n' printf '[test] PASS: unexpected fixture exit remains blocking\n'
printf 'next-git-present\texit\t1\n' > "$TMP/shrunk.tsv"
if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/shrunk.tsv" \
bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 1 >/dev/null 2>&1; then
echo '[test] FAIL: one-row manifest shrink still certified the detector' >&2
exit 1
fi
printf '[test] PASS: manifest shrink cannot delete the structural contract\n'
manifest="$ROOT/tools/fixtures/greenfield-expected-red.tsv"
grep -v $'^next-git-present\tphase\tP8=' "$manifest" > "$TMP/missing-phase.tsv"
if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/missing-phase.tsv" \
bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 1 >/dev/null 2>&1; then
echo '[test] FAIL: missing P8 disposition was accepted' >&2; exit 1
fi
printf '[test] PASS: every case requires one P0-P9 disposition\n'
cp "$manifest" "$TMP/duplicate.tsv"
printf 'next-git-present\tphase\tP3=PASS\n' >> "$TMP/duplicate.tsv"
if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/duplicate.tsv" \
bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 1 >/dev/null 2>&1; then
echo '[test] FAIL: duplicate phase key was accepted' >&2; exit 1
fi
printf '[test] PASS: duplicate structural keys are rejected\n'
cp "$manifest" "$TMP/unknown-case.tsv"
printf 'invented-case\texit\t1\n' >> "$TMP/unknown-case.tsv"
if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/unknown-case.tsv" \
bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 1 >/dev/null 2>&1; then
echo '[test] FAIL: unknown case was accepted' >&2; exit 1
fi
printf '[test] PASS: unknown case rows are rejected\n'
cp "$manifest" "$TMP/unknown-kind.tsv"
printf 'next-git-present\toptional\tanything\n' >> "$TMP/unknown-kind.tsv"
if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/unknown-kind.tsv" \
bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 1 >/dev/null 2>&1; then
echo '[test] FAIL: unknown row kind was accepted' >&2; exit 1
fi
printf '[test] PASS: unknown manifest kinds are rejected\n'