Compare commits

..
Author SHA1 Message Date
be-coder-08andMos 80a45b1e1c feat(pr-merge): preserve linked authors in squash messages (#1066)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: be-coder-08 <[email protected]>
2026-08-06 05:36:59 +00:00
50 changed files with 1452 additions and 5756 deletions
-139
View File
@@ -1,139 +0,0 @@
# C1 detector gate. The fixture itself is intentionally RED; CI is green only
# when its exact phase verdicts/reasons match the versioned expected-RED manifest.
when:
- event: [pull_request, manual]
- event: push
branch: [next, main]
steps:
greenfield-case-denominator-init:
image: node:22-bookworm-slim
commands:
- |
coverage_run="${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}"
coverage_root=.mosaic-test-work/greenfield-execution-coverage
bash tools/verify-greenfield-execution-coverage.sh init cases \
tools/fixtures/greenfield-expected-red.tsv "$coverage_root" \
"$coverage_run"
bash tools/verify-greenfield-execution-coverage.sh init arms \
tools/fixtures/greenfield-expected-arms.txt "$coverage_root" \
"$coverage_run"
greenfield-git-present:
image: node:22-bookworm-slim
depends_on:
- greenfield-case-denominator-init
commands:
- |
set +e
MOSAIC_GREENFIELD_CONTAINER=1 \
bash tools/e2e-install-test.sh --lane next --source checkout --git present \
> /tmp/greenfield-git-present.log 2>&1
fixture_status=$?
set -e
cat /tmp/greenfield-git-present.log
bash tools/verify-greenfield-expected-red.sh \
next-git-present /tmp/greenfield-git-present.log "$fixture_status"
coverage_run="${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}"
coverage_root=.mosaic-test-work/greenfield-execution-coverage
bash tools/verify-greenfield-execution-coverage.sh mark cases \
tools/fixtures/greenfield-expected-red.tsv "$coverage_root" \
"$coverage_run" next-git-present
bash tools/verify-greenfield-execution-coverage.sh mark arms \
tools/fixtures/greenfield-expected-arms.txt "$coverage_root" \
"$coverage_run" greenfield-git-present
greenfield-main-git-present:
image: node:22-bookworm-slim
depends_on:
- greenfield-case-denominator-init
commands:
- |
set +e
MOSAIC_GREENFIELD_CONTAINER=1 \
bash tools/e2e-install-test.sh --lane main --source checkout --git present \
> /tmp/greenfield-main-git-present.log 2>&1
fixture_status=$?
set -e
cat /tmp/greenfield-main-git-present.log
bash tools/verify-greenfield-expected-red.sh \
main-git-present /tmp/greenfield-main-git-present.log "$fixture_status"
coverage_run="${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}"
coverage_root=.mosaic-test-work/greenfield-execution-coverage
bash tools/verify-greenfield-execution-coverage.sh mark cases \
tools/fixtures/greenfield-expected-red.tsv "$coverage_root" \
"$coverage_run" main-git-present
bash tools/verify-greenfield-execution-coverage.sh mark arms \
tools/fixtures/greenfield-expected-arms.txt "$coverage_root" \
"$coverage_run" greenfield-main-git-present
greenfield-remote-installer-contract:
image: node:22-bookworm-slim
depends_on:
- greenfield-case-denominator-init
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" \
MOSAIC_FIXTURE_SOURCE_COMMIT="${CI_COMMIT_SHA}" \
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"
coverage_run="${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}"
coverage_root=.mosaic-test-work/greenfield-execution-coverage
bash tools/verify-greenfield-execution-coverage.sh mark arms \
tools/fixtures/greenfield-expected-arms.txt "$coverage_root" \
"$coverage_run" greenfield-remote-installer-contract
greenfield-git-absent:
image: node:22-bookworm-slim
depends_on:
- greenfield-case-denominator-init
commands:
- |
set +e
MOSAIC_GREENFIELD_CONTAINER=1 \
bash tools/e2e-install-test.sh --lane next --source checkout --git absent \
> /tmp/greenfield-git-absent.log 2>&1
fixture_status=$?
set -e
cat /tmp/greenfield-git-absent.log
bash tools/verify-greenfield-expected-red.sh \
next-git-absent /tmp/greenfield-git-absent.log "$fixture_status"
coverage_run="${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}"
coverage_root=.mosaic-test-work/greenfield-execution-coverage
bash tools/verify-greenfield-execution-coverage.sh mark cases \
tools/fixtures/greenfield-expected-red.tsv "$coverage_root" \
"$coverage_run" next-git-absent
bash tools/verify-greenfield-execution-coverage.sh mark arms \
tools/fixtures/greenfield-expected-arms.txt "$coverage_root" \
"$coverage_run" greenfield-git-absent
greenfield-case-denominator:
image: node:22-bookworm-slim
# Publish exact execution coverage after the full matrix, even if a case failed.
depends_on:
- greenfield-git-present
- greenfield-main-git-present
- greenfield-remote-installer-contract
- greenfield-git-absent
when:
- status: [success, failure]
commands:
- |
coverage_run="${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}"
coverage_root=.mosaic-test-work/greenfield-execution-coverage
bash tools/verify-greenfield-execution-coverage-gate.sh \
tools/verify-greenfield-execution-coverage.sh \
tools/fixtures/greenfield-expected-red.tsv \
tools/fixtures/greenfield-expected-arms.txt \
"$coverage_root" "$coverage_run"
+21 -32
View File
@@ -7,21 +7,20 @@ Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi —
## Quick Install
```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)
curl -fsSL https://mosaicstack.dev/install.sh | bash
```
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.
Or use the direct URL:
```bash
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
```
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
```bash
(cd "$d" && bash install.sh --yes) # Accept all defaults
(cd "$d" && bash install.sh --yes --no-auto-launch) # Install only, skip wizard
bash <(curl -fsSL …) --yes # Accept all defaults
bash <(curl -fsSL …) --yes --no-auto-launch # Install only, skip wizard
```
This installs both components:
@@ -31,16 +30,6 @@ This installs both components:
| **Framework** | Bash launcher, guides, runtime configs, tools, skills | `~/.config/mosaic/` |
| **@mosaicstack/mosaic** | Unified `mosaic` CLI — TUI, gateway client, wizard, auto-updater | `~/.npm-global/bin/` |
### Install lanes
| Lane | Command | Use when | Source |
| ------------------------ | ------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Stable | `bash tools/install.sh` | You want the released Mosaic CLI/framework | npm registry `@mosaicstack/mosaic@latest` + framework archive at `main` |
| Prerelease integration | `bash tools/install.sh --next` | You want the current `next` integration branch | Exact `@next` CLI/gateway versions + pinned `next` framework commit; pinned-source fallback |
| Contributor/source build | `bash tools/install.sh --dev --ref X` | You are testing a branch before release; `--ref` wins | Build-from-source at the requested ref |
`--next` selects the prerelease integration lane. It installs the exact CLI/gateway versions resolved from the aligned `@next` tags, and pins the framework archive to the resolved `next` commit. If the registry path fails, it builds from that pinned source. An explicit `--ref` or `MOSAIC_REF` wins and selects source mode.
After install, the wizard runs automatically or you can invoke it manually:
```bash
@@ -49,14 +38,10 @@ mosaic wizard # Full guided setup (gateway install → verify)
### Requirements
- Linux x86_64 with glibc (Debian is the greenfield CI platform; musl/Alpine, macOS, and ARM64 currently fail as unsupported)
- Node.js ≥ 20 and npm ≥ 9
- `bash`, `curl`, `git`, `python3`, `tar`, and standard core utilities (`awk`, `df`, `find`, `flock`, `grep`, `install`, `realpath`, `sed`, `sha256sum`, `stat`, `sync`)
- At least 256 MiB free disk and 1,000 free inodes at the npm prefix
- Node.js ≥ 20
- npm (for global @mosaicstack/mosaic install)
- One or more runtimes: [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai), or [Pi](https://github.com/mariozechner/pi-coding-agent)
The installer evaluates canonical phases P0P9 and does not print `Done.` unless every committed postcondition passes. A failed phase exits non-zero, names the phase, and points to its durable journal under `${XDG_STATE_HOME:-~/.local/state}/mosaic/install/`. See [Installer state machine and recovery](docs/guides/installer-state-machine.md).
## Usage
### Launching Agent Sessions
@@ -349,10 +334,16 @@ Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding),
## Upgrading
Run the same verified installer flow again — it handles upgrades automatically:
Run the installer again — it handles upgrades automatically:
```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)
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)
```
Or use the CLI:
@@ -367,17 +358,15 @@ The CLI also performs a background update check on every invocation (cached for
### Installer Flags
```bash
bash tools/install.sh --check # Side-effect-free P0-P8 postcondition check
bash tools/install.sh --check # Version check only
bash tools/install.sh --framework # Framework only (skip npm CLI)
bash tools/install.sh --cli # npm CLI only (skip framework)
bash tools/install.sh --next # Prerelease lane: exact @next versions + pinned-source fallback
bash tools/install.sh --dev # Contributor lane: source build at --ref/main
bash tools/install.sh --ref v1.0 # Install from a specific git ref (--ref wins over --next)
bash tools/install.sh --ref v1.0 # Install from a specific git ref
bash tools/install.sh --yes # Non-interactive, accept all defaults
bash tools/install.sh --no-auto-launch # Skip auto-launch of wizard
```
The installer rejects unrecognized flags or positional arguments before making changes and prints the supported-option usage. `--check` reports one PASS/FAIL row for each P0P8 predicate and exits non-zero if any row fails; it does not create the npm prefix, lock, journal, manifest, or runtime files.
The installer rejects unrecognized flags or positional arguments before making changes and prints the supported-option usage.
## Contributing
-40
View File
@@ -1368,43 +1368,3 @@ All work is **alpha** (< 0.1.0) until Jason approves 0.1.0 beta release.
10. ASSUMPTION: **Conversations and messages get their own PG tables** (not stored in brain's entity model). They follow a chat-specific schema with proper foreign keys to users and projects. Rationale: Chat has different access patterns (streaming, pagination, search) than brain entities.
11. RESOLVED: **Pi handles all target LLM providers natively.** Anthropic, OpenAI/Codex, Z.ai, Ollama, LM Studio, and llama.cpp are all supported via Pi's built-in providers or `models.json` configuration with `openai-completions` API type. No custom provider adapters needed in @mosaicstack/agent — only configuration management.
---
## Greenfield install correctness — C1 (#1050)
### Problem and objective
A from-zero install can report success while leaving the target host unusable because the installer has no transactional state machine capable of certifying its own postconditions. C1 supplies the structural spine and red-first fixture; later cards repair the individual failed postconditions.
### Normative requirements
1. The installer SHALL implement the canonical P0P9 numbering from the greenfield-install PRD v2: P0 Resolve context; P1 Preflight; P2 Acquire artifacts; P3 Install CLI; P4 Install framework + skills; P5 Identity; P6 Runtime linking / activation; P7 Services; P8 Shell discoverability; P9 Verify + commit. P2 is scoped to installer-distribution artifacts and SHALL NOT foreclose credentialed downstream acquisition. P5 owns validating any credential capability required by requested downstream work; P7 may provision credential-dependent resources only after that P5 postcondition commits.
2. Every phase SHALL declare preconditions, action, committed postconditions, and rollback. An unverifiable postcondition SHALL fail the install non-zero with the named phase and a remediation line; no best-effort failure may still certify success. P1's required-tool closure includes tools invoked by later phases, including `git`; a downstream prerequisite may not remain undeclared and degrade silently.
3. A durable mutation journal SHALL open before the first mutation and commit at P9. Fallible command output needed to diagnose a phase SHALL be journaled and surfaced, never discarded.
4. `--check` SHALL run exactly the P0P8 postcondition predicates without mutation, report each phase PASS/FAIL, and exit non-zero if any predicate fails.
5. P4 SHALL consume a checkout-free, lane/versioned shipped-set declaration published by the installer. C1 SHALL NOT select among the currently disagreeing framework-payload, repository-root, sync-source, and W-jarvis populations; while no declaration exists, P4 reports `NOT-MEASURED / UNDECLARED` and remains blocking rather than fabricating a count. C5 owns the declaration's contents and containment/loadability fulfillment.
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 P2P8 phase SHALL prove either clean rollback or a durable, honestly reported resumable partial state, with no journal incorrectly left in progress.
9. Unsupported musl/Alpine and unavailable Docker SHALL fail loudly rather than skip as pass. The repository's installer tests SHALL nevertheless run in the canonical Alpine CI image by explicitly modeling a supported non-root/glibc target and using portable filesystem enumeration.
10. P0 SHALL bind the effective uid and username to the authoritative passwd HOME and shell and state/reject unsafe root or sudo-with-inherited-HOME privilege contexts.
11. Created paths SHALL satisfy phase-specific target owner/group and mode policy: P3 executables are not group/world writable, framework/runtime trees are not group/world writable, and identity/credential material is private.
12. The expected-RED comparator SHALL validate the complete manifest before selecting a case: exact case population, one exit and P0P9 disposition per case, pinned require/forbid classes, and no malformed, duplicate, or unknown rows.
13. The published installer contract SHALL reject failed fetches, HTTP-success empty bodies, and digest mismatch, then execute the exact digest-verified body. The remote CI arm SHALL enumerate every payload-acquisition path and report a bound/found denominator. It SHALL bind both the installer body and the downstream stack framework/source it consumes to the same immutable CI commit, while retaining `--next` as the lane selector. Source resolution/acquisition SHALL fail closed, and the arm SHALL verify the realised source commit and archive digest after installation. Any out-of-scope unpinned sibling SHALL be named and counted rather than silently included in a broader pinning claim. `ASSUMPTION:` the configured repository's authenticated exact-commit endpoint is trusted to map that commit ID to the returned archive bytes; independent signed provenance/authenticity against repository or TLS trust-root compromise remains excluded by canonical greenfield-install PRD v2 §3.
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.
15. The CI fixture SHALL publish pipeline-level execution coverage for both the unconditional case set derived from the expected-RED manifest and the explicitly declared required pipeline-arm set, including the immutable remote-installer contract. Per-case and per-arm success markers SHALL be run-scoped, stored beneath an existing checkout-archive exclusion, and written only after that arm's verifier passes. A final step SHALL depend on the complete fixture matrix, run after prior success or failure, emit both `cases_defined=N cases_executed=M` and `arms_defined=N arms_executed=M`, and invoke the same testable aggregation helper whose complete case/arm PASS/FAIL truth table is covered. It SHALL fail unless both expected/executed name sets are exactly equal; missing, unexpected, stale, newly added unexecuted, or checkout-contaminating state SHALL fail closed. The archive-purity control SHALL bind to the production checkout-archive selector so deleting the production exclusion makes the control RED. Coverage tooling SHALL execute under both the canonical Alpine/BusyBox CI image and the Debian greenfield image; a green result from either runtime alone is insufficient portability evidence. Expected-set membership SHALL consume a fully materialized producer result so an early-closing consumer cannot turn a valid marker into a `pipefail` rejection.
### 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 plus the immutable remote-installer contract; its pipeline-level coverage gate reports exact equality for the manifest-derived case set and the declared arm set, while skipped-case and skipped-remote-arm controls prove non-execution is red. C1 does not repair P4/P5/P8 or activate #869.
5. Negative controls prove manifest shrink/duplicates/unknown rows fail, unsafe P0/P3/P4/P5 contexts fail, the P2P8 fault seam enters real actions rather than synthetic writes, empty/mismatched fetched bodies fail, and a deliberately emitted secret canary is redacted from every persisted/output population.
### Explicit exclusions and dependencies
- C2 owns P8/PATH, C3 owns P5/headless identity, C4 owns P6 activation policy, and C5 owns P4/skills.
- Main-lane execution is a promotion precondition owned by #1037; C1 only makes the fixture lane-parametric.
- RM-02 and #869 activation are out of scope.
-5
View File
@@ -9,11 +9,6 @@
- [Whole mutator-class gate](architecture/mutator-class-gate.md) — default-deny policy, revoke-first/promote-last state machine, TTL, runtime adapters, and T-B/T-C assurance boundary.
- [Compaction revocation lifecycle](architecture/compaction-revocation.md) — Claude/Pi observer matrix, same-PID generation rollover, failure fencing, and the named bounded residual stale window.
## Installation and upgrades
- [Installer state machine and recovery](guides/installer-state-machine.md) — canonical P0P9 phases, side-effect-free checks, durable journal states, rollback/remediation, and the Debian greenfield CI gate.
- [Upgrade safety and recovery](guides/upgrade-safety-and-recovery.md) — framework ownership, durable operator snapshots, verify net, and projection regeneration.
## CLI and skill management
- [Skill registration user guide](guides/user-guide.md#claude-code-skill-registration) — register, unregister, list statuses, automatic install/update reconciliation, and Claude reload behavior.
-99
View File
@@ -1,99 +0,0 @@
# Installer State Machine and Recovery
The unified installer uses a transactional P0P9 model. It may report success only after P9 reasserts every applicable committed postcondition. Internal phases invoke the CLI by P3's absolute path; shell discovery is checked only at P8.
## Canonical phases
| Phase | Responsibility | Failure disposition |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| P0 Resolve context | 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; 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 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 |
| P9 Verify + commit | Re-run P0P8, commit the manifest, and seal the journal | Leave an honestly reported resumable failure or restore the pre-install snapshot |
The phase numbers are a cross-workstream contract and must not be renumbered.
## Side-effect-free check
```bash
bash tools/install.sh --check # stable/latest lane
bash tools/install.sh --check --next # prerelease lane
```
`--check`:
- emits exactly one `[P0]` through `[P8]` PASS/FAIL row;
- exits non-zero if any predicate fails;
- does not create the npm prefix, lock, journal, manifest, shell profile, or runtime file;
- uses temporary npm observation storage outside the target HOME and removes it before exit.
P4 currently fails as `NOT-MEASURED / UNDECLARED` until the installer publishes `~/.config/mosaic/.install-shipped-skills.json`. C1 deliberately does not select among the conflicting candidate populations; C5 owns publishing and fulfilling that declaration. Once present, the P4 predicate requires the declaration's lane/version to match the resolved install and every named skill to remain contained under `skills/<name>/SKILL.md` with matching loadable frontmatter.
## Durable journal
Each mutating run creates a private transaction directory:
```text
${XDG_STATE_HOME:-~/.local/state}/mosaic/install/
active.json
<UTC-run-id>/
journal.ndjson
journal.ndjson.sha256 # committed runs only
commands.log
snapshot/
```
Before each mutation scope is touched, `journal.ndjson` records:
- phase and path;
- whether prior state existed and where its snapshot lives;
- the reversal action;
- the captured command-output location and command status.
Journal, action-status, manifest, or command-log write/sync failure is fatal. An unrecorded mutation is not allowed. 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.
`active.json` is the current projection:
- `in-progress`: incomplete/open transaction;
- `rolled-back`: a fault restored the snapshot;
- `rollback-failed`: restoration failed or refused a replaced/unsafe target and requires manual recovery;
- `failed-resumable`: named postconditions failed and the recorded partial state remains for remediation;
- `committed`: P9 passed and the journal is sealed.
## Failure recovery
1. Read the named phase and remediation line from installer stderr.
2. Inspect `active.json`, then the referenced `journal.ndjson` and `commands.log`. Command output needed to diagnose a failure is preserved and surfaced; it is not redirected away.
3. For `rolled-back`, verify the target paths match their pre-install state before retrying.
4. For `failed-resumable`, repair the named phase owner requirement, then run `install.sh --check` before retrying the installer.
5. Do not activate the #869 enforcement hooks merely to turn P6 green. A broker-less host with those hooks is a failed P6 state.
## Greenfield CI gate
`.woodpecker/greenfield-install.yml` runs `tools/e2e-install-test.sh` from zero in Debian/glibc as a non-root uid with `env -i`. No host HOME, npm cache, credentials, or bind mount enters the target process. Checkout mode packages the complete current checkout into an archive, pins its SHA-256 through an internal fixture seam, and copies the self-contained fixture into the container; framework-installer changes in the PR are therefore exercised rather than fetched from an older remote branch.
The C1 fixture intentionally returns an attributable RED while C2C5 remain open. CI itself remains green only when the fixture's final P0P9 verdicts, required discriminator rows, seeded secret-canary scan, and non-zero exit match the versioned contract in `tools/fixtures/greenfield-expected-red.tsv`. The comparator validates the complete three-case schema before selecting a case: exactly one exit and P0P9 disposition per case, pinned require/forbid populations, and no duplicate or unknown rows. Any later remediation that changes an observed verdict makes CI red until the owning lane deliberately updates that manifest:
- `git` present: P1 and strict P3 pass; P4/P5/P6/P8 fail for their own reasons; P9 refuses success.
- `git` absent: P1 fails before target mutation and the installer emits no `Done.`.
The fixture is lane-parametric:
```bash
bash tools/e2e-install-test.sh --lane next --git present
bash tools/e2e-install-test.sh --lane main --git present
```
CI exercises both lane parameters as expected-RED structural checks. A separate remote-contract arm fetches the installer at the immutable CI commit, rejects failed or empty HTTP-success bodies, compares it to the reviewed `tools/install.sh.sha256`, and executes that exact fetched artifact. The P2P8 fault matrix runs the real phase actions (including the P3 npm path, P4 framework path, and wizard path) rather than synthetic representative writes, then compares the complete target tree to its pre-install fingerprint. Delivery targets `main` under the trunk-only merge rule; `next` remains a non-merging integration lane. The linked installer issue stays open after merge and closes only after Jarvis independently validates the greenfield behavior.
## Source trust boundary
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.
@@ -12,20 +12,6 @@ with no snapshot to fall back to.
Protection is layered. Each layer is independent; a later layer catches what an
earlier one misses.
## Layer 0 — Transaction journal (install-wide recovery)
The unified installer opens a private journal under
`${XDG_STATE_HOME:-~/.local/state}/mosaic/install/` before the first target
mutation. Every mutation scope records its path, prior snapshot, and reversal
instructions before it is touched. Journal write/sync failure is fatal, and P9
seals successful journals with a SHA-256 sidecar. See
[Installer state machine and recovery](./installer-state-machine.md).
This transaction journal is distinct from the retained operator-only backup
below. The transaction journal is required for correctness and rollback;
Layer 2's durable backup remains a separately stated, fail-open recovery bonus
for a manifest bug that the normal transaction did not detect.
## Layer 1 — Manifest-owned sync (prevention)
The single source of truth for ownership is
@@ -1,14 +0,0 @@
subject_head=3edde464b3891ad439019fcc19aad7728e4c2fb8
source=git show HEAD:tools/install-next-lane.test.sh
477 echo 'credentialed URL userinfo leaked to terminal output' >&2; exit 1
478 fi
479 [[ "$(grep -oF '[REDACTED]@' <<<"$OUTPUT" | wc -l | tr -d ' ')" -ge 5 ]] \
480 || { echo 'credentialed URL redaction controls were not all exercised' >&2; exit 1; }
481 secret_active="$TMP/secret-state/active.json"
--
525 echo 'framework nested capture leaked credential diagnostics' >&2; exit 1
526 fi
527 [[ "$(grep -oF '[REDACTED]@' "$framework_log" | wc -l | tr -d ' ')" -ge 5 ]] \
528 || { echo 'framework URL redaction controls were not exercised' >&2; exit 1; }
529
@@ -1,16 +0,0 @@
source=/tmp/c1-ci-next-x.log (exact failing canonical-image xtrace)
credential material is already replaced by the redactor token [REDACTED]; no live secret is reproduced
urls=https://[REDACTED]@example.com/a https://[REDACTED]@example.net/b https://[REDACTED]@example.org/c https://[REDACTED]@example.dev/d https://[REDACTED]@example.io/e
urls=https://[REDACTED]@example.com/a https://[REDACTED]@example.net/b https://[REDACTED]@example.org/c https://[REDACTED]@example.dev/d https://[REDACTED]@example.io/e
line_count=2
occurrence_count=10
observed_assertion_value=2 (from xtrace: [[ 2 -ge 5 ]])
canonical-image discriminator (same locally cached digest as failing run):
image_id=sha256:d40fb1a218b72d3dcbf8a427a5076facf2a6d958b6854e6bbd057f7264540841 repo_digests=["git.mosaicstack.dev/mosaicstack/stack/ci-base@sha256:0f1d996a6cfcc09e6dcf979ee66c872a1b0be4f1bfde852b4790f520ddd0d776"]
busybox=BusyBox v1.37.0 (2026-01-10 15:38:28 UTC)
regex_-o_single_line=5
fixed_-oF_single_line=1
fixed_-oF_two_lines=2
@@ -1,14 +0,0 @@
positive_control_exit=1
seeded_line=https://[MASKED-USERINFO]@example.io/e (actual synthetic userinfo intentionally omitted here)
expected_failure=credentialed URL redaction control missing for example.io
transcript_tail:
[test] --next fast path pins resolved package versions
[test] fast path failure falls back to source build
[test] source-build failure is fatal and restores the pre-install prefix
[test] corrupt source archive is fatal and restores the pre-install prefix
[test] --dev source install does not require registry version resolution
[test] explicit --ref keeps source lane and avoids @next lookup
[test] --check --next rejects mismatched prerelease pipeline suffixes
[test] full framework path receives P3 absolute CLI without relying on PATH
[test] captured diagnostics redact seeded credential canary everywhere
credentialed URL redaction control missing for example.io
@@ -1,10 +0,0 @@
[test] --next fast path pins resolved package versions
[test] fast path failure falls back to source build
[test] source-build failure is fatal and restores the pre-install prefix
[test] corrupt source archive is fatal and restores the pre-install prefix
[test] --dev source install does not require registry version resolution
[test] explicit --ref keeps source lane and avoids @next lookup
[test] --check --next rejects mismatched prerelease pipeline suffixes
[test] full framework path receives P3 absolute CLI without relying on PATH
[test] captured diagnostics redact seeded credential canary everywhere
credentialed URL redaction control missing for example.io
@@ -1,579 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="/work"
# shellcheck source=tools/test-enumeration-assertions.sh
source "$ROOT/tools/test-enumeration-assertions.sh"
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"
PREFIX="$HOME_DIR/prefix"
MOSAIC_HOME="$HOME_DIR/mosaic"
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
LOG="${MOSAIC_TEST_NPM_LOG:?}"
STATE="${MOSAIC_TEST_STATE:?}"
echo "$*" >> "$LOG"
if [[ "${1:-}" == "--version" ]]; then
echo "10.6.2"
exit 0
fi
install_cli() {
local version="$1"
echo "$version" > "$STATE/mosaic"
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"
}
if [[ "$1" == "view" ]]; then
if [[ "${MOSAIC_TEST_FAIL_NPM_VIEW:-0}" == "1" ]]; then
echo "forced registry metadata failure" >&2
exit 1
fi
case "$2 $3" in
"@mosaicstack/mosaic@next version") echo "0.0.49-next.999" ;;
"@mosaicstack/gateway@next version") echo "${MOSAIC_TEST_GATEWAY_NEXT_VERSION:-0.0.7-next.999}" ;;
"@mosaicstack/mosaic version") echo "0.0.48" ;;
*) echo "unexpected npm view: $*" >&2; exit 1 ;;
esac
exit 0
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://public.example/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"
;;
*"@mosaicstack/[email protected]"*)
if [[ "${MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL:-0}" == "1" ]]; then
echo "forced gateway install failure" >&2
exit 1
fi
echo "0.0.7-next.999" > "$STATE/gateway"
;;
*"mosaicstack-mosaic-0.0.0-source.tgz"*)
install_cli "0.0.0-source"
;;
*"mosaicstack-gateway-0.0.0-source.tgz"*)
echo "0.0.0-source" > "$STATE/gateway"
;;
*) echo "unexpected npm install: $*" >&2; exit 1 ;;
esac
exit 0
fi
if [[ "$1" == "ls" ]]; then
cli="$(cat "$STATE/mosaic" 2>/dev/null || true)"
gateway="$(cat "$STATE/gateway" 2>/dev/null || true)"
node -e '
const cli = process.argv[1];
const gateway = process.argv[2];
const dependencies = {};
if (cli) dependencies["@mosaicstack/mosaic"] = { version: cli };
if (gateway) dependencies["@mosaicstack/gateway"] = { version: gateway };
process.stdout.write(JSON.stringify({ dependencies }));
' "$cli" "$gateway"
exit 0
fi
echo "unexpected npm command: $*" >&2
exit 1
FAKE_NPM
chmod +x "$FAKE_BIN/npm"
cat > "$FAKE_BIN/curl" <<'FAKE_CURL'
#!/usr/bin/env bash
set -euo pipefail
headers=""; output=""; url=""
while [[ $# -gt 0 ]]; do
case "$1" in
-D) headers="$2"; shift 2 ;;
-o) output="$2"; shift 2 ;;
--max-filesize) shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
case "$url" in
*/api/v1/repos/mosaicstack/stack/commits?sha=*)
printf 'HTTP/1.1 200 OK\r\ncontent-type: application/json; charset=utf-8\r\n\r\n' > "$headers"
printf '[{"sha":"1111111111111111111111111111111111111111"}]\n' > "$output"
;;
*/archive/*.tar.gz)
if [[ "${MOSAIC_TEST_CORRUPT_ARCHIVE:-0}" == "1" ]]; then
printf 'not-a-tarball\n' > "$output"
else
archive_root="$(mktemp -d)"
mkdir -p "$archive_root/stack"
printf 'fixture\n' > "$archive_root/stack/.fixture"
/bin/tar czf "$output" -C "$archive_root" stack
rm -rf "$archive_root"
fi
;;
esac
FAKE_CURL
chmod +x "$FAKE_BIN/curl"
cat > "$FAKE_BIN/tar" <<'FAKE_TAR'
#!/usr/bin/env bash
set -euo pipefail
dest=""; list=false
while [[ $# -gt 0 ]]; do
case "$1" in
-C) dest="$2"; shift 2 ;;
-*t*|t*) list=true; shift ;;
*) shift ;;
esac
done
[[ "$list" == true ]] && exit 0
if [[ -z "$dest" ]]; then
echo "fake tar missing -C destination" >&2
exit 1
fi
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"
cat > "$FAKE_BIN/pnpm" <<'FAKE_PNPM'
#!/usr/bin/env bash
set -euo pipefail
LOG="${MOSAIC_TEST_NPM_LOG:?}"
echo "pnpm $*" >> "$LOG"
if [[ "$1" == "pack" ]]; then
out=""
while [[ $# -gt 0 ]]; do
case "$1" in
--pack-destination) out="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$out" ]]; then
echo "fake pnpm pack missing destination" >&2
exit 1
fi
mkdir -p "$out"
case "$PWD" in
*/apps/gateway) touch "$out/mosaicstack-gateway-0.0.0-source.tgz" ;;
*/packages/mosaic) touch "$out/mosaicstack-mosaic-0.0.0-source.tgz" ;;
*) echo "unexpected pnpm pack cwd: $PWD" >&2; exit 1 ;;
esac
exit 0
fi
if [[ "${MOSAIC_TEST_FAIL_PNPM_INSTALL:-0}" == "1" && "$1" == "install" ]]; then
echo "forced pnpm install failure" >&2
exit 42
fi
# Other install/build commands are no-ops in this harness.
exit 0
FAKE_PNPM
chmod +x "$FAKE_BIN/pnpm"
reset_state() {
: > "$LOG"
rm -f "$STATE"/*
}
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="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch
)"
grep -qF 'Installed @next packages: CLI 0.0.49-next.999, gateway 0.0.7-next.999' <<<"$OUTPUT"
grep -qF 'install -g @mosaicstack/[email protected]' "$LOG"
grep -qF 'install -g @mosaicstack/[email protected]' "$LOG"
if grep -qE '^install -g .+@next( |$)' "$LOG"; then
echo "expected exact-version installs, found mutable @next install" >&2
exit 1
fi
if grep -qF 'Downloading source ref next at pinned commit' <<<"$OUTPUT"; then
echo "fast path unexpectedly fell back to source" >&2
exit 1
fi
ACTIVE="$HOME_DIR/.local/state/mosaic/install/active.json"
[[ "$(node -p "require('$ACTIVE').status")" == "committed" ]]
JOURNAL="$(node -p "require('$ACTIVE').journal")"
[[ "$(stat -c '%a' "$JOURNAL")" == "444" ]]
( cd "$(dirname "$JOURNAL")" && sha256sum -c "$(basename "$JOURNAL").sha256" >/dev/null )
grep -q '"event":"mutation".*"phase":"P3".*path=.*prior=.*reverse=' "$JOURNAL"
reset_state
echo "[test] fast path failure falls back to source build"
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch
)"
grep -qF 'Fast gateway @next install failed.' <<<"$OUTPUT"
grep -qF 'Falling back to source build at ref next; --next will not hard-fail on registry issues.' <<<"$OUTPUT"
grep -qF 'Downloading source ref next at pinned commit 1111111111111111111111111111111111111111' <<<"$OUTPUT"
grep -qF 'Installed from source: CLI 0.0.0-source' <<<"$OUTPUT"
grep -qF 'install -g @mosaicstack/[email protected]' "$LOG"
grep -qE 'install -g .*/mosaicstack-gateway-0\.0\.0-source\.tgz' "$LOG"
grep -qE 'install -g .*/mosaicstack-mosaic-0\.0\.0-source\.tgz' "$LOG"
[[ "$(cat "$STATE/mosaic")" == "0.0.0-source" ]]
[[ "$(cat "$STATE/gateway")" == "0.0.0-source" ]]
reset_state
echo "[test] source-build failure is fatal and restores the pre-install prefix"
before_prefix="$(prefix_fingerprint)"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \
MOSAIC_TEST_FAIL_PNPM_INSTALL=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
)"
FAIL_STATUS=$?
set -e
[[ "$FAIL_STATUS" -ne 0 ]]
[[ "$(prefix_fingerprint)" == "$before_prefix" ]]
grep -qF 'forced pnpm install failure' <<<"$OUTPUT"
[[ "$(node -p "require('$ACTIVE').status")" == "rolled-back" ]]
reset_state
echo "[test] corrupt source archive is fatal and restores the pre-install prefix"
before_prefix="$(prefix_fingerprint)"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \
MOSAIC_TEST_CORRUPT_ARCHIVE=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
)"
FAIL_STATUS=$?
set -e
[[ "$FAIL_STATUS" -ne 0 ]]
[[ "$(prefix_fingerprint)" == "$before_prefix" ]]
grep -qF 'archive safety/integrity check failed' <<<"$OUTPUT"
[[ "$(node -p "require('$ACTIVE').status")" == "rolled-back" ]]
reset_state
echo "[test] --dev source install does not require registry version resolution"
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NPM_VIEW=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --dev --ref feature-x --yes --no-auto-launch
)"
grep -qF 'Downloading source ref feature-x at pinned commit 1111111111111111111111111111111111111111' <<<"$OUTPUT"
grep -qF 'Installed from source: CLI 0.0.0-source' <<<"$OUTPUT"
grep -q '^\[P2\] PASS: source_ref=feature-x pinned_commit=1111111111111111111111111111111111111111 sha256=' <<<"$OUTPUT"
reset_state
echo "[test] explicit --ref keeps source lane and avoids @next lookup"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --check --cli --next --ref feature-x
)"
CHECK_STATUS=$?
set -e
[[ "$CHECK_STATUS" -ne 0 ]]
grep -q '^\[P2\] PASS: source_ref=feature-x pinned_commit=1111111111111111111111111111111111111111 sha256=' <<<"$OUTPUT"
if grep -qF '@next version' "$LOG"; then
echo "explicit ref should not query @next dist-tags" >&2
exit 1
fi
reset_state
echo "[test] --check --next rejects mismatched prerelease pipeline suffixes"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_GATEWAY_NEXT_VERSION="0.0.7-next.1000" \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --check --cli --next
)"
CHECK_STATUS=$?
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
)"
# Positive control: replace the removed redacted example.io source with one deliberately unredacted userinfo URL.
OUTPUT+=$'\nhttps://[email protected]/e'
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
for host in example.com example.net example.org example.dev example.io; do
grep -qF "https://[REDACTED]@$host" <<<"$OUTPUT" \
|| { echo "credentialed URL redaction control missing for $host" >&2; exit 1; }
done
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
test_assert_find_empty 'redacted diagnostic staging files' \
"$TMPDIR" -maxdepth 1 -type f \
\( -name 'mosaic-phase-redacted.*' -o -name 'mosaic-post-redacted.*' \) || exit 1
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
for host in example.com example.net example.org example.dev example.io; do
grep -qF "https://[REDACTED]@$host" "$framework_log" \
|| { echo "framework URL redaction control missing for $host" >&2; exit 1; }
done
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"
test_assert_no_file_content_match "$phase fault-state" \
'"status"[[:space:]]*:[[:space:]]*"in-progress"' "$TMP/fault-$phase" || exit 1
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"
@@ -1,13 +0,0 @@
[test] --next fast path pins resolved package versions
[test] fast path failure falls back to source build
[test] source-build failure is fatal and restores the pre-install prefix
[test] corrupt source archive is fatal and restores the pre-install prefix
[test] source archive with multiple extracted roots fails instead of selecting by find order
[test] --dev source install does not require registry version resolution
[test] explicit --ref keeps source lane and avoids @next lookup
[test] --check --next rejects mismatched prerelease pipeline suffixes
[test] full framework path receives P3 absolute CLI without relying on PATH
[test] captured diagnostics redact seeded credential canary everywhere
[test] framework nested capture redacts the same canary and URL variants
[test] real P2-P8 actions run under fault injection and restore actual surfaces
P2 left an in-progress transaction
@@ -1,14 +0,0 @@
[test] --next fast path pins resolved package versions
[test] fast path failure falls back to source build
[test] source-build failure is fatal and restores the pre-install prefix
[test] corrupt source archive is fatal and restores the pre-install prefix
[test] source archive with multiple extracted roots fails instead of selecting by find order
[test] --dev source install does not require registry version resolution
[test] explicit --ref keeps source lane and avoids @next lookup
[test] --check --next rejects mismatched prerelease pipeline suffixes
[test] full framework path receives P3 absolute CLI without relying on PATH
[test] captured diagnostics redact seeded credential canary everywhere
[test] framework nested capture redacts the same canary and URL variants
[test] real P2-P8 actions run under fault injection and restore actual surfaces
[test] stale projection is preserved while the real fault path acquires a free OS lock
[test] installer next lane tests passed
@@ -1,2 +0,0 @@
[test] enumeration failure cannot mask a planted in-progress transaction
[test] FAIL: planted in-progress transaction plus failed enumeration passed the full suite
@@ -1,14 +0,0 @@
[test] --next fast path pins resolved package versions
[test] fast path failure falls back to source build
[test] source-build failure is fatal and restores the pre-install prefix
[test] corrupt source archive is fatal and restores the pre-install prefix
[test] source archive with multiple extracted roots fails instead of selecting by find order
[test] --dev source install does not require registry version resolution
[test] explicit --ref keeps source lane and avoids @next lookup
[test] --check --next rejects mismatched prerelease pipeline suffixes
[test] full framework path receives P3 absolute CLI without relying on PATH
[test] captured diagnostics redact seeded credential canary everywhere
[test] framework nested capture redacts the same canary and URL variants
[test] real P2-P8 actions run under fault injection and restore actual surfaces
find: /tmp/mosaic-next-install-test-hqL7U0/fault-P2/blocked: Permission denied
[test] ERROR: P2 fault-state enumeration failed
@@ -1,121 +0,0 @@
#1050 C1 ROUND 3 — REMOTE-ARM DOWNSTREAM SOURCE BINDING
IDENTITY / SCOPE
- branch: feat/1050-install-state-machine-red-fixture
- predecessor/provider lease pin: ff3f0d29f1763bed44a60610d073036112e66b77
- status language: believed-fixed, pending jarvis validation
- in scope: remote arm's mosaicstack/stack framework/source archive (#7)
- unchanged/fenced: expected-RED manifest including P6=FAIL; #869; #1068 sequencing
- corrected reference citation: the container-image acquisition path in fa-install.sh only
ACQUISITION-PATH CENSUS (10/10 CODE-READ; COMPLETE RUNTIME PATH CENSUS NOT MEASURED)
1. Woodpecker checkout at CI commit — fully constrained by intended CI identity.
2. node:22-bookworm-slim image — unpinned sibling, out of scope.
3. apt package set — unpinned sibling, out of scope.
4. tools/install.sh at CI_COMMIT_SHA + verified SHA-256 — fully constrained.
5. @mosaicstack/mosaic@next — P2 CLI value is authoritative; realised disagreement is detected at P3 — fully constrained at the top-level package version.
6. @mosaicstack/gateway@next — later re-resolution wins and is verified against itself; drift from the first value can be silently accepted — partially constrained TOCTOU sibling, out of scope.
7. stack framework/source archive — pre-fix internally pinned to WRONG identity 4df478cdd150fdf8d52ea109f02ade5d85017acd; post-fix bound to intended CI commit.
8. unversioned sequential-thinking npx package — unpinned sibling, out of scope.
9. Excalidraw dependency graph — shipped lockfile exact versions + registry integrity hashes — fully constrained by lock/integrity.
10. agent-skills default-branch clone — unpinned sibling, out of scope.
Post-fix mechanism breakdown: fully constrained 5/10 (#1,#4,#5,#7,#9); partially constrained 1/10 (#6); unpinned 4/10 (#2,#3,#8,#10). Same-CI stack payload hops moved from 1/2 to 2/2. The fix does not increase the number of constrained paths; it corrects #7 from wrong identity to intended identity. This census is a LOWER BOUND on defects and an UPPER BOUND on assurance: questioning revised the claimed assurance downward each time (blended 6/10 -> mechanism split -> 5 full + 1 partial + 4 unpinned), never upward.
#5/#6 RESOLUTION-FAILURE / TOCTOU DISPOSITION (CODE-READ; DISAGREEMENT INJECTION NOT MEASURED)
- P1 metadata failure blocks before mutation.
- Failed package install falls back to the one immutable stack archive resolved in P2; source resolution/fetch failure is fatal.
- #5 CLI: P2 RESOLVED_CLI_VERSION is authoritative; a later installed CLI mismatch is detected by P3 and rolls back.
- #6 gateway: no authoritative P2 gateway value is persisted. The last resolution in install_next_cli_from_registry wins and is compared with itself, so successful same-suffix gateway drift can be silently accepted. Reported, not fixed.
RED-FIRST — STALE NEXT REJECTED BEFORE ACQUISITION FIX
Command shape: exact ff3f0d2 installer URL/digest + expected source commit/digest, remote next lane.
- expected source commit: ff3f0d29f1763bed44a60610d073036112e66b77
- expected source SHA-256: 944c6db1b01b23c83169e6dc83e0d31262b1d24a2825270441745eb65c269c25
- fixture rc: 1
- realised source commit: 4df478cdd150fdf8d52ea109f02ade5d85017acd
- realised source SHA-256: 3e6d831efe13c3b2c0501507099d4a566af5abf877dacf85e5d7e4284d35e5c7
- source control: FAIL (realised != expected)
- P6 final fixture value: PASS (false pass from stale framework)
- forbidden evidence: CLI not found on PATH present
- comparator rc: 1; failures=8/checks=19
IMPLEMENTED PATH #7
- .woodpecker/greenfield-install.yml supplies MOSAIC_FIXTURE_SOURCE_COMMIT=${CI_COMMIT_SHA} while still invoking --lane next and --source remote.
- tools/e2e-install-test.sh validates 40-hex commit input, derives the exact commit archive URL, rejects failed/empty fetch, computes SHA-256 over that exact body, and passes the body/commit/digest through MOSAIC_INSTALL_LOCAL_SOURCE_{ARCHIVE,COMMIT,SHA256}.
- The existing installer local-source seam re-hashes the body and rejects mismatch.
- The fixture verifies realised .install-manifest.json sourceCommit and sourceSha256 against the exact fetched body after installation. Mismatch exits before the phase comparator; there is no fallback to next.
EXACT-SOURCE REPRODUCIBILITY RESULT — N=5 IDENTICAL FINAL-TREE EXECUTIONS
Invariant inputs on every run:
- fixture mode: --lane next --source remote --git present
- installer/source commit: ff3f0d29f1763bed44a60610d073036112e66b77
- installer SHA-256: e59cb441a2f37ae9150f8eae470238e9d858a1816df93343d9784a6796676096
- realised source SHA-256: 944c6db1b01b23c83169e6dc83e0d31262b1d24a2825270441745eb65c269c25
- resolved/realised CLI: @mosaicstack/mosaic@next = 0.0.50-next.2207
- fixture rc=1, P3 PASS, SOURCE-CONTROL PASS, installer_exit=1, done_claims=0, P6 FAIL, P9 FAIL
Per-run outcomes:
run 1: comparator rc=0; checks=19/19; dead_hooks=0; P6 reason=runtime linking/activation action reported a required failure; elapsed/load NOT MEASURED
run 2: comparator rc=0; checks=19/19; dead_hooks=0; P6 reason=runtime linking/activation action reported a required failure; elapsed/load NOT MEASURED
run 3: comparator rc=1; checks=18/19; dead_hooks=2; P6 reason=broker absent but dead enforcement hooks are active (count=2); elapsed=912s; load before=7.79 7.51 8.93; load after=12.28 14.82 11.79
run 4: comparator rc=0; checks=19/19; dead_hooks=0; P6 reason=runtime linking/activation action reported a required failure; elapsed=938s; load before=12.28 14.82 11.79; load after=9.84 15.18 13.83
run 5: comparator rc=0; checks=19/19; dead_hooks=0; P6 reason=runtime linking/activation action reported a required failure; elapsed=954s; load before=9.84 15.18 13.83; load after=3.94 4.76 8.18
Rate, not verdict:
- comparator rc=0: 4/5
- comparator rc=1: 1/5
- action-failure P6 reason: 4/5
- dead-hooks-active count=2 P6 reason: 1/5
- source commit/digest property: PASS 5/5
- P6 outcome property: FAIL 5/5
No comparator verdict is claimed. Identical source, digest, and package version produced different comparator values; this gate is nondeterministic on the #869 probe path. Load does not explain the small sample monotonically: run 4 had the highest observed 5/15-minute load but produced rc=0. The 2000ms capability-probe timeout remains a code-read hypothesis, NOT MEASURED as causal.
CHECKOUT CONTROL (EXECUTED, NOT PART OF REMOTE N=5)
The checkout-source arm independently produced: fixture rc=1; @next CLI 0.0.50-next.2207; P3 PASS; P6 FAIL with dead hooks active count=2; comparator rc=1 with the same 1/19 required-reason miss. This showed that outcome can occur independently of the remote #7 binding; its reproducibility was not separately measured.
PROPERTY / SIGNAL FINDING (MANIFEST UNCHANGED)
P6=FAIL remains the correct property and is unchanged. The comparator additionally pins one reason signal: "runtime linking/activation action reported a required failure". Exact-source P6 failed on all 5/5 runs, while the reason signal varied. The reason assertion was not widened to fit either observation.
#869 FINDING (BEHAVIOR EXECUTED; ROOT-CAUSE ATTRIBUTION CODE-READ)
Current @next now carries the lease capability. defaultSupervisorProbe treats bundled launcher+daemon file presence plus a nonempty resolved socket PATH as supervisor presence; it does not require the socket itself to exist. One of five identical clean broker-absent runs wired two dead hooks and let the linker exit zero; four reported the action failure. The 2000ms defaultCapabilityProbe subprocess timeout is a code-read candidate for the variance, NOT MEASURED as causal. Fixing this is #869 scope and was not performed.
TRUST BOUNDARY / INDEPENDENT PROVENANCE
The exact-commit archive URL binds source identity under the configured repository provider's authenticated mapping of commit ID to response bytes. The computed SHA-256 proves that the bytes executed are the bytes that exact URL served and that they did not change between fetch, installer consumption, and manifest verify-after. That is the measured control against stale-next substitution: SOURCE-CONTROL passed 5/5 exact-source runs and failed when R7 deleted the binding. The digest does NOT prove that those bytes are what the repository's authors published if the repository service or TLS trust root is compromised. Provider + TLS are the trust root by design. Independent signed provenance/authenticity remains an inherited deferral explicitly sourced to canonical greenfield-install PRD v2 §3, matching the already documented install.sh sidecar boundary. No stronger supply-chain claim is made. Codex security review's initial HIGH/CWE-494 finding is retained as the named trust-root deferral rather than bypassed or silently reclassified.
R7 DELETE-THE-SUBJECT MUTANT (EXECUTED)
Deleted the three MOSAIC_INSTALL_LOCAL_SOURCE_{ARCHIVE,COMMIT,SHA256} binding exports from tools/e2e-install-test.sh temporarily while retaining the realised source assertion.
- fixture rc: 1
- realised source commit reverted to 4df478cdd150fdf8d52ea109f02ade5d85017acd
- expected source commit remained ff3f0d29f1763bed44a60610d073036112e66b77
- realised source SHA-256: 3e6d831efe13c3b2c0501507099d4a566af5abf877dacf85e5d7e4284d35e5c7
- expected source SHA-256: 944c6db1b01b23c83169e6dc83e0d31262b1d24a2825270441745eb65c269c25
- SOURCE-CONTROL: FAIL
- comparator rc: 1; failures=8/checks=19
- elapsed: 171s; load before=4.13 4.71 8.05; load after=4.94 4.58 7.42
- subject file SHA-256 before mutant: a93113565aa69f2c6f3d792b78251021bb3bbe3f7813d5fed547ab0099fa3b98
- subject file SHA-256 after restoration: a93113565aa69f2c6f3d792b78251021bb3bbe3f7813d5fed547ab0099fa3b98
EVIDENCE LOGS (LOCAL, NOT COMMITTED RAW TRANSCRIPTS)
- /tmp/c1-round3-red.log and /tmp/c1-round3-red-summary.txt
- /tmp/c1-round3-green.log and /tmp/c1-round3-green-summary.txt
- /tmp/c1-round3-green-comparator.log
- /tmp/c1-round3-checkout-control.log and /tmp/c1-round3-checkout-summary.txt
- /tmp/c1-round3-repro-n5.tsv and /tmp/c1-round3-repro-{3,4,5}.log
- /tmp/c1-round3-final-r7.log and /tmp/c1-round3-final-r7-summary.txt
BASELINES / INDEPENDENT REVIEW
- bash -n tools/e2e-install-test.sh: PASS
- shellcheck tools/e2e-install-test.sh: PASS
- invalid remote source commit control: rc=2, named 40-hex requirement
- pnpm test:installer: PASS
- pnpm typecheck: PASS, 45/45 tasks
- pnpm lint: PASS, 25/25 tasks
- pnpm format:check: PASS
- git diff --check: PASS
- Codex code review: APPROVE, confidence 0.92, 6 files, zero findings
- Initial Codex security review: HIGH/CWE-494 on independent provider-compromise provenance; retained and bounded explicitly in PRD/report as the canonical v2 §3 deferral
- Codex security re-review after trust-boundary documentation: risk NONE, confidence 0.96, 6 files, zero findings
PUSH / CI
Not yet recorded in this artifact. No CI polling is authorised after the single push.
@@ -1,83 +0,0 @@
#1050 C1 ROUND 4 — PIPELINE CASE-COVERAGE DENOMINATOR
IDENTITY / SCOPE
- predecessor head: 0e2eef1c1261444b83c089df6047a2a06592d5c6
- branch: feat/1050-install-state-machine-red-fixture
- only open requirement: pipeline-level expected-case/executed-case set equality
- unchanged/fenced: tools/fixtures/greenfield-expected-red.tsv, tools/verify-greenfield-expected-red.sh, #869, P6 detector/producer, absolute-P3 CLI linking, and per-case fixture dispositions
- status language: believed-fixed, pending jarvis validation
STALE-RULING WITHDRAWAL HONOURED
Requirements 1-3 were withdrawn after pipeline 2242 at 0e2eef1c reached terminal green and showed P6 failure propagation, absolute-P3 CLI use, and an executed git-absent arm. A temporary local P6-consumer edit made while the stale ruling was in flight was restored before this change set; tools/e2e-install-test.sh has no final-tree delta. Requirement 5 alone remains in this round.
RED-FIRST
Before the change, .woodpecker/greenfield-install.yml had no pipeline-level initialized marker store, no per-case success marker, and no final exact-set gate. A static assertion for a final run-on-failure case-coverage step failed. Pipeline 2242's green result proved that all three cases ran once, but no gate required that population on a later run.
IMPLEMENTATION
- tools/verify-greenfield-case-coverage.sh is a new pipeline-level instrument; the existing per-case verifier is unchanged.
- Expected case names are derived from the first TSV field of tools/fixtures/greenfield-expected-red.tsv and sorted uniquely. There is no literal expected count.
- Marker storage is scoped by ${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}. The init action validates the manifest and empties only that run directory, so stale markers cannot satisfy a retry or later run.
- Every case/contract step explicitly depends on initialization. Each unconditional case step calls mark only after its exact tools/verify-greenfield-expected-red.sh invocation succeeds.
- The final greenfield-case-denominator step explicitly depends on all four case/contract steps, has status eligibility [success, failure], emits cases_defined=N cases_executed=M, and compares sorted expected and actual case-name sets. Missing or unexpected names fail even when the two counts are equal.
- A newly defined manifest case changes the expected set automatically and fails until a corresponding successful case step marks it.
MEASURED CONTROLS
1. Skipped expected arm:
rc=1
[fixture-suite] cases_defined=3 cases_executed=2
[fixture-suite] missing_case=next-git-absent
2. Count-inflation control (one expected missing, one unexpected added):
rc=1
[fixture-suite] cases_defined=3 cases_executed=3
[fixture-suite] missing_case=next-git-absent
[fixture-suite] unexpected_case=unexpected-case
3. Stale-marker initialization control (same run re-initialized after markers existed):
rc=1
[fixture-suite] cases_defined=3 cases_executed=0
missing_case rows emitted for all three expected names
4. Exact-set positive control:
rc=0
[fixture-suite] cases_defined=3 cases_executed=3
5. Future manifest case control:
rc=1
[fixture-suite] cases_defined=4 cases_executed=3
[fixture-suite] missing_case=future-case
WORKFLOW / FOCUSED VALIDATION
- bash -n new subject and test: PASS
- shellcheck new subject and test: PASS
- bash tools/verify-greenfield-case-coverage.test.sh: PASS
- test statically proves each mark follows its exact per-case verifier, every producer depends on initialization, the final gate depends on the complete matrix, and final status eligibility includes success+failure
- woodpecker-cli lint --strict .woodpecker/greenfield-install.yml: PASS
- Prettier check for workflow/package/PRD: PASS
- git diff --check: PASS
CURRENT-HEAD FIXTURE MEASUREMENTS TAKEN BEFORE THE STALE RULING WAS WITHDRAWN
These measurements were run from 0e2eef1c before requirement 5 changed only pipeline instrumentation. They are retained as measured outcomes, not as justification for modifying requirements 1-3.
- next checkout/git-present: fixture rc=1; resolved @next=0.0.50-next.2207; P0/P1/P2/P3/P7 PASS; P4/P5/P6/P8/P9 FAIL; installer_exit=1; done_claims=0; P6 reason was dead enforcement hooks active count=2.
- next checkout/git-absent: fixture rc=1; P0/P6/P7 PASS; P1/P2/P3/P4/P5/P8/P9 FAIL; installer_exit=1; done_claims=0.
- /home free before the first run: 8.5G; after concurrent authorized fleet reclaim completed: 11G; no be-coder-05 generated payload was deleted because active fixture work made that churn.
INDEPENDENT REVIEW CYCLE
- Initial Codex code review: REQUEST_CHANGES, confidence 0.96; one blocker found that the first draft omitted depends_on ordering and could race initialization/final checking.
- Disposition: accepted. Added explicit init -> all case/contract steps -> final dependency graph and a regression assertion for every dependency edge.
- Initial Codex security review: risk NONE, confidence 0.94, zero findings. The sandbox could not create its own temp directory; local focused/full tests provide the dynamic evidence.
- First post-dependency code re-review: REQUEST_CHANGES, confidence 0.99; the case extractor treated the production manifest's comment lines as case names, while the synthetic test omitted comments.
- Disposition: accepted. Case derivation now excludes blank/comment lines; the focused test includes representative comments and executes init/mark/check against the repository's real manifest.
- Post-dependency security re-review: risk NONE, confidence 0.96, zero findings.
- Final Codex code re-review after the comment fix: APPROVE, confidence 0.94, 8 files, zero findings. It confirmed ordering, run scope, fail-closed set equality, PRD alignment, and negative controls.
- Final Codex security re-review: risk NONE, confidence 0.96, 8 files, zero findings.
FINAL LOCAL BASELINES
- pnpm test:installer: PASS, including the new coverage suite
- pnpm typecheck: PASS, 45/45
- pnpm lint: PASS, 25/25
- pnpm format:check: PASS
- bash -n + ShellCheck for new shell surfaces: PASS
- Woodpecker strict lint: PASS
- git diff --check: PASS
- /home free before/after local baselines: 11G
PENDING BEFORE PUSH
- commit, queue guard, one push, provider attribution read-back
- announce the new full SHA to rev-security-02; do not merge or close
@@ -1,81 +0,0 @@
#1050 C1 ROUND 5 — ARM COVERAGE + CHECKOUT PURITY
IDENTITY / SCOPE
- predecessor head: f33bd0da96bee3ce518744d6888d018bebb1c841
- branch: feat/1050-install-state-machine-red-fixture
- review inputs: be-coder-06 review 110; rev-security-02 review 111
- in scope: exact required pipeline-arm set including greenfield-remote-installer-contract; success-only arm marks; skipped-remote RED control; coverage-state exclusion from checkout source archives
- unchanged/fenced: tools/fixtures/greenfield-expected-red.tsv, tools/verify-greenfield-expected-red.sh, tools/e2e-install-test.sh, #869, and all per-case dispositions
- status: believed-fixed, pending jarvis validation
RED-FIRST — FOUR ARMS WERE REPRESENTED BY THREE CASES
The f33bd0d gate represented only the three expected-RED case identities. With all three case names marked and no remote-arm identity available, the production checker returned:
SKIPPED_REMOTE_ARM_CURRENT_GATE_RC=0
[fixture-suite] cases_defined=3 cases_executed=3
The fourth required arm, greenfield-remote-installer-contract, reused next-git-present and therefore could skip without changing case-set equality.
RED-FIRST — COVERAGE STATE CONTAMINATED CHECKOUT ARCHIVES
The f33bd0d workflow created .greenfield-case-state in the shared repository before the checkout-mode fixture arms. An independent reproduction using the exact e2e-install-test.sh tar exclusions created .greenfield-case-state/remote-arm.ran and measured:
OLD_STATE_ARCHIVE_MATCHES=1
The state instrument was therefore part of the checkout source payload it was intended only to observe.
IMPLEMENTATION
- Renamed the checker to tools/verify-greenfield-execution-coverage.sh and generalized it over two validated dimensions: cases and arms.
- Added tools/fixtures/greenfield-expected-arms.txt as the explicit required arm set:
greenfield-git-present
greenfield-main-git-present
greenfield-remote-installer-contract
greenfield-git-absent
- Expected cases remain derived from the unchanged expected-RED TSV. Expected arms are derived from the new one-name-per-line arm declaration. Both inputs allow comments/blanks and validate every resulting name.
- Each of the four fixture arms writes its unique arm marker only after its exact per-case verifier succeeds. The three distinct expected-RED cases continue to write case markers after successful verification.
- The final step checks both dimensions even when the first check fails, emits both denominators, and fails unless both exact name sets match.
- Coverage state moved to .mosaic-test-work/greenfield-execution-coverage/${CI_PIPELINE_NUMBER}-${CI_WORKFLOW_NUMBER}/{cases,arms}. The checkout archive already excludes every */.mosaic-test-work subtree.
- The expected-RED manifest, per-case verifier, and detector are untouched.
MEASURED SKIPPED-REMOTE CONTROL
With all three cases marked and only the three non-remote arms marked:
case gate rc=0
[fixture-suite] cases_defined=3 cases_executed=3
arm gate rc=1
[fixture-suite] arms_defined=4 arms_executed=3
[fixture-suite] missing_arm=greenfield-remote-installer-contract
This is the discriminating control: the case denominator remains green while the arm denominator catches the exact previously invisible skip.
OTHER DYNAMIC CONTROLS
- missing case: cases 3/2 => rc1
- case count inflation (one missing + one unexpected): cases 3/3 => rc1
- stale case markers after re-init: cases 3/0 => rc1
- exact case set: cases 3/3 => rc0
- future manifest case: cases 4/3 => rc1
- arm count inflation (remote missing + unexpected): arms 4/4 => rc1
- exact arm set: arms 4/4 => rc0
- production expected-RED manifest comments/blanks: accepted; exact set 3/3 => rc0
- archive selector firing control: a non-excluded root marker is present in the archive
- archive purity control: no .mosaic-test-work path is present in the archive
STATIC / STRUCTURAL CONTROLS
- every case mark follows that arm's successful per-case verifier
- every arm mark, including the remote contract, follows that arm's successful per-case verifier
- all four arms depend on coverage initialization
- final denominator depends on all four arms and is eligible after success or failure
- final denominator runs both checks and aggregates their statuses
- workflow contains the already-excluded .mosaic-test-work state root and no .greenfield-case-state reference
LOCAL BASELINES
- bash -n + ShellCheck on generalized checker/test: PASS
- pnpm test:installer: PASS, including execution-coverage controls
- pnpm typecheck: PASS, 45/45
- pnpm lint: PASS, 25/25
- pnpm format:check: PASS
- woodpecker-cli lint --strict: PASS
- git diff --check: PASS
- /home free before/after: 11G
INDEPENDENT REVIEW
- Codex code review: APPROVE, confidence 0.93, zero findings. It confirmed exact arm coverage, success-only marking, dual-status aggregation, excluded state, checkout-purity controls, and PRD alignment.
- Codex security review: risk NONE, confidence 0.96, zero findings. CI/path identifiers are constrained, expansions quoted, expected sets treated as data, and checks fail closed.
- Review sandboxes could not execute their own dynamic suite/ShellCheck because their filesystem is read-only; the local measured baselines above provide that evidence.
PENDING
- commit, queue guard, one push, attribution read-back
- announce the new full SHA; no merge or closure
@@ -1,60 +0,0 @@
# #1050 C1 fix round — Round 6 caller coupling and production archive binding
BASE HEAD
- df705828a439c6795cd47c938ed7f838b956ebf3
- Reviews 114 (rev-security-02) and 115 (be-coder-06) independently reproduced the same caller-level mutant: ignoring arms_status in the workflow left the focused helper suite green.
- Both reviewers confirmed the production aggregation was correct and requested test reachability, not a logic rewrite.
- rev-974 separately deleted only the production e2e-install-test.sh .mosaic-test-work tar exclusion; the copied-selector suite remained green.
RED
- After adding the four-cell caller truth-table expectations but before creating the workflow helper:
bash tools/verify-greenfield-execution-coverage.test.sh => rc=1
The expected helper did not exist, so the test could not reach a passing aggregation implementation.
- Prior independent controls:
ignore-arms workflow mutant => rc=0 before this round (reviews 114/115)
delete-production-archive-exclusion mutant => rc=0 before this round (rev-974 exact-head sandbox)
IMPLEMENTATION
- Extracted the existing set +e / two checks / status capture / final OR into tools/verify-greenfield-execution-coverage-gate.sh without changing its semantics.
- .woodpecker/greenfield-install.yml now invokes that helper directly; its inline duplicate was removed.
- The focused suite invokes the same helper used by the workflow across all four cells:
cases PASS / arms FAIL => RED
cases FAIL / arms PASS => RED
both FAIL => both diagnostic outputs and RED
both PASS => GREEN
- The archive-purity test now first locates the production checkout-archive command in tools/e2e-install-test.sh and requires its .mosaic-test-work exclusion, then proves the exclusion's archive effect.
- GREENFIELD_COVERAGE_GATE and GREENFIELD_E2E_INSTALLER are test-only subject injection seams used to prove the requested mutants; production paths remain the defaults.
MUTATION CONTROLS
- ignore-arms aggregation helper => focused suite rc=1
- ignore-cases aggregation helper => focused suite rc=1
- always-RED aggregation helper => focused suite rc=1
- delete only production .mosaic-test-work archive exclusion => focused suite rc=1
- restored production subjects => greenfield execution coverage tests passed
- final mutant workspace: /tmp/c1-round6-final-mutants.u0ZaL6
- logs: /tmp/c1-round6-final-gate-ignore-arms.log, /tmp/c1-round6-final-gate-ignore-cases.log, /tmp/c1-round6-final-gate-always-red.log, /tmp/c1-round6-final-archive-mutant.log
FENCED FILES
- tools/e2e-install-test.sh HEAD/worktree SHA-256: a93113565aa69f2c6f3d792b78251021bb3bbe3f7813d5fed547ab0099fa3b98 (byte-identical)
- tools/fixtures/greenfield-expected-red.tsv HEAD/worktree SHA-256: baae06c664d0ab7ffe4c32ec106bfcb6f9dd74b46461d3c4440b73dc582af1a2 (byte-identical)
- No per-case disposition or #869 wiring changed.
LOCAL BASELINES
- /home free before build: 11G
- bash -n focused scripts: PASS
- ShellCheck focused scripts: PASS
- bash tools/verify-greenfield-execution-coverage.test.sh: PASS
- pnpm test:installer: PASS
- pnpm typecheck: PASS (45/45)
- pnpm lint: PASS (25/25)
- pnpm format:check: PASS
- woodpecker-cli lint --strict .woodpecker/greenfield-install.yml: PASS
- git diff --check: PASS
INDEPENDENT REVIEW
- Codex code review: APPROVE, confidence 0.94, seven files reviewed, zero findings. It confirmed the workflow invokes the exact four-cell-tested helper, both diagnostics remain observable, and archive purity binds to the production selector.
- Codex security review: risk NONE, confidence 0.97, seven files reviewed, zero findings. It confirmed fail-closed behavior, quoted inputs, repository-controlled production arguments, and no new secret/injection/access-control exposure.
- The code-review sandbox could not execute the dynamic suite because its filesystem is read-only; the local measured baselines and mutation controls above provide dynamic evidence.
PENDING
- commit, queue guard, one lease-pinned push, provider attribution read-back
@@ -1,56 +0,0 @@
# #1050 C1 fix round — Round 7 canonical Alpine/BusyBox portability
BASE HEAD
- d66e91b1f22489b911eef982aec3faa62c5edeb3
- Review 119 (rev-974) found tools/verify-greenfield-execution-coverage.sh used GNU-only find -printf.
- Pipeline 2246's canonical Alpine ci-base test failed while the Debian greenfield workflow passed; the stale pipeline is diagnostic evidence only and is not inherited as a ruling for the replacement head.
RED
- Before remediation:
docker run --rm -v "$PWD:/workspace" -w /workspace git.mosaicstack.dev/mosaicstack/stack/ci-base:latest bash tools/verify-greenfield-execution-coverage.test.sh
=> canonical_alpine_red_rc=1
- Independent review 119 captured BusyBox 1.37 rejecting `find: unrecognized: -printf`.
- Debian/GNU execution remained green, proving that one runtime's success did not establish portability.
IMPLEMENTATION
- Removed the GNU find -printf inventory.
- Marker inventory now uses Bash nullglob/dotglob plus parameter expansion, all available in the script's declared Bash runtime.
- The checker verifies the run directory is readable/searchable before inventory and fails closed otherwise.
- Only direct regular, non-symlink `*.ran` files are included, preserving the prior find boundary; identities remain sorted and compared as exact sets.
- No production fixture disposition, expected-RED manifest, e2e installer, #869 wiring, or acquisition behavior changed.
GREEN
- Local focused suite: greenfield execution coverage tests passed.
- Canonical Alpine/BusyBox focused suite:
docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/workspace" -w /workspace git.mosaicstack.dev/mosaicstack/stack/ci-base:latest bash tools/verify-greenfield-execution-coverage.test.sh
=> greenfield execution coverage tests passed.
- Existing Round-6 controls retained:
ignore-arms aggregation helper => rc=1
ignore-cases aggregation helper => rc=1
always-RED aggregation helper => rc=1
delete production .mosaic-test-work archive exclusion => rc=1
- Mutant workspace: /tmp/c1-round7-mutants.LocpBH
FENCED FILES
- tools/e2e-install-test.sh HEAD/worktree SHA-256: a93113565aa69f2c6f3d792b78251021bb3bbe3f7813d5fed547ab0099fa3b98 (byte-identical)
- tools/fixtures/greenfield-expected-red.tsv HEAD/worktree SHA-256: baae06c664d0ab7ffe4c32ec106bfcb6f9dd74b46461d3c4440b73dc582af1a2 (byte-identical)
LOCAL BASELINES
- /home free before and after validation: 11G
- Bash syntax focused scripts: PASS
- ShellCheck focused scripts: PASS
- pnpm test:installer: PASS
- pnpm typecheck: PASS (45/45)
- pnpm lint: PASS (25/25)
- pnpm format:check: PASS
- woodpecker-cli lint --strict .woodpecker/greenfield-install.yml: PASS
- git diff --check: PASS
INDEPENDENT REVIEW
- Codex code review: APPROVE, confidence 0.96, zero findings. It confirmed hidden/empty/direct-regular/non-symlink semantics, exact sorting/comparison, dual-runtime execution, and PRD alignment.
- Codex security review: risk NONE, confidence 0.97, zero findings. It confirmed quoted paths, fail-closed exact-set behavior, and no new injection, traversal, authorization, secret, cryptographic, dependency, or data-integrity risk.
- The security-review sandbox could not execute the focused suite because its filesystem is read-only; the local and canonical-container measured runs above provide dynamic evidence.
PENDING
- commit, queue guard, one lease-pinned push, provider attribution read-back
- replacement-head canonical CI; no manual pipeline trigger or polling
@@ -1,56 +0,0 @@
# #1050 C1 fix round — Round 8 pipefail-safe expected-set membership
BASE HEAD
- 58ada98d2b430da28c46cf92eebdb6d45b8b1846
- Review 122 (rev-security-02) found expected_names | grep -Fxq could falsely reject valid markers under pipefail when grep closed the pipe after an early match and the producer exited 141.
- Independent canonical-Alpine live-manifest stress reproduced intermittent false failures at the current four-arm set size. No small-manifest safe threshold is claimed.
DETERMINISTIC RED
- The focused suite now generates a 20,001-name manifest with a valid target sorted first and invokes the real mark path.
- Before remediation, in canonical Alpine ci-base:
docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/workspace" -w /workspace git.mosaicstack.dev/mosaicstack/stack/ci-base:latest bash tools/verify-greenfield-execution-coverage.test.sh
=> canonical_sigpipe_tdd_red_rc=1
=> [fixture-suite] case is not in the expected set: a-target
- This control exercises the production checker invocation rather than an approximated producer and makes the early-close race deterministic by exceeding pipe capacity.
IMPLEMENTATION
- The mark path now fully materializes expected_names into expected_snapshot and fails closed if production fails.
- grep -Fxq reads the completed snapshot through a here-string; there is no producer/consumer pipeline to close early.
- Missing names still fail with the same attributable message. No `|| true` or other failure suppression was introduced.
- Exact-set checks, marker inventory, aggregation, archive binding, fixture dispositions, expected-RED manifest, e2e installer, #869 wiring, and acquisition behavior are unchanged.
GREEN
- Local focused suite: greenfield execution coverage tests passed.
- Canonical Alpine focused suite: greenfield execution coverage tests passed.
- Canonical Alpine live production-manifest stress: 1,000 valid greenfield-git-absent arm marks, failures=0.
- Canonical grep-q pipeline mutant through the real checker path => rc=1 with the attributable false rejection; restored subject => GREEN.
- Existing Round-6 controls retained:
ignore-arms aggregation helper => rc=1
ignore-cases aggregation helper => rc=1
always-RED aggregation helper => rc=1
delete production .mosaic-test-work archive exclusion => rc=1
- Round-8 mutant workspace: /tmp/c1-round8-mutants.aS1xTI
FENCED FILES
- tools/e2e-install-test.sh HEAD/worktree SHA-256: a93113565aa69f2c6f3d792b78251021bb3bbe3f7813d5fed547ab0099fa3b98 (byte-identical)
- tools/fixtures/greenfield-expected-red.tsv HEAD/worktree SHA-256: baae06c664d0ab7ffe4c32ec106bfcb6f9dd74b46461d3c4440b73dc582af1a2 (byte-identical)
LOCAL BASELINES
- /home free before and after validation: 11G
- Bash syntax focused scripts: PASS
- ShellCheck focused scripts: PASS
- pnpm test:installer: PASS
- pnpm typecheck: PASS (45/45)
- pnpm lint: PASS (25/25)
- pnpm format:check: PASS
- woodpecker-cli lint --strict .woodpecker/greenfield-install.yml: PASS
- git diff --check: PASS
INDEPENDENT REVIEW
- Codex code review: APPROVE, confidence 0.96, six files reviewed, zero findings. It confirmed full materialization removes the SIGPIPE race and the deterministic regression control exercises the real early-match mark path.
- Codex security review: risk NONE, confidence 0.98, six files reviewed, zero findings. It confirmed fail-closed producer handling, constrained/quoted names, and no new injection, traversal, secret, access-control, cryptographic, dependency, or logging risk.
- The code-review sandbox could not execute the suite or install ShellCheck because its filesystem is read-only; local and canonical-container dynamic evidence above supplies those gates.
PENDING
- commit, queue guard, one lease-pinned push, provider attribution read-back
- replacement-head canonical CI; no manual trigger or polling
@@ -1,33 +0,0 @@
# #1050 C1 fix-round verification
Frozen reviewed head before remediation: `378bc1afe3bc485adb8614897d66c5edccd4a527`.
Status: **believed-fixed, pending jarvis validation**. PR #1054 is not self-merged and issue #1050 remains open.
## Blocker B — fail-closed test enumeration
The RED-first control used a real filesystem permission failure, not binary shadowing or PATH interception.
1. A planted `{"status":"in-progress"}` file in a readable P2 fault tree made the complete real walk fail the frozen suite at `P2 left an in-progress transaction` (`01-pre-fix-positive-control.log`, exit 1).
2. The same planted defect beneath a target-owned mode-0100 directory made real `find` report a permission failure. The frozen suite erased the producer failure and exited 0 with `installer next lane tests passed` (`02-pre-fix-permission-failure-attack.log`).
3. The committed regression control initially failed because the child full-suite attack still exited 0 (`03-regression-test-red.log`).
4. After remediation, the same child full-suite input exits 1 and names `[test] ERROR: P2 fault-state enumeration failed` (`04-post-fix-permission-failure-attack.log`). The ordinary full suite remains green.
`tools/test-enumeration-assertions.sh` now captures each complete NUL-delimited population and checks the producer status before asserting absence. Content checks inspect the captured population and distinguish “no match” from a read error. The shared fail-closed implementation covers:
- `tools/install-next-lane.test.sh`: redacted staging-file cleanup and fault-state transaction scan;
- `tools/verified-installer-fetch.test.sh`: temporary-download cleanup;
- `tools/install-state-machine.test.sh`: symlink-target non-mutation;
- `docs/reports/verification/1050-b8-redaction-control/positive-control.test.sh`: both copied counterparts.
No assertion was loosened. A1, A2, upgrade-guard, source-root, the species-2 sweep, #869, and expected-RED verdict rows remain outside this remediation.
## Blocker A — installer digest
The stale sidecar value was replaced with the exact `sha256sum` record for `tools/install.sh`:
```text
e59cb441a2f37ae9150f8eae470238e9d858a1816df93343d9784a6796676096 install.sh
```
RED-first documentation control from the repository root: `sha256sum -c tools/install.sh.sha256` exits 1 with `install.sh: FAILED open or read` because the sidecar records a path relative to `tools/`. The command actually executed, `(cd tools && sha256sum -c install.sh.sha256)`, exits 0 with `install.sh: OK`; the workflow's separate exact expected/actual equality also passes. The immutable provider-fetch arm at the new `${CI_COMMIT_SHA}` is recorded in the freeze artifact after push; local equality alone is not treated as sufficient evidence.
@@ -1,28 +0,0 @@
# Documentation completion checklist — #1050 C1 fix round
## Required artifacts
- [x] `docs/PRD.md` exists; #1050 C1 requirement 13 binds the complete in-scope remote stack source chain, and requirement 15 binds exact case+arm execution coverage, executable aggregation, production-bound archive purity, dual-runtime portability, and pipefail-safe materialized membership.
- [x] User guide: not applicable; no user-facing production installer behavior changed.
- [x] Admin guide: not applicable; no operator procedure or deployment behavior changed.
- [x] Developer guide: existing `docs/guides/installer-state-machine.md` defines the fail-closed installer model; the C1-specific remote-arm contract and evidence live in the PRD, scratchpad, and scoped verification report.
- [x] OpenAPI and endpoint index: not applicable; no API changed.
- [x] Sitemap: not applicable; no navigation changed.
## API and structural coverage
- [x] API schema/auth/error coverage: not applicable; no endpoint changed.
- [x] Guide book indexes: not applicable; no guide page was added or moved.
- [x] Root hygiene preserved; all new artifacts are under `docs/reports/verification/1050-c1-fix-round/` and the active scratchpad remains under `docs/scratchpads/`.
## Review and publishing
- [x] Verification documentation is in the same logical change set as the shell-test remediation.
- [x] Trust boundary states both halves: the digest proves fetched bytes remain identical through execution/verify-after; it does not authenticate authorship against provider/TLS compromise. Independent provenance is the inherited PRD v2 §3 deferral.
- [x] Round-3 Codex code review APPROVE (confidence 0.92, zero findings) and security re-review risk NONE (confidence 0.96, zero findings). The initial HIGH trust-root finding remains documented as the canonical signed-provenance deferral, not hidden.
- [x] Round-4 review cycle closed: two blocking Codex findings (missing dependency ordering; production manifest comments misparsed) were accepted and fixed. Final code review APPROVE (confidence 0.94, zero findings) and security risk NONE (confidence 0.96, zero findings).
- [x] Round-5 exact arm coverage and archive-purity controls reviewed: Codex code APPROVE (confidence 0.93, zero findings) and security risk NONE (confidence 0.96, zero findings).
- [x] Round-6 executable aggregation and production archive-selector binding reviewed: Codex code APPROVE (confidence 0.94, zero findings) and security risk NONE (confidence 0.97, zero findings).
- [x] Round-7 BusyBox-safe marker inventory and dual-runtime evidence reviewed: Codex code APPROVE (confidence 0.96, zero findings) and security risk NONE (confidence 0.97, zero findings).
- [x] Round-8 materialized membership and deterministic canonical-Alpine control reviewed: Codex code APPROVE (confidence 0.96, zero findings) and security risk NONE (confidence 0.98, zero findings).
- [x] Canonical evidence remains in-repo. No external publishing action was requested or performed.
@@ -1,66 +0,0 @@
# #1050 successor remediation verification
Head under test before remediation: `e5d5c8495a070af2dcd393cace287fe74a8a819e`.
This change strengthens the expected-RED detector; it does not repair the intentionally failing greenfield rows. The #869 hooks remain unwired.
## A1 — P0 reason binding
RED first:
```text
$ bash tools/verify-greenfield-expected-red.test.sh
[test] FAIL: vacuous P0 PASS satisfied the expected-RED contract without identity/context evidence
exit=1
```
The pinned manifest now has an explicit `phase-reason` binding against the final P0 row: target `mosaic`, uid `1001`, equal `HOME` and passwd HOME, `/bin/bash`, `privilege=user`, `x86_64`, glibc, and version-shaped Node/npm evidence. All three cases structurally require exactly one P0 reason binding. The greenfield fixture's final P0 row now emits and validates the same complete identity/context evidence, so an unrelated earlier P0 line cannot satisfy the binding for a vacuous final row.
Pipeline 2224 and the successor's pre-change fixture run also exposed a stale next-lane P6 reason left behind by the already-closed B6 remediation: actual behavior is a fail-closed runtime-link action refusal with `#869` hooks left inactive and a persisted required P6 failure, while the manifest still expected dead hooks to be active. The pinned reason now matches the stronger measured refusal (`runtime linking/activation action reported a required failure`); no verdict changed and #869 remains unwired.
GREEN:
```text
[test] PASS: P0 PASS must bind target identity, HOME, shell, privilege, architecture, and runtime reason
```
## A2 — fail-closed P4 enumeration
RED first: a `find` control emitted only the safe root, omitted an unsafe mode-`0666` child, and exited `73`. The process-substitution consumer discarded that status:
```text
[test] FAIL: P4 accepted a partial created-path inventory after find failed
[test] FAIL: P4 did not report failed created-path enumeration
exit=1
```
P4 now captures the NUL-delimited walk into a temporary file, checks `find` to completion, and only then evaluates the complete inventory. A failed walk reports that enumeration failed and returns a P4 finding.
GREEN:
```text
[test] PASS: P4 rejects an incomplete created-path inventory
[test] PASS: P4 attributes the failed created-path enumeration
```
## B — deterministic TERM no-exit control
Woodpecker pipeline 2224 at the original head reported `32 passed, 2 failed`: the no-exit fixture did not exit zero or report sync success. The premise was not stale: the same fixture passed `34/34` on another filesystem.
A controlled reverse-sorted `find -print0` walk reproduced the pipeline result exactly (`32 passed, 2 failed`). Root cause: signal injection was tied to `guides/E2E-DELIVERY.md`; whether required `tools/` content remained after restore depended on filesystem enumeration order. The test was measuring path order as well as trap semantics.
The generated fixtures now damage a real target path after the snapshot is armed, self-signal immediately before the complete normal sync, and differ only in the explicit handler exit. Therefore a no-exit handler always restores, returns, runs the full sync, mutates the restored target again, and reports completion independent of walk order.
GREEN on both native and reverse-sorted enumeration:
```text
RESULT: 36 passed, 0 failed
```
Mutation sensitivity: restoring `exit 1` to the nominal no-exit fixture makes the control RED (`32 passed, 4 failed`), including failures of the zero-exit and resumed-success assertions. The control can still fail for its stated reason.
## Enumeration-class sweep
The sweep covered production enumeration in `tools/install.sh` and `packages/mosaic/framework/install.sh`, plus process-substitution consumers in the C1 shell-test surfaces. Framework installer file, operator, durable-snapshot, and pruning walks already capture and check their producer status. P4's created-path walk was the reviewed unchecked instance.
One additional order/completeness dependency was found in source acquisition: `find "$WORK_DIR" ... | head -1` hid `find` failure and selected arbitrarily when an archive produced multiple top-level directories. RED first, the extraction fake produced two roots and the lane test stopped at that new assertion with exit 1 because today's code selected one. Source acquisition now captures and checks the complete NUL-delimited walk and requires exactly one extracted root. The lane suite is green with the multiple-root rejection. No remaining production installer enumeration uses unchecked process substitution or first-row order as authority.
@@ -1,186 +0,0 @@
# #1050 — Installer P0P9 state machine and red-first fixture
## Objective
Implement C1 from the canonical greenfield-install PRD v2: a transactional P0P9 installer spine, a side-effect-free P0P8 `--check`, and a lane-parametric Debian/glibc non-root from-zero fixture. The acceptance milestone is an attributable RED on the pre-C1 installer while preserving P3 PASS.
## Authority and scope
- Canonical requirements: `jason.woltje/jarvis-brain` `docs/plans/2026-08-04-greenfield-install-blockers-PRD-v2.md`. Currency was re-derived after compaction: authenticated fetch resolved `origin/main` to `cb23e5fbc8a282fa967b93d7a134fa48d11b4bb1`; the PRD and charters are byte-identical to the previously read remote copies.
- Tracking: `mosaicstack/stack#1050` on `git.mosaicstack.dev` (author read back as `be-coder-05`).
- Historical implementation base: `origin/next` `4df478cdd150fdf8d52ea109f02ade5d85017acd`. Delivery PR #1054 targets `main` under L0's trunk-only rule; `next` remains a non-merging integration lane.
- Out of scope: PATH, skills, headless wizard/identity, activation remediation, #869 wiring, RM-02, main promotion.
- `docs/TASKS.md` is orchestrator-single-writer and is not modified by this worker.
## Plan
1. Pre-register the canonical phase/output/side-effect-free/fault-injection checks and observe RED against the base installer.
2. Commit the immutable red-first acceptance fixture before implementation.
3. Add the state-machine/journal/postcondition spine without repairing P4/P5/P8 symptoms.
4. Wire the expected-RED from-zero fixture into Woodpecker using Debian/glibc and a non-root target user.
5. Run shell/static baselines, situational container validation, code review, security review, then deliver through a PR to `next` under the coordinator-owned merge path.
## Budget
- Working estimate: 32K reasoning/output tokens.
- Hard external cap: none stated.
- Adaptation: keep implementation in shell surfaces already in scope; no package dependency install unless repository gates require it.
## Pre-registered acceptance checks
| ID | Exact case | Expected pre-fix result |
|---|---|---|
| C1-R1 | `tools/e2e-install-test.sh --lane next` in a clean Debian 12 container as uid 1001 | non-zero; P3 PASS; P4 `NOT-MEASURED / UNDECLARED`; P5/P6/P8 FAIL with own reasons |
| C1-R2 | `tools/install-state-machine.test.sh` phase table case | RED because base installer does not enumerate canonical P0P9 contracts |
| C1-R3 | side-effect-free `--check` case over a fingerprinted HOME | RED because base `--check` is version-only rather than P0P8 predicates |
| C1-R4 | fault injection after each P2…P8 | RED because base installer has no injectable durable journal/rollback state |
| C1-R5 | Docker unavailable | base harness incorrectly exits 0; replacement must fail non-zero |
| C1-R6 | lane resolution | bare checkout is forbidden; fixture must pass `--next` and assert the resolved prerelease version |
| C1-R7 | same Debian fixture with `git` absent vs present | absent: P1 FAIL while legacy installer exits 0 and sync degrades; present: P1 PASS and observed store/runtime containment 101/101 |
## Progress
- [x] Charter, doctrine, delivery/CI/QA/docs guides read and re-anchored after compaction.
- [x] Canonical PRD v2/v3 addenda and charters read from fetched `origin/main`; numbering reconciles with the TL spec. No numbering conflict found. INV-B/C/D are binding and implemented without renumbering.
- [x] Target base reachability verified with `merge-base --is-ancestor`.
- [x] Issue #1050 created and provider author read back.
- [x] Initial RED captured; TL rejected P4's repo-root count as a false RED. Four populations disagree (framework payload 1, repo root 13, sync store 101 in the fixture, W-jarvis observation 7), so C1 now requires a checkout-free declared shipped-set artifact and reports P4 `NOT-MEASURED / UNDECLARED` until C5 supplies it.
- [x] P6 strengthens #869: the two dead enforcement hooks reproduce from zero on a clean broker-less container. C1 asserts the breach but neither wires nor unwires it.
- [x] P1 false pass identified from the P4 evidence row: `git` is absent from the Debian base and was undeclared even though skill sync shells out to it. C1 adds `git` to P1; the fixture matrix preserves absent/present controls. The prior claim that web1's missing runtime skills reproduce this greenfield mechanism is withdrawn by the TL and is not carried here.
- [x] Corrected RED transcript captured and reported, including the git-present/absent controls and strict P3 PASS.
- [x] State-machine implementation complete: private pre-mutation journal/snapshot, P0P8 `--check`, P2P8 fault seam, rollback, durable manifest/journal seal, action-status persistence, safe rollback roots, and stale-projection recovery.
- [x] 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. Reviews 80 (`rev-security-02`) and 81 (`rev-974`) requested changes at `3934e03f`; their eight non-overlapping detector findings are being remediated red-first. Current remediation adds canonical-image portability, absolute P3 CLI propagation, exact expected-RED schema/cardinality, passwd-HOME binding, created-path owner/mode policy, real-action P2P8 fault injection, verified non-empty remote installer execution, and seeded secret-canary/redacted diagnostics. Both old verdicts become void when the remediation head moves and require fresh independent review.
- [x] Successor remediation for review 90 is RED-first and recorded in `docs/reports/verification/1050-successor-remediation/`: the manifest now binds the complete supported final P0 reason; P4 rejects an incomplete created-path walk instead of discarding `find` failure; and the TERM no-exit control is independent of filesystem enumeration order while retaining a proven RED mutation. Pipeline 2224's 32/2 result was a path-order-sensitive control, not evidence that the resume bug's premise became stale. The enumeration-class sweep additionally replaced order-dependent `find | head -1` source-root selection with a checked complete inventory requiring exactly one extracted root.
- [x] C1 fix round for reviews 92/93: Blocker B was completed first. RED was reproduced before implementation: a planted `{"status":"in-progress"}` record makes the complete real walk fail the suite at P2, while the same planted defect beneath a target-owned mode-0100 directory makes real `find` fail and the frozen suite falsely exit 0 with `installer next lane tests passed`. One shared helper now captures and checks the complete NUL-delimited population before testing absence across the primary and copied harness sites; grep no-match is distinct from read failure. The same attack child drives the full suite RED with a named enumeration error, while ordinary native and root/container runs pass. Blocker A then regenerated the exact installer sidecar; provider-fetch validation remains the required post-push bar. Working estimate: 16K tokens; no external hard cap; 60% context is the stop/report gate.
## Risks / blockers
- The deployed create wrappers do not expose `--dry-run`; identity preflight was performed through `pr-merge.sh --dry-run` on the same HOMELAB repo, which resolved `git.mosaicstack.dev` + `be-coder-05`. The issue create then fell back from tea to the API but provider read-back confirmed author `be-coder-05`.
- `next` is a non-merging integration lane; PR #1054 targets `main`. The old “pending promotion to main” caution dissolved when the base moved. #1050 remains open after merge and closes only after Jarvis validates the greenfield behavior.
- #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 P0P9 row each, pinned require/forbid populations, and rejects malformed/duplicate/unknown rows; shrink is a negative control.
- B4 RED: uid 1001 with a passwd HOME different from ambient HOME produced P0 PASS. P0 now binds uid, username, passwd HOME and shell and explicitly rejects root and sudo-with-inherited-HOME controls.
- B5 RED: mode-0777 CLI, mode-0644 identity, and mode-0755 credential storage passed. P3/P4/P5 now apply target owner/group plus executable/shared/private policies; framework credential storage is created 0700.
- B6 RED: fault injection only wrote `.selftest-*` files. The synthetic path was removed; the P2P8 matrix enters the normal action flow, proves an action observation occurred, injects after each real phase, and fingerprints rollback.
- B7 RED: an HTTP-200 empty body exits zero when piped to Bash. The fetched installer must now be non-empty, digest-equal to `tools/install.sh.sha256`, and that exact file is executed; failed/empty/mismatch controls are blocking and CI has a remote immutable-commit arm.
- B8 RED: raw combined command output was duplicated to terminal and `commands.log`. Both capture layers now redact before output/persistence; a seeded canary is positively emitted by the fake credential-capable registry and must remain absent from terminal, command log, npmrc, generated files and observed argv. The real greenfield fixture also scans those populations.
- Advisory code review findings are fixed: URL userinfo redaction now handles raw `@`, token-only and percent-encoded forms, repeated `:`, multiple URLs, Authorization/Basic, npm `_auth`, and Cookie headers in both capture layers; the real greenfield path positively emits its canary through `state_run_captured`; verified-fetch removes its temporary body after successful execution; and plaintext diagnostics exist only in process-substitution pipes rather than interruptible temporary files.
- 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 P0P8 rows, passwd-HOME/privilege discrimination, owner/group/mode attacks, persisted P4/P6 action failures, no synthetic fault implementation, unsafe/overlapping/symlink roots, and fatal journal initialization.
- `bash tools/install-next-lane.test.sh` passes inside `ci-base:latest`, including exact `@next` versions, immutable source fallback, source-build/archive-failure rollback, offline `--dev`, explicit refs, prerelease suffix mismatch, absolute P3 CLI propagation, secret redaction, real-action P2P8 rollback, and stale projection recovery.
- Comparator controls pass for verdict drift, unexpected exit, manifest shrink, missing phases, duplicate rows, unknown cases, and unknown kinds. Verified-fetch controls pass for successful execution and failed/empty/digest-mismatch rejection.
- `bash tools/e2e-install-test.sh --lane next --source checkout --git present` returns the required expected RED in clean Debian/glibc as uid 1001: installer P0/P1/P2/P3/P7 PASS; P4/P5/P6/P8 and P9 blocking; no `Done.` claim; checkout archive digest pinned and current framework installer exercised. `tools/verify-greenfield-expected-red.sh` converts that expected detector result into a green CI assertion and fails on any unreviewed verdict drift.
- 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.
- Review-93 RED evidence at frozen `378bc1a`: isolated positive-control full-suite exit `1` with `P2 left an in-progress transaction`; isolated permission-failure attack full-suite exit `0` with final `installer next lane tests passed`. No binary shadowing or PATH interception was used; the failure came from a real target-owned mode-0100 directory.
- Fix-round GREEN: `pnpm test:installer`, native next-lane, root/`ci-base:latest` next-lane, state-machine, verified-fetch, Bash syntax, ShellCheck, `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, and `git diff --check` pass. The direct attack child exits 1 and names `P2 fault-state enumeration failed`. All 81 Mosaic Vitest files / 1508 tests pass under an isolated npm prefix; the wider framework-shell chain reaches the pre-existing #973 Bash-line-number gate and exits 97, matching the known host-specific condition rather than this delta. Codex code review approved at 0.93 confidence with zero findings; Codex security review reported no risk at 0.96 confidence with zero findings.
- Sidecar RED-first documentation control from the repository root: `sha256sum -c tools/install.sh.sha256` exits 1 with `install.sh: FAILED open or read` because the sidecar path is relative to `tools/`. Sidecar GREEN with the command actually executed: `(cd tools && sha256sum -c install.sh.sha256)` exits 0 with `install.sh: OK`; the workflow's separate expected/actual comparison resolves `e59cb441a2f37ae9150f8eae470238e9d858a1816df93343d9784a6796676096`. This is not substituted for the required immutable provider-fetch arm at the pushed head.
## Round 3 — remote-arm downstream source binding
### Objective and constraints
- Tracking remains `mosaicstack/stack#1050`, delivery PR #1054, branch `feat/1050-install-state-machine-red-fixture`; `docs/TASKS.md` remains orchestrator-single-writer.
- Bind the remote arm's downstream stack framework/source archive to the same immutable `${CI_COMMIT_SHA}` as the digest-verified `install.sh`, while retaining `--next` and its exact resolved CLI/gateway lane-version assertion.
- RED first: the realised source commit/digest assertion must reject the existing stale `origin/next` substitution; GREEN must restore the exact expected `P6 FAIL` and pass the comparator. R7 then deletes the binding and requires RED again.
- Fenced out: the expected-RED manifest (including `P6=FAIL`), #869 activation, and #1068 digest-before-comparator sequencing.
- Push budget: one force-with-lease push pinned to provider head `ff3f0d29f1763bed44a60610d073036112e66b77`; run the queue guard first; do not poll CI after push.
- Working estimate: 14K reasoning/output tokens; no external hard token cap. Scope reduction order: reuse the existing local-source archive seam, add one realised-state assertion, avoid installer behavior changes.
### Corrected reference citation
The transferable reference is only **the container-image acquisition path in `fa-install.sh`**: resolve mutable input once to `Docker-Content-Digest`, fail closed if unresolved, render `@sha256`, then verify running images against the resolved digest. It is not a claim about USC's installer as a whole; the sibling root bundle extraction in `Install-FieldAgentOnPanel.ps1:82` is unpinned. The applicable structure is `RESOLVE-ONCE -> PIN -> FAIL-CLOSED ON RESOLUTION -> VERIFY-AFTER`, applied per acquisition path.
### Remote-arm acquisition-path census before the fix
Code-path enumeration only; execution of the complete list is **NOT MEASURED** in this round yet. Logical payload paths are counted once even when one command is invoked repeatedly or installs a dependency graph.
| # | Acquisition path taken by the arm | Pre-fix binding state | Scope disposition |
|---|---|---|---|
| 1 | Woodpecker checkout containing the workflow, fixture, sidecar, and comparator | CI commit checkout (commit-bound by runner contract) | existing arm substrate |
| 2 | `node:22-bookworm-slim` step image | mutable image tag | unpinned sibling; outside this fix fence |
| 3 | Debian package set acquired by `apt-get update/install` | repository-current, no package versions/snapshot | unpinned sibling; outside this fix fence |
| 4 | remote `tools/install.sh` | exact `${CI_COMMIT_SHA}` URL plus checked SHA-256 | in scope; already bound first hop |
| 5 | `@mosaicstack/mosaic@next` CLI package graph | mutable lane resolved to an exact top-level version, exact version installed, realised top-level version verified | lane assertion; must remain |
| 6 | `@mosaicstack/gateway@next` package graph | later re-resolution wins and is verified against itself; drift from the first value can be silently accepted | partially constrained TOCTOU sibling; outside this fix |
| 7 | `mosaicstack/stack` framework/source archive consumed by `ensure_monorepo` | `next` resolves once to `4df478cdd150fdf8d52ea109f02ade5d85017acd`, not the arm's CI commit | **in-scope defect; bind to CI commit** |
| 8 | sequential-thinking MCP package warmed via unversioned `npx -y @modelcontextprotocol/server-sequential-thinking` | mutable package resolution | unpinned sibling; outside this fix fence |
| 9 | Excalidraw npm dependency graph | shipped `package-lock.json` exact versions + registry integrity hashes; `npm install` consumes the lock | already lock/integrity-bound, separate from CI-source identity |
| 10 | canonical skills repo cloned by `mosaic-sync-skills` | mutable default branch of `mosaic/agent-skills.git` | unpinned sibling; outside this fix fence |
Pre-fix mechanism breakdown (do not blend unlike strengths): **2 / 10 intended-CI-commit-bound** (#1 checkout, #4 installer); **1 / 10 resolve-then-pin with comparison to the authoritative value** (#5 CLI); **1 / 10 partially constrained because verify-after compares #6 gateway to its later re-resolution rather than an authoritative pin**; **1 / 10 lockfile+integrity-bound** (#9 Excalidraw); **1 / 10 internally pinned to an immutable commit+digest but bound to the wrong arm identity** (#7 framework at `4df478cd`); **4 / 10 unpinned siblings** (#2, #3, #8, #10). The in-scope same-CI stack payload ratio is **1 / 2** (`install.sh` yes; framework/source no).
Resolution-failure behavior is separately stated rather than hidden in the counts. For #5/#6, a metadata-resolution failure during P1 blocks before mutation. Later package metadata/install failure enters the documented source fallback, but that fallback consumes the immutable stack archive already resolved in P2; failure to resolve or fetch that source is fatal. For #5, the P2 CLI value is authoritative and a later realised mismatch blocks P3 and rolls back. For #6, no authoritative P2 gateway value persists; a later same-suffix gateway drift can be silently accepted because the later value is compared with itself. Changing that TOCTOU path is outside this fix. The new #7 path accepts the 40-hex CI commit directly, fetches only its exact commit URL, rejects failed/empty acquisition, computes and passes the exact body's digest through the existing local-source seam, and rejects any realised manifest commit/digest mismatch without falling back to `next`.
Denominator corrections were sent to and accepted by the lane lead before implementation. Four expressly unpinned siblings make `7/10` impossible. Post-fix, the primary mechanism breakdown is **fully constrained 5 / 10** (#1, #4, #5, #7, #9), **partially constrained 1 / 10** (#6), and **unpinned 4 / 10** (#2, #3, #8, #10). The meaningful repaired ratio is **2 / 2 same-CI stack payload hops**. The fix does not increase coverage; it moves #7 from wrong identity to intended identity. Every census revision moved assurance downward under additional questioning, never upward, so the census is a lower bound on defects and an upper bound on assurance.
### Implementation plan
1. Add the realised source commit+archive-digest assertion and pass the expected immutable identity through the remote fixture; run the current stale-`next` mechanism and capture RED before changing acquisition.
2. Fetch the stack archive from the exact commit URL once, fail closed on failed/empty acquisition, compute its digest, pass that exact body through the installer's existing local-source archive seam, and verify the manifest's realised `sourceCommit` and `sourceSha256` against those values.
3. Run exact-source GREEN: require `P6 FAIL` values and a passing `next-git-present` comparator; run focused installer/baseline gates.
4. R7: delete the binding in a temporary mutant, run the same assertion to RED, then restore and re-run GREEN.
5. Independent code/security review, commit with command-scoped identity, queue guard, one force-with-lease push pinned to `ff3f0d29`; stop without CI polling and report `believed-fixed, pending jarvis validation`.
### Executed outcome
- RED-first stale substitution: fixture `rc=1`; realised source `4df478cdd150fdf8d52ea109f02ade5d85017acd` / `3e6d831efe13c3b2c0501507099d4a566af5abf877dacf85e5d7e4284d35e5c7` rejected against expected `ff3f0d29f1763bed44a60610d073036112e66b77` / `944c6db1b01b23c83169e6dc83e0d31262b1d24a2825270441745eb65c269c25`; comparator `rc=1`, 8/19 mismatches.
- Exact-source reproducibility, N=5 identical final-tree executions: every run had fixture `rc=1`, `@mosaicstack/mosaic@next=0.0.50-next.2207`, P3 PASS, realised source `ff3f0d29…` / `944c6db1…`, SOURCE-CONTROL PASS, `installer_exit=1`, `done_claims=0`, P6 FAIL, and P9 FAIL. Comparator `rc=0` occurred 4/5 (19/19 checks; action-failure reason); comparator `rc=1` occurred 1/5 (18/19; dead hooks active count `2`). The rate is the finding; no comparator verdict is claimed.
- Timings/load: runs 12 NOT MEASURED; run 3 rc1 elapsed 912s, load 7.79/7.51/8.93 -> 12.28/14.82/11.79; run 4 rc0 elapsed 938s, load 12.28/14.82/11.79 -> 9.84/15.18/13.83; run 5 rc0 elapsed 954s, load 9.84/15.18/13.83 -> 3.94/4.76/8.18. Load does not explain this sample monotonically.
- The `P6=FAIL` row remains untouched; P6 failed 5/5 while its reason signal varied. No comparator widening was performed. Checkout-source control independently exhibited the rc1/dead-hooks outcome, but its reproducibility was not separately measured.
- #869 out-of-scope finding: current `defaultSupervisorProbe` checks bundled supervisor artifact presence and a resolvable socket path, not socket existence. The 2000ms capability-probe timeout is a code-read hypothesis for variance, NOT MEASURED as causal.
- Final-tree R7 deleted the three local-source binding exports temporarily. The realised source reverted to `4df478cd…` / `3e6d831…`, SOURCE-CONTROL failed against `ff3f0d29…` / `944c6db…`, and comparator `rc=1` with 8/19 mismatches. The subject file SHA-256 was `a93113565aa69f2c6f3d792b78251021bb3bbe3f7813d5fed547ab0099fa3b98` before mutation and after restoration.
- Trust boundary: the exact-commit URL trusts the configured repository provider's authenticated commit-to-archive mapping. The computed digest pins transfer/consumption but does not authenticate against repository/TLS compromise; signed provenance remains the canonical PRD v2 §3 deferral. Initial Codex security review retained this as HIGH/CWE-494; no stronger claim or out-of-scope signing change was made.
- Final baselines: Bash syntax, ShellCheck, `pnpm test:installer`, `pnpm typecheck` (45/45), `pnpm lint` (25/25), `pnpm format:check`, and `git diff --check` pass. Codex code review APPROVE confidence 0.92 with zero findings; after explicit trust-boundary documentation, security re-review risk NONE confidence 0.96 with zero findings. The initial HIGH trust-root finding remains recorded as the signed-provenance deferral.
- Full evidence and named paths: `docs/reports/verification/1050-c1-fix-round/09-round3-source-binding.txt`.
## Round 4 — pipeline case-coverage denominator
- Pipeline 2242 at `0e2eef1c` superseded the lane lead's earlier pipeline-2229 ruling: requirements 13 were already satisfied. The temporary local P6-consumer edit started while that ruling was in flight was restored; `tools/e2e-install-test.sh`, the expected-RED manifest, the per-case verifier, and #869 remain unchanged in the final tree.
- Requirement 5 adds a pipeline-level instrument above the three per-case invocations. Expected names are derived from the manifest, markers are scoped by pipeline+workflow run, and each marker is written only after that exact per-case verifier succeeds. Every producer depends on initialization; the final step depends on the complete case/contract matrix, runs after success or failure, and requires exact expected/actual set equality rather than count equality.
- Measured firing controls: skipped arm `3/2` rc1; count inflation `3/3` rc1 with one missing and one unexpected name; stale re-initialized run `3/0` rc1; exact set `3/3` rc0; future manifest case `4/3` rc1. Codex review found and blocked two independent defects: first the missing `depends_on` graph, then comment lines being parsed as case names. Both were accepted; dependency edges are regression-asserted, blank/comments are excluded, and the focused test now consumes the production manifest directly. Final Codex code re-review APPROVE confidence 0.94 and security re-review risk NONE confidence 0.96, both with zero findings. Woodpecker strict lint, Bash syntax, ShellCheck, focused/full installer tests, typecheck, lint, Prettier, and diff check pass.
- Full evidence: `docs/reports/verification/1050-c1-fix-round/10-round4-case-coverage.txt`.
## Round 5 — arm coverage and checkout purity
- Reviews 110/111 identified two blockers at `f33bd0da`: the three-case set could not represent the fourth `greenfield-remote-installer-contract` arm, and the root `.greenfield-case-state` directory was included in checkout fixture archives. The expected-RED manifest, per-case verifier, detector, and #869 remain fenced.
- RED controls: with all three cases complete and no remote-arm identity, the old gate returned rc0 at `cases_defined=3 cases_executed=3`; the exact fixture tar selector archived `.greenfield-case-state/remote-arm.ran` once.
- The checker is generalized across exact `cases` and `arms` dimensions. A new explicit four-arm declaration includes the remote contract; all four arms mark only after successful per-case verification. With the remote omitted, cases remain 3/3 rc0 while arms report 4/3 rc1 and name `greenfield-remote-installer-contract` as missing.
- State moved beneath `.mosaic-test-work/greenfield-execution-coverage`, which the existing checkout selector excludes. The regression control proves a non-excluded root marker is archived while no `.mosaic-test-work` path is archived.
- Final review: Codex code APPROVE confidence 0.93 and security risk NONE confidence 0.96, both with zero findings. Full installer tests, typecheck 45/45, lint 25/25, format, Bash syntax, ShellCheck, Woodpecker strict lint, and diff check pass with 11G free before/after.
- Full evidence: `docs/reports/verification/1050-c1-fix-round/11-round5-arm-coverage-and-purity.txt`.
## Round 6 — executable aggregation and production archive binding
- Reviews 114/115 independently reproduced the same caller-level mutant at `df705828`: ignoring `arms_status` in the workflow still left the helper-bounded focused suite green. The production OR was correct and was not rewritten; its aggregation moved byte-for-byte into `verify-greenfield-execution-coverage-gate.sh`, which the workflow invokes directly.
- The focused suite now exercises that exact helper across the complete truth table: cases PASS/arms FAIL RED; cases FAIL/arms PASS RED; both FAIL emits both outputs and is RED; both PASS GREEN. Local ignore-arms, ignore-cases, and always-RED mutants each make the suite RED.
- Review `rev-974` separately proved that deleting only the production `e2e-install-test.sh` `.mosaic-test-work` tar exclusion left the copied-selector control green. The control now structurally binds its semantic archive test to the production checkout-archive command; the delete-production-binding mutant is RED. The fenced production installer remains byte-unchanged.
- Final review: Codex code APPROVE confidence 0.94 and security risk NONE confidence 0.97, both with zero findings. Full installer tests, typecheck 45/45, lint 25/25, format, Bash syntax, ShellCheck, Woodpecker strict lint, focused truth-table and four mutant controls, and diff check pass with 11G free before validation.
- Full evidence: `docs/reports/verification/1050-c1-fix-round/12-round6-caller-coupling-and-archive-binding.txt`.
## Round 7 — canonical Alpine/BusyBox portability
- Review 119 found the exact-head focused suite RED in the canonical Alpine `ci-base`: GNU `find -printf` is unsupported by BusyBox 1.37. The same script remained green in the Debian greenfield workflow, so greenfield success alone did not prove canonical runtime compatibility.
- Local canonical-image RED was reproduced at rc1 before remediation. Marker inventory now uses Bash nullglob/dotglob and parameter expansion, checks state-directory readability, preserves the prior regular-file/non-symlink boundary, and retains sorted exact-set comparison without GNU-only flags.
- The focused suite is green both locally and inside `git.mosaicstack.dev/mosaicstack/stack/ci-base:latest`. Canonical pipeline evidence remains pending the replacement frozen-head push; no stale pipeline is inherited.
- Final review: Codex code APPROVE confidence 0.96 and security risk NONE confidence 0.97, both with zero findings. Full installer tests, typecheck 45/45, lint 25/25, format, Bash syntax, ShellCheck, Woodpecker strict lint, dual-runtime focused tests, retained Round-6 mutants, and diff check pass with 11G free.
- Full evidence: `docs/reports/verification/1050-c1-fix-round/13-round7-busybox-portability.txt`.
## Round 8 — pipefail-safe expected-set membership
- Review 122 found `expected_names | grep -Fxq` could falsely reject a valid early-sorted marker: `grep -q` closes after its match, the upstream sorter can exit 141, and `pipefail` selects that producer failure. Independent canonical-Alpine stress measured failures at the live four-arm manifest size, so no safe small-set threshold is claimed.
- A deterministic 20,001-entry real-script mark control makes the unfixed canonical-Alpine path RED with `case is not in the expected set: a-target`. The mark path now materializes and validates the complete expected-name snapshot before applying `grep -q` via a here-string, eliminating the producer/consumer pipe while retaining fail-closed producer errors.
- The focused suite is green locally and in canonical Alpine after remediation. A separate 1,000-mark canonical-Alpine stress against the live four-arm production manifest recorded zero false failures.
- Final review: Codex code APPROVE confidence 0.96 and security risk NONE confidence 0.98, both with zero findings. Full installer tests, typecheck 45/45, lint 25/25, format, Bash syntax, ShellCheck, Woodpecker strict lint, canonical deterministic/stress controls, retained Round-6 mutants, and diff check pass with 11G free.
- Full evidence: `docs/reports/verification/1050-c1-fix-round/14-round8-pipefail-membership.txt`.
@@ -0,0 +1,99 @@
# PR merge squash message field
- **Charter:** `/home/hermes/agent-work/CHARTER-PRMERGE-MESSAGE-FIELD.md`
- **Owner:** `be-coder-08`
- **Branch:** `fix/pr-merge-message-field`
- **Base:** remote `main` / local `origin/main` at `85d2108e4ed15c744ad3b87a5b629e7b2d39405a`
- **Estate:** HOMELAB tooling shared by HOMELAB and USC
## Objective
Add an optional, identity-checked Gitea squash message to `pr-merge.sh` so genuine multi-author PRs retain non-poster branch authors without weakening hardcoded squash behavior.
## Binding requirements
1. `Do` remains hardcoded to `squash`; no provider/repository default may select merge style.
2. A verified trailer uses a PR commit's linked `author.login` and that same commit's author email. No `/users/{login}` primary-email lookup occurs. Recorded rationale: this asks only what the provider can answer.
3. A commit with `author.login` null blocks before merge, prints both the null provider fact and commit email fact, and names the escalation principal.
4. The BLOCK arm must be observed firing; a normal canonical single-author API payload remains explicit squash plus its reviewed `head_commit_id`.
5. Every provider mutation is read back from the provider; no real PR is merged during tests.
## Derived interface decisions
- Add `--co-author-trailers` rather than accepting arbitrary message text. The wrapper enumerates PR commits and constructs trailers, making an unchecked `Co-authored-by` line unexpressible.
- Require `--escalate-to PRINCIPAL` with `--co-author-trailers`, so the BLOCK diagnostic always names a principal rather than a generic role.
- Do not expose `MergeTitleField` separately. When trailers exist, set it from the provider PR title and set `MergeMessageField` only to construction-generated trailers. This preserves one provider source for the title and avoids an unrelated caller-controlled degree of freedom.
- Preserve first-commit order and emit one trailer per distinct non-poster `author.login`, using that first linked commit's own email.
## Canonical delivery plan
1. Port the capability into the installed source of truth, `packages/mosaic/framework/tools/git/pr-merge.sh`; do not retain `infra/fleet/tools/git` as a second copy.
2. Preserve canonical `--expect-head`, exact head branch/repository/SHA queue inspection, Gitea atomic head pinning, GitHub `--match-head-commit`, and delete-after-merge semantics.
3. Do not port the deployed-only `--skip-queue-guard` bypass. Add the focused harness to the canonical framework-shell suite and re-establish RED/GREEN on the packaged baseline.
4. Deliver through a reviewed package release followed by `mosaic update` with its default framework reseed. The installer snapshots, manifest-syncs framework-owned `tools/**`, and rolls back on failure.
5. Before either estate relies on the change, require installed/package hash equality, `MergeMessageField` presence, and a green focused harness. Release/reseed ownership is currently unassigned and blocks activation after source merge.
## Evidence
- RED against the byte-identical deployed baseline (`sha256 08a65e8584c5…`): rc 1 with eight named failures. The wrapper rejected `--co-author-trailers`; the null-login path emitted none of the required BLOCK facts/principal; and both verified/ordinary API paths failed the stdin-config credential assertion (ordinary path exposed the fixture token through curl argv). Log: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-message-field-red.log`.
- GREEN on the deployed-baseline candidate: verified linked multi-author payload, null-login BLOCK, required named principal, explicit squash, stdin-config token transport, and absence of `/users` lookup all passed. Log: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-message-field-green.log`.
- RED against canonical packaged baseline `c581ef48…`: rc 1 with 32 assertions. It rejects the new option, and the first harness version did not satisfy canonical head branch/repository/SHA metadata. Log: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-packaged-baseline-red.log`. The port adapts the fixture rather than weakening canonical head controls.
- Provider capability probe against `git.mosaicstack.dev`: authenticated `be-coder-08` POST to deliberately nonexistent PR `2147483647` with both message fields returned JSON HTTP 404; the unauthenticated same request returned JSON HTTP 401 (not the charter's predicted 403). The authenticated-vs-unauthenticated differential proves write authorization resolved while no mergeable subject existed. `tl-mosaic` ruled the literal non-load-bearing: preserve the observed 404/401 pair and do not manufacture a 403 case. No cause was inferred and no real PR was targeted.
- Provider-generated trailer behavior is not treated as exclusive or absent. The wrapper's VERIFIED/BLOCK decision binds each requested non-poster trailer to commit `author.login` plus that commit's email; it does not assume `MergeMessageField` is the squash's only trailer source. The poster is omitted from the constructed list because the resulting squash author already records the poster; any additional provider-generated trailer is outside this change's unmeasured mechanism.
- An early candidate SHA-256 `5de32876990e4f26920448cb3220cc7f1146d558b4dd2bc1ee1a2abee2f2cbe6` passed the initial harness, then author-side review found credential-fallback and argv-exposure defects. The live deployed wrapper was atomically restored to baseline SHA-256 `08a65e8584c52c6d41ea1c686f8b95585c21e4b37320a2447eba09359a0e02c1`; the remediated candidate remains only in the worktree.
## Remediation and current review state
1. Token and Basic Auth now use stdin curl configuration, not argv. PR title, contributor email, and the JSON payload also remain out of child argv.
2. Each credential attempt binds commit inspection and merge. A token failure during either inspection or mutation causes Basic fallback to repeat inspection before mutation; the payload pins the inspected `head_commit_id`.
3. Focused tests cover token-resolution fail-closed behavior, both HTTP-401 fallback seams, metadata/credential argv absence, null-login BLOCK, explicit squash, canonical reviewed-head binding, unchanged ordinary payload, and retained log-safe provider diagnostics. Token-resolution RED: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-token-resolution-red.log`.
4. Codex review rounds 35 requested retained provider error text, log-safe provider diagnostics, fail-closed credential fallback, stable value-option parsing, and PR-title trailer-injection prevention. These are remediated with regression assertions. A post-remediation independent review is still required.
5. **Accepted linkage limitation:** `author.login` resolution proves that the commit address maps to a registered provider account. It does not prove that the named principal authored the commit because Git author metadata is self-asserted. This gate checks attribution linkage, not authorship; commit signing is out of scope and currently unadopted. Coordinators explicitly ruled that this does not add a third state.
6. Codex's sandbox could not execute the harness because its checkout was read-only; that environmental limitation is recorded separately from host-side test results.
## Disposable provider fixture acceptance
- Use a retained scratch repository only, with two branch authors and `author != committer` on at least one commit.
- Arm A supplies a message-field trailer for one non-poster; record whether that value lands without forcing the partial-pair result into under-specified `APPENDS`/`REPLACES` labels. Demonstrate an absence control.
- Arm B includes a registered trailer for a different non-poster on a branch commit; record whether it survives or drops. Verify identity through an existing commit whose `author.login` resolves and demonstrate an absence control.
- Parse landed trailers key-agnostically with `^[A-Za-z-]+-[Bb]y:` and record generated poster pair presence/absence plus resulting poster attribution.
- Record `/users/<login>` status and raw email only as non-gating estate telemetry. Never read `active`, `visibility`, or any profile field as an identity gate.
- Use distinct principals: poster `be-coder-08`, merger `Mos`, Arm A `be-coder-07`, and Arm B `be-coder-06`. Capture every trailer-shaped line verbatim and in order. Zero trailer lines means the generator did not fire and the run is `VOID`, not evidence that either arm dropped.
- Report the same read-back evidence to `mos-claude` on socket `default` and `tl-mosaic` on socket `mosaic-fleet`. Report values rather than mechanism inferences and stop on any poster-attribution regression.
## Fixture preflight
- Retained public repository: `mosaicstack/prmerge-trailer-fixture`; PR `#1`, posted by `be-coder-08` and reserved for merge by `Mos`.
- Existing `mosaicstack/stack` commits resolve `be-coder-07` and `be-coder-06` through `author.login`; exact addresses are `[email protected]` and `[email protected]`.
- Non-gating HOMELAB telemetry for authenticated reader `be-coder-08`: `/api/v1/users/be-coder-06` returned HTTP 200 with raw `email` value `[email protected]`.
- Provider preflight showed PR commit enumeration is newest-first. A new RED test proved that deriving `head_commit_id` from the final array element selected the wrong commit. The candidate now reads `.head.sha` from the authenticated PR endpoint before enumeration, verifies it appears in the commit set, and atomically pins that SHA in the explicit squash payload. RED: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-head-order-red.log`.
- Fixture PR head `f6ba6e5105031fa21f5ff7bd8e4379d99c16e1de` has `author.login=be-coder-07`, `committer.login=be-coder-08`, and branch-message trailer `Co-authored-by: be-coder-06 <[email protected]>`.
## Fixture result
- `Mos` merged retained fixture PR `#1` through staged candidate SHA-256 `60e779a85fd13b729d859ea7c986d1e9b1641b97991611329226c1b3113ffb6e`; resulting squash commit: `3f550715d9bc716426fd355a65fe997b3a90fa7d` with one parent.
- Provider read-back: poster/commit author `be-coder-08`, committer/merger `Mos`. The run is non-void.
- Trailer-shaped lines, verbatim and in order:
1. `Co-authored-by: be-coder-07 <[email protected]>`
2. `Co-authored-by: be-coder-08 <[email protected]>`
- Arm A supplied field value (`be-coder-07`) landed. Arm B branch trailer (`be-coder-06`) dropped. Both fabricated absence controls remained absent. No `Co-committed-by:` line landed.
- The candidate payload construction explicitly excludes the poster and supplied only the Arm A `be-coder-07` line. Therefore the landed poster line was provider-generated, not candidate-composed. The raw result supports `FIELD LANDS`, `BRANCH DROPS`, and `POSTER GENERATED`; it does not support a claim that candidate code supplied the poster. Evidence: `/home/hermes/agent-work/be-coder-08/evidence/prmerge-fixture-readback.log` and the retained provider object.
- Retained fixture PR `#2` measured the N=2 shape needed by `#1030`: supplied `be-coder-07` then `be-coder-06`; both landed in that order, followed by the provider-generated poster line. No truncation or dedup occurred at N=2. Resulting squash: `39db9d13aed0…`.
## Current hold point
PR `mosaicstack/stack#1066` is open. Its first frozen head `f4b162fa…` was terminal-green in Woodpecker `mosaic` pipeline `#2225`, but that evidence becomes stale when the canonical port moves the head. The deployed wrapper remains baseline `08a65e85…`; no manual copy will occur. Canonical port tests, commit amendment, rebase, one guarded force-with-lease, exact-head CI, and new independent review remain. Even after source merge, activation remains blocked on an assigned package-release/reseed owner and installed-byte read-back.
## Security review 96 remediation
Exact reviewed predecessor head: `1ceb11058f64dd7f4a817ceb2124f980a1c4dd23`.
RED-first focused harness produced 10 named failures: all curl calls lacked size/time/connect bounds; raw ESC email reached mutation; oversized and stalled curl failures were discarded and reached mutation; nonempty Basic output with resolver rc 91 authorized mutation.
Security remediation:
- Removed the cross-principal HTTP-401 Basic fallback. Both inspection-401 and merge-401 paths now refuse without Basic resolution or mutation; `get_gitea_basic_auth` references in the merge subject are 0.
- Applied `--max-filesize`, `--max-time`, and `--connect-timeout` to all 3/3 provider curl sites and fail closed on curl transport rc at all 3/3 sites.
- Required linked email bytes to be ASCII and printable before constructing `MergeMessageField`; guarded construction sites 1/1.
GREEN: message-field, exact-head, empty-UID/API, queue branch/repository/SHA, bash syntax, ShellCheck, and diff check pass. R7 total-removal mutants went RED: email guard 3 rows; bound switches 1 row; transport-rc guards 4 rows; HTTP-401 refusal 3 rows. R7 bound: mutants prove total removal only; explicit denominators above prove site coverage.
+1 -2
View File
@@ -10,8 +10,7 @@
"clean:generated": "node scripts/clean-generated.mjs",
"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 && bash tools/verify-greenfield-execution-coverage.test.sh && bash tools/verified-installer-fetch.test.sh",
"test": "pnpm test:checkout && turbo run test",
"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"
+33 -176
View File
@@ -58,7 +58,6 @@ done
# packages/mosaic/src/framework/manifest.ts — both consume framework-manifest.txt.
# Sourcing does not run its CLI dispatch (guarded by BASH_SOURCE==$0).
# shellcheck source=tools/_lib/manifest.sh
# shellcheck disable=SC1091 # Dynamic SOURCE_DIR; the path is validated by set -e.
source "$SOURCE_DIR/tools/_lib/manifest.sh"
# Which paths a keep-mode upgrade may touch is no longer a hand-maintained
@@ -223,14 +222,12 @@ prune_durable_snapshots() {
[[ "$keep" =~ ^[0-9]+$ ]] && (( keep >= 1 )) || keep=5
list="$(mktemp)"
if ! find "$root" -maxdepth 1 -type d -name 'pre-update-*' > "$list"; then
warn "Backup pruning skipped; policy: retention cleanup is optional and a failed enumeration must preserve every existing recovery snapshot."
rm -f "$list"; return 0
fi
# Newest-first ordering needs `sort` (`-o` writes back in place — no `mv`
# dependency); if it is somehow unavailable, leave the backups untouched rather
# than risk pruning in an undefined order.
if ! LC_ALL=C sort -r -o "$list" "$list" 2>/dev/null; then
warn "Backup pruning skipped; policy: ordering failure preserves all snapshots rather than risking deletion in an undefined order."
rm -f "$list"; return 0
fi
while IFS= read -r d; do
@@ -269,11 +266,7 @@ make_durable_snapshot() {
warn "Durable snapshot skipped: cannot create backup dir $root (upgrade continues; operator files remain manifest-protected)."
return 0
fi
if ! chmod 700 "$root"; then
umask "$old_umask"
warn "Durable snapshot skipped: backup root permissions could not be made private; policy: never write operator data to an insufficiently protected location."
return 0
fi
chmod 700 "$root" 2>/dev/null || true
dir="$root/pre-update-$ts"
if [[ -e "$dir" ]]; then # same-second re-run: disambiguate
local n=1; while [[ -e "$dir-$n" ]]; do n=$((n + 1)); done; dir="$dir-$n"
@@ -288,10 +281,7 @@ make_durable_snapshot() {
if ! enumerate_operator_files "$list"; then
umask "$old_umask"
warn "Durable snapshot skipped: could not enumerate operator files (upgrade continues)."
rm -f "$list"
if ! rmdir "$dir"; then
warn "Durable snapshot cleanup left $dir in place; policy: preserve unexpected content rather than deleting it recursively."
fi
rm -f "$list"; rmdir "$dir" 2>/dev/null || true
return 0
fi
while IFS= read -r -d '' rel; do
@@ -302,18 +292,12 @@ make_durable_snapshot() {
warn "Durable snapshot: could not copy operator file '$rel' (skipped)."
continue
fi
if ! chmod 600 "$dst"; then
rm -f "$dst"
warn "Durable snapshot: copied '$rel' could not be made private and was removed; policy: do not retain an insecure recovery copy."
continue
fi
chmod 600 "$dst" 2>/dev/null || true
count=$((count + 1))
done < "$list"
rm -f "$list"
# Tighten every dir the copy created (mkdir -p already honored umask 077).
if ! find "$dir" -type d -exec chmod 700 {} +; then
warn "Durable snapshot directory permission recheck failed; policy: continue because every directory was created under umask 077, while retaining the diagnostic."
fi
# Tighten every dir the copy created (mkdir -p honors umask, but be explicit).
find "$dir" -type d -exec chmod 700 {} + 2>/dev/null || true
umask "$old_umask" # UMASK-RESTORE-NORMAL — restore before the upgrade proper resumes (see above)
DURABLE_SNAPSHOT_DIR="$dir"
ok "Durable pre-update snapshot: $count operator file(s) saved to $dir (recover with: mosaic restore --list)"
@@ -360,9 +344,7 @@ verify_operator_surface() {
continue
fi
if cp "$snap" "$cur"; then
if ! chmod 600 "$cur"; then
warn "Operator file '$rel' was restored but its mode could not be tightened to 0600; policy: preserve recovered content and require manual permission repair."
fi
chmod 600 "$cur" 2>/dev/null || true
warn "Operator file was modified by the upgrade and has been restored from the pre-update snapshot: $rel"
healed=$((healed + 1))
else
@@ -553,7 +535,7 @@ sync_framework_keep() {
# (unreadable dir) is surfaced as a warning rather than silently swallowed;
# the "directory not empty" races we tolerate are ignored via -delete's own
# rc, not by hiding stderr — so a real error is still visible to the operator.
if ! find "$dst/$root" -type d -empty -delete; then
if ! find "$dst/$root" -type d -empty -delete 2>/dev/null; then
warn "prune: could not fully sweep empty framework dirs under $root (left as-is)"
fi
done < <(manifest_subtree_roots)
@@ -599,7 +581,7 @@ run_migrations() {
MIGRATION_REMOVED_PATHS+=("bin" "rails")
if [[ -d "$TARGET_DIR/bin" ]]; then
ok "Removing legacy bin/ directory (executables now in npm CLI)"
rm -rf "${TARGET_DIR:?}/bin"
rm -rf "$TARGET_DIR/bin"
fi
# Remove old mosaic PATH entry from shell profiles
@@ -710,11 +692,9 @@ trap 'restore_snapshot; exit 1' ERR INT TERM
sync_framework
# Ensure persistent directories exist. Credentials are private material and
# must never inherit a permissive umask/default mode.
# Ensure persistent directories exist
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
@@ -726,23 +706,13 @@ chmod 0700 "$TARGET_DIR/credentials"
# by `mosaic init` from templates with user-supplied values.
reconcile_framework_files
# Ensure tool scripts are executable. These are P4 postconditions, not
# best-effort cleanup: a chmod failure leaves shipped tools unloadable.
if ! find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} +; then
fail "Could not mark shipped shell tools executable."
exit 1
fi
if ! find "$TARGET_DIR/tools/_scripts" -type f -exec chmod +x {} +; then
fail "Could not mark shipped runtime scripts executable."
exit 1
fi
# Ensure tool scripts are executable
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
find "$TARGET_DIR/tools/_scripts" -type f -exec chmod +x {} + 2>/dev/null || true
# git-credential-mosaic (per-agent Gitea identity helper) ships without a .sh
# suffix — git resolves credential helpers by exact name/path, not extension.
if [[ -f "$TARGET_DIR/tools/git/git-credential-mosaic" ]] \
&& ! chmod +x "$TARGET_DIR/tools/git/git-credential-mosaic"; then
fail "Could not mark git-credential-mosaic executable."
exit 1
fi
# suffix — git resolves credential helpers by exact name/path, not extension
# so the *.sh glob above does not cover it; chmod it explicitly.
[[ -f "$TARGET_DIR/tools/git/git-credential-mosaic" ]] && chmod +x "$TARGET_DIR/tools/git/git-credential-mosaic" 2>/dev/null || true
ok "Framework synced to $TARGET_DIR"
@@ -769,162 +739,49 @@ step "Post-install tasks"
SCRIPTS="$TARGET_DIR/tools/_scripts"
# Capture every fallible post-install command. A failure's text is surfaced and
# also appended to the parent transaction's private command log. Failure to
# write that log is fatal: continuing would recreate the false-clean diagnosis
# INV-C forbids.
record_phase_outcome() {
local phase="$1" status="$2" reason="$3"
[[ -n "${MOSAIC_INSTALL_PHASE_STATUS_FILE:-}" ]] || return 0
if ! printf '%s\t%s\t%s\n' "$phase" "$status" "$reason" >> "$MOSAIC_INSTALL_PHASE_STATUS_FILE" \
|| ! sync "$MOSAIC_INSTALL_PHASE_STATUS_FILE"; then
fail "Could not durably record $phase action outcome for the parent transaction."
exit 1
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" redacted redactor_pid capture_fd status=0 redact_status=0
shift
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 "$redacted"; } >> "$MOSAIC_INSTALL_COMMAND_LOG" \
|| ! sync "$MOSAIC_INSTALL_COMMAND_LOG"; then
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 "$redacted" >&2; fi
rm -f "$redacted"
return "$status"
}
if [[ -x "$SCRIPTS/mosaic-link-runtime-assets" ]]; then
link_args=()
[[ "$ALLOW_INACTIVE_ENFORCEMENT" == "1" ]] && link_args+=(--allow-inactive-enforcement)
if run_captured "runtime asset linking" "$SCRIPTS/mosaic-link-runtime-assets" "${link_args[@]}"; then
record_phase_outcome P6 committed "runtime asset linker exited zero"
# stdout is suppressed as before, but stderr is left connected: the
# install-ordering guard's FAIL LOUD message (#869 Point-1 C2) must reach
# the operator, not be swallowed silently.
if "$SCRIPTS/mosaic-link-runtime-assets" "${link_args[@]}" >/dev/null; then
ok "Runtime assets linked"
else
record_phase_outcome P6 failed "runtime asset linker exited non-zero"
warn "Runtime asset linking did not commit; policy: continue only to enumerate all phase diagnostics, while P6/P9 remain blocking."
warn "Runtime asset linking failed (non-fatal) — see message above for details."
fi
else
record_phase_outcome P6 failed "required runtime asset linker is missing or not executable"
warn "Runtime asset linking was not attempted; policy: a missing required linker remains a blocking P6/P9 failure."
fi
if [[ -x "$SCRIPTS/mosaic-ensure-sequential-thinking" ]]; then
if run_captured "sequential-thinking setup" "$SCRIPTS/mosaic-ensure-sequential-thinking"; then
if "$SCRIPTS/mosaic-ensure-sequential-thinking" >/dev/null 2>&1; then
ok "sequential-thinking MCP configured"
elif [[ "${MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING:-0}" == "1" ]]; then
record_phase_outcome P6 failed "sequential-thinking setup failed under diagnostic-continuation compatibility mode"
warn "sequential-thinking setup did not commit; policy: the unified installer compatibility flag allows diagnostic continuation, while P6/P9 remain blocking."
else
fail "sequential-thinking MCP setup failed (hard requirement)."
exit 1
if [[ "${MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING:-0}" == "1" ]]; then
warn "sequential-thinking MCP setup bypassed (MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1)"
else
fail "sequential-thinking MCP setup failed (hard requirement)."
exit 1
fi
fi
fi
if [[ -x "$SCRIPTS/mosaic-ensure-excalidraw" ]]; then
if run_captured "excalidraw setup" "$SCRIPTS/mosaic-ensure-excalidraw"; then
ok "excalidraw MCP configured"
else
warn "excalidraw setup did not commit; policy: optional integration failure is retained in the journal and does not define core install readiness."
fi
"$SCRIPTS/mosaic-ensure-excalidraw" >/dev/null 2>&1 && ok "excalidraw MCP configured" || warn "excalidraw MCP setup failed (non-fatal)"
fi
if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" == "1" ]]; then
record_phase_outcome P4 failed "required skills sync explicitly skipped"
warn "Skills sync was skipped; policy: diagnostic continuation is allowed, but P4/P9 cannot certify an incomplete requested framework install."
elif [[ -x "$SCRIPTS/mosaic-sync-skills" ]]; then
if run_captured "skills sync" "$SCRIPTS/mosaic-sync-skills"; then
record_phase_outcome P4 committed "skills sync exited zero"
ok "Skills synced"
else
record_phase_outcome P4 failed "skills sync exited non-zero"
warn "Skills sync did not commit; policy: continue to collect P4 diagnostics, but P4/P9 must not certify the install."
fi
else
record_phase_outcome P4 failed "required skills sync command is missing or not executable"
warn "Skills sync was not attempted; policy: a missing required sync command remains a blocking P4/P9 failure."
if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" != "1" ]] && [[ -x "$SCRIPTS/mosaic-sync-skills" ]]; then
"$SCRIPTS/mosaic-sync-skills" >/dev/null 2>&1 && ok "Skills synced" || warn "Skills sync failed (non-fatal)"
fi
if [[ -x "$SCRIPTS/mosaic-migrate-local-skills" ]]; then
if run_captured "local skills migration" "$SCRIPTS/mosaic-migrate-local-skills" --apply; then
ok "Local skills migrated"
else
record_phase_outcome P4 failed "local skills migration exited non-zero"
warn "Local skill migration did not commit; policy: preserve user content and continue diagnostics, while P4/P9 remain blocking."
fi
"$SCRIPTS/mosaic-migrate-local-skills" --apply >/dev/null 2>&1 && ok "Local skills migrated" || warn "Local skill migration failed (non-fatal)"
fi
if [[ -x "$SCRIPTS/mosaic-doctor" ]]; then
if run_captured "health audit" "$SCRIPTS/mosaic-doctor"; then
ok "Health audit passed"
else
warn "Health audit found unresolved state; policy: preserve its diagnostics and let P9 issue the authoritative failure."
fi
"$SCRIPTS/mosaic-doctor" >/dev/null 2>&1 && ok "Health audit passed" || warn "Health audit reported issues — run 'mosaic doctor' for details"
fi
# The version stamp records the successfully committed framework file sync.
# Post-install failures are carried separately into P4/P6 and cannot be erased
# by this stamp.
# Write version stamp AFTER everything succeeds
write_framework_version
# ── Summary ──────────────────────────────────────────────────
@@ -68,15 +68,8 @@ copy_claude_settings_guarded() {
guard_args+=(--allow-inactive-enforcement)
fi
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
if command -v mosaic >/dev/null 2>&1; then
if mosaic "${guard_args[@]}"; then
return 0
fi
echo "[mosaic-link] Enforcement hooks were NOT wired into $dst (see message above)." >&2
@@ -84,7 +77,7 @@ copy_claude_settings_guarded() {
return 0
fi
echo "[mosaic-link] ERROR: P3 absolute mosaic CLI unavailable — cannot confirm lease-enforcement" >&2
echo "[mosaic-link] ERROR: 'mosaic' CLI not found on PATH — 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
+469 -59
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# pr-merge.sh - Merge pull requests on Gitea or GitHub
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d]
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--co-author-trailers --escalate-to PRINCIPAL]
set -euo pipefail
@@ -14,6 +14,8 @@ MERGE_METHOD="squash"
DELETE_BRANCH=false
DRY_RUN=false
EXPECT_HEAD=""
CO_AUTHOR_TRAILERS=false
ESCALATE_TO=""
usage() {
cat <<EOF
@@ -27,12 +29,16 @@ Options:
-d, --delete-branch Delete the head branch after merge
--dry-run Run metadata/login preflight without merging
--expect-head SHA Refuse unless the PR head matches this full commit SHA
--co-author-trailers Build verified trailers from linked PR commit authors
--escalate-to NAME Named principal for an unresolved-author BLOCK
-h, --help Show this help message
Examples:
$(basename "$0") -n 42 # Merge PR #42
$(basename "$0") -n 42 -m squash # Squash merge
$(basename "$0") -n 42 -d # Squash merge and delete branch
$(basename "$0") -n 42 --expect-head 0123456789abcdef0123456789abcdef01234567
$(basename "$0") -n 42 --co-author-trailers --escalate-to tl-mosaic
EOF
exit "${1:-1}"
}
@@ -57,9 +63,25 @@ while [[ $# -gt 0 ]]; do
shift
;;
--expect-head)
if [[ $# -lt 2 ]]; then
echo "Error: --expect-head requires one full commit SHA." >&2
exit 1
fi
EXPECT_HEAD="$2"
shift 2
;;
--co-author-trailers)
CO_AUTHOR_TRAILERS=true
shift
;;
--escalate-to)
if [[ $# -lt 2 ]]; then
echo "Error: --escalate-to requires one principal name." >&2
exit 1
fi
ESCALATE_TO="$2"
shift 2
;;
-h|--help)
usage 0
;;
@@ -88,17 +110,30 @@ if [[ -n "$EXPECT_HEAD" && ! "$EXPECT_HEAD" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: --expect-head must be a full 40-character hexadecimal commit SHA." >&2
exit 1
fi
if [[ "$CO_AUTHOR_TRAILERS" == true && -z "$ESCALATE_TO" ]]; then
echo "Error: --co-author-trailers requires --escalate-to with a named principal." >&2
exit 1
fi
if [[ -n "$ESCALATE_TO" && ! "$ESCALATE_TO" =~ ^[A-Za-z0-9_.-]+$ ]]; then
echo "Error: --escalate-to must be one exact principal name." >&2
exit 1
fi
if [[ "$CO_AUTHOR_TRAILERS" != true && -n "$ESCALATE_TO" ]]; then
echo "Error: --escalate-to is valid only with --co-author-trailers." >&2
exit 1
fi
PR_METADATA="$("$SCRIPT_DIR/pr-metadata.sh" -n "$PR_NUMBER")"
BASE_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("baseRefName") or "").strip())')"
HEAD_BRANCH="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefName") or "").strip())')"
HEAD_SHA="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("headRefOid") or "").strip())')"
HEAD_REPO="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("headRepository") or ""; print((value.get("nameWithOwner") or value.get("full_name") or "") if isinstance(value, dict) else str(value).strip())')"
PR_TITLE="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("title") or "").strip())')"
PR_AUTHOR="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=json.load(sys.stdin).get("author") or ""; print((value.get("login") or "").strip() if isinstance(value, dict) else str(value).strip())')"
if [[ "$BASE_BRANCH" != "main" ]]; then
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' (found '$BASE_BRANCH')." >&2
exit 1
fi
if [[ -z "$HEAD_BRANCH" || -z "$HEAD_REPO" || ! "$HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Error: Could not resolve the PR head branch, repository, and full commit SHA for queue inspection." >&2
exit 1
@@ -122,70 +157,442 @@ PLATFORM=$(detect_platform)
OWNER=$(get_repo_owner)
REPO=$(get_repo_name)
merge_gitea_with_api() {
local host="$1" api_url token basic_auth body_file raw_code payload
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/merge"
mkdir -p "${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
body_file=$(mktemp "${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}/pr-merge-api-response.XXXXXX")
payload=$(python3 - "$HEAD_SHA" "$DELETE_BRANCH" <<'PY'
write_curl_auth_config() {
local mode="$1" credential="$2"
printf '%s' "$credential" | python3 -c '
import sys
mode = sys.argv[1]
credential = sys.stdin.read()
if not credential or any(char in credential for char in "\r\n"):
raise SystemExit(1)
escaped = credential.replace("\\", "\\\\").replace("\"", "\\\"")
if mode == "token":
print(f"header = \"Authorization: token {escaped}\"")
elif mode == "basic":
print(f"user = \"{escaped}\"")
else:
raise SystemExit(1)
' "$mode"
}
LAST_GITEA_HTTP_CODE="000"
LAST_GITEA_ERROR=""
MERGE_TEMP_DIRS=()
GITEA_CURL_MAX_BYTES="${MOSAIC_GITEA_CURL_MAX_BYTES:-1048576}"
GITEA_CURL_MAX_TIME="${MOSAIC_GITEA_CURL_MAX_TIME_SEC:-30}"
GITEA_CURL_CONNECT_TIMEOUT="${MOSAIC_GITEA_CURL_CONNECT_TIMEOUT_SEC:-10}"
for bound in "$GITEA_CURL_MAX_BYTES" "$GITEA_CURL_MAX_TIME" "$GITEA_CURL_CONNECT_TIMEOUT"; do
if [[ ! "$bound" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: Gitea curl bounds must be positive integers; refusing request." >&2
exit 1
fi
done
GITEA_CURL_BOUNDS=(
--max-filesize "$GITEA_CURL_MAX_BYTES"
--max-time "$GITEA_CURL_MAX_TIME"
--connect-timeout "$GITEA_CURL_CONNECT_TIMEOUT"
)
format_gitea_error_response() {
local response_file="$1"
python3 - "$response_file" <<'PY'
import json
import sys
head_sha, delete_branch = sys.argv[1:]
with open(sys.argv[1], "rb") as handle:
raw = handle.read(65536)
try:
response = json.loads(raw.decode("utf-8", errors="replace"))
except (UnicodeDecodeError, json.JSONDecodeError):
message = "non-JSON response omitted"
else:
if isinstance(response, dict):
message = response.get("message") or response.get("error")
if not message and response.get("errors") is not None:
message = json.dumps(response["errors"], separators=(",", ":"))
else:
message = None
if not message:
message = "JSON response contained no error message"
message = str(message)
if len(message) > 500:
message = message[:500] + "..."
print(ascii(message))
PY
}
cleanup_merge_temp_dirs() {
local path
for path in "${MERGE_TEMP_DIRS[@]}"; do
[[ -n "$path" ]] && rm -rf -- "$path"
done
}
trap cleanup_merge_temp_dirs EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
fetch_gitea_pr_head() {
local host="$1" auth_mode="$2" credential="$3" work_root="$4"
local response_file raw_code api_url auth_config curl_rc
response_file=$(mktemp "$work_root/pr-merge-pr.XXXXXX")
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}"
if ! auth_config=$(write_curl_auth_config "$auth_mode" "$credential"); then
echo "Error: Could not construct Gitea authentication config; refusing request." >&2
rm -f "$response_file"
return 1
fi
raw_code=$(curl -sS -K - "${GITEA_CURL_BOUNDS[@]}" -w '%{http_code}' -o "$response_file" \
-H "User-Agent: curl/8" "$api_url" <<<"$auth_config")
curl_rc=$?
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ "$curl_rc" -ne 0 ]]; then
LAST_GITEA_ERROR="curl transport failed (rc=$curl_rc)"
rm -f "$response_file"
return 1
fi
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$response_file")
rm -f "$response_file"
return 1
fi
if ! python3 - "$response_file" <<'PY'
import json
import re
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
pull = json.load(handle)
head = pull.get("head") if isinstance(pull, dict) else None
sha = str(head.get("sha") or "") if isinstance(head, dict) else ""
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
raise SystemExit(1)
print(sha)
PY
then
echo "Error: Gitea PR response has no valid head SHA; refusing merge." >&2
rm -f "$response_file"
return 1
fi
rm -f "$response_file"
}
fetch_gitea_pr_commits() {
local host="$1" auth_mode="$2" credential="$3" work_root="$4"
local page page_file combined_file merged_file raw_code page_count api_url auth_config curl_rc
mkdir -p "$work_root"
if ! auth_config=$(write_curl_auth_config "$auth_mode" "$credential"); then
echo "Error: Could not construct Gitea authentication config; refusing request." >&2
return 1
fi
combined_file=$(mktemp "$work_root/pr-merge-commits.XXXXXX")
printf '[]' > "$combined_file"
page=1
while true; do
page_file=$(mktemp "$work_root/pr-merge-commits-page.XXXXXX")
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/commits?limit=50&page=${page}"
raw_code=$(curl -sS -K - "${GITEA_CURL_BOUNDS[@]}" -w '%{http_code}' -o "$page_file" \
-H "User-Agent: curl/8" "$api_url" <<<"$auth_config")
curl_rc=$?
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ "$curl_rc" -ne 0 ]]; then
LAST_GITEA_ERROR="curl transport failed (rc=$curl_rc)"
rm -f "$page_file" "$combined_file"
return 1
fi
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$page_file")
rm -f "$page_file" "$combined_file"
return 1
fi
if ! page_count=$(python3 - "$page_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
page = json.load(handle)
if not isinstance(page, list):
raise SystemExit(1)
print(len(page))
PY
); then
echo "Error: Gitea PR commits response is not a JSON array; refusing merge." >&2
rm -f "$page_file" "$combined_file"
return 1
fi
merged_file=$(mktemp "$work_root/pr-merge-commits-merged.XXXXXX")
if ! python3 - "$combined_file" "$page_file" > "$merged_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
combined = json.load(handle)
with open(sys.argv[2], encoding="utf-8") as handle:
page = json.load(handle)
json.dump(combined + page, sys.stdout, separators=(",", ":"))
PY
then
echo "Error: Could not combine paginated PR commit metadata; refusing merge." >&2
rm -f "$page_file" "$combined_file" "$merged_file"
return 1
fi
mv "$merged_file" "$combined_file"
rm -f "$page_file"
if [[ "$page_count" -lt 50 ]]; then
break
fi
page=$((page + 1))
if [[ "$page" -gt 1000 ]]; then
echo "Error: PR commit pagination exceeded 1000 pages; refusing merge." >&2
rm -f "$combined_file"
return 1
fi
done
cat "$combined_file"
rm -f "$combined_file"
}
# LIMITATION: author.login resolution proves the commit address maps to a registered account.
# It does NOT prove the named principal authored the commit — git author metadata is self-asserted.
# This gate checks ATTRIBUTION LINKAGE, not AUTHORSHIP. Commit signing is out of scope and unadopted.
build_coauthor_message_fields() {
local commits_file="$1" context_file="$2" head_file="$3"
python3 - "$commits_file" "$context_file" "$head_file" <<'PY'
import json
import re
import sys
commits_path, context_path, head_path = sys.argv[1:]
with open(commits_path, encoding="utf-8") as handle:
commits = json.load(handle)
head_sha = open(head_path, encoding="utf-8").read().strip()
context_parts = open(context_path, "rb").read().split(b"\0")
if len(context_parts) != 4 or context_parts[-1] != b"":
raise SystemExit(1)
poster, title, principal = (part.decode("utf-8") for part in context_parts[:3])
if not isinstance(commits, list) or not commits:
print(
f"BLOCK: provider returned no PR commits; author identity is unmeasurable. "
f"Refusing merge; escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not poster:
print(
f"BLOCK: PR poster login is empty; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not re.fullmatch(r"[0-9a-fA-F]{40}", head_sha):
print(
f"BLOCK: inspected PR head SHA is invalid; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
seen = set()
trailers = []
head_seen = False
for item in commits:
if not isinstance(item, dict):
print(f"BLOCK: malformed PR commit metadata; escalate to named principal '{principal}'.", file=sys.stderr)
raise SystemExit(75)
sha = str(item.get("sha") or "<unknown>")
if sha == head_sha:
head_seen = True
commit = item.get("commit") if isinstance(item.get("commit"), dict) else {}
commit_author = commit.get("author") if isinstance(commit.get("author"), dict) else {}
email = str(commit_author.get("email") or "").strip()
provider_author = item.get("author") if isinstance(item.get("author"), dict) else {}
login = str(provider_author.get("login") or "").strip()
if not login:
diagnostic_email = email or "<missing>"
print(
f"BLOCK: commit {sha!r} has author.login=NULL while "
f"commit.author.email={diagnostic_email!r}; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if (
not email.isascii()
or not email.isprintable()
or not re.fullmatch(r"[A-Za-z0-9_.-]+", login)
or not re.fullmatch(r"[^<>\s]+@[^<>\s]+", email)
):
print(
f"BLOCK: commit {sha!r} has unusable linked identity "
f"author.login={login!r}, commit.author.email={email!r}; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if login == poster or login in seen:
continue
seen.add(login)
trailers.append(f"Co-authored-by: {login} <{email}>")
if not head_seen:
print(
f"BLOCK: inspected PR head is absent from commit enumeration; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not trailers:
print("{}")
raise SystemExit(0)
if not title:
print(
f"BLOCK: PR title is empty; refusing merge; escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
if not title.isprintable() or re.match(r"^[A-Za-z-]+-[Bb]y:", title):
print(
f"BLOCK: PR title is not one printable, non-trailer line; refusing merge; "
f"escalate to named principal '{principal}'.",
file=sys.stderr,
)
raise SystemExit(75)
print(json.dumps({
"MergeTitleField": title,
"MergeMessageField": "\n".join(trailers),
}, separators=(",", ":")))
PY
}
merge_gitea_api_attempt() {
local host="$1" auth_mode="$2" credential="$3"
local api_url attempt_dir body_file raw_code commits_file fields_file context_file head_file payload_file work_root attempt_rc auth_config curl_rc
LAST_GITEA_HTTP_CODE="000"
LAST_GITEA_ERROR=""
api_url="https://${host}/api/v1/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/merge"
work_root="${AGENT_WORK_ROOT:-${HOME:-/tmp}/mosaic/agent-work}"
mkdir -p "$work_root"
attempt_dir=$(mktemp -d "$work_root/pr-merge-attempt.XXXXXX")
chmod 0700 "$attempt_dir"
MERGE_TEMP_DIRS+=("$attempt_dir")
body_file=$(mktemp "$attempt_dir/api-response.XXXXXX")
fields_file=$(mktemp "$attempt_dir/message-fields.XXXXXX")
payload_file=$(mktemp "$attempt_dir/payload.XXXXXX")
printf '{}' > "$fields_file"
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
commits_file=$(mktemp "$attempt_dir/pr-merge-commits-input.XXXXXX")
context_file=$(mktemp "$attempt_dir/pr-merge-message-context.XXXXXX")
head_file=$(mktemp "$attempt_dir/pr-merge-head-input.XXXXXX")
printf '%s\0%s\0%s\0' "$PR_AUTHOR" "$PR_TITLE" "$ESCALATE_TO" > "$context_file"
if fetch_gitea_pr_head "$host" "$auth_mode" "$credential" "$attempt_dir" > "$head_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
if [[ "$(<"$head_file")" != "$HEAD_SHA" ]]; then
echo "BLOCK: authenticated PR head moved from reviewed $HEAD_SHA to $(<"$head_file"); refusing merge; escalate to named principal '$ESCALATE_TO'." >&2
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return 75
fi
if fetch_gitea_pr_commits "$host" "$auth_mode" "$credential" "$attempt_dir" > "$commits_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
if build_coauthor_message_fields "$commits_file" "$context_file" "$head_file" > "$fields_file"; then
:
else
attempt_rc=$?
rm -f "$body_file" "$fields_file" "$payload_file" "$commits_file" "$context_file" "$head_file"
return "$attempt_rc"
fi
rm -f "$commits_file" "$context_file" "$head_file"
fi
if ! python3 - "$fields_file" "$HEAD_SHA" "$DELETE_BRANCH" > "$payload_file" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
fields = json.load(handle)
head_sha, delete_branch = sys.argv[2:]
payload = {"Do": "squash", "head_commit_id": head_sha}
if delete_branch == "true":
payload["delete_branch_after_merge"] = True
payload.update(fields)
allowed = {"Do", "head_commit_id", "delete_branch_after_merge", "MergeTitleField", "MergeMessageField"}
if payload.get("Do") != "squash" or set(payload) - allowed:
raise SystemExit(1)
print(json.dumps(payload, separators=(",", ":")))
PY
)
token=$(get_gitea_token "$host" || true)
if [[ -n "$token" ]]; then
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" \
-X POST \
-H "User-Agent: curl/8" \
-H "Authorization: token $token" \
-H 'Content-Type: application/json' \
-d "$payload" \
"$api_url" || true)
if [[ "$raw_code" =~ ^2 ]]; then
rm -f "$body_file"
return 0
fi
then
rm -f "$body_file" "$fields_file" "$payload_file"
return 1
fi
rm -f "$fields_file"
basic_auth=$(get_gitea_basic_auth "$host" || true)
if [[ -n "$basic_auth" ]]; then
raw_code=$(curl -sS -w '%{http_code}' -o "$body_file" \
-X POST \
-u "$basic_auth" \
-H "User-Agent: curl/8" \
-H 'Content-Type: application/json' \
-d "$payload" \
"$api_url" || true)
if [[ "$raw_code" =~ ^2 ]]; then
rm -f "$body_file"
return 0
fi
if ! auth_config=$(write_curl_auth_config "$auth_mode" "$credential"); then
echo "Error: Could not construct Gitea authentication config; refusing request." >&2
rm -f "$body_file" "$payload_file"
return 1
fi
raw_code=$(curl -sS -K - "${GITEA_CURL_BOUNDS[@]}" -w '%{http_code}' -o "$body_file" \
-X POST -H "User-Agent: curl/8" \
-H 'Content-Type: application/json' \
--data-binary "@$payload_file" "$api_url" <<<"$auth_config")
curl_rc=$?
LAST_GITEA_HTTP_CODE="${raw_code:-000}"
if [[ "$curl_rc" -ne 0 ]]; then
LAST_GITEA_ERROR="curl transport failed (rc=$curl_rc)"
rm -f "$body_file" "$payload_file"
rm -rf -- "$attempt_dir"
return 1
fi
if [[ ! "$raw_code" =~ ^2 ]]; then
LAST_GITEA_ERROR=$(format_gitea_error_response "$body_file")
fi
rm -f "$body_file" "$payload_file"
rm -rf -- "$attempt_dir"
[[ "$raw_code" =~ ^2 ]]
}
python3 - "${raw_code:-000}" "$body_file" <<'PY' >&2
import json
import sys
code, path = sys.argv[1], sys.argv[2]
try:
with open(path, encoding="utf-8", errors="replace") as handle:
raw = handle.read(500)
data = json.loads(raw) if raw else {}
message = data.get("message") or data.get("error") or raw or "empty response"
except Exception:
try:
message = open(path, encoding="utf-8", errors="replace").read(500) or "empty response"
except Exception:
message = "unreadable response"
print(f"Error: Gitea API merge failed with HTTP {code}: {message}")
PY
rm -f "$body_file"
merge_gitea_with_api() {
local host="$1" token attempt_rc
if ! token=$(get_gitea_token "$host"); then
echo "Error: Could not resolve the required Gitea token; refusing merge without changing principals." >&2
return 1
fi
if [[ -z "$token" ]]; then
echo "Error: Required Gitea token resolved empty; refusing merge without changing principals." >&2
return 1
fi
if merge_gitea_api_attempt "$host" token "$token"; then
return 0
else
attempt_rc=$?
fi
if [[ "$attempt_rc" -eq 75 ]]; then
return 75
fi
if [[ "$LAST_GITEA_HTTP_CODE" != "401" ]]; then
echo "Error: Gitea API merge failed with the identity-bound token (HTTP ${LAST_GITEA_HTTP_CODE:-000}).${LAST_GITEA_ERROR:+ Provider response: $LAST_GITEA_ERROR}" >&2
return 1
fi
echo "Error: Gitea API rejected the identity-bound token with HTTP 401; refusing cross-principal credential fallback." >&2
return 1
}
@@ -195,11 +602,10 @@ if [[ "$DRY_RUN" == true ]]; then
echo "Error: Cannot determine host from origin remote URL" >&2
exit 1
}
TEA_LOGIN="$(get_gitea_login_for_host "$HOST" || true)"
if [[ -n "$TEA_LOGIN" ]]; then
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with tea login '$TEA_LOGIN' (base=$BASE_BRANCH, method=squash)."
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
echo "Dry run: would verify PR commit authors and merge PR #$PR_NUMBER on $HOST with authenticated Gitea API message fields (base=$BASE_BRANCH, method=squash)."
else
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with authenticated Gitea API fallback (base=$BASE_BRANCH, method=squash)."
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with the authenticated exact-head Gitea API path (base=$BASE_BRANCH, method=squash)."
fi
else
echo "Dry run: would merge PR #$PR_NUMBER on $PLATFORM (base=$BASE_BRANCH, method=squash)."
@@ -209,6 +615,10 @@ fi
case "$PLATFORM" in
github)
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
echo "Error: --co-author-trailers currently requires the Gitea REST message-field contract." >&2
exit 1
fi
cmd=(gh pr merge "$PR_NUMBER" --squash --match-head-commit "$HEAD_SHA")
[[ "$DELETE_BRANCH" == true ]] && cmd+=(--delete-branch)
"${cmd[@]}"
@@ -219,7 +629,7 @@ case "$PLATFORM" in
exit 1
}
# Gitea's API head_commit_id is an atomic compare-and-merge precondition.
# tea cannot express it, so exact-head merges use the authenticated API path.
# tea cannot express it, so every Gitea merge uses the authenticated API path.
merge_gitea_with_api "$HOST"
;;
*)
@@ -51,22 +51,23 @@ for arg in "$@"; do
prev=""
continue
fi
if [[ "$prev" == "-d" ]]; then
if [[ "$prev" == "data" ]]; then
post_data="$arg"
[[ "$post_data" == @* ]] && post_data=$(<"${post_data#@}")
prev=""
continue
fi
if [[ "$arg" == "-o" ]]; then
prev="-o"
if [[ "$prev" == "config" ]]; then
[[ "$arg" == "-" ]] && cat >/dev/null
prev=""
continue
fi
if [[ "$arg" == "-d" ]]; then
prev="-d"
continue
fi
if [[ "$arg" == "-w" ]]; then
write_code=true
fi
case "$arg" in
-o) prev="-o" ;;
-d|--data|--data-binary) prev="data" ;;
-K|--config) prev="config" ;;
-w) write_code=true ;;
esac
done
emit_response() {
local body="$1"
@@ -36,13 +36,30 @@ cat > "$WORK_DIR/gitea/curl" <<'SH'
#!/usr/bin/env bash
set -euo pipefail
payload=""
for ((i=1; i<=$#; i++)); do
if [[ "${!i}" == "-d" ]]; then
j=$((i + 1))
payload="${!j}"
fi
out_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
-d|--data|--data-binary)
payload="$2"
[[ "$payload" == @* ]] && payload=$(<"${payload#@}")
shift 2
;;
-o)
out_file="$2"
shift 2
;;
-K|--config)
[[ "$2" == "-" ]] && cat >/dev/null
shift 2
;;
-w|-X|-H)
shift 2
;;
*) shift ;;
esac
done
printf '%s' "$payload" > "${MOSAIC_MERGE_PAYLOAD_LOG:?}"
[[ -n "$out_file" ]] && printf '{}' > "$out_file"
printf '200'
SH
chmod +x "$WORK_DIR/gitea/curl"
@@ -0,0 +1,541 @@
#!/usr/bin/env bash
# Regression harness for the optional, identity-checked Gitea squash message.
set -u
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUBJECT="${MOSAIC_TEST_SUBJECT:-$SCRIPT_DIR/pr-merge.sh}"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-message-field}"
ORIG_PATH="$PATH"
failures=0
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
fail() {
echo "FAIL $1" >&2
failures=$((failures + 1))
}
make_case() {
local name="$1" case_dir
case_dir="$WORK_DIR/$name"
mkdir -p "$case_dir/bin" "$case_dir/agent"
cp "$SUBJECT" "$case_dir/pr-merge.sh"
chmod +x "$case_dir/pr-merge.sh"
cat > "$case_dir/detect-platform.sh" <<'SH'
#!/usr/bin/env bash
detect_platform() { PLATFORM=gitea; printf 'gitea\n'; }
get_repo_owner() { printf 'acme\n'; }
get_repo_name() { printf 'widgets\n'; }
get_remote_host() { printf 'git.example.test\n'; }
get_gitea_token() {
printf 'resolved\n' >> "${MOSAIC_TEST_TOKEN_RESOLUTION_LOG:?}"
if [[ "${MOSAIC_TEST_TOKEN_AVAILABLE:-true}" != "true" ]]; then
return 1
fi
printf 'fixture-token\n'
}
get_gitea_basic_auth() {
printf 'resolved\n' >> "${MOSAIC_TEST_BASIC_RESOLUTION_LOG:?}"
if [[ "${MOSAIC_TEST_BASIC_AVAILABLE:-false}" == "true" ]]; then
printf 'fixture-user:fixture-password\n'
return "${MOSAIC_TEST_BASIC_RC:-0}"
fi
return 1
}
get_gitea_login_for_host() { return 1; }
SH
cat > "$case_dir/pr-metadata.sh" <<'SH'
#!/usr/bin/env bash
if [[ "${MOSAIC_TEST_TITLE_MODE:-safe}" == "injection" ]]; then
title='Preserve authors\n\nCo-authored-by: victim <[email protected]>'
else
title='Preserve both branch authors'
fi
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
verified) head_sha=2222222222222222222222222222222222222222 ;;
null-login|unsafe-identity) head_sha=3333333333333333333333333333333333333333 ;;
single) head_sha=1111111111111111111111111111111111111111 ;;
*) echo "unknown commits mode" >&2; exit 2 ;;
esac
printf '{"number":42,"title":"%s","author":"poster","baseRefName":"main","headRefName":"feature/fixture","headRefOid":"%s","headRepository":"acme/widgets"}\n' "$title" "$head_sha"
SH
cat > "$case_dir/ci-queue-wait.sh" <<'SH'
#!/usr/bin/env bash
exit 0
SH
cat > "$case_dir/bin/python3" <<'SH'
#!/usr/bin/env bash
for arg in "$@"; do
case "$arg" in
*"Preserve both branch authors"*|*"[email protected]"*)
: > "${MOSAIC_TEST_METADATA_ARGV_MARKER:?}"
;;
esac
done
exec "${MOSAIC_TEST_REAL_PYTHON:?}" "$@"
SH
cat > "$case_dir/bin/curl" <<'SH'
#!/usr/bin/env bash
set -eu
for arg in "$@"; do
case "$arg" in
*"Preserve both branch authors"*|*"[email protected]"*)
: > "${MOSAIC_TEST_METADATA_ARGV_MARKER:?}"
;;
esac
done
url=""
method="GET"
out_file=""
data=""
config=""
auth_mode="none"
has_max_filesize=0
has_max_time=0
has_connect_timeout=0
while [[ $# -gt 0 ]]; do
case "$1" in
-o)
out_file="$2"
shift 2
;;
-w)
shift 2
;;
-X)
method="$2"
shift 2
;;
-d|--data|--data-binary)
data="$2"
if [[ "$data" == @* ]]; then
data=$(<"${data#@}")
fi
shift 2
;;
-K|--config)
if [[ "$2" == "-" ]]; then
config=$(cat)
fi
shift 2
;;
--max-filesize)
has_max_filesize=1
shift 2
;;
--max-time)
has_max_time=1
shift 2
;;
--connect-timeout)
has_connect_timeout=1
shift 2
;;
-H|--header|-u|--user)
if [[ "$2" == *"fixture-token"* ]]; then
: > "${MOSAIC_TEST_TOKEN_ARGV_MARKER:?}"
fi
if [[ "$2" == *"fixture-password"* ]]; then
: > "${MOSAIC_TEST_BASIC_ARGV_MARKER:?}"
fi
shift 2
;;
http://*|https://*)
url="$1"
shift
;;
*)
shift
;;
esac
done
if [[ "$config" == *"Authorization: token fixture-token"* ]]; then
auth_mode="token"
: > "${MOSAIC_TEST_AUTH_CONFIG_MARKER:?}"
elif [[ "$config" == *"user = \"fixture-user:fixture-password\""* ]]; then
auth_mode="basic"
: > "${MOSAIC_TEST_BASIC_CONFIG_MARKER:?}"
fi
printf '%s %s %s\n' "$method" "$auth_mode" "$url" >> "${MOSAIC_TEST_CURL_LOG:?}"
printf '%s:%s:%s\n' "$has_max_filesize" "$has_max_time" "$has_connect_timeout" >> "${MOSAIC_TEST_CURL_BOUNDS_LOG:?}"
case "$url" in
*/pulls/42)
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
verified) head_sha=2222222222222222222222222222222222222222 ;;
null-login|unsafe-identity) head_sha=3333333333333333333333333333333333333333 ;;
single) head_sha=1111111111111111111111111111111111111111 ;;
*) echo "unknown commits mode" >&2; exit 2 ;;
esac
if [[ "${MOSAIC_TEST_HEAD_MODE:-stable}" == "moved" ]]; then
head_sha=4444444444444444444444444444444444444444
fi
body="{\"head\":{\"sha\":\"$head_sha\"}}"
code=200
if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "inspection" && "$auth_mode" == "token" ]]; then
body='{"message":"token rejected"}'
code=401
fi
;;
*/pulls/42/commits*)
case "${MOSAIC_TEST_COMMITS_MODE:?}" in
verified)
if [[ "${MOSAIC_TEST_EMAIL_MODE:-safe}" == "escape" ]]; then
body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"alice+\u001b[[email protected]"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
else
body='[{"sha":"2222222222222222222222222222222222222222","commit":{"author":{"name":"Alice","email":"[email protected]"}},"author":{"login":"alice"}},{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
fi
;;
null-login)
body='[{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}},{"sha":"3333333333333333333333333333333333333333","commit":{"author":{"name":"Unresolved Author","email":"[email protected]\n\u001b[31m"}},"author":null}]'
;;
unsafe-identity)
body='[{"sha":"unsafe\n\u001b[31m","commit":{"author":{"name":"Unsafe","email":"not-an-email"}},"author":{"login":"unsafe"}},{"sha":"3333333333333333333333333333333333333333","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
;;
single)
body='[{"sha":"1111111111111111111111111111111111111111","commit":{"author":{"name":"Poster","email":"[email protected]"}},"author":{"login":"poster"}}]'
;;
*)
echo "unknown commits mode" >&2
exit 2
;;
esac
code=200
if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "inspection" && "$auth_mode" == "token" ]]; then
body='{"message":"token rejected"}'
code=401
fi
;;
*/pulls/42/merge)
body='{}'
code=200
if [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "merge" && "$auth_mode" == "token" ]]; then
body='{"message":"token rejected"}'
code=401
elif [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "provider-error" ]]; then
body='{"message":"branch policy rejected\n\u001b[31m"}'
code=409
elif [[ "${MOSAIC_TEST_FALLBACK_MODE:-none}" == "forbidden" ]]; then
body='{"message":"permission denied"}'
code=403
else
printf '%s' "$data" > "${MOSAIC_TEST_MERGE_PAYLOAD:?}"
fi
;;
*/users/*)
body='{"message":"not found"}'
code=404
;;
*)
body='{"message":"unexpected URL"}'
code=500
;;
esac
if [[ -n "$out_file" ]]; then
printf '%s' "$body" > "$out_file"
else
printf '%s' "$body"
fi
printf '%s' "$code"
case "${MOSAIC_TEST_CURL_FAILURE:-none}" in
oversize) exit 63 ;;
stalled) exit 28 ;;
esac
SH
chmod +x "$case_dir/detect-platform.sh" "$case_dir/pr-metadata.sh" \
"$case_dir/ci-queue-wait.sh" "$case_dir/bin/curl" "$case_dir/bin/python3"
printf '%s\n' "$case_dir"
}
run_case() {
local case_dir="$1" mode="$2"
shift 2
MOSAIC_TEST_COMMITS_MODE="$mode" \
MOSAIC_TEST_CURL_LOG="$case_dir/curl.log" \
MOSAIC_TEST_CURL_BOUNDS_LOG="$case_dir/curl-bounds.log" \
MOSAIC_TEST_MERGE_PAYLOAD="$case_dir/merge-payload.json" \
MOSAIC_TEST_TOKEN_ARGV_MARKER="$case_dir/token-in-argv" \
MOSAIC_TEST_BASIC_ARGV_MARKER="$case_dir/basic-in-argv" \
MOSAIC_TEST_AUTH_CONFIG_MARKER="$case_dir/auth-via-config" \
MOSAIC_TEST_BASIC_CONFIG_MARKER="$case_dir/basic-via-config" \
MOSAIC_TEST_TOKEN_RESOLUTION_LOG="$case_dir/token-resolution.log" \
MOSAIC_TEST_BASIC_RESOLUTION_LOG="$case_dir/basic-resolution.log" \
MOSAIC_TEST_METADATA_ARGV_MARKER="$case_dir/metadata-in-argv" \
MOSAIC_TEST_REAL_PYTHON="$(command -v python3)" \
AGENT_WORK_ROOT="$case_dir/agent" \
PATH="$case_dir/bin:$ORIG_PATH" \
"$case_dir/pr-merge.sh" -n 42 "$@"
}
# Verified multi-author path: the non-poster trailer is built from one commit's
# linked author.login and that same commit's author email. No /users lookup.
verified_dir=$(make_case verified)
set +e
verified_output=$(run_case "$verified_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
verified_rc=$?
set -e
if [[ "$verified_rc" -ne 0 ]]; then
fail "verified multi-author merge expected rc=0, got rc=$verified_rc: $verified_output"
elif [[ ! -s "$verified_dir/merge-payload.json" ]]; then
fail "verified multi-author merge did not reach the API payload"
else
python3 - "$verified_dir/merge-payload.json" <<'PY' || fail "verified payload did not preserve squash and exact message fields"
import json
import sys
payload = json.load(open(sys.argv[1], encoding="utf-8"))
assert payload == {
"Do": "squash",
"head_commit_id": "2222222222222222222222222222222222222222",
"MergeTitleField": "Preserve both branch authors",
"MergeMessageField": "Co-authored-by: alice <[email protected]>",
}, payload
PY
fi
[[ -e "$verified_dir/auth-via-config" ]] || fail "verified path did not authenticate curl through stdin config"
[[ ! -e "$verified_dir/token-in-argv" ]] || fail "verified path placed the Gitea token in curl argv"
[[ ! -e "$verified_dir/metadata-in-argv" ]] || fail "verified path placed PR title or contributor email in child argv"
[[ "$(wc -l < "$verified_dir/token-resolution.log")" -eq 1 ]] || fail "verified path did not bind inspection and merge to one credential resolution"
if grep -q '/users/' "$verified_dir/curl.log" 2>/dev/null; then
fail "verified path performed a forbidden second /users lookup"
fi
if grep -qv '^1:1:1$' "$verified_dir/curl-bounds.log"; then
fail "verified path did not apply size/max-time/connect-time bounds to every provider download"
fi
# A linked email containing a terminal escape must block before mutation.
escape_email_dir=$(make_case escape-email)
set +e
escape_email_output=$(MOSAIC_TEST_EMAIL_MODE=escape run_case "$escape_email_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
escape_email_rc=$?
set -e
[[ "$escape_email_rc" -ne 0 ]] || fail "control-byte email unexpectedly passed"
[[ "$escape_email_output" == *"unusable linked identity"* ]] || fail "control-byte email refusal lost its diagnostic"
[[ ! -e "$escape_email_dir/merge-payload.json" ]] || fail "control-byte email reached the merge API"
# Curl transfer and duration failures must remain failures even with HTTP 200.
for failure_mode in oversize stalled; do
failure_dir=$(make_case "curl-$failure_mode")
set +e
failure_output=$(MOSAIC_TEST_CURL_FAILURE="$failure_mode" run_case "$failure_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
failure_rc=$?
set -e
[[ "$failure_rc" -ne 0 ]] || fail "curl $failure_mode failure was discarded: $failure_output"
[[ ! -e "$failure_dir/merge-payload.json" ]] || fail "curl $failure_mode failure reached the merge API"
done
# The authenticated head is re-read under the mutation credential but cannot
# replace the canonical preflight/review head. A move blocks before enumeration
# or mutation even though the provider returned a valid new SHA.
moved_dir=$(make_case moved-head)
set +e
moved_output=$(MOSAIC_TEST_HEAD_MODE=moved \
run_case "$moved_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
moved_rc=$?
set -e
[[ "$moved_rc" -ne 0 ]] || fail "moved authenticated head unexpectedly passed"
[[ "$moved_output" == *"authenticated PR head moved from reviewed"* ]] || fail "moved head refusal lost its diagnostic"
[[ "$moved_output" == *"tl-mosaic"* ]] || fail "moved head refusal omitted the named escalation principal"
[[ ! -e "$moved_dir/merge-payload.json" ]] || fail "moved head refusal reached the merge API"
moved_sequence=$(awk '{print $1 ":" $2}' "$moved_dir/curl.log" | paste -sd, -)
[[ "$moved_sequence" == "GET:token" ]] || fail "moved head refusal performed post-move inspection/mutation (calls=$moved_sequence)"
# Token resolution failure is not an authentication response. It must fail
# closed instead of borrowing a Basic credential under a different principal.
token_missing_dir=$(make_case token-missing)
set +e
token_missing_output=$(MOSAIC_TEST_TOKEN_AVAILABLE=false MOSAIC_TEST_BASIC_AVAILABLE=true \
run_case "$token_missing_dir" single 2>&1)
token_missing_rc=$?
set -e
[[ "$token_missing_rc" -ne 0 ]] || fail "missing token unexpectedly borrowed Basic Auth"
[[ "$token_missing_output" == *"required Gitea token"* ]] || fail "missing token refusal lost its diagnostic"
[[ ! -e "$token_missing_dir/basic-resolution.log" ]] || fail "missing token resolved Basic Auth after identity failure"
[[ ! -e "$token_missing_dir/curl.log" ]] || fail "missing token reached a provider request"
# A failed Basic resolver must never use its nonempty output or reach mutation.
basic_rc_dir=$(make_case basic-resolver-rc)
set +e
basic_rc_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_BASIC_RC=91 MOSAIC_TEST_FALLBACK_MODE=inspection \
run_case "$basic_rc_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
basic_rc_rc=$?
set -e
[[ "$basic_rc_rc" -ne 0 ]] || fail "failed Basic resolver output unexpectedly authorized a merge: $basic_rc_output"
[[ ! -e "$basic_rc_dir/merge-payload.json" ]] || fail "failed Basic resolver reached the merge API"
# HTTP 401 never changes principals: inspection rejection fails closed without
# resolving or attempting Basic Auth.
fallback_inspect_dir=$(make_case fallback-inspection)
set +e
fallback_inspect_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=inspection \
run_case "$fallback_inspect_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
fallback_inspect_rc=$?
set -e
[[ "$fallback_inspect_rc" -ne 0 ]] || fail "inspection token rejection unexpectedly changed principals"
[[ "$fallback_inspect_output" == *"refusing cross-principal credential fallback"* ]] || fail "inspection token rejection lost its refusal diagnostic"
[[ ! -e "$fallback_inspect_dir/basic-resolution.log" ]] || fail "inspection token rejection resolved Basic Auth"
[[ ! -e "$fallback_inspect_dir/merge-payload.json" ]] || fail "inspection token rejection reached merge mutation"
inspect_sequence=$(awk '{print $1 ":" $2}' "$fallback_inspect_dir/curl.log" | paste -sd, -)
[[ "$inspect_sequence" == "GET:token" ]] || fail "inspection rejection made unexpected provider calls (calls=$inspect_sequence)"
# Token rejection at merge likewise fails closed without cross-principal retry.
fallback_merge_dir=$(make_case fallback-merge)
set +e
fallback_merge_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=merge \
run_case "$fallback_merge_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
fallback_merge_rc=$?
set -e
[[ "$fallback_merge_rc" -ne 0 ]] || fail "merge token rejection unexpectedly changed principals"
[[ "$fallback_merge_output" == *"refusing cross-principal credential fallback"* ]] || fail "merge token rejection lost its refusal diagnostic"
[[ ! -e "$fallback_merge_dir/basic-resolution.log" ]] || fail "merge token rejection resolved Basic Auth"
[[ ! -e "$fallback_merge_dir/merge-payload.json" ]] || fail "merge token rejection recorded a successful payload"
merge_sequence=$(awk '{print $1 ":" $2}' "$fallback_merge_dir/curl.log" | paste -sd, -)
[[ "$merge_sequence" == "GET:token,GET:token,POST:token" ]] || fail "merge rejection made unexpected provider calls (calls=$merge_sequence)"
# BLOCK path: a commit email exists but author.login is null. It must name both
# facts, name the escalation principal, and never reach the merge endpoint.
null_dir=$(make_case null-login)
set +e
null_output=$(run_case "$null_dir" null-login --co-author-trailers --escalate-to tl-mosaic 2>&1)
null_rc=$?
set -e
[[ "$null_rc" -ne 0 ]] || fail "null-login author expected a non-zero BLOCK"
[[ "$null_output" == *"BLOCK"* ]] || fail "null-login author omitted BLOCK diagnostic"
[[ "$null_output" == *"author.login=NULL"* ]] || fail "null-login author omitted the null provider fact"
[[ "$null_output" == *"[email protected]"* ]] || fail "null-login author omitted the commit email fact"
[[ "$null_output" == *'\n\x1b[31m'* ]] || fail "null-login author diagnostic did not escape control characters"
[[ "$null_output" != *$'\033'* ]] || fail "null-login author diagnostic emitted a raw terminal escape"
[[ "$(printf '%s\n' "$null_output" | wc -l)" -eq 1 ]] || fail "null-login author diagnostic permitted newline injection"
[[ "$null_output" == *"tl-mosaic"* ]] || fail "null-login author omitted the named escalation principal"
[[ ! -e "$null_dir/merge-payload.json" ]] || fail "null-login BLOCK still reached the merge API"
# Every provider-derived field in alternate BLOCK diagnostics is log-safe too,
# including an invalid non-head SHA that contains control characters.
unsafe_dir=$(make_case unsafe-identity)
set +e
unsafe_output=$(run_case "$unsafe_dir" unsafe-identity --co-author-trailers --escalate-to tl-mosaic 2>&1)
unsafe_rc=$?
set -e
[[ "$unsafe_rc" -ne 0 ]] || fail "unsafe identity expected a non-zero BLOCK"
[[ "$unsafe_output" == *"unusable linked identity"* ]] || fail "unsafe identity omitted its BLOCK reason"
[[ "$unsafe_output" == *'\n\x1b[31m'* ]] || fail "unsafe identity SHA did not escape control characters"
[[ "$unsafe_output" != *$'\033'* ]] || fail "unsafe identity diagnostic emitted a raw terminal escape"
[[ "$(printf '%s\n' "$unsafe_output" | wc -l)" -eq 1 ]] || fail "unsafe identity diagnostic permitted newline injection"
[[ ! -e "$unsafe_dir/merge-payload.json" ]] || fail "unsafe identity BLOCK still reached the merge API"
# The provider PR title cannot add an unchecked trailer outside the constructed
# message field: multi-line and trailer-shaped titles block before mutation.
title_dir=$(make_case title-injection)
set +e
title_output=$(MOSAIC_TEST_TITLE_MODE=injection \
run_case "$title_dir" verified --co-author-trailers --escalate-to tl-mosaic 2>&1)
title_rc=$?
set -e
[[ "$title_rc" -ne 0 ]] || fail "title trailer injection unexpectedly passed"
[[ "$title_output" == *"not one printable, non-trailer line"* ]] || fail "title injection refusal lost its diagnostic"
[[ ! -e "$title_dir/merge-payload.json" ]] || fail "title injection reached the merge API"
# Provider failures remain diagnosable after their temporary response file is
# removed, but provider-controlled control characters stay log-safe.
error_dir=$(make_case provider-error)
set +e
error_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=provider-error \
run_case "$error_dir" single 2>&1)
error_rc=$?
set -e
[[ "$error_rc" -ne 0 ]] || fail "provider error unexpectedly passed"
[[ "$error_output" == *"HTTP 409"* ]] || fail "provider error omitted the HTTP status"
[[ "$error_output" == *"branch policy rejected"* ]] || fail "provider error response was discarded"
[[ "$error_output" == *'\n\x1b[31m'* ]] || fail "provider error response did not escape control characters"
[[ "$error_output" != *$'\033'* ]] || fail "provider error response emitted a raw terminal escape"
[[ "$error_output" != *"Basic Auth fallback"* ]] || fail "provider error advertised removed Basic Auth fallback"
[[ ! -e "$error_dir/basic-resolution.log" ]] || fail "HTTP 409 policy denial incorrectly triggered Basic Auth fallback"
# Authorization denials likewise fail closed instead of changing principals.
forbidden_dir=$(make_case forbidden)
set +e
forbidden_output=$(MOSAIC_TEST_BASIC_AVAILABLE=true MOSAIC_TEST_FALLBACK_MODE=forbidden \
run_case "$forbidden_dir" single 2>&1)
forbidden_rc=$?
set -e
[[ "$forbidden_rc" -ne 0 ]] || fail "HTTP 403 authorization denial unexpectedly passed"
[[ "$forbidden_output" == *"HTTP 403"* ]] || fail "authorization denial omitted the HTTP status"
[[ "$forbidden_output" != *"Basic Auth fallback"* ]] || fail "authorization denial advertised removed Basic Auth fallback"
[[ ! -e "$forbidden_dir/basic-resolution.log" ]] || fail "HTTP 403 authorization denial incorrectly triggered Basic Auth fallback"
# The BLOCK destination cannot be generic or inferred after failure: opting in
# without a named principal is refused before any provider operation.
principal_dir=$(make_case missing-principal)
set +e
principal_output=$(run_case "$principal_dir" verified --co-author-trailers 2>&1)
principal_rc=$?
set -e
[[ "$principal_rc" -ne 0 ]] || fail "co-author mode without a named principal unexpectedly passed"
[[ "$principal_output" == *"requires --escalate-to with a named principal"* ]] || fail "missing-principal refusal lost its diagnostic"
[[ ! -e "$principal_dir/merge-payload.json" ]] || fail "missing-principal refusal reached the merge API"
# A trailing value-taking option receives a stable CLI diagnostic instead of a
# set -u unbound-variable crash.
value_dir=$(make_case missing-principal-value)
set +e
value_output=$(run_case "$value_dir" verified --co-author-trailers --escalate-to 2>&1)
value_rc=$?
set -e
[[ "$value_rc" -ne 0 ]] || fail "missing --escalate-to value unexpectedly passed"
[[ "$value_output" == *"--escalate-to requires one principal name"* ]] || fail "missing --escalate-to value lost its diagnostic"
[[ "$value_output" != *"unbound variable"* ]] || fail "missing --escalate-to value crashed under set -u"
[[ ! -e "$value_dir/merge-payload.json" ]] || fail "missing --escalate-to value reached the merge API"
# Negative control: ordinary single-author merge remains byte-for-byte payload
# compatible and hardcoded to squash, with no optional message fields.
single_dir=$(make_case single)
set +e
single_output=$(run_case "$single_dir" single 2>&1)
single_rc=$?
set -e
if [[ "$single_rc" -ne 0 ]]; then
fail "ordinary single-author merge expected rc=0, got rc=$single_rc: $single_output"
elif [[ ! -s "$single_dir/merge-payload.json" ]]; then
fail "ordinary single-author merge did not reach the API payload"
else
python3 - "$single_dir/merge-payload.json" <<'PY' || fail "ordinary single-author payload changed"
import json
import sys
payload = json.load(open(sys.argv[1], encoding="utf-8"))
assert payload == {
"Do": "squash",
"head_commit_id": "1111111111111111111111111111111111111111",
}, payload
PY
fi
[[ -e "$single_dir/auth-via-config" ]] || fail "ordinary path did not authenticate curl through stdin config"
[[ ! -e "$single_dir/token-in-argv" ]] || fail "ordinary path placed the Gitea token in curl argv"
[[ "$(wc -l < "$single_dir/token-resolution.log")" -eq 1 ]] || fail "ordinary path did not use exactly one credential resolution"
# Squash is not defaultable: an explicit non-squash method must remain refused.
method_dir=$(make_case method-refusal)
set +e
method_output=$(run_case "$method_dir" single -m merge 2>&1)
method_rc=$?
set -e
[[ "$method_rc" -ne 0 ]] || fail "non-squash method unexpectedly passed"
[[ "$method_output" == *"enforces squash merge only"* ]] || fail "non-squash refusal lost its policy diagnostic"
[[ ! -e "$method_dir/merge-payload.json" ]] || fail "non-squash refusal reached the merge API"
if [[ "$failures" -ne 0 ]]; then
echo "pr-merge message-field regression failed ($failures assertions)" >&2
exit 1
fi
echo "pr-merge message-field regression passed (verified, BLOCK, and unchanged squash control)"
@@ -180,16 +180,15 @@ chk "[control] without -E the mid-sync corruption survives (no rollback)" \
# ── Part C: an INT/TERM interrupt must terminate, not resume (blocker-A) ──────
# A bash signal trap that merely returns lets the script continue past the
# interrupt — restoring the snapshot, then resuming the install and reporting
# success. Generate two installer fixtures that first damage a real target path
# after the snapshot is armed, then signal their own Bash process immediately
# before the normal sync. This fixed injection point is independent of `find`
# enumeration order: after a no-exit handler restores and returns, the complete
# sync still remains to run, so the historical resume bug is deterministic on
# every filesystem. Their TERM handlers emit the same observable before
# diverging, so missing signal delivery fails BOTH arms rather than manufacturing
# a pass. The only semantic difference between fixtures is the explicit `exit 1`
# whose load-bearing behavior this control proves.
# interrupt — restoring the snapshot, then resuming the sync and reporting
# success. The earlier test used a child cp shim to signal its parent, making
# child completion race Bash's interrupted wait. Concurrency is not part of the
# guarded property: sync_framework_keep() runs in the installer's own Bash
# process, and `kill` is a builtin. Generate two installer fixtures that signal
# themselves at the same known mid-sync point. Their TERM handlers emit the same
# observable before diverging, so missing signal delivery fails BOTH arms rather
# than manufacturing a pass. The only semantic difference between fixtures is
# the explicit `exit 1` whose load-bearing behavior this control proves.
TERM_MARKER='[test-control] TERM handler entered'
HANDLER_WITH_EXIT="trap 'echo \"$TERM_MARKER\" >&2; restore_snapshot; exit 1' TERM # TEST-TERM-HANDLER"
HANDLER_WITHOUT_EXIT="trap 'echo \"$TERM_MARKER\" >&2; restore_snapshot' TERM # TEST-TERM-HANDLER"
@@ -197,17 +196,19 @@ HANDLER_WITHOUT_EXIT="trap 'echo \"$TERM_MARKER\" >&2; restore_snapshot' TERM #
make_signal_installer() {
local output="$1" handler="$2"
local target_trap="trap 'restore_snapshot; exit 1' ERR INT TERM"
local target_sync='sync_framework'
local inject_damage="printf '%s' '$GARBAGE' > \"\$TARGET_DIR/$POISON_REL\" # TEST-TERM-DAMAGE"
local inject_kill='kill -TERM "$$" # TEST-TERM-INJECTION'
local target_cp=' cp "$abs" "$dst/$rel"'
local inject_open=" if [[ \"\$rel\" == \"$POISON_REL\" ]]; then"
local inject_kill=' kill -TERM "$$" # TEST-TERM-INJECTION'
local inject_close=' fi'
if ! awk \
-v target_trap="$target_trap" -v target_sync="$target_sync" \
-v handler="$handler" -v inject_damage="$inject_damage" \
-v inject_kill="$inject_kill" '
$0 == target_sync {
print inject_damage
-v target_trap="$target_trap" -v target_cp="$target_cp" \
-v handler="$handler" -v inject_open="$inject_open" \
-v inject_kill="$inject_kill" -v inject_close="$inject_close" '
$0 == target_cp {
print inject_open
print inject_kill
print inject_close
injection_sites++
}
{ print }
@@ -230,8 +231,7 @@ make_signal_installer "$SIGNALED" "$HANDLER_WITH_EXIT"
make_signal_installer "$NOEXIT" "$HANDLER_WITHOUT_EXIT"
signal_fixture_ready() {
local fixture="$1" expected_handler="$2"
[[ "$(grep -cF '# TEST-TERM-DAMAGE' "$fixture")" -eq 1 ]] \
&& [[ "$(grep -cF '# TEST-TERM-INJECTION' "$fixture")" -eq 1 ]] \
[[ "$(grep -cF '# TEST-TERM-INJECTION' "$fixture")" -eq 1 ]] \
&& [[ "$(grep -cF '# TEST-TERM-HANDLER' "$fixture")" -eq 1 ]] \
&& grep -Fqx "$expected_handler" "$fixture"
}
@@ -261,12 +261,10 @@ run_signal_upgrade() {
IFS=$'\t' read -r rcC OUTC HC < <(run_signal_upgrade "$SIGNALED")
chk "[signal] TERM handler observable fires exactly once" \
"[ \"\$(grep -cF '$TERM_MARKER' '$OUTC')\" -eq 1 ]"
chk "[signal] SIGTERM after target mutation aborts non-zero (trap exits, does not resume)" \
chk "[signal] SIGTERM mid-sync aborts non-zero (trap exits, does not resume)" \
"[ '$rcC' -ne 0 ]"
chk "[signal] restore_snapshot fires on the interrupt" \
"grep -q 'restoring previous state from snapshot' '$OUTC'"
chk "[signal] the deliberately damaged target is restored before termination" \
"[ \"\$(cat '$HC/$POISON_REL')\" = '$GOOD' ]"
chk "[signal] does NOT resume to report sync success after the interrupt" \
"! grep -q 'file phase complete' '$OUTC'"
@@ -279,8 +277,6 @@ chk "[control] without 'exit 1' the installer exits zero after resuming" \
"[ '$rcD' -eq 0 ]"
chk "[control] without 'exit 1' the trap resumes and reports sync success (the bug)" \
"grep -q 'file phase complete' '$OUTD'"
chk "[control] the resumed full sync mutates the restored target again" \
"! grep -qxF '$GOOD' '$HD/$POISON_REL' && cmp -s '$FW/$POISON_REL' '$HD/$POISON_REL'"
# ── Part D: a failed source/prune `find` scan must abort + roll back (D1) ─────
# A `< <(find …)` process substitution discards find's exit status, so an
+1 -1
View File
@@ -25,7 +25,7 @@
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
},
"dependencies": {
"@mosaicstack/brain": "workspace:*",
+157 -428
View File
@@ -1,455 +1,184 @@
#!/usr/bin/env bash
# Greenfield installer acceptance fixture.
# ─── Mosaic Stack — End-to-End Install Test ────────────────────────────────────
#
# The fixture itself is intentionally RED until the C2-C5 phase owners repair
# their postconditions. C1's CI gate executes it and validates that the RED is
# attributable (including the discriminating P3 PASS); it does not turn the
# failed install into a false green.
# Runs a clean-container install test to verify the full first-run flow:
# tools/install.sh -> mosaic wizard (non-interactive)
# -> mosaic gateway install
# -> mosaic gateway verify
#
# Usage:
# bash tools/e2e-install-test.sh
#
# Requirements:
# - Docker (skips gracefully if not available)
# - Run from the repository root
#
# How it works:
# 1. Mounts the repository into a node:22-alpine container.
# 2. Installs prerequisites (bash, curl, jq, git) inside the container.
# 3. Runs `bash tools/install.sh --yes --no-auto-launch` to install the
# framework and CLI from the Gitea registry.
# 4. Runs `mosaic wizard --non-interactive` to set up SOUL/USER.
# 5. Runs `mosaic gateway install` with piped defaults (non-interactive).
# 6. Runs `mosaic gateway verify` and checks its exit code.
# NOTE: `mosaic gateway verify` is a new command added in the
# feat/mosaic-first-run-ux branch. If the installed CLI version
# pre-dates this branch (does not have `gateway verify`), the test
# marks this step as EXPECTED-SKIP and reports the installed version.
# 7. Reports PASS or FAIL with a summary.
#
# To run manually:
# cd /path/to/mosaic-stack
# bash tools/e2e-install-test.sh
#
# ──────────────────────────────────────────────────────────────────────────────
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LANE="${MOSAIC_INSTALL_LANE:-next}"
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:-}"
SOURCE_COMMIT="${MOSAIC_FIXTURE_SOURCE_COMMIT:-}"
SOURCE_SHA256="${MOSAIC_FIXTURE_SOURCE_SHA256:-}"
IN_CLEAN_CONTAINER="${MOSAIC_GREENFIELD_CONTAINER:-0}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
IMAGE="node:22-alpine"
CONTAINER_NAME="mosaic-e2e-install-$$"
usage() {
cat <<'EOF'
Usage: tools/e2e-install-test.sh [--lane next|main] [--source checkout|remote] [--git present|absent]
Runs the documented installer command from zero in Debian/glibc as a non-root
uid with an isolated HOME. The fixture exits non-zero when any P0-P8
postcondition fails. `next` is always selected with the --next installer flag.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--lane) LANE="${2:-}"; shift 2 ;;
--source) SOURCE="${2:-}"; shift 2 ;;
--git) GIT_MODE="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "[fixture] unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
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; }
[[ "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]] \
|| { echo '[fixture] remote source requires MOSAIC_FIXTURE_SOURCE_COMMIT=40hex' >&2; exit 2; }
[[ -z "$SOURCE_SHA256" || "$SOURCE_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|| { echo '[fixture] MOSAIC_FIXTURE_SOURCE_SHA256 must be empty or 64hex' >&2; exit 2; }
# ─── Colour helpers ───────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[0;33m' BOLD=$'\033[1m' RESET=$'\033[0m'
else
R="" G="" Y="" BOLD="" RESET=""
fi
if [[ "$IN_CLEAN_CONTAINER" != "1" ]]; then
if ! command -v docker >/dev/null 2>&1; then
echo "[fixture] FAIL: Docker is required; greenfield validation was NOT RUN." >&2
exit 2
fi
if ! docker info >/dev/null 2>&1; then
echo "[fixture] FAIL: Docker daemon is unavailable; greenfield validation was NOT RUN." >&2
exit 2
fi
info() { echo "${BOLD}[e2e]${RESET} $*"; }
ok() { echo "${G}[PASS]${RESET} $*"; }
fail() { echo "${R}[FAIL]${RESET} $*" >&2; }
warn() { echo "${Y}[WARN]${RESET} $*"; }
# ─── Docker availability check ────────────────────────────────────────────────
if ! command -v docker &>/dev/null; then
warn "Docker not found — skipping e2e install test."
warn "Install Docker and re-run this script to exercise the full install flow."
exit 0
fi
installer_b64=""
framework_payload_count="NOT-MEASURED"
repo_root_count="NOT-MEASURED"
checkout_archive=""
checkout_digest=""
checkout_content_id=""
if [[ "$SOURCE" == "checkout" ]]; then
installer_b64="$(base64 -w0 "$INSTALLER_FILE")"
[[ -d "$ROOT/packages/mosaic/framework/skills" ]] \
&& 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")"
repo_parent="$(dirname "$ROOT")"
repo_name="$(basename "$ROOT")"
tar -C "$repo_parent" \
--exclude='*/.git' --exclude='*/node_modules' --exclude='*/dist' \
--exclude='*/coverage' --exclude='*/.turbo' --exclude='*/.mosaic-test-work' \
--exclude='*/.env' --exclude='*/.env.*' \
-czf "$checkout_archive" "$repo_name"
checkout_digest="$(sha256sum "$checkout_archive" | awk '{print $1}')"
checkout_content_id="${checkout_digest:0:40}"
if ! docker info &>/dev/null 2>&1; then
warn "Docker daemon is not running or not accessible — skipping e2e install test."
exit 0
fi
inner="$(mktemp "${TMPDIR:-/tmp}/mosaic-greenfield-inner.XXXXXX")"
trap 'rm -f "$inner" "$checkout_archive"' EXIT
cat > "$inner" <<'INNER'
#!/usr/bin/env bash
set -euo pipefail
info "Docker available — proceeding with e2e install test."
info "Repo root: ${REPO_ROOT}"
info "Container image: ${IMAGE}"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
packages=(bash ca-certificates curl jq passwd python3 util-linux)
[[ "$FIXTURE_GIT_MODE" == "present" ]] && packages+=(git)
apt-get install -y -qq "${packages[@]}" >/dev/null
# ─── Inline script that runs INSIDE the container ────────────────────────────
INNER_SCRIPT="$(mktemp /tmp/mosaic-e2e-inner-XXXXXX.sh)"
trap 'rm -f "$INNER_SCRIPT"' EXIT
source_archive=/tmp/source-checkout.tar.gz
source_commit=""
source_sha256=""
expected_source_commit=""
expected_source_sha256=""
if [[ "$FIXTURE_SOURCE" == "checkout" ]]; then
awk 'found { print } /^__MOSAIC_CHECKOUT_ARCHIVE__$/ { found=1; next }' "$0" | base64 -d > "$source_archive"
source_sha256="$(sha256sum "$source_archive" | awk '{print $1}')"
if [[ "$source_sha256" != "$FIXTURE_CHECKOUT_SHA256" ]]; then
echo "[fixture] checkout archive transport digest mismatch" >&2
exit 1
fi
source_commit="$FIXTURE_CHECKOUT_CONTENT_ID"
fi
useradd --create-home --uid 1001 --shell /bin/bash mosaic
install -d -o mosaic -g mosaic /home/mosaic/work
case "$FIXTURE_SOURCE" in
checkout)
printf '%s' "$FIXTURE_INSTALLER_B64" | base64 -d > /tmp/install.sh
;;
remote)
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
}
curl -fsSL --max-filesize 268435456 \
"https://git.mosaicstack.dev/mosaicstack/stack/archive/${FIXTURE_SOURCE_COMMIT}.tar.gz" \
-o "$source_archive"
[[ -s "$source_archive" ]] || { echo '[fixture] remote source archive returned an empty HTTP-success body' >&2; exit 1; }
source_sha256="$(sha256sum "$source_archive" | awk '{print $1}')"
if [[ -n "$FIXTURE_SOURCE_SHA256" && "$source_sha256" != "$FIXTURE_SOURCE_SHA256" ]]; then
echo "[fixture] remote source archive digest mismatch got=$source_sha256 expected=$FIXTURE_SOURCE_SHA256" >&2
exit 1
fi
source_commit="$FIXTURE_SOURCE_COMMIT"
expected_source_commit="$source_commit"
expected_source_sha256="$source_sha256"
;;
esac
chmod 0755 /tmp/install.sh
sha256sum /tmp/install.sh | sed 's/^/[fixture] installer sha256: /'
cat > /tmp/run-as-target.sh <<'TARGET'
#!/usr/bin/env bash
set -uo pipefail
lane="$FIXTURE_LANE"
home="$HOME"
prefix="$home/.npm-global"
mosaic_home="$home/.config/mosaic"
install_log="$home/install.log"
failures=0
phase_pass() { printf '[%s] PASS: %s\n' "$1" "$2"; }
phase_fail() { printf '[%s] FAIL: %s\n' "$1" "$2"; failures=$((failures + 1)); }
lane_args=()
resolved_spec='@mosaicstack/mosaic'
if [[ "$lane" == "next" ]]; then
lane_args+=(--next)
resolved_spec='@mosaicstack/mosaic@next'
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 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=$?
cat > "$INNER_SCRIPT" <<'INNER_SCRIPT_EOF'
#!/bin/sh
# Bootstrap: /bin/sh until bash is installed, then re-exec.
set -e
cat "$install_log"
if [[ -n "$FIXTURE_EXPECTED_SOURCE_COMMIT" ]]; then
manifest="$mosaic_home/.install-manifest.json"
realised_source_commit="$(node -p "require('$manifest').sourceCommit || ''" 2>/dev/null || true)"
realised_source_sha256="$(node -p "require('$manifest').sourceSha256 || ''" 2>/dev/null || true)"
if [[ "$realised_source_commit" != "$FIXTURE_EXPECTED_SOURCE_COMMIT" \
|| "$realised_source_sha256" != "$FIXTURE_EXPECTED_SOURCE_SHA256" ]]; then
printf '[SOURCE-CONTROL] FAIL: realised_commit=%s expected_commit=%s realised_sha256=%s expected_sha256=%s\n' \
"${realised_source_commit:-missing}" "$FIXTURE_EXPECTED_SOURCE_COMMIT" \
"${realised_source_sha256:-missing}" "$FIXTURE_EXPECTED_SOURCE_SHA256" >&2
exit 1
fi
printf '[SOURCE-CONTROL] PASS: realised_commit=%s realised_sha256=%s\n' \
"$realised_source_commit" "$realised_source_sha256"
echo "=== [inner] Installing system prerequisites ==="
apk add --no-cache bash curl jq git 2>/dev/null || \
apt-get install -y -q bash curl jq git 2>/dev/null || true
# Re-exec under bash.
if [ -z "${BASH_VERSION:-}" ] && command -v bash >/dev/null 2>&1; then
exec bash "$0" "$@"
fi
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)"
# ── bash from here ────────────────────────────────────────────────────────────
set -euo pipefail
# P0 Resolve context. Keep this final fixture row as discriminating as the
# installer's own P0 row: the expected-RED comparator binds this exact reason.
passwd_row="$(getent passwd "$(id -u)")"
target_user="$(printf '%s' "$passwd_row" | cut -d: -f1)"
passwd_home="$(printf '%s' "$passwd_row" | cut -d: -f6)"
shell="$(printf '%s' "$passwd_row" | cut -d: -f7)"
if [[ "$(id -u)" -eq 1001 && "$target_user" == "mosaic" \
&& "$home" == "/home/mosaic" && "$home" == "$passwd_home" && "$shell" == "/bin/bash" ]] \
&& ldd --version 2>&1 | grep -i 'glibc\|gnu libc' >/dev/null \
&& [[ "$(node -p 'Number(process.versions.node.split(".")[0])')" -ge 20 ]]; then
phase_pass P0 "target=$target_user uid=$(id -u) HOME=$home passwd_HOME=$passwd_home shell=$shell privilege=user arch=$(uname -m) libc=glibc node=$(node --version) npm=$(npm --version)"
else
phase_fail P0 "context unresolved or unsupported (target=${target_user:-unknown} uid=$(id -u) HOME=$home passwd_HOME=${passwd_home:-unknown} shell=${shell:-unknown} privilege=user)"
fi
echo "=== [inner] Node.js / npm versions ==="
node --version
npm --version
# P1 Preflight
missing_tools=()
for tool in bash curl git node npm python3 tar; do
command -v "$tool" >/dev/null 2>&1 || missing_tools+=("$tool")
done
if [[ "${#missing_tools[@]}" -eq 0 && -n "$resolved_version" && -w "$home" ]]; then
phase_pass P1 "required tools present (including downstream git); target HOME writable; registry lane resolved"
else
phase_fail P1 "undeclared/missing prerequisite(s)=${missing_tools[*]:-none}; target_writable=$([[ -w "$home" ]] && echo yes || echo no) registry_resolved=$([[ -n "$resolved_version" ]] && echo yes || echo no)"
fi
echo "=== [inner] Setting up npm global prefix ==="
export NPM_PREFIX="/root/.npm-global"
mkdir -p "$NPM_PREFIX/bin"
npm config set prefix "$NPM_PREFIX" 2>/dev/null || true
export PATH="$NPM_PREFIX/bin:$PATH"
# P2 Acquire artifacts
if [[ -n "$resolved_version" ]] && grep -qF "$resolved_version" "$install_log"; then
phase_pass P2 "lane=$lane pinned_version=$resolved_version recorded in installer transcript"
else
phase_fail P2 "lane=$lane did not resolve and record a pinned artifact version"
fi
echo "=== [inner] Running install.sh --yes --no-auto-launch ==="
# Install both framework and CLI from the Gitea registry.
MOSAIC_SKIP_SKILLS_SYNC=1 \
MOSAIC_ASSUME_YES=1 \
bash /repo/tools/install.sh --yes --no-auto-launch
# P3 Install CLI — the discriminating row. Use the known absolute path only.
cli="$prefix/bin/mosaic"
cli_version=""
if [[ -x "$cli" ]]; then
cli_version="$($cli --version 2>/dev/null | tail -n 1 | tr -d '\r' || true)"
fi
if [[ -x "$cli" && "$cli_version" == "$resolved_version" ]]; then
phase_pass P3 "absolute_path=$cli version=$cli_version equals resolved lane version"
else
phase_fail P3 "absolute_path=$cli executable=$([[ -x "$cli" ]] && echo yes || echo no) got=${cli_version:-missing} expected=${resolved_version:-unresolved}"
fi
INSTALLED_VERSION="$(mosaic --version 2>/dev/null || echo 'unknown')"
echo "[inner] mosaic CLI installed: ${INSTALLED_VERSION}"
# P4 Framework + skills. C1 does not choose among the four disagreeing
# candidate populations. It requires the installer to publish a lane/versioned
# shipped-set declaration that a checkout-free install can resolve; C5 owns its
# contents. Without that artifact P4 is NOT-MEASURED, never a fabricated count.
declared_set="$mosaic_home/.install-shipped-skills.json"
sync_store_count=0
runtime_link_count=0
[[ -d "$mosaic_home/skills" ]] \
&& sync_store_count="$(find "$mosaic_home/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
[[ -d "$home/.pi/agent/skills" ]] \
&& runtime_link_count="$(find "$home/.pi/agent/skills" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) | wc -l | tr -d ' ')"
printf '[P4-EVIDENCE] candidate_populations framework_payload=%s repo_root=%s sync_store=%s jarvis_W-jarvis_observation=7 runtime_links=%s\n' \
"$FIXTURE_FRAMEWORK_PAYLOAD_COUNT" "$FIXTURE_REPO_ROOT_COUNT" "$sync_store_count" "$runtime_link_count"
if [[ ! -s "$declared_set" ]]; then
phase_fail P4 "NOT-MEASURED / UNDECLARED: installer published no checkout-free, lane/versioned shipped-set artifact at $declared_set"
elif EXPECTED_LANE="$([[ "$lane" == next ]] && echo next || echo latest)" EXPECTED_VERSION="$resolved_version" \
MOSAIC_SKILLS_ROOT="$mosaic_home/skills" node - "$declared_set" <<'NODE'
const fs = require('fs');
const path = require('path');
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const root = path.resolve(process.env.MOSAIC_SKILLS_ROOT);
if (!data || data.lane !== process.env.EXPECTED_LANE || data.version !== process.env.EXPECTED_VERSION ||
!Array.isArray(data.skills) || data.skills.length === 0) process.exit(1);
for (const name of data.skills) {
if (typeof name !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) process.exit(1);
const skill = path.join(root, name, 'SKILL.md');
let real;
try { real = fs.realpathSync(skill); } catch { process.exit(1); }
const text = fs.readFileSync(real, 'utf8');
const declaredName = text.match(/^---\s*$[\s\S]*?^name:\s*([^\s]+)\s*$/m)?.[1];
if (!real.startsWith(root + path.sep) || !fs.statSync(real).isFile() || !text || declaredName !== name) process.exit(1);
echo "=== [inner] Running mosaic wizard (non-interactive) ==="
mosaic wizard \
--non-interactive \
--name "test-agent" \
--user-name "tester" \
--pronouns "they/them" \
--timezone "UTC" || {
echo "[WARN] mosaic wizard exited non-zero — continuing"
}
NODE
then
declared_count="$(node -p "require('$declared_set').skills.length")"
if [[ -s "$mosaic_home/.install-manifest.json" ]] \
&& [[ "$(node -p "require('$mosaic_home/.install-manifest.json').phaseOutcomes?.P4 || 'committed'")" == failed ]]; then
phase_fail P4 "declared skills are present but the required framework/skills action reported failure"
else
phase_pass P4 "declared shipped-set matches lane/version and all $declared_count skill(s) are contained and loadable"
fi
echo "=== [inner] Running mosaic gateway install ==="
# Feed non-interactive answers:
# "1" → storage tier: local
# "" → port: accept default (14242)
# "" → ANTHROPIC_API_KEY: skip
# "" → CORS origin: accept default
# Then admin bootstrap: name, email, password
printf '1\n\n\n\nTest Admin\[email protected]\ntestpassword123\n' \
| mosaic gateway install
INSTALL_EXIT="$?"
if [ "${INSTALL_EXIT}" -ne 0 ]; then
echo "[ERR] mosaic gateway install exited ${INSTALL_EXIT}"
mosaic gateway status 2>/dev/null || true
exit "${INSTALL_EXIT}"
fi
echo "=== [inner] Running mosaic gateway verify ==="
# `gateway verify` was added in feat/mosaic-first-run-ux.
# If the installed version pre-dates this, skip gracefully.
if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then
echo "[SKIP] 'mosaic gateway verify' not available in installed version ${INSTALLED_VERSION}."
echo "[SKIP] This command was added in the feat/mosaic-first-run-ux release."
echo "[SKIP] Re-run after the new version is published to validate this step."
# Treat as pass — the install flow itself worked.
exit 0
fi
mosaic gateway verify
VERIFY_EXIT="$?"
echo "=== [inner] verify exit code: ${VERIFY_EXIT} ==="
exit "${VERIFY_EXIT}"
INNER_SCRIPT_EOF
chmod +x "$INNER_SCRIPT"
# ─── Pull image ───────────────────────────────────────────────────────────────
info "Pulling ${IMAGE}"
docker pull "${IMAGE}" --quiet
# ─── Run container ────────────────────────────────────────────────────────────
info "Starting container ${CONTAINER_NAME}"
EXIT_CODE=0
docker run --rm \
--name "${CONTAINER_NAME}" \
--volume "${REPO_ROOT}:/repo:ro" \
--volume "${INNER_SCRIPT}:/e2e-inner.sh:ro" \
--network host \
"${IMAGE}" \
/bin/sh /e2e-inner.sh \
|| EXIT_CODE=$?
# ─── Report ───────────────────────────────────────────────────────────────────
echo ""
if [[ "$EXIT_CODE" -eq 0 ]]; then
ok "End-to-end install test PASSED (exit ${EXIT_CODE})"
else
phase_fail P4 "shipped-set artifact is malformed, wrong-lane/version, or its declared skills are not contained and loadable"
fi
# P5 Identity
identity_ok=true
identity_reason=()
for f in SOUL.md USER.md; do
path="$mosaic_home/$f"
if [[ ! -s "$path" ]]; then
identity_ok=false; identity_reason+=("$f missing-or-empty"); continue
fi
owner="$(stat -c '%u' "$path")"; mode="$(stat -c '%a' "$path")"
if [[ "$owner" != "$(id -u)" || "$mode" =~ [2367]$ ]]; then
identity_ok=false; identity_reason+=("$f owner=$owner mode=$mode")
fi
done
if [[ "$identity_ok" == true ]]; then
phase_pass P5 "SOUL.md and USER.md are non-empty and target-user owned with non-world-writable modes"
else
phase_fail P5 "${identity_reason[*]}"
fi
# P6 Runtime linking / activation. #869 must remain unwired without its broker.
manifest="$mosaic_home/.install-manifest.json"
broker_present=false
[[ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mosaic-lease/broker.sock" ]] && broker_present=true
dead_hooks=0
if [[ -f "$home/.claude/settings.json" ]]; then
dead_hooks="$(grep -Ec 'mutator-gate\.py|receipt-observer-client\.py' "$home/.claude/settings.json" || true)"
fi
p6_action_failed=false
if [[ -s "$manifest" ]]; then
p6_action_failed="$(node -p "require('$manifest').phaseOutcomes?.P6 === 'failed' ? 'true' : 'false'" 2>/dev/null || echo true)"
fi
if [[ "$p6_action_failed" == true ]]; then
phase_fail P6 "runtime linking/activation action reported a required failure"
elif [[ "$broker_present" == false && "$dead_hooks" -eq 0 ]]; then
phase_pass P6 "broker absent and #869 enforcement hooks remain inactive"
elif [[ "$broker_present" == true ]]; then
phase_pass P6 "activation broker present; hook state is evaluable"
else
phase_fail P6 "broker absent but dead enforcement hooks are active (count=$dead_hooks)"
fi
# P7 Services — none requested by --no-auto-launch.
phase_pass P7 "no services requested by this fixture"
# P8 Shell discoverability — actual target shell, fresh login and non-login.
base_env=(env -i HOME="$home" USER=mosaic LOGNAME=mosaic SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin)
login_path="$("${base_env[@]}" /bin/bash -lc 'command -v mosaic' 2>/dev/null || true)"
nonlogin_path="$("${base_env[@]}" /bin/bash -c 'command -v mosaic' 2>/dev/null || true)"
if [[ "$login_path" == "$cli" && "$nonlogin_path" == "$cli" ]]; then
phase_pass P8 "login=$login_path nonlogin=$nonlogin_path equals P3 path"
else
phase_fail P8 "fresh bash login=${login_path:-missing} nonlogin=${nonlogin_path:-missing} expected=$cli"
fi
manifest="$mosaic_home/.install-manifest.json"
p0_p8_failures="$failures"
if [[ "$p0_p8_failures" -eq 0 && -s "$manifest" ]]; then
phase_pass P9 "P0-P8 reasserted; manifest present"
else
phase_fail P9 "P0-P8_failed_postconditions=$p0_p8_failures manifest=$([[ -s "$manifest" ]] && echo present || echo missing); install must not certify success"
fi
printf '[fixture] P0-P9_failed_rows=%d (includes P9 aggregate row)\n' "$failures"
if [[ "$failures" -ne 0 ]]; then
fail "End-to-end install test FAILED (exit ${EXIT_CODE})"
echo ""
echo " Troubleshooting:"
echo " - Review the output above for the failing step."
echo " - Re-run with bash -x tools/e2e-install-test.sh for verbose trace."
echo " - Run mosaic gateway logs inside a manual container for daemon output."
exit 1
fi
TARGET
chmod 0755 /tmp/run-as-target.sh
chown mosaic:mosaic /tmp/run-as-target.sh
exec runuser -u mosaic -- env -i \
HOME=/home/mosaic USER=mosaic LOGNAME=mosaic SHELL=/bin/bash \
PATH=/usr/local/bin:/usr/bin:/bin \
FIXTURE_LANE="$FIXTURE_LANE" \
FIXTURE_GIT_MODE="$FIXTURE_GIT_MODE" \
FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$FIXTURE_FRAMEWORK_PAYLOAD_COUNT" \
FIXTURE_REPO_ROOT_COUNT="$FIXTURE_REPO_ROOT_COUNT" \
FIXTURE_EXPECTED_SOURCE_COMMIT="$expected_source_commit" \
FIXTURE_EXPECTED_SOURCE_SHA256="$expected_source_sha256" \
MOSAIC_INSTALL_LOCAL_SOURCE_ARCHIVE="$source_archive" \
MOSAIC_INSTALL_LOCAL_SOURCE_COMMIT="$source_commit" \
MOSAIC_INSTALL_LOCAL_SOURCE_SHA256="$source_sha256" \
/bin/bash /tmp/run-as-target.sh
INNER
if [[ "$SOURCE" == "checkout" ]]; then
{
printf '\n__MOSAIC_CHECKOUT_ARCHIVE__\n'
base64 "$checkout_archive"
} >> "$inner"
fi
chmod 0755 "$inner"
printf '[fixture] platform=Debian/glibc image=%s target_uid=1001 lane=%s source=%s git=%s\n' "$IMAGE" "$LANE" "$SOURCE" "$GIT_MODE"
printf '[fixture] host inheritance: no bind mounts, no host HOME, no npm cache, no credentials\n'
if [[ "$IN_CLEAN_CONTAINER" == "1" ]]; then
# Woodpecker already supplies the clean Debian container. The target install
# still runs through runuser + env -i, so CI variables/credentials do not
# enter the target user's process.
FIXTURE_LANE="$LANE" \
FIXTURE_SOURCE="$SOURCE" \
FIXTURE_GIT_MODE="$GIT_MODE" \
FIXTURE_INSTALLER_B64="$installer_b64" \
FIXTURE_INSTALLER_URL="$INSTALLER_URL" \
FIXTURE_INSTALLER_SHA256="$INSTALLER_SHA256" \
FIXTURE_SOURCE_COMMIT="$SOURCE_COMMIT" \
FIXTURE_SOURCE_SHA256="$SOURCE_SHA256" \
FIXTURE_CHECKOUT_SHA256="$checkout_digest" \
FIXTURE_CHECKOUT_CONTENT_ID="$checkout_content_id" \
FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \
FIXTURE_REPO_ROOT_COUNT="$repo_root_count" \
/bin/bash "$inner"
else
# Copy the self-contained script+archive into a stopped container instead of
# bind-mounting the checkout or passing host paths. The target runtime still
# inherits no host HOME/cache/credentials, and the multi-megabyte checkout
# payload avoids argv/environment size limits.
fixture_cid="$(docker create \
--network bridge \
--env FIXTURE_LANE="$LANE" \
--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_SOURCE_COMMIT="$SOURCE_COMMIT" \
--env FIXTURE_SOURCE_SHA256="$SOURCE_SHA256" \
--env FIXTURE_CHECKOUT_SHA256="$checkout_digest" \
--env FIXTURE_CHECKOUT_CONTENT_ID="$checkout_content_id" \
--env FIXTURE_FRAMEWORK_PAYLOAD_COUNT="$framework_payload_count" \
--env FIXTURE_REPO_ROOT_COUNT="$repo_root_count" \
"$IMAGE" /bin/bash /tmp/mosaic-greenfield-fixture.sh)"
docker cp "$inner" "$fixture_cid:/tmp/mosaic-greenfield-fixture.sh"
set +e
docker start -a "$fixture_cid"
fixture_status=$?
set -e
docker rm "$fixture_cid" >/dev/null
exit "$fixture_status"
fi
@@ -1,5 +0,0 @@
# Required Woodpecker execution arms for C1 acceptance criterion 4.
greenfield-git-present
greenfield-main-git-present
greenfield-remote-installer-contract
greenfield-git-absent
@@ -1,57 +0,0 @@
# Pinned C1 expected-RED contract. Updating a verdict/reason requires review by the owning remediation lane.
# case kind key/value
next-git-present exit 1
next-git-present phase P0=PASS
next-git-present phase P1=PASS
next-git-present phase P2=PASS
next-git-present phase P3=PASS
next-git-present phase P4=FAIL
next-git-present phase P5=FAIL
next-git-present phase P6=FAIL
next-git-present phase P7=PASS
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 phase-reason P0=target=mosaic uid=1001 HOME=/home/mosaic passwd_HOME=/home/mosaic shell=/bin/bash privilege=user arch=x86_64 libc=glibc node=v[0-9]+\.[0-9]+\.[0-9]+ npm=[0-9]+\.[0-9]+\.[0-9]+
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: runtime linking/activation action reported a required failure$
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
main-git-present phase P2=PASS
main-git-present phase P3=PASS
main-git-present phase P4=FAIL
main-git-present phase P5=FAIL
main-git-present phase P6=FAIL
main-git-present phase P7=PASS
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 phase-reason P0=target=mosaic uid=1001 HOME=/home/mosaic passwd_HOME=/home/mosaic shell=/bin/bash privilege=user arch=x86_64 libc=glibc node=v[0-9]+\.[0-9]+\.[0-9]+ npm=[0-9]+\.[0-9]+\.[0-9]+
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: 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
next-git-absent phase P2=FAIL
next-git-absent phase P3=FAIL
next-git-absent phase P4=FAIL
next-git-absent phase P5=FAIL
next-git-absent phase P6=PASS
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 phase-reason P0=target=mosaic uid=1001 HOME=/home/mosaic passwd_HOME=/home/mosaic shell=/bin/bash privilege=user arch=x86_64 libc=glibc node=v[0-9]+\.[0-9]+\.[0-9]+ npm=[0-9]+\.[0-9]+\.[0-9]+
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\.|MOSAIC_C1_CANARY_
1 # Pinned C1 expected-RED contract. Updating a verdict/reason requires review by the owning remediation lane.
2 # case kind key/value
3 next-git-present exit 1
4 next-git-present phase P0=PASS
5 next-git-present phase P1=PASS
6 next-git-present phase P2=PASS
7 next-git-present phase P3=PASS
8 next-git-present phase P4=FAIL
9 next-git-present phase P5=FAIL
10 next-git-present phase P6=FAIL
11 next-git-present phase P7=PASS
12 next-git-present phase P8=FAIL
13 next-git-present phase P9=FAIL
14 next-git-present require ^\[fixture\] resolved lane=next .*version=[0-9]+\.[0-9]+\.[0-9]+-next\.
15 next-git-present require ^\[fixture\] installer_exit=1 done_claims=0$
16 next-git-present phase-reason P0=target=mosaic uid=1001 HOME=/home/mosaic passwd_HOME=/home/mosaic shell=/bin/bash privilege=user arch=x86_64 libc=glibc node=v[0-9]+\.[0-9]+\.[0-9]+ npm=[0-9]+\.[0-9]+\.[0-9]+
17 next-git-present require ^\[SECRET-CONTROL\] PASS:
18 next-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$
19 next-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:
20 next-git-present require ^\[P6\] FAIL: runtime linking/activation action reported a required failure$
21 next-git-present forbid Done\.|MOSAIC_C1_CANARY_|CLI not found on PATH
22 main-git-present exit 1
23 main-git-present phase P0=PASS
24 main-git-present phase P1=PASS
25 main-git-present phase P2=PASS
26 main-git-present phase P3=PASS
27 main-git-present phase P4=FAIL
28 main-git-present phase P5=FAIL
29 main-git-present phase P6=FAIL
30 main-git-present phase P7=PASS
31 main-git-present phase P8=FAIL
32 main-git-present phase P9=FAIL
33 main-git-present require ^\[fixture\] resolved lane=main .*version=[0-9]+\.[0-9]+\.[0-9]+$
34 main-git-present require ^\[fixture\] installer_exit=1 done_claims=0$
35 main-git-present phase-reason P0=target=mosaic uid=1001 HOME=/home/mosaic passwd_HOME=/home/mosaic shell=/bin/bash privilege=user arch=x86_64 libc=glibc node=v[0-9]+\.[0-9]+\.[0-9]+ npm=[0-9]+\.[0-9]+\.[0-9]+
36 main-git-present require ^\[SECRET-CONTROL\] PASS:
37 main-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$
38 main-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:
39 main-git-present require ^\[P6\] FAIL: runtime linking/activation action reported a required failure
40 main-git-present forbid Done\.|MOSAIC_C1_CANARY_|CLI not found on PATH
41 next-git-absent exit 1
42 next-git-absent phase P0=PASS
43 next-git-absent phase P1=FAIL
44 next-git-absent phase P2=FAIL
45 next-git-absent phase P3=FAIL
46 next-git-absent phase P4=FAIL
47 next-git-absent phase P5=FAIL
48 next-git-absent phase P6=PASS
49 next-git-absent phase P7=PASS
50 next-git-absent phase P8=FAIL
51 next-git-absent phase P9=FAIL
52 next-git-absent require ^\[fixture\] installer_exit=1 done_claims=0$
53 next-git-absent phase-reason P0=target=mosaic uid=1001 HOME=/home/mosaic passwd_HOME=/home/mosaic shell=/bin/bash privilege=user arch=x86_64 libc=glibc node=v[0-9]+\.[0-9]+\.[0-9]+ npm=[0-9]+\.[0-9]+\.[0-9]+
54 next-git-absent require ^\[SECRET-CONTROL\] PASS:
55 next-git-absent require ^\[P1\] FAIL: undeclared/missing prerequisite\(s\)=git;
56 next-git-absent require ^\[P3\] FAIL: .*executable=no
57 next-git-absent forbid Done\.|MOSAIC_C1_CANARY_
-635
View File
@@ -1,635 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=tools/test-enumeration-assertions.sh
source "$ROOT/tools/test-enumeration-assertions.sh"
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"
PREFIX="$HOME_DIR/prefix"
MOSAIC_HOME="$HOME_DIR/mosaic"
STATE="$TMP/state"
LOG="$TMP/npm.log"
mkdir -p "$FAKE_BIN" "$HOME_DIR" "$STATE"
if [[ "${MOSAIC_TEST_ENUMERATION_FAILURE_CHILD:-0}" != "1" ]]; then
printf '[test] enumeration failure cannot mask a planted in-progress transaction\n'
set +e
if [[ "$(/usr/bin/id -u)" -eq 0 ]]; then
enumeration_control_output="$(
su -s /bin/bash nobody -c \
"TMPDIR=/tmp MOSAIC_TEST_ENUMERATION_FAILURE_CHILD=1 bash '$0'" 2>&1
)"
enumeration_control_status=$?
else
enumeration_control_output="$(MOSAIC_TEST_ENUMERATION_FAILURE_CHILD=1 bash "$0" 2>&1)"
enumeration_control_status=$?
fi
set -e
printf '%s\n' "$enumeration_control_output" > "$TMP/enumeration-failure-control.log"
if [[ "$enumeration_control_status" -eq 0 ]]; then
echo '[test] FAIL: planted in-progress transaction plus failed enumeration passed the full suite' >&2
exit 1
fi
grep -qF '[test] ERROR: P2 fault-state enumeration failed' "$TMP/enumeration-failure-control.log" || {
echo '[test] FAIL: failed fault-state enumeration was not named' >&2
exit 1
}
fi
# 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
LOG="${MOSAIC_TEST_NPM_LOG:?}"
STATE="${MOSAIC_TEST_STATE:?}"
echo "$*" >> "$LOG"
if [[ "${1:-}" == "--version" ]]; then
echo "10.6.2"
exit 0
fi
install_cli() {
local version="$1"
echo "$version" > "$STATE/mosaic"
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"
}
if [[ "$1" == "view" ]]; then
if [[ "${MOSAIC_TEST_FAIL_NPM_VIEW:-0}" == "1" ]]; then
echo "forced registry metadata failure" >&2
exit 1
fi
case "$2 $3" in
"@mosaicstack/mosaic@next version") echo "0.0.49-next.999" ;;
"@mosaicstack/gateway@next version") echo "${MOSAIC_TEST_GATEWAY_NEXT_VERSION:-0.0.7-next.999}" ;;
"@mosaicstack/mosaic version") echo "0.0.48" ;;
*) echo "unexpected npm view: $*" >&2; exit 1 ;;
esac
exit 0
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"
;;
*"@mosaicstack/[email protected]"*)
if [[ "${MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL:-0}" == "1" ]]; then
echo "forced gateway install failure" >&2
exit 1
fi
echo "0.0.7-next.999" > "$STATE/gateway"
;;
*"mosaicstack-mosaic-0.0.0-source.tgz"*)
install_cli "0.0.0-source"
;;
*"mosaicstack-gateway-0.0.0-source.tgz"*)
echo "0.0.0-source" > "$STATE/gateway"
;;
*) echo "unexpected npm install: $*" >&2; exit 1 ;;
esac
exit 0
fi
if [[ "$1" == "ls" ]]; then
cli="$(cat "$STATE/mosaic" 2>/dev/null || true)"
gateway="$(cat "$STATE/gateway" 2>/dev/null || true)"
node -e '
const cli = process.argv[1];
const gateway = process.argv[2];
const dependencies = {};
if (cli) dependencies["@mosaicstack/mosaic"] = { version: cli };
if (gateway) dependencies["@mosaicstack/gateway"] = { version: gateway };
process.stdout.write(JSON.stringify({ dependencies }));
' "$cli" "$gateway"
exit 0
fi
echo "unexpected npm command: $*" >&2
exit 1
FAKE_NPM
chmod +x "$FAKE_BIN/npm"
cat > "$FAKE_BIN/curl" <<'FAKE_CURL'
#!/usr/bin/env bash
set -euo pipefail
headers=""; output=""; url=""
while [[ $# -gt 0 ]]; do
case "$1" in
-D) headers="$2"; shift 2 ;;
-o) output="$2"; shift 2 ;;
--max-filesize) shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
case "$url" in
*/api/v1/repos/mosaicstack/stack/commits?sha=*)
printf 'HTTP/1.1 200 OK\r\ncontent-type: application/json; charset=utf-8\r\n\r\n' > "$headers"
printf '[{"sha":"1111111111111111111111111111111111111111"}]\n' > "$output"
;;
*/archive/*.tar.gz)
if [[ "${MOSAIC_TEST_CORRUPT_ARCHIVE:-0}" == "1" ]]; then
printf 'not-a-tarball\n' > "$output"
else
archive_root="$(mktemp -d)"
mkdir -p "$archive_root/stack"
printf 'fixture\n' > "$archive_root/stack/.fixture"
/bin/tar czf "$output" -C "$archive_root" stack
rm -rf "$archive_root"
fi
;;
esac
FAKE_CURL
chmod +x "$FAKE_BIN/curl"
cat > "$FAKE_BIN/tar" <<'FAKE_TAR'
#!/usr/bin/env bash
set -euo pipefail
dest=""; list=false
while [[ $# -gt 0 ]]; do
case "$1" in
-C) dest="$2"; shift 2 ;;
-*t*|t*) list=true; shift ;;
*) shift ;;
esac
done
[[ "$list" == true ]] && exit 0
if [[ -z "$dest" ]]; then
echo "fake tar missing -C destination" >&2
exit 1
fi
mkdir -p "$dest/stack/packages/mosaic/framework" "$dest/stack/apps/gateway"
if [[ "${MOSAIC_TEST_EXTRA_ARCHIVE_ROOT:-0}" == "1" ]]; then
mkdir -p "$dest/unexpected-second-root"
fi
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"
cat > "$FAKE_BIN/pnpm" <<'FAKE_PNPM'
#!/usr/bin/env bash
set -euo pipefail
LOG="${MOSAIC_TEST_NPM_LOG:?}"
echo "pnpm $*" >> "$LOG"
if [[ "$1" == "pack" ]]; then
out=""
while [[ $# -gt 0 ]]; do
case "$1" in
--pack-destination) out="$2"; shift 2 ;;
*) shift ;;
esac
done
if [[ -z "$out" ]]; then
echo "fake pnpm pack missing destination" >&2
exit 1
fi
mkdir -p "$out"
case "$PWD" in
*/apps/gateway) touch "$out/mosaicstack-gateway-0.0.0-source.tgz" ;;
*/packages/mosaic) touch "$out/mosaicstack-mosaic-0.0.0-source.tgz" ;;
*) echo "unexpected pnpm pack cwd: $PWD" >&2; exit 1 ;;
esac
exit 0
fi
if [[ "${MOSAIC_TEST_FAIL_PNPM_INSTALL:-0}" == "1" && "$1" == "install" ]]; then
echo "forced pnpm install failure" >&2
exit 42
fi
# Other install/build commands are no-ops in this harness.
exit 0
FAKE_PNPM
chmod +x "$FAKE_BIN/pnpm"
reset_state() {
: > "$LOG"
rm -f "$STATE"/*
}
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="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch
)"
grep -qF 'Installed @next packages: CLI 0.0.49-next.999, gateway 0.0.7-next.999' <<<"$OUTPUT"
grep -qF 'install -g @mosaicstack/[email protected]' "$LOG"
grep -qF 'install -g @mosaicstack/[email protected]' "$LOG"
if grep -qE '^install -g .+@next( |$)' "$LOG"; then
echo "expected exact-version installs, found mutable @next install" >&2
exit 1
fi
if grep -qF 'Downloading source ref next at pinned commit' <<<"$OUTPUT"; then
echo "fast path unexpectedly fell back to source" >&2
exit 1
fi
ACTIVE="$HOME_DIR/.local/state/mosaic/install/active.json"
[[ "$(node -p "require('$ACTIVE').status")" == "committed" ]]
JOURNAL="$(node -p "require('$ACTIVE').journal")"
[[ "$(stat -c '%a' "$JOURNAL")" == "444" ]]
( cd "$(dirname "$JOURNAL")" && sha256sum -c "$(basename "$JOURNAL").sha256" >/dev/null )
grep -q '"event":"mutation".*"phase":"P3".*path=.*prior=.*reverse=' "$JOURNAL"
reset_state
echo "[test] fast path failure falls back to source build"
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch
)"
grep -qF 'Fast gateway @next install failed.' <<<"$OUTPUT"
grep -qF 'Falling back to source build at ref next; --next will not hard-fail on registry issues.' <<<"$OUTPUT"
grep -qF 'Downloading source ref next at pinned commit 1111111111111111111111111111111111111111' <<<"$OUTPUT"
grep -qF 'Installed from source: CLI 0.0.0-source' <<<"$OUTPUT"
grep -qF 'install -g @mosaicstack/[email protected]' "$LOG"
grep -qE 'install -g .*/mosaicstack-gateway-0\.0\.0-source\.tgz' "$LOG"
grep -qE 'install -g .*/mosaicstack-mosaic-0\.0\.0-source\.tgz' "$LOG"
[[ "$(cat "$STATE/mosaic")" == "0.0.0-source" ]]
[[ "$(cat "$STATE/gateway")" == "0.0.0-source" ]]
reset_state
echo "[test] source-build failure is fatal and restores the pre-install prefix"
before_prefix="$(prefix_fingerprint)"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \
MOSAIC_TEST_FAIL_PNPM_INSTALL=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
)"
FAIL_STATUS=$?
set -e
[[ "$FAIL_STATUS" -ne 0 ]]
[[ "$(prefix_fingerprint)" == "$before_prefix" ]]
grep -qF 'forced pnpm install failure' <<<"$OUTPUT"
[[ "$(node -p "require('$ACTIVE').status")" == "rolled-back" ]]
reset_state
echo "[test] corrupt source archive is fatal and restores the pre-install prefix"
before_prefix="$(prefix_fingerprint)"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \
MOSAIC_TEST_CORRUPT_ARCHIVE=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
)"
FAIL_STATUS=$?
set -e
[[ "$FAIL_STATUS" -ne 0 ]]
[[ "$(prefix_fingerprint)" == "$before_prefix" ]]
grep -qF 'archive safety/integrity check failed' <<<"$OUTPUT"
[[ "$(node -p "require('$ACTIVE').status")" == "rolled-back" ]]
reset_state
echo "[test] source archive with multiple extracted roots fails instead of selecting by find order"
before_prefix="$(prefix_fingerprint)"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NEXT_GATEWAY_INSTALL=1 \
MOSAIC_TEST_EXTRA_ARCHIVE_ROOT=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
)"
FAIL_STATUS=$?
set -e
[[ "$FAIL_STATUS" -ne 0 ]]
[[ "$(prefix_fingerprint)" == "$before_prefix" ]]
grep -qF 'expected exactly one extracted source root' <<<"$OUTPUT"
[[ "$(node -p "require('$ACTIVE').status")" == "rolled-back" ]]
reset_state
echo "[test] --dev source install does not require registry version resolution"
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_FAIL_NPM_VIEW=1 \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --dev --ref feature-x --yes --no-auto-launch
)"
grep -qF 'Downloading source ref feature-x at pinned commit 1111111111111111111111111111111111111111' <<<"$OUTPUT"
grep -qF 'Installed from source: CLI 0.0.0-source' <<<"$OUTPUT"
grep -q '^\[P2\] PASS: source_ref=feature-x pinned_commit=1111111111111111111111111111111111111111 sha256=' <<<"$OUTPUT"
reset_state
echo "[test] explicit --ref keeps source lane and avoids @next lookup"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --check --cli --next --ref feature-x
)"
CHECK_STATUS=$?
set -e
[[ "$CHECK_STATUS" -ne 0 ]]
grep -q '^\[P2\] PASS: source_ref=feature-x pinned_commit=1111111111111111111111111111111111111111 sha256=' <<<"$OUTPUT"
if grep -qF '@next version' "$LOG"; then
echo "explicit ref should not query @next dist-tags" >&2
exit 1
fi
reset_state
echo "[test] --check --next rejects mismatched prerelease pipeline suffixes"
set +e
OUTPUT="$(
HOME="$HOME_DIR" \
MOSAIC_HOME="$MOSAIC_HOME" \
MOSAIC_PREFIX="$PREFIX" \
MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" \
MOSAIC_TEST_STATE="$STATE" \
MOSAIC_TEST_GATEWAY_NEXT_VERSION="0.0.7-next.1000" \
PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --check --cli --next
)"
CHECK_STATUS=$?
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
for host in example.com example.net example.org example.dev example.io; do
grep -qF "https://[REDACTED]@$host" <<<"$OUTPUT" \
|| { echo "credentialed URL redaction control missing for $host" >&2; exit 1; }
done
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
test_assert_find_empty 'redacted diagnostic staging files' \
"$TMPDIR" -maxdepth 1 -type f \
\( -name 'mosaic-phase-redacted.*' -o -name 'mosaic-post-redacted.*' \) || exit 1
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
for host in example.com example.net example.org example.dev example.io; do
grep -qF "https://[REDACTED]@$host" "$framework_log" \
|| { echo "framework URL redaction control missing for $host" >&2; exit 1; }
done
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 [[ "${MOSAIC_TEST_ENUMERATION_FAILURE_CHILD:-0}" == "1" && "$phase" == "P2" ]]; then
mkdir -p "$TMP/fault-$phase/blocked"
printf '{"status":"in-progress"}\n' > "$TMP/fault-$phase/blocked/planted-in-progress.json"
chmod 0666 "$TMP/fault-$phase/blocked/planted-in-progress.json"
chmod 0100 "$TMP/fault-$phase/blocked"
trap 'chmod 0700 "$TMP/fault-P2/blocked" 2>/dev/null || true; rm -rf "$TMP"' EXIT
fi
test_assert_no_file_content_match "$phase fault-state" \
'"status"[[:space:]]*:[[:space:]]*"in-progress"' "$TMP/fault-$phase" || exit 1
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"
-431
View File
@@ -1,431 +0,0 @@
#!/usr/bin/env bash
# Red-first acceptance checks for #1050. This file is committed before the
# installer implementation. Do not weaken these properties to make it green.
# pass_case always returns zero and fail_case records the aggregate failure;
# the compact A&&pass||fail assertions are intentional.
# shellcheck disable=SC2015
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=tools/test-enumeration-assertions.sh
source "$ROOT/tools/test-enumeration-assertions.sh"
TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-state-test.XXXXXX")"
trap 'rm -rf "$TMP"' EXIT
failures=0
COMPAT_BIN="$TMP/compat-bin"
mkdir -p "$COMPAT_BIN"
cat > "$COMPAT_BIN/realpath" <<'REALPATH'
#!/usr/bin/env python3
import os
import 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]))
REALPATH
chmod 0755 "$COMPAT_BIN/realpath"
fail_case() { printf '[test] FAIL: %s\n' "$*" >&2; failures=$((failures + 1)); }
pass_case() { printf '[test] PASS: %s\n' "$*"; }
fingerprint() {
local dir="$1"
if [[ ! -d "$dir" ]]; then printf 'ABSENT\n'; return; fi
python3 - "$dir" <<'PY'
import hashlib
import os
import stat
import 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)
rel = os.path.relpath(path, root)
meta = os.lstat(path)
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), meta.st_uid, meta.st_gid, target, digest))
payload = "\n".join("|".join(map(str, row)) for row in sorted(rows)).encode()
print(hashlib.sha256(payload).hexdigest())
PY
}
make_fake_npm() {
local bin="$1"
mkdir -p "$bin"
cat > "$bin/npm" <<'FAKE'
#!/bin/bash
set -euo pipefail
if [[ "${1:-}" == "--version" ]]; then echo '10.6.2'; exit 0; fi
case "${1:-} ${2:-} ${3:-}" in
'view @mosaicstack/mosaic@next version') echo '0.0.50-next.999' ;;
'view @mosaicstack/gateway@next version') echo '0.0.7-next.999' ;;
'view @mosaicstack/mosaic version') echo '0.0.49' ;;
'ls -g --depth=0'|'ls -g --json') echo '{"dependencies":{"@mosaicstack/mosaic":{"version":"0.0.50-next.999"},"@mosaicstack/gateway":{"version":"0.0.7-next.999"}}}' ;;
ls*) echo '{"dependencies":{"@mosaicstack/mosaic":{"version":"0.0.50-next.999"},"@mosaicstack/gateway":{"version":"0.0.7-next.999"}}}' ;;
*) echo "unexpected fake npm command: $*" >&2; exit 1 ;;
esac
FAKE
chmod 0755 "$bin/npm"
}
printf '[test] case: --check enumerates exactly P0-P8, discriminates, and mutates nothing\n'
check_home="$TMP/check-home"
check_bin="$TMP/check-bin"
mkdir -p "$check_home/.config/mosaic/skills/alpha" "$check_home/.npm-global/bin" "$check_bin"
printf '# framework\n' > "$check_home/.config/mosaic/AGENTS.md"
printf '# skill\n' > "$check_home/.config/mosaic/skills/alpha/SKILL.md"
cat > "$check_home/.npm-global/bin/mosaic" <<'CLI'
#!/usr/bin/env bash
printf '0.0.50-next.999\n'
CLI
chmod 0755 "$check_home/.npm-global/bin/mosaic"
make_fake_npm "$check_bin"
before="$(fingerprint "$check_home")"
set +e
HOME="$check_home" MOSAIC_HOME="$check_home/.config/mosaic" MOSAIC_PREFIX="$check_home/.npm-global" \
MOSAIC_NO_COLOR=1 PATH="$check_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/check.log" 2>&1
check_status=$?
set -e
after="$(fingerprint "$check_home")"
[[ "$before" == "$after" ]] && pass_case '--check left the complete HOME fingerprint unchanged' \
|| fail_case "--check mutated HOME (before=$before after=$after)"
[[ "$check_status" -ne 0 ]] && pass_case '--check exited non-zero for failed P4/P5/P8 predicates' \
|| fail_case '--check returned zero on the deliberately broken host'
phase_rows=0
for phase in P0 P1 P2 P3 P4 P5 P6 P7 P8; do
count="$(grep -Ec "^\[$phase\] (PASS|FAIL):" "$TMP/check.log" || true)"
[[ "$count" -eq 1 ]] || fail_case "$phase expected exactly one PASS/FAIL row, got $count"
phase_rows=$((phase_rows + count))
done
[[ "$phase_rows" -eq 9 ]] && pass_case '--check emitted exactly nine P0-P8 result rows' \
|| fail_case "--check emitted $phase_rows canonical rows instead of 9"
grep -q '^\[P3\] PASS:.*0\.0\.50-next\.999' "$TMP/check.log" \
&& pass_case 'P3 preserves the absolute-path exact-version discriminator' \
|| fail_case 'P3 did not PASS with the exact resolved next-lane version'
grep -q '^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:' "$TMP/check.log" \
&& pass_case 'P4 refuses fabricated precision when no shipped-set declaration exists' \
|| fail_case 'P4 did not report the declared-set population as NOT-MEASURED / UNDECLARED'
for phase in P5 P8; do
grep -q "^\[$phase\] FAIL:" "$TMP/check.log" \
&& pass_case "$phase remains an attributable expected RED" \
|| fail_case "$phase did not report its own expected failure"
done
printf '[test] case: --check discriminates a constructed good host without mutation\n'
good_home="$TMP/good-home"
good_bin="$TMP/good-bin"
good_prefix="$good_home/.npm-global"
good_mosaic="$good_home/.config/mosaic"
mkdir -p "$good_bin" "$good_prefix/bin" "$good_mosaic/skills/declared-skill"
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 "$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
[[ "${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'
#!/bin/bash
exit 0
CURL
cat > "$good_bin/ldd" <<'LDD'
#!/bin/bash
echo 'ldd (GNU libc) 2.36'
LDD
chmod 0755 "$good_bin/id" "$good_bin/stat" "$good_bin/curl" "$good_bin/ldd"
cat > "$good_prefix/bin/mosaic" <<'CLI'
#!/usr/bin/env bash
printf '0.0.50-next.999\n'
CLI
chmod 0755 "$good_prefix/bin/mosaic"
cat > "$good_bin/getent" <<GETENT
#!/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
if [[ "\${*: -1}" == 'command -v mosaic' ]]; then
printf '%s\\n' '$good_prefix/bin/mosaic'
exit 0
fi
exec /bin/bash "\$@"
SHELL
chmod 0755 "$good_bin/getent" "$good_bin/bash"
printf '# Soul\n\nConfigured.\n' > "$good_mosaic/SOUL.md"
printf '# User\n\nConfigured.\n' > "$good_mosaic/USER.md"
chmod 0600 "$good_mosaic/SOUL.md" "$good_mosaic/USER.md"
cat > "$good_mosaic/skills/declared-skill/SKILL.md" <<'SKILL'
---
name: declared-skill
description: Constructed loadable acceptance skill.
---
# Declared skill
SKILL
printf '{"lane":"next","version":"0.0.50-next.999","skills":["declared-skill"]}\n' > "$good_mosaic/.install-shipped-skills.json"
printf '{\n "lane": "next",\n "cliVersion": "0.0.50-next.999"\n}\n' > "$good_mosaic/.install-manifest.json"
before="$(fingerprint "$good_home")"
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/good-check.log" 2>&1
status=$?
set -e
after="$(fingerprint "$good_home")"
[[ "$status" -eq 0 ]] && pass_case 'good-host --check exited zero' || fail_case "good-host --check exited $status"
[[ "$before" == "$after" ]] && pass_case 'good-host --check left HOME unchanged' || fail_case 'good-host --check mutated HOME'
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: P4 fails closed when created-path enumeration is incomplete\n'
real_find="$(command -v find)"
cat > "$good_bin/find" <<FIND
#!/bin/bash
if [[ "\${1:-}" == '$good_mosaic' && "\${2:-}" == '-xdev' && "\${3:-}" == '-print0' ]]; then
printf '%s\\0' '$good_mosaic'
exit 73
fi
exec '$real_find' "\$@"
FIND
chmod 0755 "$good_bin/find"
printf '# hidden unsafe child\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-enumeration-failure.log" 2>&1
p4_enumeration_status=$?
set -e
rm -f "$good_bin/find"
[[ "$p4_enumeration_status" -ne 0 ]] \
&& pass_case 'P4 rejects an incomplete created-path inventory' \
|| fail_case 'P4 accepted a partial created-path inventory after find failed'
grep -q '^\[P4\] FAIL:.*enumeration failed' "$TMP/p4-enumeration-failure.log" \
&& pass_case 'P4 attributes the failed created-path enumeration' \
|| fail_case 'P4 did not report failed created-path enumeration'
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 '
const fs=require("fs"); const p=process.argv[1]; const phase=process.argv[2];
const m=JSON.parse(fs.readFileSync(p,"utf8")); m.phaseOutcomes={P4:"committed",P6:"committed"};
m.phaseOutcomes[phase]="failed"; fs.writeFileSync(p,JSON.stringify(m)+"\n");
' "$good_mosaic/.install-manifest.json" "$blocked_phase"
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/action-$blocked_phase.log" 2>&1
status=$?
set -e
[[ "$status" -ne 0 ]] || fail_case "$blocked_phase action failure returned zero"
grep -q "^\[$blocked_phase\] FAIL:.*action reported a required $blocked_phase failure" "$TMP/action-$blocked_phase.log" \
&& pass_case "$blocked_phase action failure remained blocking in a later --check" \
|| fail_case "$blocked_phase persisted action failure was not attributed"
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: 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"
mkdir -p "$unsafe_home"
for case_name in root-target home-target overlap-target; do
case "$case_name" in
root-target) unsafe_mosaic=/; unsafe_prefix="$unsafe_home/.npm-global" ;;
home-target) unsafe_mosaic="$unsafe_home"; unsafe_prefix="$unsafe_home/.npm-global" ;;
overlap-target) unsafe_mosaic="$unsafe_home/.config"; unsafe_prefix="$unsafe_home/.config/mosaic/prefix" ;;
esac
before="$(fingerprint "$unsafe_home")"
set +e
HOME="$unsafe_home" MOSAIC_HOME="$unsafe_mosaic" MOSAIC_PREFIX="$unsafe_prefix" \
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
after="$(fingerprint "$unsafe_home")"
[[ "$status" -ne 0 ]] || fail_case "$case_name unsafe path returned zero"
grep -q '^\[P0\] FAIL:.*unsafe context' "$TMP/$case_name.log" \
&& pass_case "$case_name was rejected by P0" || fail_case "$case_name lacked an attributable P0 failure"
[[ "$before" == "$after" ]] || fail_case "$case_name mutated HOME"
done
symlink_home="$TMP/symlink-home"
symlink_outside="$TMP/symlink-outside"
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_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
[[ "$status" -ne 0 ]] || fail_case 'symlink-parent unsafe path returned zero'
grep -q '^\[P0\] FAIL:.*unsafe context' "$TMP/symlink-target.log" \
&& pass_case 'symlinked rollback parent was rejected by P0' \
|| fail_case 'symlinked rollback parent lacked an attributable P0 failure'
test_assert_find_empty 'symlink target mutation check' "$symlink_outside" -mindepth 1 \
|| fail_case 'symlink target was mutated or could not be enumerated'
printf '[test] case: journal initialization failure is fatal before mutation\n'
journal_home="$TMP/journal-failure/home"
mkdir -p "$journal_home/.config/mosaic"
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_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
after="$(fingerprint "$journal_home")"
[[ "$status" -ne 0 ]] && pass_case 'unwritable journal directory failed non-zero' \
|| fail_case 'unwritable journal directory returned zero'
grep -q 'cannot create private journal directory' "$TMP/journal-failure.log" \
&& pass_case 'journal initialization failure was named' \
|| fail_case 'journal initialization failure lacked a named diagnostic'
[[ "$before" == "$after" ]] && pass_case 'journal failure occurred before target mutation' \
|| fail_case "journal failure mutated target HOME (before=$before after=$after)"
if [[ "$failures" -ne 0 ]]; then
printf '[test] install state-machine acceptance RED: %d failed assertion(s)\n' "$failures" >&2
printf '[test] --check transcript: %s\n' "$TMP/check.log" >&2
exit 1
fi
printf '[test] installer state-machine acceptance passed\n'
+73 -1313
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
e59cb441a2f37ae9150f8eae470238e9d858a1816df93343d9784a6796676096 install.sh
-72
View File
@@ -1,72 +0,0 @@
#!/usr/bin/env bash
# Fail-closed filesystem-enumeration assertions for shell test harnesses.
# Usage: test_assert_find_empty <label> <find arguments...>
test_assert_find_empty() {
local label="$1"
shift
local inventory
inventory="$(mktemp "${TMPDIR:-/tmp}/mosaic-test-find.XXXXXX")" || {
printf '[test] ERROR: %s inventory allocation failed\n' "$label" >&2
return 2
}
if ! find "$@" -print0 > "$inventory"; then
rm -f "$inventory"
printf '[test] ERROR: %s enumeration failed\n' "$label" >&2
return 2
fi
if [[ -s "$inventory" ]]; then
rm -f "$inventory"
printf '[test] FAIL: %s was not empty\n' "$label" >&2
return 1
fi
rm -f "$inventory"
return 0
}
# Usage: test_assert_no_file_content_match <label> <extended-regex> <find roots/options...>
test_assert_no_file_content_match() {
local label="$1"
local pattern="$2"
shift 2
local inventory path grep_status result=0
inventory="$(mktemp "${TMPDIR:-/tmp}/mosaic-test-find.XXXXXX")" || {
printf '[test] ERROR: %s inventory allocation failed\n' "$label" >&2
return 2
}
if ! find "$@" -type f -print0 > "$inventory"; then
rm -f "$inventory"
printf '[test] ERROR: %s enumeration failed\n' "$label" >&2
return 2
fi
while IFS= read -r -d '' path; do
grep_status=0
grep -Eq -- "$pattern" "$path" || grep_status=$?
if [[ "$grep_status" -eq 0 ]]; then
result=1
break
fi
if [[ "$grep_status" -ne 1 ]]; then
result=2
break
fi
done < "$inventory"
rm -f "$inventory"
if [[ "$result" -eq 1 ]]; then
printf '[test] FAIL: %s contained a forbidden match\n' "$label" >&2
return 1
fi
if [[ "$result" -eq 2 ]]; then
printf '[test] ERROR: %s content inspection failed\n' "$label" >&2
return 2
fi
return 0
}
-20
View File
@@ -1,20 +0,0 @@
#!/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"
-66
View File
@@ -1,66 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=tools/test-enumeration-assertions.sh
source "$ROOT/tools/test-enumeration-assertions.sh"
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' ]]
test_assert_find_empty 'verified-installer temporary downloads' "$TMP/downloads" -mindepth 1
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'
@@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "usage: $0 <coverage-checker> <case-manifest> <arm-manifest> <state-root> <run-id>" >&2
}
[[ "$#" -eq 5 ]] || { usage; exit 2; }
checker="$1"
case_manifest="$2"
arm_manifest="$3"
state_root="$4"
run_id="$5"
[[ -f "$checker" ]] \
|| { echo "[fixture-suite] coverage checker missing: $checker" >&2; exit 2; }
set +e
bash "$checker" check cases "$case_manifest" "$state_root" "$run_id"
cases_status=$?
bash "$checker" check arms "$arm_manifest" "$state_root" "$run_id"
arms_status=$?
set -e
if [ "$cases_status" -ne 0 ] || [ "$arms_status" -ne 0 ]; then
exit 1
fi
@@ -1,108 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "usage: $0 <init|mark|check> <cases|arms> <expected-set> <state-root> <run-id> [name]" >&2
}
[[ "$#" -ge 5 ]] || { usage; exit 2; }
mode="$1"
dimension="$2"
expected_set="$3"
state_root="$4"
run_id="$5"
name="${6:-}"
[[ "$dimension" == cases || "$dimension" == arms ]] \
|| { echo "[fixture-suite] invalid coverage dimension: $dimension" >&2; exit 2; }
[[ -s "$expected_set" ]] \
|| { echo "[fixture-suite] expected set missing or empty: $expected_set" >&2; exit 2; }
[[ -n "$state_root" && "$state_root" != / ]] \
|| { echo "[fixture-suite] unsafe state root" >&2; exit 2; }
[[ "$run_id" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] \
|| { echo "[fixture-suite] invalid run id: $run_id" >&2; exit 2; }
singular="${dimension%s}"
run_dir="$state_root/$run_id/$dimension"
expected_names() {
awk -F '\t' 'NF && $0 !~ /^[[:space:]]*#/ && !seen[$1]++ { sub(/\r$/, "", $1); print $1 }' \
"$expected_set" | LC_ALL=C sort
}
validate_expected_names() {
local found=0 expected
while IFS= read -r expected; do
[[ -n "$expected" ]] || continue
found=1
[[ "$expected" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] \
|| { echo "[fixture-suite] invalid expected $singular name: $expected" >&2; return 1; }
done < <(expected_names)
[[ "$found" -eq 1 ]] \
|| { echo "[fixture-suite] expected set defines no $dimension" >&2; return 1; }
}
validate_expected_names
case "$mode" in
init)
[[ "$#" -eq 5 ]] || { usage; exit 2; }
mkdir -p "$run_dir"
find "$run_dir" -mindepth 1 -delete
;;
mark)
[[ "$#" -eq 6 ]] || { usage; exit 2; }
[[ "$name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] \
|| { echo "[fixture-suite] invalid $singular marker name: $name" >&2; exit 2; }
[[ -d "$run_dir" ]] \
|| { echo "[fixture-suite] $dimension state was not initialized: $run_id" >&2; exit 1; }
if ! expected_snapshot="$(expected_names)"; then
echo "[fixture-suite] failed to read expected $dimension" >&2
exit 1
fi
grep -Fxq -- "$name" <<<"$expected_snapshot" \
|| { echo "[fixture-suite] $singular is not in the expected set: $name" >&2; exit 1; }
: > "$run_dir/$name.ran"
;;
check)
[[ "$#" -eq 5 ]] || { usage; exit 2; }
mkdir -p "$run_dir"
expected_file="$(mktemp "$run_dir/.expected.XXXXXX")"
actual_file="$(mktemp "$run_dir/.actual.XXXXXX")"
cleanup() { rm -f "$expected_file" "$actual_file"; }
trap cleanup EXIT
expected_names > "$expected_file"
[[ -r "$run_dir" && -x "$run_dir" ]] \
|| { echo "[fixture-suite] coverage state is not readable: $run_dir" >&2; exit 1; }
(
shopt -s nullglob dotglob
for marker in "$run_dir"/*.ran; do
[[ -f "$marker" && ! -L "$marker" ]] || continue
marker_name="${marker##*/}"
printf '%s\n' "${marker_name%.ran}"
done
) | LC_ALL=C sort -u > "$actual_file"
defined="$(wc -l < "$expected_file" | tr -d ' ')"
executed="$(wc -l < "$actual_file" | tr -d ' ')"
printf '[fixture-suite] %s_defined=%s %s_executed=%s\n' \
"$dimension" "$defined" "$dimension" "$executed"
if ! cmp -s "$expected_file" "$actual_file"; then
while IFS= read -r missing; do
[[ -n "$missing" ]] \
&& printf '[fixture-suite] missing_%s=%s\n' "$singular" "$missing" >&2
done < <(comm -23 "$expected_file" "$actual_file")
while IFS= read -r unexpected; do
[[ -n "$unexpected" ]] \
&& printf '[fixture-suite] unexpected_%s=%s\n' "$singular" "$unexpected" >&2
done < <(comm -13 "$expected_file" "$actual_file")
exit 1
fi
;;
*)
usage
exit 2
;;
esac
@@ -1,297 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SUBJECT="${GREENFIELD_COVERAGE_SUBJECT:-$ROOT/tools/verify-greenfield-execution-coverage.sh}"
GATE="${GREENFIELD_COVERAGE_GATE:-$ROOT/tools/verify-greenfield-execution-coverage-gate.sh}"
WORKFLOW="$ROOT/.woodpecker/greenfield-install.yml"
INSTALLER="${GREENFIELD_E2E_INSTALLER:-$ROOT/tools/e2e-install-test.sh}"
mkdir -p "$ROOT/.mosaic-test-work"
TMP="$(mktemp -d "$ROOT/.mosaic-test-work/execution-coverage.XXXXXX")"
selector_control="$ROOT/greenfield-coverage-selector-control.ran"
cleanup() {
rm -f "$selector_control"
rm -rf "$TMP"
}
trap cleanup EXIT
manifest="$TMP/expected-red.tsv"
state_root="$TMP/state"
run_id="pipeline-1"
printf '%s\n' \
'# representative manifest comment' \
$'# case\tkind\tkey/value' \
'' \
$'next-git-present\texit\t1' \
$'main-git-present\texit\t1' \
$'next-git-absent\texit\t1' > "$manifest"
bash "$SUBJECT" init cases "$manifest" "$state_root" "$run_id"
# Positive firing control: a skipped expected case makes the final gate red.
bash "$SUBJECT" mark cases "$manifest" "$state_root" "$run_id" next-git-present
bash "$SUBJECT" mark cases "$manifest" "$state_root" "$run_id" main-git-present
set +e
skipped_output="$(bash "$SUBJECT" check cases "$manifest" "$state_root" "$run_id" 2>&1)"
skipped_status=$?
set -e
[[ "$skipped_status" -eq 1 ]]
grep -qF '[fixture-suite] cases_defined=3 cases_executed=2' <<<"$skipped_output"
grep -qF '[fixture-suite] missing_case=next-git-absent' <<<"$skipped_output"
# Count inflation cannot pass: equal counts with one missing and one unexpected remain red.
: > "$state_root/$run_id/cases/unexpected-case.ran"
set +e
inflated_output="$(bash "$SUBJECT" check cases "$manifest" "$state_root" "$run_id" 2>&1)"
inflated_status=$?
set -e
[[ "$inflated_status" -eq 1 ]]
grep -qF '[fixture-suite] cases_defined=3 cases_executed=3' <<<"$inflated_output"
grep -qF '[fixture-suite] missing_case=next-git-absent' <<<"$inflated_output"
grep -qF '[fixture-suite] unexpected_case=unexpected-case' <<<"$inflated_output"
# Re-initializing the same run clears stale markers instead of certifying a later run.
bash "$SUBJECT" init cases "$manifest" "$state_root" "$run_id"
set +e
stale_output="$(bash "$SUBJECT" check cases "$manifest" "$state_root" "$run_id" 2>&1)"
stale_status=$?
set -e
[[ "$stale_status" -eq 1 ]]
grep -qF '[fixture-suite] cases_defined=3 cases_executed=0' <<<"$stale_output"
# Exact case-set equality is green.
for case_name in next-git-present main-git-present next-git-absent; do
bash "$SUBJECT" mark cases "$manifest" "$state_root" "$run_id" "$case_name"
done
complete_output="$(bash "$SUBJECT" check cases "$manifest" "$state_root" "$run_id")"
grep -qF '[fixture-suite] cases_defined=3 cases_executed=3' <<<"$complete_output"
# A successful early match must consume the full producer and remain green under pipefail.
large_manifest="$TMP/large-expected.tsv"
awk 'BEGIN {
print "a-target\texit\t1"
for (i = 0; i < 20000; i++) printf "z-case-%05d\texit\t1\n", i
}' > "$large_manifest"
bash "$SUBJECT" init cases "$large_manifest" "$state_root" large-set
bash "$SUBJECT" mark cases "$large_manifest" "$state_root" large-set a-target
# The production manifest, including comments, initializes and checks.
production_manifest="$ROOT/tools/fixtures/greenfield-expected-red.tsv"
production_arms="$ROOT/tools/fixtures/greenfield-expected-arms.txt"
production_state="$TMP/production-state"
bash "$SUBJECT" init cases "$production_manifest" "$production_state" production-1
for case_name in next-git-present main-git-present next-git-absent; do
bash "$SUBJECT" mark cases "$production_manifest" "$production_state" production-1 "$case_name"
done
production_output="$(bash "$SUBJECT" check cases "$production_manifest" "$production_state" production-1)"
grep -qF '[fixture-suite] cases_defined=3 cases_executed=3' <<<"$production_output"
mark_all_cases() {
local root="$1" run="$2"
for case_name in next-git-present main-git-present next-git-absent; do
bash "$SUBJECT" mark cases "$production_manifest" "$root" "$run" "$case_name"
done
}
mark_all_arms() {
local root="$1" run="$2"
for arm_name in \
greenfield-git-present \
greenfield-main-git-present \
greenfield-remote-installer-contract \
greenfield-git-absent; do
bash "$SUBJECT" mark arms "$production_arms" "$root" "$run" "$arm_name"
done
}
gate_state="$TMP/gate-state"
gate_run="truth-table"
# Final aggregation truth table: cases PASS / arms FAIL is red.
bash "$SUBJECT" init cases "$production_manifest" "$gate_state" "$gate_run"
bash "$SUBJECT" init arms "$production_arms" "$gate_state" "$gate_run"
mark_all_cases "$gate_state" "$gate_run"
for arm_name in greenfield-git-present greenfield-main-git-present greenfield-git-absent; do
bash "$SUBJECT" mark arms "$production_arms" "$gate_state" "$gate_run" "$arm_name"
done
set +e
gate_cases_pass_output="$(bash "$GATE" "$SUBJECT" "$production_manifest" "$production_arms" "$gate_state" "$gate_run" 2>&1)"
gate_cases_pass_status=$?
set -e
[[ "$gate_cases_pass_status" -eq 1 ]]
grep -qF '[fixture-suite] cases_defined=3 cases_executed=3' <<<"$gate_cases_pass_output"
grep -qF '[fixture-suite] arms_defined=4 arms_executed=3' <<<"$gate_cases_pass_output"
grep -qF '[fixture-suite] missing_arm=greenfield-remote-installer-contract' <<<"$gate_cases_pass_output"
# Final aggregation truth table: cases FAIL / arms PASS is red.
bash "$SUBJECT" init cases "$production_manifest" "$gate_state" "$gate_run"
bash "$SUBJECT" init arms "$production_arms" "$gate_state" "$gate_run"
for case_name in next-git-present main-git-present; do
bash "$SUBJECT" mark cases "$production_manifest" "$gate_state" "$gate_run" "$case_name"
done
mark_all_arms "$gate_state" "$gate_run"
set +e
gate_arms_pass_output="$(bash "$GATE" "$SUBJECT" "$production_manifest" "$production_arms" "$gate_state" "$gate_run" 2>&1)"
gate_arms_pass_status=$?
set -e
[[ "$gate_arms_pass_status" -eq 1 ]]
grep -qF '[fixture-suite] cases_defined=3 cases_executed=2' <<<"$gate_arms_pass_output"
grep -qF '[fixture-suite] missing_case=next-git-absent' <<<"$gate_arms_pass_output"
grep -qF '[fixture-suite] arms_defined=4 arms_executed=4' <<<"$gate_arms_pass_output"
# Final aggregation truth table: both FAIL emits both failures and is red.
bash "$SUBJECT" init cases "$production_manifest" "$gate_state" "$gate_run"
bash "$SUBJECT" init arms "$production_arms" "$gate_state" "$gate_run"
for case_name in next-git-present main-git-present; do
bash "$SUBJECT" mark cases "$production_manifest" "$gate_state" "$gate_run" "$case_name"
done
for arm_name in greenfield-git-present greenfield-main-git-present greenfield-git-absent; do
bash "$SUBJECT" mark arms "$production_arms" "$gate_state" "$gate_run" "$arm_name"
done
set +e
gate_both_fail_output="$(bash "$GATE" "$SUBJECT" "$production_manifest" "$production_arms" "$gate_state" "$gate_run" 2>&1)"
gate_both_fail_status=$?
set -e
[[ "$gate_both_fail_status" -eq 1 ]]
grep -qF '[fixture-suite] missing_case=next-git-absent' <<<"$gate_both_fail_output"
grep -qF '[fixture-suite] missing_arm=greenfield-remote-installer-contract' <<<"$gate_both_fail_output"
# Final aggregation truth table: both PASS is green.
bash "$SUBJECT" init cases "$production_manifest" "$gate_state" "$gate_run"
bash "$SUBJECT" init arms "$production_arms" "$gate_state" "$gate_run"
mark_all_cases "$gate_state" "$gate_run"
mark_all_arms "$gate_state" "$gate_run"
gate_both_pass_output="$(bash "$GATE" "$SUBJECT" "$production_manifest" "$production_arms" "$gate_state" "$gate_run")"
grep -qF '[fixture-suite] cases_defined=3 cases_executed=3' <<<"$gate_both_pass_output"
grep -qF '[fixture-suite] arms_defined=4 arms_executed=4' <<<"$gate_both_pass_output"
# A future manifest case changes the derived set and cannot silently escape coverage.
printf '%s\n' $'future-case\texit\t1' >> "$manifest"
set +e
future_output="$(bash "$SUBJECT" check cases "$manifest" "$state_root" "$run_id" 2>&1)"
future_status=$?
set -e
[[ "$future_status" -eq 1 ]]
grep -qF '[fixture-suite] cases_defined=4 cases_executed=3' <<<"$future_output"
grep -qF '[fixture-suite] missing_case=future-case' <<<"$future_output"
# A skipped remote arm is red even while the three case identities are complete.
bash "$SUBJECT" init arms "$production_arms" "$production_state" production-1
for arm_name in greenfield-git-present greenfield-main-git-present greenfield-git-absent; do
bash "$SUBJECT" mark arms "$production_arms" "$production_state" production-1 "$arm_name"
done
set +e
remote_skipped_output="$(bash "$SUBJECT" check arms "$production_arms" "$production_state" production-1 2>&1)"
remote_skipped_status=$?
set -e
[[ "$remote_skipped_status" -eq 1 ]]
grep -qF '[fixture-suite] arms_defined=4 arms_executed=3' <<<"$remote_skipped_output"
grep -qF '[fixture-suite] missing_arm=greenfield-remote-installer-contract' <<<"$remote_skipped_output"
# Arm counts cannot be inflated either.
: > "$production_state/production-1/arms/unexpected-arm.ran"
set +e
arm_inflated_output="$(bash "$SUBJECT" check arms "$production_arms" "$production_state" production-1 2>&1)"
arm_inflated_status=$?
set -e
[[ "$arm_inflated_status" -eq 1 ]]
grep -qF '[fixture-suite] arms_defined=4 arms_executed=4' <<<"$arm_inflated_output"
grep -qF '[fixture-suite] missing_arm=greenfield-remote-installer-contract' <<<"$arm_inflated_output"
grep -qF '[fixture-suite] unexpected_arm=unexpected-arm' <<<"$arm_inflated_output"
# Exact arm-set equality is green.
bash "$SUBJECT" init arms "$production_arms" "$production_state" production-1
for arm_name in \
greenfield-git-present \
greenfield-main-git-present \
greenfield-remote-installer-contract \
greenfield-git-absent; do
bash "$SUBJECT" mark arms "$production_arms" "$production_state" production-1 "$arm_name"
done
arm_complete_output="$(bash "$SUBJECT" check arms "$production_arms" "$production_state" production-1)"
grep -qF '[fixture-suite] arms_defined=4 arms_executed=4' <<<"$arm_complete_output"
# Bind archive purity to the production checkout-archive selector before proving its effect.
python3 - "$INSTALLER" <<'PY'
import re
import sys
from pathlib import Path
text = Path(sys.argv[1]).read_text(encoding="utf-8")
archive = re.search(
r'(?ms)^ tar -C "\$repo_parent" \\\n(?P<selectors>.*?)^ -czf "\$checkout_archive" "\$repo_name"$',
text,
)
assert archive, "production checkout-archive command not found"
assert "--exclude='*/.mosaic-test-work'" in archive.group("selectors"), (
"production checkout archive does not exclude .mosaic-test-work"
)
PY
# The production-bound checkout-archive selector excludes coverage state under .mosaic-test-work.
: > "$selector_control"
archive="$TMP/checkout.tar.gz"
archive_list="$TMP/checkout.list"
repo_parent="$(dirname "$ROOT")"
repo_name="$(basename "$ROOT")"
tar -C "$repo_parent" \
--exclude='*/.git' --exclude='*/node_modules' --exclude='*/dist' \
--exclude='*/coverage' --exclude='*/.turbo' --exclude='*/.mosaic-test-work' \
--exclude='*/.env' --exclude='*/.env.*' \
-czf "$archive" "$repo_name"
tar -tzf "$archive" > "$archive_list"
grep -qF "$repo_name/greenfield-coverage-selector-control.ran" "$archive_list"
if grep -qF "$repo_name/.mosaic-test-work/" "$archive_list"; then
echo 'coverage state leaked into checkout archive' >&2
exit 1
fi
# Workflow contract: success-only case+arm markers, excluded state root, and complete ordering.
python3 - "$WORKFLOW" <<'PY'
import re
import sys
from pathlib import Path
text = Path(sys.argv[1]).read_text(encoding="utf-8")
steps = {
match.group(1): match.group(2)
for match in re.finditer(r"(?ms)^ ([A-Za-z0-9_-]+):\n(.*?)(?=^ [A-Za-z0-9_-]+:\n|\Z)", text)
}
case_steps = (
("greenfield-git-present", "next-git-present"),
("greenfield-main-git-present", "main-git-present"),
("greenfield-git-absent", "next-git-absent"),
)
for step, case in case_steps:
block = steps[step]
verify = block.index("bash tools/verify-greenfield-expected-red.sh")
mark = block.index(f'"$coverage_run" {case}')
assert verify < mark, f"{step} marks its case before successful verification"
arm_steps = (
"greenfield-git-present",
"greenfield-main-git-present",
"greenfield-remote-installer-contract",
"greenfield-git-absent",
)
for step in arm_steps:
block = steps[step]
verify = block.index("bash tools/verify-greenfield-expected-red.sh")
mark = block.index(f'"$coverage_run" {step}')
assert verify < mark, f"{step} marks its arm before successful verification"
assert re.search(
r"(?m)^ depends_on:\n - greenfield-case-denominator-init$", block
), f"{step} can race marker initialization"
assert ".greenfield-case-state" not in text
assert ".mosaic-test-work/greenfield-execution-coverage" in text
final = steps["greenfield-case-denominator"]
assert "status: [success, failure]" in final
assert "bash tools/verify-greenfield-execution-coverage-gate.sh" in final
assert "check cases" not in final and "check arms" not in final
assert "cases_status" not in final and "arms_status" not in final
for dependency in arm_steps:
assert f" - {dependency}\n" in final, f"final gate can race {dependency}"
PY
printf 'greenfield execution coverage tests passed\n'
-142
View File
@@ -1,142 +0,0 @@
#!/usr/bin/env bash
# Verify that the detector found exactly the pinned C1 phase verdicts. The
# fixture is expected to exit non-zero; this verifier is the green CI contract.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST="${MOSAIC_EXPECTED_RED_MANIFEST:-$ROOT/tools/fixtures/greenfield-expected-red.tsv}"
CASE="${1:?usage: verify-greenfield-expected-red.sh <case> <log> <fixture-exit>}"
LOG="${2:?usage: verify-greenfield-expected-red.sh <case> <log> <fixture-exit>}"
FIXTURE_EXIT="${3:?usage: verify-greenfield-expected-red.sh <case> <log> <fixture-exit>}"
[[ -r "$MANIFEST" ]] || { echo "expected-RED manifest is unreadable: $MANIFEST" >&2; exit 2; }
[[ -r "$LOG" ]] || { echo "fixture log is unreadable: $LOG" >&2; exit 2; }
[[ "$FIXTURE_EXIT" =~ ^[0-9]+$ ]] || { echo "fixture exit is not numeric: $FIXTURE_EXIT" >&2; exit 2; }
# 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=() phase_reason_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))
;;
phase-reason)
[[ "$expectation" =~ ^P0=(.+)$ ]] \
|| { echo "invalid expected-RED phase reason: case=$case_name value=$expectation" >&2; exit 2; }
phase_reason_count[$case_name|P0]=$((${phase_reason_count[$case_name|P0]:-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
[[ "${phase_reason_count[$case_name|P0]:-0}" -eq 1 ]] \
|| { echo "expected-RED manifest requires exactly one P0 reason binding for case=$case_name" >&2; exit 2; }
[[ "${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 + 1 + 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
[[ -n "$case_name" && "${case_name:0:1}" != "#" ]] || continue
[[ "$case_name" == "$CASE" ]] || continue
checks=$((checks + 1))
case "$kind" in
exit)
if [[ "$FIXTURE_EXIT" != "$expectation" ]]; then
echo "expected-RED mismatch: case=$CASE fixture_exit=$FIXTURE_EXIT expected=$expectation" >&2
failures=$((failures + 1))
fi
;;
phase)
phase="${expectation%%=*}"
expected_verdict="${expectation#*=}"
last_row="$(grep -E "^\[$phase\] (PASS|FAIL):" "$LOG" | tail -n 1 || true)"
actual_verdict="$(printf '%s\n' "$last_row" | sed -n "s/^\[$phase\] \(PASS\|FAIL\):.*/\1/p")"
if [[ "$actual_verdict" != "$expected_verdict" ]]; then
echo "expected-RED mismatch: case=$CASE phase=$phase got=${actual_verdict:-missing} expected=$expected_verdict" >&2
failures=$((failures + 1))
fi
;;
phase-reason)
phase="${expectation%%=*}"
expected_reason="${expectation#*=}"
last_row="$(grep -E "^\[$phase\] (PASS|FAIL):" "$LOG" | tail -n 1 || true)"
actual_reason="${last_row#*: }"
if [[ -z "$last_row" ]] || ! grep -Eq -- "^${expected_reason}$" <<<"$actual_reason"; then
echo "expected-RED phase reason mismatch: case=$CASE phase=$phase reason=${actual_reason:-missing}" >&2
failures=$((failures + 1))
fi
;;
require)
if ! grep -Eq -- "$expectation" "$LOG"; then
echo "expected-RED missing required evidence: case=$CASE regex=$expectation" >&2
failures=$((failures + 1))
fi
;;
forbid)
if grep -Eq -- "$expectation" "$LOG"; then
echo "expected-RED found forbidden evidence: case=$CASE regex=$expectation" >&2
failures=$((failures + 1))
fi
;;
*)
echo "invalid expected-RED manifest kind: case=$case_name kind=$kind" >&2
exit 2
;;
esac
done < "$MANIFEST"
[[ "$checks" -gt 0 ]] || { echo "expected-RED manifest has no checks for case=$CASE" >&2; exit 2; }
if [[ "$failures" -ne 0 ]]; then
echo "expected-RED verification failed: case=$CASE failures=$failures checks=$checks" >&2
exit 1
fi
printf 'expected-RED verification passed: case=%s checks=%d\n' "$CASE" "$checks"
@@ -1,91 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TMP="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-expected-red-test.XXXXXX")"
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: target=mosaic uid=1001 HOME=/home/mosaic passwd_HOME=/home/mosaic shell=/bin/bash privilege=user arch=x86_64 libc=glibc node=v24.0.0 npm=10.6.2
[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: runtime linking/activation action reported a required failure
[P7] PASS: no services requested
[P8] FAIL: shell path absent
[P9] FAIL: aggregate refusal
LOG
bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 1 >/dev/null
printf '[test] PASS: matching detector findings make the CI verifier green\n'
sed 's/^\[P0\] PASS:.*/[P0] PASS: arbitrary unconditional success/' "$TMP/match.log" > "$TMP/vacuous-p0.log"
if bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/vacuous-p0.log" 1 >/dev/null 2>&1; then
echo '[test] FAIL: vacuous P0 PASS satisfied the expected-RED contract without identity/context evidence' >&2
exit 1
fi
printf '[test] PASS: P0 PASS must bind target identity, HOME, shell, privilege, architecture, and runtime reason\n'
sed 's/^\[P4\] FAIL:/[P4] PASS:/' "$TMP/match.log" > "$TMP/drift.log"
if bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/drift.log" 1 >/dev/null 2>&1; then
echo '[test] FAIL: changed P4 verdict did not invalidate the pinned manifest' >&2
exit 1
fi
printf '[test] PASS: changed phase verdict requires a deliberate manifest update\n'
if bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 0 >/dev/null 2>&1; then
echo '[test] FAIL: unexpected fixture exit did not invalidate the pinned manifest' >&2
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'
grep -v $'^next-git-present\tphase-reason\tP0=' "$manifest" > "$TMP/missing-p0-reason.tsv"
if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/missing-p0-reason.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 P0 reason binding was accepted' >&2; exit 1
fi
printf '[test] PASS: every case requires one discriminating P0 reason binding\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'