fix(installer): close detector false-pass gaps
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/greenfield-install Pipeline failed

This commit is contained in:
2026-08-05 19:00:22 -05:00
parent e5d5c8495a
commit 378bc1afe3
10 changed files with 227 additions and 39 deletions
@@ -0,0 +1,66 @@
# #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.
@@ -51,6 +51,7 @@ Implement C1 from the canonical greenfield-install PRD v2: a transactional P0
- [x] State-machine implementation complete: private pre-mutation journal/snapshot, P0P8 `--check`, P2P8 fault seam, rollback, durable manifest/journal seal, action-status persistence, safe rollback roots, and stale-projection recovery. - [x] State-machine implementation complete: private pre-mutation journal/snapshot, P0P8 `--check`, P2P8 fault seam, rollback, durable manifest/journal seal, action-status persistence, safe rollback roots, and stale-projection recovery.
- [x] Debian/glibc checkout fixture now packages the complete current checkout, verifies its digest in-container, and reaches the expected attributable RED without host inheritance. CI compares its exact final phase map/reasons to `tools/fixtures/greenfield-expected-red.tsv`; the fixture remains red while the detector job is green only on an exact match. - [x] Debian/glibc checkout fixture now packages the complete current checkout, verifies its digest in-container, and reaches the expected attributable RED without host inheritance. CI compares its exact final phase map/reasons to `tools/fixtures/greenfield-expected-red.tsv`; the fixture remains red while the detector job is green only on an exact match.
- [ ] Reviews complete. 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. - [ ] 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.
## Risks / blockers ## Risks / blockers
@@ -180,15 +180,16 @@ chk "[control] without -E the mid-sync corruption survives (no rollback)" \
# ── Part C: an INT/TERM interrupt must terminate, not resume (blocker-A) ────── # ── 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 # A bash signal trap that merely returns lets the script continue past the
# interrupt — restoring the snapshot, then resuming the sync and reporting # interrupt — restoring the snapshot, then resuming the install and reporting
# success. The earlier test used a child cp shim to signal its parent, making # success. Generate two installer fixtures that first damage a real target path
# child completion race Bash's interrupted wait. Concurrency is not part of the # after the snapshot is armed, then signal their own Bash process immediately
# guarded property: sync_framework_keep() runs in the installer's own Bash # before the normal sync. This fixed injection point is independent of `find`
# process, and `kill` is a builtin. Generate two installer fixtures that signal # enumeration order: after a no-exit handler restores and returns, the complete
# themselves at the same known mid-sync point. Their TERM handlers emit the same # sync still remains to run, so the historical resume bug is deterministic on
# observable before diverging, so missing signal delivery fails BOTH arms rather # every filesystem. Their TERM handlers emit the same observable before
# than manufacturing a pass. The only semantic difference between fixtures is # diverging, so missing signal delivery fails BOTH arms rather than manufacturing
# the explicit `exit 1` whose load-bearing behavior this control proves. # 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' TERM_MARKER='[test-control] TERM handler entered'
HANDLER_WITH_EXIT="trap 'echo \"$TERM_MARKER\" >&2; restore_snapshot; exit 1' TERM # TEST-TERM-HANDLER" 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" HANDLER_WITHOUT_EXIT="trap 'echo \"$TERM_MARKER\" >&2; restore_snapshot' TERM # TEST-TERM-HANDLER"
@@ -196,19 +197,17 @@ HANDLER_WITHOUT_EXIT="trap 'echo \"$TERM_MARKER\" >&2; restore_snapshot' TERM #
make_signal_installer() { make_signal_installer() {
local output="$1" handler="$2" local output="$1" handler="$2"
local target_trap="trap 'restore_snapshot; exit 1' ERR INT TERM" local target_trap="trap 'restore_snapshot; exit 1' ERR INT TERM"
local target_cp=' cp "$abs" "$dst/$rel"' local target_sync='sync_framework'
local inject_open=" if [[ \"\$rel\" == \"$POISON_REL\" ]]; then" local inject_damage="printf '%s' '$GARBAGE' > \"\$TARGET_DIR/$POISON_REL\" # TEST-TERM-DAMAGE"
local inject_kill=' kill -TERM "$$" # TEST-TERM-INJECTION' local inject_kill='kill -TERM "$$" # TEST-TERM-INJECTION'
local inject_close=' fi'
if ! awk \ if ! awk \
-v target_trap="$target_trap" -v target_cp="$target_cp" \ -v target_trap="$target_trap" -v target_sync="$target_sync" \
-v handler="$handler" -v inject_open="$inject_open" \ -v handler="$handler" -v inject_damage="$inject_damage" \
-v inject_kill="$inject_kill" -v inject_close="$inject_close" ' -v inject_kill="$inject_kill" '
$0 == target_cp { $0 == target_sync {
print inject_open print inject_damage
print inject_kill print inject_kill
print inject_close
injection_sites++ injection_sites++
} }
{ print } { print }
@@ -231,7 +230,8 @@ make_signal_installer "$SIGNALED" "$HANDLER_WITH_EXIT"
make_signal_installer "$NOEXIT" "$HANDLER_WITHOUT_EXIT" make_signal_installer "$NOEXIT" "$HANDLER_WITHOUT_EXIT"
signal_fixture_ready() { signal_fixture_ready() {
local fixture="$1" expected_handler="$2" local fixture="$1" expected_handler="$2"
[[ "$(grep -cF '# TEST-TERM-INJECTION' "$fixture")" -eq 1 ]] \ [[ "$(grep -cF '# TEST-TERM-DAMAGE' "$fixture")" -eq 1 ]] \
&& [[ "$(grep -cF '# TEST-TERM-INJECTION' "$fixture")" -eq 1 ]] \
&& [[ "$(grep -cF '# TEST-TERM-HANDLER' "$fixture")" -eq 1 ]] \ && [[ "$(grep -cF '# TEST-TERM-HANDLER' "$fixture")" -eq 1 ]] \
&& grep -Fqx "$expected_handler" "$fixture" && grep -Fqx "$expected_handler" "$fixture"
} }
@@ -261,10 +261,12 @@ run_signal_upgrade() {
IFS=$'\t' read -r rcC OUTC HC < <(run_signal_upgrade "$SIGNALED") IFS=$'\t' read -r rcC OUTC HC < <(run_signal_upgrade "$SIGNALED")
chk "[signal] TERM handler observable fires exactly once" \ chk "[signal] TERM handler observable fires exactly once" \
"[ \"\$(grep -cF '$TERM_MARKER' '$OUTC')\" -eq 1 ]" "[ \"\$(grep -cF '$TERM_MARKER' '$OUTC')\" -eq 1 ]"
chk "[signal] SIGTERM mid-sync aborts non-zero (trap exits, does not resume)" \ chk "[signal] SIGTERM after target mutation aborts non-zero (trap exits, does not resume)" \
"[ '$rcC' -ne 0 ]" "[ '$rcC' -ne 0 ]"
chk "[signal] restore_snapshot fires on the interrupt" \ chk "[signal] restore_snapshot fires on the interrupt" \
"grep -q 'restoring previous state from snapshot' '$OUTC'" "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" \ chk "[signal] does NOT resume to report sync success after the interrupt" \
"! grep -q 'file phase complete' '$OUTC'" "! grep -q 'file phase complete' '$OUTC'"
@@ -277,6 +279,8 @@ chk "[control] without 'exit 1' the installer exits zero after resuming" \
"[ '$rcD' -eq 0 ]" "[ '$rcD' -eq 0 ]"
chk "[control] without 'exit 1' the trap resumes and reports sync success (the bug)" \ chk "[control] without 'exit 1' the trap resumes and reports sync success (the bug)" \
"grep -q 'file phase complete' '$OUTD'" "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) ───── # ── Part D: a failed source/prune `find` scan must abort + roll back (D1) ─────
# A `< <(find …)` process substitution discards find's exit status, so an # A `< <(find …)` process substitution discards find's exit status, so an
+10 -5
View File
@@ -182,14 +182,19 @@ fi
printf '[fixture] installer_exit=%d done_claims=%s\n' \ printf '[fixture] installer_exit=%d done_claims=%s\n' \
"$install_status" "$(grep -cF 'Done.' "$install_log" || true)" "$install_status" "$(grep -cF 'Done.' "$install_log" || true)"
# P0 Resolve context # P0 Resolve context. Keep this final fixture row as discriminating as the
shell="$(getent passwd "$(id -u)" | cut -d: -f7)" # installer's own P0 row: the expected-RED comparator binds this exact reason.
if [[ "$(id -u)" -ne 0 && "$home" == "/home/mosaic" && "$shell" == "/bin/bash" ]] \ 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 \ && ldd --version 2>&1 | grep -i 'glibc\|gnu libc' >/dev/null \
&& [[ "$(node -p 'Number(process.versions.node.split(".")[0])')" -ge 20 ]]; then && [[ "$(node -p 'Number(process.versions.node.split(".")[0])')" -ge 20 ]]; then
phase_pass P0 "target=mosaic uid=$(id -u) HOME=$home shell=$shell libc=glibc node=$(node --version)" 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 else
phase_fail P0 "context unresolved or unsupported (uid=$(id -u) HOME=$home shell=${shell:-unknown})" 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 fi
# P1 Preflight # P1 Preflight
+4 -1
View File
@@ -13,10 +13,11 @@ next-git-present phase P8=FAIL
next-git-present phase P9=FAIL next-git-present phase P9=FAIL
next-git-present require ^\[fixture\] resolved lane=next .*version=[0-9]+\.[0-9]+\.[0-9]+-next\. next-git-present require ^\[fixture\] resolved lane=next .*version=[0-9]+\.[0-9]+\.[0-9]+-next\.
next-git-present require ^\[fixture\] installer_exit=1 done_claims=0$ next-git-present require ^\[fixture\] installer_exit=1 done_claims=0$
next-git-present 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 ^\[SECRET-CONTROL\] PASS:
next-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$ next-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$
next-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED: next-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:
next-git-present require ^\[P6\] FAIL: broker absent but dead enforcement hooks are active 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 next-git-present forbid Done\.|MOSAIC_C1_CANARY_|CLI not found on PATH
main-git-present exit 1 main-git-present exit 1
main-git-present phase P0=PASS main-git-present phase P0=PASS
@@ -31,6 +32,7 @@ main-git-present phase P8=FAIL
main-git-present phase P9=FAIL main-git-present phase P9=FAIL
main-git-present require ^\[fixture\] resolved lane=main .*version=[0-9]+\.[0-9]+\.[0-9]+$ main-git-present require ^\[fixture\] resolved lane=main .*version=[0-9]+\.[0-9]+\.[0-9]+$
main-git-present require ^\[fixture\] installer_exit=1 done_claims=0$ main-git-present require ^\[fixture\] installer_exit=1 done_claims=0$
main-git-present 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 ^\[SECRET-CONTROL\] PASS:
main-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$ main-git-present require ^\[P3\] PASS: absolute_path=.* version=.* equals resolved lane version$
main-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED: main-git-present require ^\[P4\] FAIL: NOT-MEASURED / UNDECLARED:
@@ -48,6 +50,7 @@ next-git-absent phase P7=PASS
next-git-absent phase P8=FAIL next-git-absent phase P8=FAIL
next-git-absent phase P9=FAIL next-git-absent phase P9=FAIL
next-git-absent require ^\[fixture\] installer_exit=1 done_claims=0$ next-git-absent require ^\[fixture\] installer_exit=1 done_claims=0$
next-git-absent 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 ^\[SECRET-CONTROL\] PASS:
next-git-absent require ^\[P1\] FAIL: undeclared/missing prerequisite\(s\)=git; next-git-absent require ^\[P1\] FAIL: undeclared/missing prerequisite\(s\)=git;
next-git-absent require ^\[P3\] FAIL: .*executable=no next-git-absent require ^\[P3\] FAIL: .*executable=no
1 # Pinned C1 expected-RED contract. Updating a verdict/reason requires review by the owning remediation lane.
13 next-git-present
14 next-git-present
15 next-git-present
16 next-git-present
17 next-git-present
18 next-git-present
19 next-git-present
20 next-git-present
21 next-git-present
22 main-git-present
23 main-git-present
32 main-git-present
33 main-git-present
34 main-git-present
35 main-git-present
36 main-git-present
37 main-git-present
38 main-git-present
50 next-git-absent
51 next-git-absent
52 next-git-absent
53 next-git-absent
54 next-git-absent
55 next-git-absent
56 next-git-absent
+26
View File
@@ -201,6 +201,9 @@ if [[ -z "$dest" ]]; then
exit 1 exit 1
fi fi
mkdir -p "$dest/stack/packages/mosaic/framework" "$dest/stack/apps/gateway" 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' cat > "$dest/stack/packages/mosaic/framework/install.sh" <<'FRAMEWORK'
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
@@ -384,6 +387,29 @@ set -e
grep -qF 'archive safety/integrity check failed' <<<"$OUTPUT" grep -qF 'archive safety/integrity check failed' <<<"$OUTPUT"
[[ "$(node -p "require('$ACTIVE').status")" == "rolled-back" ]] [[ "$(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 reset_state
echo "[test] --dev source install does not require registry version resolution" echo "[test] --dev source install does not require registry version resolution"
OUTPUT="$( OUTPUT="$(
+28
View File
@@ -304,6 +304,34 @@ grep -q '^\[P4\] FAIL:.*owner/mode policy' "$TMP/p4-tree-mode.log" \
|| fail_case 'P4 did not attribute unsafe created-path mode' || fail_case 'P4 did not attribute unsafe created-path mode'
chmod 0644 "$good_mosaic/AGENTS.md" 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' printf '[test] case: persisted required-action failures remain blocking\n'
for blocked_phase in P4 P6; do for blocked_phase in P4 P6; do
node -e ' node -e '
+32 -8
View File
@@ -657,15 +657,27 @@ state_path_owner_mode_ok() {
} }
state_tree_owner_mode_ok() { state_tree_owner_mode_ok() {
local root="$1" path policy local root="$1" path policy scan valid=true
[[ -e "$root" ]] || return 0 [[ -e "$root" ]] || return 0
scan="$(mktemp)" \
|| { STATE_POLICY_REASON="$root enumeration staging failed"; return 1; }
if ! find "$root" -xdev -print0 > "$scan"; then
STATE_POLICY_REASON="$root enumeration failed; created-path inventory is incomplete"
rm -f "$scan"
return 1
fi
while IFS= read -r -d '' path; do while IFS= read -r -d '' path; do
policy=shared-read policy=shared-read
case "$path" in case "$path" in
"$MOSAIC_HOME/credentials"|"$MOSAIC_HOME/credentials"/*|"$MOSAIC_HOME/SOUL.md"|"$MOSAIC_HOME/USER.md") policy=private ;; "$MOSAIC_HOME/credentials"|"$MOSAIC_HOME/credentials"/*|"$MOSAIC_HOME/SOUL.md"|"$MOSAIC_HOME/USER.md") policy=private ;;
esac esac
state_path_owner_mode_ok "$path" "$policy" || return if ! state_path_owner_mode_ok "$path" "$policy"; then
done < <(find "$root" -xdev -print0) valid=false
break
fi
done < "$scan"
rm -f "$scan"
[[ "$valid" == true ]]
} }
state_resolved_version() { state_resolved_version() {
@@ -1255,13 +1267,25 @@ PY
tar xzf "$archive" -C "$WORK_DIR" || return tar xzf "$archive" -C "$WORK_DIR" || return
state_json_line artifact P2 committed "lane=$GIT_REF source_commit=$RESOLVED_SOURCE_COMMIT sha256=$RESOLVED_SOURCE_DIGEST" || return state_json_line artifact P2 committed "lane=$GIT_REF source_commit=$RESOLVED_SOURCE_COMMIT sha256=$RESOLVED_SOURCE_DIGEST" || return
# Gitea archives extract to <repo-name>/ inside the work dir # Gitea archives must extract to exactly one <repo-name>/ directory. Capture
EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)" || return # and check the complete walk before selecting it: `find | head -1` both hides
if [[ -z "$EXTRACTED_DIR" ]] || [[ ! -d "$EXTRACTED_DIR" ]]; then # a failed enumeration and makes multiple roots depend on filesystem order.
fail "Could not locate extracted source in archive." local extracted_roots_file
ls -la "$WORK_DIR" >&2 || true # Diagnostic only; the named P2 failure is authoritative. local -a extracted_roots=()
extracted_roots_file="$(mktemp)" \
|| { fail "P2 Acquire artifacts failed: could not stage extracted-root inventory."; return 1; }
if ! find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d -print0 > "$extracted_roots_file"; then
rm -f "$extracted_roots_file"
fail "P2 Acquire artifacts failed: could not enumerate extracted source roots."
return 1 return 1
fi fi
mapfile -d '' -t extracted_roots < "$extracted_roots_file"
rm -f "$extracted_roots_file"
if [[ "${#extracted_roots[@]}" -ne 1 ]] || [[ ! -d "${extracted_roots[0]:-}" ]]; then
fail "P2 Acquire artifacts failed: expected exactly one extracted source root; found ${#extracted_roots[@]}."
return 1
fi
EXTRACTED_DIR="${extracted_roots[0]}"
} }
# Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both # Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both
+19 -2
View File
@@ -23,7 +23,7 @@ declare -A allowed_case=(
declare -A expected_requires=( declare -A expected_requires=(
[next-git-present]=6 [main-git-present]=6 [next-git-absent]=4 [next-git-present]=6 [main-git-present]=6 [next-git-absent]=4
) )
declare -A row_count=() exit_count=() require_count=() forbid_count=() phase_count=() unique_rows=() declare -A row_count=() exit_count=() require_count=() forbid_count=() phase_count=() phase_reason_count=() unique_rows=()
while IFS= read -r raw; do while IFS= read -r raw; do
[[ -n "$raw" && "${raw:0:1}" != "#" ]] || continue [[ -n "$raw" && "${raw:0:1}" != "#" ]] || continue
field_count="$(awk -F '\t' '{print NF}' <<<"$raw")" field_count="$(awk -F '\t' '{print NF}' <<<"$raw")"
@@ -46,6 +46,11 @@ while IFS= read -r raw; do
phase_key="$case_name|$phase" phase_key="$case_name|$phase"
phase_count[$phase_key]=$((${phase_count[$phase_key]:-0} + 1)) 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) require)
[[ -n "$expectation" ]] || { echo "empty expected-RED require row: case=$case_name" >&2; exit 2; } [[ -n "$expectation" ]] || { echo "empty expected-RED require row: case=$case_name" >&2; exit 2; }
require_count[$case_name]=$((${require_count[$case_name]:-0} + 1)) require_count[$case_name]=$((${require_count[$case_name]:-0} + 1))
@@ -65,11 +70,13 @@ for case_name in "${expected_cases[@]}"; do
[[ "${phase_count[$case_name|$phase]:-0}" -eq 1 ]] \ [[ "${phase_count[$case_name|$phase]:-0}" -eq 1 ]] \
|| { echo "expected-RED manifest requires exactly one $phase disposition for case=$case_name" >&2; exit 2; } || { echo "expected-RED manifest requires exactly one $phase disposition for case=$case_name" >&2; exit 2; }
done 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]}" ]] \ [[ "${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; } || { echo "expected-RED manifest require-row population changed for case=$case_name" >&2; exit 2; }
[[ "${forbid_count[$case_name]:-0}" -eq 1 ]] \ [[ "${forbid_count[$case_name]:-0}" -eq 1 ]] \
|| { echo "expected-RED manifest requires exactly one forbid row for case=$case_name" >&2; exit 2; } || { echo "expected-RED manifest requires exactly one forbid row for case=$case_name" >&2; exit 2; }
expected_total=$((1 + 10 + expected_requires[$case_name] + 1)) expected_total=$((1 + 10 + 1 + expected_requires[$case_name] + 1))
[[ "${row_count[$case_name]:-0}" -eq "$expected_total" ]] \ [[ "${row_count[$case_name]:-0}" -eq "$expected_total" ]] \
|| { echo "expected-RED manifest row population changed for case=$case_name" >&2; exit 2; } || { echo "expected-RED manifest row population changed for case=$case_name" >&2; exit 2; }
done done
@@ -98,6 +105,16 @@ while IFS=$'\t' read -r case_name kind expectation; do
failures=$((failures + 1)) failures=$((failures + 1))
fi 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) require)
if ! grep -Eq -- "$expectation" "$LOG"; then if ! grep -Eq -- "$expectation" "$LOG"; then
echo "expected-RED missing required evidence: case=$CASE regex=$expectation" >&2 echo "expected-RED missing required evidence: case=$CASE regex=$expectation" >&2
+16 -2
View File
@@ -8,13 +8,13 @@ cat > "$TMP/match.log" <<'LOG'
[fixture] resolved lane=next package=@mosaicstack/mosaic@next version=0.0.50-next.999 [fixture] resolved lane=next package=@mosaicstack/mosaic@next version=0.0.50-next.999
[fixture] installer_exit=1 done_claims=0 [fixture] installer_exit=1 done_claims=0
[SECRET-CONTROL] PASS: seeded canary absent from complete scan population [SECRET-CONTROL] PASS: seeded canary absent from complete scan population
[P0] PASS: supported context [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 [P1] PASS: preflight complete
[P2] PASS: pinned artifact [P2] PASS: pinned artifact
[P3] PASS: absolute_path=/home/test/.npm-global/bin/mosaic version=0.0.50-next.999 equals resolved lane version [P3] PASS: absolute_path=/home/test/.npm-global/bin/mosaic version=0.0.50-next.999 equals resolved lane version
[P4] FAIL: NOT-MEASURED / UNDECLARED: declaration absent [P4] FAIL: NOT-MEASURED / UNDECLARED: declaration absent
[P5] FAIL: identity absent [P5] FAIL: identity absent
[P6] FAIL: broker absent but dead enforcement hooks are active [P6] FAIL: runtime linking/activation action reported a required failure
[P7] PASS: no services requested [P7] PASS: no services requested
[P8] FAIL: shell path absent [P8] FAIL: shell path absent
[P9] FAIL: aggregate refusal [P9] FAIL: aggregate refusal
@@ -23,6 +23,13 @@ LOG
bash "$ROOT/tools/verify-greenfield-expected-red.sh" next-git-present "$TMP/match.log" 1 >/dev/null 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' 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" 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 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 echo '[test] FAIL: changed P4 verdict did not invalidate the pinned manifest' >&2
@@ -52,6 +59,13 @@ if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/missing-phase.tsv" \
fi fi
printf '[test] PASS: every case requires one P0-P9 disposition\n' 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" cp "$manifest" "$TMP/duplicate.tsv"
printf 'next-git-present\tphase\tP3=PASS\n' >> "$TMP/duplicate.tsv" printf 'next-git-present\tphase\tP3=PASS\n' >> "$TMP/duplicate.tsv"
if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/duplicate.tsv" \ if MOSAIC_EXPECTED_RED_MANIFEST="$TMP/duplicate.tsv" \