From 58a5567fb9b76c5454eb614ee17ba75c400cb3a4 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 10 Jul 2026 00:16:32 -0700 Subject: [PATCH] Make remote corpus gates fail closed Install the shell utilities Tangled's Nixery image does not provide, preflight every external command used by the corpus and wiki gates, and pin the reported partial-PATH failure as a regression check. Defense: wiki/process/meta.md acceptance criterion 9 requires local and CI corpus checks to invoke the same real gate. A remote audit that scanned nothing and returned success violated that contract; this makes missing audit dependencies an explicit failure. --- .tangled/workflows/check.yml | 8 ++++ tools/check.sh | 23 ++++++++++ tools/corpus_gate.sh | 15 ++++++- tools/wiki_gate.sh | 12 ++++- wiki/log/2026-07-10-ci-gates-fail-closed.md | 49 +++++++++++++++++++++ wiki/log/DEVLOG.md | 5 +++ wiki/process/workflows.md | 6 ++- 7 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 wiki/log/2026-07-10-ci-gates-fail-closed.md diff --git a/.tangled/workflows/check.yml b/.tangled/workflows/check.yml index 81a2d38f..5982ee20 100644 --- a/.tangled/workflows/check.yml +++ b/.tangled/workflows/check.yml @@ -21,6 +21,14 @@ clone: dependencies: nixpkgs: + # Shell gates are intentionally explicit: Nixery's base image does not + # provide the normal Unix text/file utilities. + - bash + - coreutils + - findutils + - gawk + - gnugrep + - gnused - rustc - cargo - rustfmt diff --git a/tools/check.sh b/tools/check.sh index 14becb6c..6c04fc5b 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -152,6 +152,27 @@ for script in tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh tools/seed- bash -n "$script" || { echo "FAIL: shell syntax: $script"; fail=1; } done +step "gate dependency preflight" +bash_bin=$(command -v bash) +partial_path=$(mktemp -d) +for available_command in comm dirname head mktemp rm sort tr wc; do + command_path=$(command -v "$available_command" || true) + [ -n "$command_path" ] || continue + ln -s "$command_path" "$partial_path/$available_command" +done +for gate in tools/corpus_gate.sh tools/wiki_gate.sh; do + if gate_output=$(PATH="$partial_path" "$bash_bin" "$gate" 2>&1); then + echo "FAIL: $gate returned success without its required commands" + fail=1 + elif ! grep -q '^FAIL: required command unavailable:' <<< "$gate_output"; then + echo "FAIL: $gate did not fail through its command preflight" + fail=1 + else + echo "$gate: rejects missing dependencies" + fi +done +rm -rf "$partial_path" + # Docs gates run in parallel with each other (and with the Rust gate when # both are needed). They never take the rust flock. docs_pids=() @@ -173,6 +194,8 @@ step "docs gates (parallel)" start_docs_gate "corpus" "bash tools/corpus_gate.sh" start_docs_gate "wiki" "bash tools/wiki_gate.sh" start_docs_gate "ledger-index" "bash tools/ledger_index.sh --check" +# The single-quoted program is evaluated inside start_docs_gate, not here. +# shellcheck disable=SC2016 start_docs_gate "env-registry" ' env_reg=wiki/engineering/env.md env_missing=0 diff --git a/tools/corpus_gate.sh b/tools/corpus_gate.sh index 71bba06a..426c9211 100755 --- a/tools/corpus_gate.sh +++ b/tools/corpus_gate.sh @@ -3,7 +3,17 @@ # Scans the wiki tree once per concern instead of re-grepping every file # for every field (the previous form was a fork storm under concurrent agents). set -uo pipefail -cd "$(dirname "$0")/.." + +missing_commands=0 +for required_command in awk dirname find grep head mktemp rm sed sort tr wc; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf 'FAIL: required command unavailable: %s\n' "$required_command" + missing_commands=1 + fi +done +[ "$missing_commands" -eq 0 ] || exit 1 + +cd "$(dirname "$0")/.." || exit 1 fail=0 @@ -30,7 +40,8 @@ tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT heading_slugs_file() { - local file="$1" cache="$tmp/headings.$(printf '%s' "$file" | tr '/ ' '__')" + local file="$1" cache + cache="$tmp/headings.$(printf '%s' "$file" | tr '/ ' '__')" if [ ! -f "$cache" ]; then # slugify_heading prints without a trailing newline; add one per heading # so the cache is line-oriented for grep -qxF. diff --git a/tools/wiki_gate.sh b/tools/wiki_gate.sh index df283639..1e50cab7 100755 --- a/tools/wiki_gate.sh +++ b/tools/wiki_gate.sh @@ -13,7 +13,17 @@ # writing that exact bracket-paren shape in prose; describe the syntax in # words instead. set -uo pipefail -cd "$(dirname "$0")/.." + +missing_commands=0 +for required_command in comm dirname find grep mktemp rm sed sort; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf 'FAIL: required command unavailable: %s\n' "$required_command" + missing_commands=1 + fi +done +[ "$missing_commands" -eq 0 ] || exit 1 + +cd "$(dirname "$0")/.." || exit 1 fail=0 tmp=$(mktemp -d) diff --git a/wiki/log/2026-07-10-ci-gates-fail-closed.md b/wiki/log/2026-07-10-ci-gates-fail-closed.md new file mode 100644 index 00000000..ddef7fd2 --- /dev/null +++ b/wiki/log/2026-07-10-ci-gates-fail-closed.md @@ -0,0 +1,49 @@ +# CI corpus gates fail closed + +``` +Type: log +``` + +## Intent + +Make remote corpus enforcement real: missing audit utilities must turn the +Tangled job red instead of letting an empty scan report success. + +## Finding + +The Tangled run for the corpus-hardening landing displayed green even though +its corpus and wiki steps printed `command not found` for `find`, `grep`, and +`sed`. Nixery did not include the ordinary shell utilities implicitly. The +scripts used `set -uo pipefail`, but the missing commands ran inside process +substitutions and loops whose failures did not reach the final exit status, so +both audits could scan nothing and report success. + +This was a process insecurity: the local gate was real, but the remote copy of +the same gate could fail open. + +## Fix + +- Tangled's check image now explicitly installs Bash, coreutils, findutils, + gawk, GNU grep, and GNU sed. +- `tools/corpus_gate.sh` and `tools/wiki_gate.sh` preflight every external + command they use and exit nonzero before scanning when one is unavailable. +- `tools/check.sh --docs` constructs the CI-like partial `PATH` that caused the + false green and requires both scripts to reject it through that preflight. +- The workflow knowledge page now states that a missing audit dependency is a + hard failure. + +## Verification + +- Reproduction before the fix: both gates exited 0 while printing missing + `find`/`grep`/`sed` errors. +- Regression after the fix: both gates exit 1 and name the missing commands + under the same partial `PATH`. +- `./tools/check.sh --docs` passes, including the new fail-closed regression + step and the normal corpus, wiki, and environment gates. +- `shellcheck tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh` passes. + +## Design impact + +No game or corpus rule changed. This restores +`wiki/process/meta.md` acceptance criterion 9 in the environment that was not +actually enforcing it. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 543cc8f8..ceae84cb 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -30,6 +30,11 @@ add or amend a session log, then re-run the generator. - Intent: Cameron: mode change should be hover over a rack and press 1-4 immediately — not enter + enter + select dial row + enter. - Log: [wiki/log/2026-07-10-hover-mode-keys.md](2026-07-10-hover-mode-keys.md) +## 2026-07-10 - CI corpus gates fail closed + +- Intent: Make remote corpus enforcement real: missing audit utilities must turn the Tangled job red instead of letting an empty scan report success. +- Log: [wiki/log/2026-07-10-ci-gates-fail-closed.md](2026-07-10-ci-gates-fail-closed.md) + ## 2026-07-10 - Bevy performance pass - Intent: The material frontend was paying full restyle/UI cost every display frame: `advance_sim` held `ResMut` and ticked a timer on `Game` every frame, so `game.is_changed()` stayed true and `restyle_3d` / `render_map` / `render_ui` rebuilt continuously. diff --git a/wiki/process/workflows.md b/wiki/process/workflows.md index 5ce77186..68d61dcc 100644 --- a/wiki/process/workflows.md +++ b/wiki/process/workflows.md @@ -165,7 +165,11 @@ rebase of every place that teaches, checks, or renders the authority model. The doorway is protected mechanically: `tools/corpus_gate.sh` bounds its size, forbids binding-page structure, validates every page role and spec header, and resolves every structured Design target and heading. Local checks and CI call -this same script so the two implementations cannot drift. +this same script so the two implementations cannot drift. Both corpus scripts +preflight their external commands and fail before auditing if any are absent; +the Tangled Nixery image explicitly supplies Bash, GNU text utilities, +findutils, and coreutils. A missing audit dependency must make CI red, never +turn an empty scan into a green result. ## Browsing the wiki -- 2.51.2