fix(installer): harden greenfield detector contracts
This commit is contained in:
@@ -34,6 +34,25 @@ steps:
|
||||
bash tools/verify-greenfield-expected-red.sh \
|
||||
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:
|
||||
image: node:22-bookworm-slim
|
||||
commands:
|
||||
|
||||
@@ -7,20 +7,21 @@ Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi —
|
||||
## Quick Install
|
||||
|
||||
```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)
|
||||
```
|
||||
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
|
||||
mismatch is fatal. Because both files come from the same repository and trust
|
||||
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:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL …) --yes # Accept all defaults
|
||||
bash <(curl -fsSL …) --yes --no-auto-launch # Install only, skip wizard
|
||||
(cd "$d" && bash install.sh --yes) # Accept all defaults
|
||||
(cd "$d" && bash install.sh --yes --no-auto-launch) # Install only, skip wizard
|
||||
```
|
||||
|
||||
This installs both components:
|
||||
@@ -348,16 +349,10 @@ Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding),
|
||||
|
||||
## Upgrading
|
||||
|
||||
Run the installer again — it handles upgrades automatically:
|
||||
Run the same verified installer flow again — it handles upgrades automatically:
|
||||
|
||||
```bash
|
||||
bash -o pipefail -c 'curl -fsSL https://mosaicstack.dev/install.sh | bash'
|
||||
```
|
||||
|
||||
Or use the direct URL:
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||||
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 CLI:
|
||||
|
||||
+8
-2
@@ -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.
|
||||
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 P2–P8 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 P0–P9 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
|
||||
|
||||
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.
|
||||
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 P2–P8 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
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ The unified installer uses a transactional P0–P9 model. It may report success
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -56,7 +56,7 @@ Before each mutation scope is touched, `journal.ndjson` records:
|
||||
- the reversal action;
|
||||
- the captured command-output location and command status.
|
||||
|
||||
Journal, action-status, manifest, or command-log write/sync failure is fatal. An unrecorded mutation is not allowed. Successful P9 runs append a seal event, write the SHA-256 sidecar, and make the journal and sidecar read-only. Required P4/P6 action failures are persisted in the manifest so a later `--check` cannot turn a failed action into a false pass.
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
The C1 fixture intentionally returns an attributable RED while C2–C5 remain open. CI itself remains green only when the fixture's final P0–P9 verdicts, required discriminator rows, and non-zero exit match the versioned contract in `tools/fixtures/greenfield-expected-red.tsv`. Any later remediation that changes an observed verdict makes CI red until the owning lane deliberately updates that manifest:
|
||||
The C1 fixture intentionally returns an attributable RED while C2–C5 remain open. CI itself remains green only when the fixture's final P0–P9 verdicts, required discriminator rows, 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 P0–P9 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` 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
|
||||
```
|
||||
|
||||
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 P2–P8 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
|
||||
|
||||
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] State-machine implementation complete: private pre-mutation journal/snapshot, P0–P8 `--check`, P2–P8 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.
|
||||
- [ ] 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 P2–P8 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
|
||||
|
||||
@@ -59,10 +59,25 @@ Implement C1 from the canonical greenfield-install PRD v2: a transactional P0–
|
||||
- #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.
|
||||
|
||||
## 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 P0–P9 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 P2–P8 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
|
||||
|
||||
- `bash -n` and ShellCheck pass for all changed shell surfaces; `git diff --check` passes.
|
||||
- `bash tools/install-state-machine.test.sh` passes, including exact P0–P8 rows, good/bad discrimination, persisted P4/P6 action failures, P2–P8 rollback, unsafe/overlapping/symlink roots, stale `active.json`, 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-state-machine.test.sh` passes, including exact P0–P8 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 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 P2–P8 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.
|
||||
- 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
@@ -11,7 +11,7 @@
|
||||
"typecheck": "pnpm preflight && turbo run typecheck",
|
||||
"test:checkout": "node --test scripts/*.test.mjs",
|
||||
"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:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"prepare": "node scripts/install-hooks.mjs"
|
||||
|
||||
@@ -710,9 +710,11 @@ trap 'restore_snapshot; exit 1' ERR INT TERM
|
||||
|
||||
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/credentials"
|
||||
chmod 0700 "$TARGET_DIR/credentials"
|
||||
|
||||
# Reconcile contract files from defaults/ into the framework root: framework-owned
|
||||
# files (CONSTITUTION/AGENTS/STANDARDS) are overwritten every upgrade (a divergent
|
||||
@@ -781,22 +783,74 @@ record_phase_outcome() {
|
||||
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() {
|
||||
local label="$1" output status=0
|
||||
local label="$1" redacted redactor_pid capture_fd status=0 redact_status=0
|
||||
shift
|
||||
output="$(mktemp "${TMPDIR:-/tmp}/mosaic-post-install.XXXXXX.log")"
|
||||
if "$@" >"$output" 2>&1; then status=0; else status=$?; fi
|
||||
redacted="$(mktemp "${TMPDIR:-/tmp}/mosaic-post-redacted.XXXXXX")"
|
||||
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 ! { 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
|
||||
cat "$output" >&2
|
||||
rm -f "$output"
|
||||
cat "$redacted" >&2
|
||||
rm -f "$redacted"
|
||||
fail "Could not durably append '$label' diagnostics to the install command log."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [[ "$status" -ne 0 ]]; then cat "$output" >&2; fi
|
||||
rm -f "$output"
|
||||
if [[ "$status" -ne 0 ]]; then cat "$redacted" >&2; fi
|
||||
rm -f "$redacted"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,15 @@ copy_claude_settings_guarded() {
|
||||
guard_args+=(--allow-inactive-enforcement)
|
||||
fi
|
||||
|
||||
if command -v mosaic >/dev/null 2>&1; then
|
||||
if mosaic "${guard_args[@]}"; then
|
||||
local mosaic_cli="${MOSAIC_CLI_PATH:-}"
|
||||
# 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
|
||||
fi
|
||||
echo "[mosaic-link] Enforcement hooks were NOT wired into $dst (see message above)." >&2
|
||||
@@ -77,7 +84,7 @@ copy_claude_settings_guarded() {
|
||||
return 0
|
||||
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] needs a published CLI carrying launch-runtime activation + a broker" >&2
|
||||
echo "[mosaic-link] supervisor; refusing to wire a dead gate (see #869)." >&2
|
||||
|
||||
@@ -14,6 +14,8 @@ SOURCE="${MOSAIC_INSTALL_SOURCE:-checkout}"
|
||||
IMAGE="${MOSAIC_INSTALL_IMAGE:-node:22-bookworm-slim}"
|
||||
GIT_MODE="${MOSAIC_INSTALL_GIT_MODE:-present}"
|
||||
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}"
|
||||
|
||||
usage() {
|
||||
@@ -39,6 +41,11 @@ done
|
||||
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 "$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 ! 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 ' ')"
|
||||
[[ -d "$ROOT/skills" ]] \
|
||||
&& 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_name="$(basename "$ROOT")"
|
||||
tar -C "$repo_parent" \
|
||||
@@ -75,7 +82,7 @@ if [[ "$SOURCE" == "checkout" ]]; then
|
||||
checkout_content_id="${checkout_digest:0:40}"
|
||||
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
|
||||
cat > "$inner" <<'INNER'
|
||||
#!/usr/bin/env bash
|
||||
@@ -104,7 +111,13 @@ case "$FIXTURE_SOURCE" in
|
||||
printf '%s' "$FIXTURE_INSTALLER_B64" | base64 -d > /tmp/install.sh
|
||||
;;
|
||||
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
|
||||
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)"
|
||||
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
|
||||
MOSAIC_NO_COLOR=1 MOSAIC_ASSUME_YES=1 \
|
||||
bash /tmp/install.sh "${lane_args[@]}" --yes --no-auto-launch >"$install_log" 2>&1
|
||||
MOSAIC_NO_COLOR=1 MOSAIC_ASSUME_YES=1 MOSAIC_INSTALL_SECRET_CANARY="$secret_canary" \
|
||||
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=$?
|
||||
set -e
|
||||
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' \
|
||||
"$install_status" "$(grep -cF 'Done.' "$install_log" || true)"
|
||||
|
||||
@@ -329,6 +368,8 @@ if [[ "$IN_CLEAN_CONTAINER" == "1" ]]; then
|
||||
FIXTURE_SOURCE="$SOURCE" \
|
||||
FIXTURE_GIT_MODE="$GIT_MODE" \
|
||||
FIXTURE_INSTALLER_B64="$installer_b64" \
|
||||
FIXTURE_INSTALLER_URL="$INSTALLER_URL" \
|
||||
FIXTURE_INSTALLER_SHA256="$INSTALLER_SHA256" \
|
||||
FIXTURE_CHECKOUT_SHA256="$checkout_digest" \
|
||||
FIXTURE_CHECKOUT_CONTENT_ID="$checkout_content_id" \
|
||||
FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \
|
||||
@@ -345,6 +386,8 @@ else
|
||||
--env FIXTURE_SOURCE="$SOURCE" \
|
||||
--env FIXTURE_GIT_MODE="$GIT_MODE" \
|
||||
--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_CONTENT_ID="$checkout_content_id" \
|
||||
--env FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \
|
||||
|
||||
@@ -13,10 +13,11 @@ next-git-present phase P8=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\] 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 ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:
|
||||
next-git-present require ^\[P6\] FAIL:
|
||||
next-git-present forbid Done\.
|
||||
next-git-present require ^\[P6\] FAIL: broker absent but dead enforcement hooks are active
|
||||
next-git-present forbid Done\.|MOSAIC_C1_CANARY_|CLI not found on PATH
|
||||
main-git-present exit 1
|
||||
main-git-present phase P0=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 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 ^\[SECRET-CONTROL\] PASS:
|
||||
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 ^\[P6\] FAIL:
|
||||
main-git-present forbid Done\.
|
||||
main-git-present require ^\[P6\] FAIL: runtime linking/activation action reported a required failure
|
||||
main-git-present forbid Done\.|MOSAIC_C1_CANARY_|CLI not found on PATH
|
||||
next-git-absent exit 1
|
||||
next-git-absent phase P0=PASS
|
||||
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 P9=FAIL
|
||||
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 ^\[P3\] FAIL: .*executable=no
|
||||
next-git-absent forbid Done\.
|
||||
next-git-absent forbid Done\.|MOSAIC_C1_CANARY_
|
||||
|
||||
|
@@ -4,6 +4,8 @@ set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-next-install-test-XXXXXX")"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
export TMPDIR="$TMP/runtime-tmp"
|
||||
mkdir -p "$TMPDIR"
|
||||
|
||||
FAKE_BIN="$TMP/bin"
|
||||
HOME_DIR="$TMP/home"
|
||||
@@ -13,6 +15,48 @@ STATE="$TMP/state"
|
||||
LOG="$TMP/npm.log"
|
||||
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'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@@ -31,6 +75,15 @@ install_cli() {
|
||||
mkdir -p "${MOSAIC_PREFIX:?}/bin"
|
||||
cat > "$MOSAIC_PREFIX/bin/mosaic" <<CLI
|
||||
#!/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'
|
||||
CLI
|
||||
chmod +x "$MOSAIC_PREFIX/bin/mosaic"
|
||||
@@ -51,6 +104,12 @@ if [[ "$1" == "view" ]]; then
|
||||
fi
|
||||
|
||||
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
|
||||
*"@mosaicstack/[email protected]"*)
|
||||
install_cli "0.0.49-next.999"
|
||||
@@ -141,7 +200,21 @@ if [[ -z "$dest" ]]; then
|
||||
echo "fake tar missing -C destination" >&2
|
||||
exit 1
|
||||
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
|
||||
chmod +x "$FAKE_BIN/tar"
|
||||
|
||||
@@ -187,15 +260,28 @@ reset_state() {
|
||||
rm -f "$STATE"/*
|
||||
}
|
||||
|
||||
prefix_fingerprint() {
|
||||
if [[ ! -d "$PREFIX" ]]; then printf 'ABSENT\n'; return; fi
|
||||
(
|
||||
cd "$PREFIX"
|
||||
find . -mindepth 1 -printf '%P|%y|%m|%l\n' | LC_ALL=C sort
|
||||
find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum
|
||||
) | sha256sum | awk '{print $1}'
|
||||
tree_fingerprint() {
|
||||
local root="$1"
|
||||
if [[ ! -d "$root" ]]; then printf 'ABSENT\n'; return; fi
|
||||
python3 - "$root" <<'PY'
|
||||
import hashlib, os, stat, sys
|
||||
root=os.path.abspath(sys.argv[1]); rows=[]
|
||||
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
|
||||
echo "[test] --next fast path pins resolved package versions"
|
||||
OUTPUT="$(
|
||||
@@ -356,4 +442,131 @@ set -e
|
||||
[[ "$CHECK_STATUS" -ne 0 ]]
|
||||
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"
|
||||
|
||||
@@ -133,16 +133,26 @@ make_fake_npm "$good_bin"
|
||||
cp "$COMPAT_BIN/realpath" "$good_bin/realpath"
|
||||
cat > "$good_bin/id" <<'ID'
|
||||
#!/bin/bash
|
||||
uid="${MOSAIC_TEST_UID:-1001}"
|
||||
gid="${MOSAIC_TEST_GID:-1001}"
|
||||
user="${MOSAIC_TEST_USER:-fixture-user}"
|
||||
case "${1:-}" in
|
||||
-u) echo 1001 ;;
|
||||
-g) echo 1001 ;;
|
||||
-un) echo fixture-user ;;
|
||||
-u) echo "$uid" ;;
|
||||
-g) echo "$gid" ;;
|
||||
-un) echo "$user" ;;
|
||||
*) exec /bin/id "$@" ;;
|
||||
esac
|
||||
ID
|
||||
cat > "$good_bin/stat" <<'STAT'
|
||||
#!/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 "$@"
|
||||
STAT
|
||||
cat > "$good_bin/curl" <<'CURL'
|
||||
@@ -161,7 +171,7 @@ CLI
|
||||
chmod 0755 "$good_prefix/bin/mosaic"
|
||||
cat > "$good_bin/getent" <<GETENT
|
||||
#!/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
|
||||
cat > "$good_bin/bash" <<SHELL
|
||||
#!/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' \
|
||||
|| { 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'
|
||||
for blocked_phase in P4 P6; do
|
||||
node -e '
|
||||
@@ -219,36 +324,12 @@ for blocked_phase in P4 P6; do
|
||||
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 '[test] case: per-phase P2-P8 fault injection restores representative host mutations\n'
|
||||
for phase in P2 P3 P4 P5 P6 P7 P8; do
|
||||
home="$TMP/fault-$phase/home"
|
||||
state="$TMP/fault-$phase/state"
|
||||
mkdir -p "$home/.config/mosaic" "$home/.npm-global/bin" "$home/.claude" "$state"
|
||||
printf 'operator-framework-sentinel\n' > "$home/.config/mosaic/operator.txt"
|
||||
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: fault injection has no synthetic mutation implementation\n'
|
||||
if grep -q '\.selftest-' "$ROOT/tools/install.sh"; then
|
||||
fail_case 'synthetic .selftest mutation path remains in the production fault seam'
|
||||
else
|
||||
pass_case 'fault seam is attached only to real P2-P8 action flow (exercised by install-next-lane.test.sh)'
|
||||
fi
|
||||
|
||||
printf '[test] case: unsafe and overlapping rollback roots fail before mutation\n'
|
||||
unsafe_home="$TMP/unsafe-home"
|
||||
@@ -262,7 +343,7 @@ for case_name in root-target home-target overlap-target; do
|
||||
before="$(fingerprint "$unsafe_home")"
|
||||
set +e
|
||||
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
|
||||
status=$?
|
||||
set -e
|
||||
@@ -279,7 +360,7 @@ mkdir -p "$symlink_home" "$symlink_outside"
|
||||
ln -s "$symlink_outside" "$symlink_home/.config"
|
||||
set +e
|
||||
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
|
||||
status=$?
|
||||
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'
|
||||
[[ -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'
|
||||
journal_home="$TMP/journal-failure/home"
|
||||
mkdir -p "$journal_home/.config/mosaic"
|
||||
@@ -316,8 +377,9 @@ printf 'journal-sentinel\n' > "$journal_home/.config/mosaic/operator.txt"
|
||||
before="$(fingerprint "$journal_home")"
|
||||
set +e
|
||||
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_NO_COLOR=1 bash "$ROOT/tools/install.sh" --state-machine-self-test \
|
||||
MOSAIC_TEST_PASSWD_HOME="$journal_home" MOSAIC_INSTALL_STATE_DIR="/proc/mosaic-journal-denied-$$" \
|
||||
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
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
+199
-70
@@ -510,27 +510,77 @@ state_action_failed() {
|
||||
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() {
|
||||
local label="$1" output status=0
|
||||
local label="$1" redacted redactor_pid capture_fd status=0 redact_status=0
|
||||
shift
|
||||
output="$(mktemp "${TMPDIR:-/tmp}/mosaic-phase-command.XXXXXX.log")" || return
|
||||
# The command is deliberately called in a conditional so its status can be
|
||||
# journaled before the caller's ERR trap rolls back. Bash disables errexit in
|
||||
# functions invoked this way, so every multi-command phase helper below must
|
||||
# explicitly return on each required command failure.
|
||||
if "$@" >"$output" 2>&1; then status=0; else status=$?; fi
|
||||
cat "$output" || { rm -f "$output"; return 1; }
|
||||
if ! { printf '\n=== %s (exit=%s) ===\n' "$label" "$status"; cat "$output"; } >> "$STATE_COMMAND_LOG"; then
|
||||
rm -f "$output"
|
||||
redacted="$(mktemp "${TMPDIR:-/tmp}/mosaic-phase-redacted.XXXXXX")" || return
|
||||
chmod 0600 "$redacted" || { rm -f "$redacted"; return 1; }
|
||||
# Process substitution preserves in-shell phase side effects while ensuring
|
||||
# plaintext diagnostics exist only in a pipe, never in a filesystem body.
|
||||
exec {capture_fd}> >(state_redact_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 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."
|
||||
return 1
|
||||
fi
|
||||
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."
|
||||
return 1
|
||||
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"
|
||||
return "$status"
|
||||
}
|
||||
@@ -569,6 +619,55 @@ state_target_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() {
|
||||
local cli gateway
|
||||
if [[ "$FLAG_DEV" == "true" ]]; then
|
||||
@@ -598,21 +697,37 @@ state_predicate() {
|
||||
local phase="$1" shell node_major installed expected
|
||||
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 passwd_row passwd_user passwd_uid passwd_home passwd_shell actual_user actual_uid
|
||||
STATE_REASON=""
|
||||
case "$phase" in
|
||||
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)"
|
||||
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 [[ -n "$HOME" && -n "$shell" && "$privilege_mode" == "user" && "$(uname -s)" == "Linux" ]] \
|
||||
if [[ "$actual_uid" == 0 && -n "${SUDO_USER:-}" ]]; then
|
||||
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 \
|
||||
&& [[ "$(uname -m)" == "x86_64" ]] && [[ "$node_major" -ge 20 ]] && [[ "$npm_major" -ge 9 ]] \
|
||||
&& 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
|
||||
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
|
||||
;;
|
||||
P1)
|
||||
@@ -684,11 +799,12 @@ state_predicate() {
|
||||
expected="$(state_expected_cli_version)"
|
||||
installed=""
|
||||
[[ -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
|
||||
STATE_REASON="absolute_path=$PREFIX/bin/mosaic version=$installed equals resolved lane version"
|
||||
if [[ -n "$expected" && -x "$PREFIX/bin/mosaic" && "$installed" == "$expected" ]] \
|
||||
&& 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
|
||||
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
|
||||
;;
|
||||
P4)
|
||||
@@ -699,6 +815,10 @@ state_predicate() {
|
||||
local expected_lane expected_version
|
||||
expected_lane="$([[ "$FLAG_NEXT" == true ]] && echo next || echo latest)"
|
||||
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
|
||||
STATE_REASON="NOT-MEASURED / UNDECLARED: installer published no checkout-free, lane/versioned shipped-set artifact at $declared_set"
|
||||
return 1
|
||||
@@ -738,12 +858,14 @@ NODE
|
||||
for skill in SOUL.md USER.md; do
|
||||
local path="$MOSAIC_HOME/$skill"
|
||||
if [[ ! -s "$path" ]] || ! grep -q '^# ' "$path" 2>/dev/null \
|
||||
|| [[ "$(stat -c '%u' "$path" 2>/dev/null || echo -1)" != "$(id -u)" ]] \
|
||||
|| [[ "$(stat -c '%a' "$path" 2>/dev/null || echo 777)" =~ [2367]$ ]]; then
|
||||
|| ! state_path_owner_mode_ok "$path" private; then
|
||||
missing+=("$skill")
|
||||
fi
|
||||
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[*]}"
|
||||
return 1
|
||||
;;
|
||||
@@ -863,7 +985,8 @@ state_validate_target_paths() {
|
||||
}
|
||||
|
||||
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
|
||||
fail "P1 Preflight refused snapshot creation: $STATE_PATH_REASON"
|
||||
return 1
|
||||
@@ -871,7 +994,9 @@ state_snapshot_create() {
|
||||
STATE_SNAPSHOT_DIR="$STATE_RUN_DIR/snapshot"
|
||||
mkdir -p "$STATE_SNAPSHOT_DIR/data"
|
||||
list="$STATE_SNAPSHOT_DIR/paths.tsv"
|
||||
parent_list="$STATE_SNAPSHOT_DIR/parents.tsv"
|
||||
: > "$list"
|
||||
: > "$parent_list"
|
||||
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/.pi" "$HOME/.codex" "$HOME/.config/opencode" "$HOME/.config/mosaic-gateway" \
|
||||
@@ -886,12 +1011,22 @@ state_snapshot_create() {
|
||||
else
|
||||
printf 'absent\t%s\t%s\n' "$path" "$key" >> "$list"
|
||||
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
|
||||
state_json_line snapshot P1 committed "pre-install snapshot=$STATE_SNAPSHOT_DIR"
|
||||
}
|
||||
|
||||
state_snapshot_restore() {
|
||||
local status target key saved
|
||||
local status target key saved parent
|
||||
[[ -s "$STATE_SNAPSHOT_DIR/paths.tsv" ]] || return 1
|
||||
while IFS=$'\t' read -r status target key; do
|
||||
[[ -n "$target" ]] || continue
|
||||
@@ -906,6 +1041,19 @@ state_snapshot_restore() {
|
||||
cp -a "$saved" "$target" || return
|
||||
fi
|
||||
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() {
|
||||
@@ -980,44 +1128,19 @@ state_mark_resumable_failure() {
|
||||
echo " Remediation: fix each named phase, then run this installer with --check; journal: $STATE_JOURNAL" >&2
|
||||
}
|
||||
|
||||
state_self_test() {
|
||||
local phase path
|
||||
state_begin_install
|
||||
state_snapshot_create
|
||||
trap 'state_handle_unexpected_failure "$?" "$STATE_CURRENT_PHASE"' ERR INT TERM
|
||||
for phase in P2 P3 P4 P5 P6 P7 P8; do
|
||||
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
|
||||
state_maybe_inject_fault() {
|
||||
local phase="$1"
|
||||
[[ "${MOSAIC_INSTALL_FAULT_AFTER:-}" == "$phase" ]] || return 0
|
||||
state_json_line fault "$phase" injected "phase=$phase after real phase action"
|
||||
echo "Injected installer fault after real action: phase=$phase" >&2
|
||||
state_handle_unexpected_failure 97 "$phase"
|
||||
}
|
||||
|
||||
resolve_source_commit() {
|
||||
local encoded_ref body headers content_type
|
||||
encoded_ref="$(node -p 'encodeURIComponent(process.argv[1])' "$GIT_REF")"
|
||||
body="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref.XXXXXX.json")" || return
|
||||
headers="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref.XXXXXX.headers")" || { rm -f "$body"; return 1; }
|
||||
body="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref-body.XXXXXX")" || return
|
||||
headers="$(mktemp "${TMPDIR:-/tmp}/mosaic-ref-headers.XXXXXX")" || { rm -f "$body"; return 1; }
|
||||
if ! curl -fsSL -D "$headers" -o "$body" \
|
||||
"https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/commits?sha=${encoded_ref}&limit=1"; then
|
||||
rm -f "$body" "$headers"
|
||||
@@ -1252,9 +1375,10 @@ install_next_cli_from_registry() {
|
||||
# ─── preflight / state-machine dispatch ──────────────────────────────────────
|
||||
|
||||
if [[ "$FLAG_STATE_SELF_TEST" == "true" ]]; then
|
||||
require_cmd node
|
||||
require_cmd flock
|
||||
state_self_test
|
||||
if [[ "${MOSAIC_INSTALL_SELF_TEST_ALLOW:-0}" != 1 || ! "${MOSAIC_INSTALL_FAULT_AFTER:-}" =~ ^P[2-8]$ ]]; then
|
||||
fail "state-machine self-test requires MOSAIC_INSTALL_SELF_TEST_ALLOW=1 and MOSAIC_INSTALL_FAULT_AFTER=P2..P8"
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
# `--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_snapshot_create
|
||||
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_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
|
||||
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_maybe_inject_fault P2
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 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.)
|
||||
info "Installing framework to ${MOSAIC_HOME}…"
|
||||
MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \
|
||||
MOSAIC_CLI_PATH="$PREFIX/bin/mosaic" \
|
||||
MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \
|
||||
MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \
|
||||
bash "$FRAMEWORK_SRC/install.sh" || return
|
||||
@@ -1539,6 +1668,7 @@ else
|
||||
fail "P3 Install CLI failed: $STATE_REASON"
|
||||
false
|
||||
fi
|
||||
state_maybe_inject_fault P3
|
||||
|
||||
state_phase_begin P4
|
||||
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 P6 "$HOME/.claude/settings.json" "restore activation settings from $STATE_SNAPSHOT_DIR"
|
||||
fi
|
||||
export MOSAIC_CLI_PATH="$PREFIX/bin/mosaic"
|
||||
state_run_captured "P4 install framework and skills; P6 evaluate activation" install_phase_p4_action
|
||||
if [[ "$FLAG_FRAMEWORK" == "false" ]]; then
|
||||
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.
|
||||
state_phase_finish P4 failed-resumable "$STATE_REASON"
|
||||
fi
|
||||
state_maybe_inject_fault P4
|
||||
|
||||
# P5/P7 actions (wizard/service requests) live in the summary flow below and
|
||||
# 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
|
||||
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
|
||||
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.
|
||||
# `mosaic wizard` now runs the full first-run flow end-to-end: identity
|
||||
# setup → runtimes → hooks preview → skills → finalize → gateway
|
||||
@@ -1589,15 +1721,11 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
|
||||
|
||||
MOSAIC_BIN="$PREFIX/bin/mosaic"
|
||||
|
||||
if ! command -v "$MOSAIC_BIN" &>/dev/null && ! command -v mosaic &>/dev/null; then
|
||||
warn "mosaic binary not found on PATH — skipping auto-launch."
|
||||
warn "Add $PREFIX/bin to PATH and run: mosaic wizard"
|
||||
if [[ ! -x "$MOSAIC_BIN" ]]; then
|
||||
warn "P3 absolute mosaic binary is unavailable — skipping auto-launch."
|
||||
warn "Repair $MOSAIC_BIN and run it with: $MOSAIC_BIN wizard"
|
||||
else
|
||||
# Prefer the absolute path from the prefix we just installed to
|
||||
MOSAIC_CMD="mosaic"
|
||||
if [[ -x "$MOSAIC_BIN" ]]; then
|
||||
MOSAIC_CMD="$MOSAIC_BIN"
|
||||
fi
|
||||
MOSAIC_CMD="$MOSAIC_BIN"
|
||||
|
||||
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"
|
||||
@@ -1741,6 +1869,7 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
|
||||
else
|
||||
state_phase_finish "$phase" failed-resumable "$STATE_REASON"
|
||||
fi
|
||||
state_maybe_inject_fault "$phase"
|
||||
done
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
4cd391b0974d3cce6c2a98455420d45bc2a04cb624e3c4bf43a813b8e28693e6 install.sh
|
||||
Executable
+20
@@ -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"
|
||||
Executable
+64
@@ -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'
|
||||
@@ -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; }
|
||||
[[ "$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
|
||||
failures=0
|
||||
while IFS=$'\t' read -r case_name kind expectation; do
|
||||
|
||||
@@ -7,13 +7,14 @@ trap 'rm -rf "$TMP"' EXIT
|
||||
cat > "$TMP/match.log" <<'LOG'
|
||||
[fixture] resolved lane=next package=@mosaicstack/mosaic@next version=0.0.50-next.999
|
||||
[fixture] installer_exit=1 done_claims=0
|
||||
[SECRET-CONTROL] PASS: seeded canary absent from complete scan population
|
||||
[P0] PASS: supported context
|
||||
[P1] PASS: preflight complete
|
||||
[P2] PASS: pinned artifact
|
||||
[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
|
||||
[P5] FAIL: identity absent
|
||||
[P6] FAIL: activation unavailable
|
||||
[P6] FAIL: broker absent but dead enforcement hooks are active
|
||||
[P7] PASS: no services requested
|
||||
[P8] FAIL: shell path absent
|
||||
[P9] FAIL: aggregate refusal
|
||||
@@ -34,3 +35,43 @@ if bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/m
|
||||
exit 1
|
||||
fi
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user