From ceb835f5d2ad9c135b915e5399fd8863ecd66b5d Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:22:16 +0100 Subject: [PATCH 1/6] fix(autofix): fail closed on stale formulaic mutations --- scripts/auto-fix-formulaic.sh | 174 +++++++---------------------- tests/auto-fix-formulaic-safety.sh | 57 ++++++++++ 2 files changed, 95 insertions(+), 136 deletions(-) create mode 100755 tests/auto-fix-formulaic-safety.sh diff --git a/scripts/auto-fix-formulaic.sh b/scripts/auto-fix-formulaic.sh index 1ba449cc..c5fd4ea7 100755 --- a/scripts/auto-fix-formulaic.sh +++ b/scripts/auto-fix-formulaic.sh @@ -1,49 +1,33 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 -# auto-fix-formulaic.sh — Direct auto-fix for predictable, formulaic issues +# auto-fix-formulaic.sh — Legacy formulaic diagnostics compatibility command # -# This bypasses the full Hypatia pipeline and directly fixes issues that -# are so predictable they should never appear in any scan. These are the -# issues that the sophisticated pipeline SHOULD catch but currently doesn't -# because the operational deployment is missing. +# Mutation is intentionally disabled. The former implementation bypassed the +# Hypatia policy pipeline, carried a stale SHA database alongside actions.lock, +# inserted permissions: read-all contrary to current least-privilege policy, +# and could stage unrelated work with git add -A. # -# Usage: ./scripts/auto-fix-formulaic.sh [repo-path|all] +# GitHub Action resolution belongs to the authoritative `gh actions-lock` +# recipe. Workflow permissions require workflow-specific policy evaluation. +# Until those transactional recipes are wired, this command is read-only. +# +# Usage: ./scripts/auto-fix-formulaic.sh # Examples: -# ./scripts/auto-fix-formulaic.sh ~/Documents/hyperpolymath-repos/aerie -# ./scripts/auto-fix-formulaic.sh all # scan all repos +# ./scripts/auto-fix-formulaic.sh /home/hyperpolymath/developer/hyper-repos/aerie set -euo pipefail -REPOS_DIR="${HOME}/Documents/hyperpolymath-repos" FIXES_APPLIED=0 REPOS_SCANNED=0 - -# Canonical SHA pins — from Hypatia.Rules.SecurityErrors -declare -A SHA_PINS=( - ["actions/checkout@v4"]="34e114876b0b11c390a56381ad16ebd13914f8d5" - ["actions/checkout@v5"]="93cb6efe18208431cddfb8368fd83d5badbf9bfd" - ["github/codeql-action@v3"]="6624720a57d4c312633c7b953db2f2da5bcb4c3a" - ["ossf/scorecard-action@v2.4.0"]="62b2cac7ed8198b15735ed49ab1e5cf35480ba46" - ["dtolnay/rust-toolchain@stable"]="4be9e76fd7c4901c61fb841f559994984270fce7" - ["Swatinem/rust-cache@v2"]="779680da715d629ac1d338a641029a2f4372abb5" - ["codecov/codecov-action@v5"]="671740ac38dd9b0130fbe1cec585b89eea48d3de" - ["trufflesecurity/trufflehog@main"]="7ee2e0fdffec27d19ccbb8fb3dcf8a83b9d7f9e8" - ["webfactory/ssh-agent@v0.9.0"]="dc588b651fe13675774614f8e6a936a468676387" - ["ocaml/setup-ocaml@v3"]="dec6499fef64fc5d7ed43d43a87251b7b1c306f5" - ["softprops/action-gh-release@v2"]="a06a81a03ee405af7f2048a818ed3f03bbf83c7b" - ["actions/configure-pages@v5"]="983d7736d9b0ae728b81ab479565c72886d7745b" - ["actions/jekyll-build-pages@v1"]="44a6e6beabd48582f863aeeb6cb2151cc1716697" - ["actions/upload-pages-artifact@v3"]="56afc609e74202658d3ffba0e8f6dda462b719fa" - ["actions/deploy-pages@v4"]="d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e" - ["ruby/setup-ruby@v1"]="09a7688d3b55cf0e976497ff046b70949eeaccfd" - ["editorconfig-checker/action-editorconfig-checker@main"]="4054fa83a075fdf090bd098bdb1c09aaf64a4169" - ["slsa-framework/slsa-github-generator@v2.1.0"]="f7dd8c54c2067bafc12ca7a55595d5ee9b75204a" -) +WARNINGS=0 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" log() { echo "[hypatia-autofix] $*"; } -warn() { echo "[hypatia-autofix] WARNING: $*" >&2; } +warn() { + echo "[hypatia-autofix] WARNING: $*" >&2 + WARNINGS=$((WARNINGS + 1)) +} fix() { echo "[hypatia-autofix] FIX: $*" FIXES_APPLIED=$((FIXES_APPLIED + 1)) @@ -56,70 +40,7 @@ record() { } # --------------------------------------------------------------------------- -# Fix 1: Pin unpinned GitHub Actions to SHA -# --------------------------------------------------------------------------- -fix_unpinned_actions() { - local repo="$1" - local workflows_dir="${repo}/.github/workflows" - [ -d "$workflows_dir" ] || return 0 - - for wf in "${workflows_dir}"/*.yml; do - [ -f "$wf" ] || continue - - # Find lines with uses: action@vN (not already SHA-pinned) - while IFS= read -r line; do - # Extract action@version - action_ref=$(echo "$line" | grep -oP 'uses:\s*\K[^@]+@v[0-9][^\s]*' || true) - [ -z "$action_ref" ] && continue - - # Check if already SHA-pinned (40-char hex after @) - if echo "$line" | grep -qP '@[0-9a-f]{40}'; then - continue - fi - - # Look up canonical SHA - local sha="${SHA_PINS[$action_ref]:-}" - if [ -n "$sha" ]; then - local action_name="${action_ref%%@*}" - local version="${action_ref##*@}" - local old_pattern="${action_ref}" - local new_pattern="${action_name}@${sha} # ${version}" - - sed -i "s|${old_pattern}|${new_pattern}|g" "$wf" - fix "Pinned ${action_ref} → ${sha} in $(basename "$wf")" - else - warn "Unknown action ${action_ref} in $(basename "$wf") — add to SHA_PINS" - fi - done < <(grep -n 'uses:.*@v[0-9]' "$wf" 2>/dev/null || true) - done -} - -# --------------------------------------------------------------------------- -# Fix 2: Add missing permissions: read-all to workflows -# --------------------------------------------------------------------------- -fix_missing_permissions() { - local repo="$1" - local workflows_dir="${repo}/.github/workflows" - [ -d "$workflows_dir" ] || return 0 - - for wf in "${workflows_dir}"/*.yml; do - [ -f "$wf" ] || continue - - if ! grep -q '^permissions:' "$wf" 2>/dev/null; then - # Insert permissions after the 'on:' block (after first blank line after on:) - # Simple approach: insert after SPDX header line or at line 2 - if grep -q 'SPDX-License-Identifier' "$wf"; then - sed -i '/^name:/i permissions: read-all\n' "$wf" - else - sed -i '1a\permissions: read-all' "$wf" - fi - fix "Added permissions: read-all to $(basename "$wf")" - fi - done -} - -# --------------------------------------------------------------------------- -# Fix 3: Detect binary artifacts tracked by git +# Diagnostic 1: Detect binary artifacts tracked by git # --------------------------------------------------------------------------- fix_tracked_binaries() { local repo="$1" @@ -155,7 +76,7 @@ fix_tracked_binaries() { } # --------------------------------------------------------------------------- -# Fix 4: Check for AGPL references (should be PMPL) +# Diagnostic 2: Check for AGPL references (should be PMPL) # --------------------------------------------------------------------------- fix_agpl_references() { local repo="$1" @@ -182,7 +103,7 @@ fix_agpl_references() { } # --------------------------------------------------------------------------- -# Fix 5: Check for missing SECURITY.md in public repos +# Diagnostic 3: Check for missing SECURITY.md in public repos # --------------------------------------------------------------------------- check_security_md() { local repo="$1" @@ -192,7 +113,7 @@ check_security_md() { } # --------------------------------------------------------------------------- -# Fix 6: Check for missing .editorconfig +# Diagnostic 4: Check for missing .editorconfig # --------------------------------------------------------------------------- check_editorconfig() { local repo="$1" @@ -212,8 +133,6 @@ scan_repo() { REPOS_SCANNED=$((REPOS_SCANNED + 1)) local before=$FIXES_APPLIED - fix_unpinned_actions "$repo" - fix_missing_permissions "$repo" fix_tracked_binaries "$repo" fix_agpl_references "$repo" check_security_md "$repo" @@ -222,52 +141,34 @@ scan_repo() { local fix_count=$((after - before)) - # Auto-commit and push if fixes were applied - if [ "$fix_count" -gt 0 ] && [ "${AUTO_PUSH:-false}" = "true" ]; then - cd "$repo" - git add -A - git commit -m "$(cat < -EOF -)" 2>/dev/null && { - git push origin HEAD 2>/dev/null && log "Pushed fixes for $(basename "$repo")" || warn "Push failed for $(basename "$repo")" - } || log "Nothing to commit in $(basename "$repo")" - fi - # Record visit with fix count - record "$repo" "scan" "fixes=${fix_count}" + record "$repo" "diagnostic_scan" "fixes=${fix_count};warnings=${WARNINGS}" } -# Parse flags +# Reject legacy mutation and incomplete-estate modes explicitly. for arg in "$@"; do case "$arg" in - --push) export AUTO_PUSH=true; shift ;; + --push) + echo "ERROR: --push is disabled; this compatibility command is diagnostic-only." >&2 + exit 2 + ;; esac done if [ "${1:-}" = "all" ]; then - log "Scanning all repos in ${REPOS_DIR}..." - for repo_dir in "${REPOS_DIR}"/*/; do - [ -d "${repo_dir}/.git" ] || continue - scan_repo "$repo_dir" - done + echo "ERROR: incomplete 'all' traversal is disabled; use the authoritative estate manifest." >&2 + exit 2 elif [ -n "${1:-}" ]; then scan_repo "$1" else - echo "Usage: $0 [--push] [repo-path|all]" - echo " $0 ~/Documents/hyperpolymath-repos/aerie" - echo " $0 --push all # scan, fix, commit, and push" + echo "Usage: $0 " + echo " $0 /home/hyperpolymath/developer/hyper-repos/aerie" + echo "" + echo "Mutation, --push, and incomplete 'all' traversal are disabled." exit 1 fi -log "Done. Scanned ${REPOS_SCANNED} repos, applied ${FIXES_APPLIED} fixes." +log "Done. Diagnostic-only scan: repos=${REPOS_SCANNED}, fixes=${FIXES_APPLIED}, warnings=${WARNINGS}." # --- Kin Protocol: write heartbeat --- KIN_DIR="${HOME}/.hypatia/kin" @@ -277,14 +178,15 @@ cat > "${KIN_DIR}/auto-fix.heartbeat.json" <"${WORKFLOW}" +printf '%s\n' 'preserve this unrelated work' >"${UNRELATED}" + +workflow_before="$(sha256sum "${WORKFLOW}")" +unrelated_before="$(sha256sum "${UNRELATED}")" + +HOME="${FAKE_HOME}" bash "${FIXER}" "${FIXTURE}" >/dev/null + +[[ "$(sha256sum "${WORKFLOW}")" == "${workflow_before}" ]] +[[ "$(sha256sum "${UNRELATED}")" == "${unrelated_before}" ]] + +heartbeat="${FAKE_HOME}/.hypatia/kin/auto-fix.heartbeat.json" +[[ -f "${heartbeat}" ]] +grep -q '"status": "diagnostic_only"' "${heartbeat}" +grep -q '"mutation_enabled": false' "${heartbeat}" +if grep -q 'sha_pin_fix\|formulaic_fix' "${heartbeat}"; then + echo "ERROR: heartbeat claims a disabled mutation capability" >&2 + exit 1 +fi + +if HOME="${FAKE_HOME}" bash "${FIXER}" --push "${FIXTURE}" >/dev/null 2>&1; then + echo "ERROR: --push unexpectedly succeeded" >&2 + exit 1 +fi + +if HOME="${FAKE_HOME}" bash "${FIXER}" all >/dev/null 2>&1; then + echo "ERROR: incomplete all-repository traversal unexpectedly succeeded" >&2 + exit 1 +fi + +echo "auto-fix-formulaic safety controls: PASS" From 73674c64ec3fc199d2051c67dcff40fa74cd5e9b Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:26:17 +0100 Subject: [PATCH 2/6] test(autofix): run fail-closed safety control in e2e --- tests/e2e.sh | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/e2e.sh b/tests/e2e.sh index 4b1ef6c9..9023ef10 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -5,11 +5,12 @@ # Hypatia — End-to-End Test Suite # # Tests the neurosymbolic scanner pipeline without docker-compose: -# 1. Elixir scanner builds and runs -# 2. Scan a fixture repository -# 3. Verify findings are detected -# 4. Verify rule modules produce output -# 5. Verify JSON output format +# 1. Legacy formulaic diagnostics remain non-mutating and fail closed +# 2. Elixir scanner builds and runs +# 3. Scan a fixture repository +# 4. Verify findings are detected +# 5. Verify rule modules produce output +# 6. Verify JSON output format # # Usage: # bash tests/e2e.sh @@ -65,12 +66,30 @@ if ! $HAS_ELIXIR && ! $HAS_RUST; then fi echo "" +# ─── Formulaic diagnostics safety control ────────────────────────── +bold "Formulaic diagnostics safety control" + +if safety_output="$(bash "${PROJECT_DIR}/tests/auto-fix-formulaic-safety.sh" 2>&1)"; then + pass "Legacy formulaic diagnostics are non-mutating and fail closed" +else + fail_test "Legacy formulaic diagnostics safety control" + printf '%s\n' "${safety_output}" +fi +echo "" + # ─── Create test fixture ──────────────────────────────────────────── FIXTURE_DIR="$PROJECT_DIR/integration/fixtures/test-repo" if [ ! -d "$FIXTURE_DIR" ]; then # Create minimal fixture if one doesn't exist FIXTURE_DIR=$(mktemp -d) - trap "rm -rf $FIXTURE_DIR" EXIT + # Invoked indirectly by the EXIT trap below. + # shellcheck disable=SC2329 + cleanup_fixture() { + if [[ -n "${FIXTURE_DIR:-}" && "${FIXTURE_DIR}" == "${TMPDIR:-/tmp}/"* ]]; then + rm -rf -- "${FIXTURE_DIR}" + fi + } + trap cleanup_fixture EXIT mkdir -p "$FIXTURE_DIR/.github/workflows" "$FIXTURE_DIR/src" # Deliberately insecure workflow for scanner to find From e5926b43a4bba6ce091512bf7208466102d3d63c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:38:55 +0100 Subject: [PATCH 3/6] fix(ci): satisfy baseline policy checks --- .github/workflows/label-triage.yml | 1 + .github/workflows/labels.yml | 1 + .hypatia-exemptions.adoc | 4 ++-- SECURITY.md | 8 ++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 SECURITY.md diff --git a/.github/workflows/label-triage.yml b/.github/workflows/label-triage.yml index 9886e920..e61baef9 100644 --- a/.github/workflows/label-triage.yml +++ b/.github/workflows/label-triage.yml @@ -46,6 +46,7 @@ permissions: jobs: triage: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Classify and label env: diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index c80b676c..c536afb9 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -32,6 +32,7 @@ permissions: jobs: sync: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Apply canonical labels env: diff --git a/.hypatia-exemptions.adoc b/.hypatia-exemptions.adoc index e839c8a1..ce5f7296 100644 --- a/.hypatia-exemptions.adoc +++ b/.hypatia-exemptions.adoc @@ -38,8 +38,8 @@ They are accepted as a category, not enumerated row-by-row: * `+.audittraining/**+` — training corpus. * `+scripts/fix-scripts/**+` — remediation scripts. -* `+test/**+` and `+**/tests/**+` — test fixtures -(e.g. `+password: "test123"+`). +* `+test/**+` and `+**/tests/**+` — test fixtures that contain deliberately + invalid credential-shaped examples. Findings against these paths under `+security_errors/secret_detected+` are kept in `+.hypatia-baseline.json+` and should remain there. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..b58a58b1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,8 @@ +# Security Policy + +The canonical security policy for this repository is +[SECURITY.adoc](SECURITY.adoc). + +Please do not disclose suspected vulnerabilities in a public issue. Use +[GitHub private vulnerability reporting](https://github.com/hyperpolymath/hypatia/security/advisories/new) +so the maintainers can investigate and coordinate a fix privately. From 00669ce598df3a22e321d07ecf1d8818ef8f38a2 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:53:58 +0100 Subject: [PATCH 4/6] fix: keep formulaic autofix diagnostic-only --- scripts/auto-fix-formulaic.sh | 4 ++-- scripts/hypatia-autofix.service | 2 +- tests/auto-fix-formulaic-safety.sh | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/auto-fix-formulaic.sh b/scripts/auto-fix-formulaic.sh index c5fd4ea7..0c83d632 100755 --- a/scripts/auto-fix-formulaic.sh +++ b/scripts/auto-fix-formulaic.sh @@ -64,7 +64,7 @@ fix_tracked_binaries() { done # Check for large tracked files (>1MB) - git ls-files -z 2>/dev/null | while IFS= read -r -d '' f; do + while IFS= read -r -d '' f; do if [ -f "$f" ]; then local size size=$(stat -c%s "$f" 2>/dev/null || echo 0) @@ -72,7 +72,7 @@ fix_tracked_binaries() { warn "Large file tracked ($(( size / 1024 ))KB): $f (in $(basename "$repo"))" fi fi - done + done < <(git ls-files -z 2>/dev/null) } # --------------------------------------------------------------------------- diff --git a/scripts/hypatia-autofix.service b/scripts/hypatia-autofix.service index 43fdcfc6..22304b00 100644 --- a/scripts/hypatia-autofix.service +++ b/scripts/hypatia-autofix.service @@ -6,5 +6,5 @@ Description=Hypatia formulaic auto-fix scan [Service] Type=oneshot -ExecStart=%h/Documents/hyperpolymath-repos/hypatia/scripts/auto-fix-formulaic.sh --push all +ExecStart=%h/Documents/hyperpolymath-repos/hypatia/scripts/auto-fix-formulaic.sh %h/Documents/hyperpolymath-repos/hypatia Environment=HOME=%h diff --git a/tests/auto-fix-formulaic-safety.sh b/tests/auto-fix-formulaic-safety.sh index a39258c5..18e58c02 100755 --- a/tests/auto-fix-formulaic-safety.sh +++ b/tests/auto-fix-formulaic-safety.sh @@ -16,6 +16,8 @@ UNRELATED="${FIXTURE}/unrelated.txt" mkdir -p "${FAKE_HOME}" "$(dirname "${WORKFLOW}")" git -C "${FIXTURE}" init --quiet +git -C "${FIXTURE}" config user.email test@example.invalid +git -C "${FIXTURE}" config user.name "Hypatia safety test" printf '%s\n' \ 'name: CI' \ @@ -26,14 +28,20 @@ printf '%s\n' \ ' steps:' \ ' - uses: actions/checkout@v7.0.1' >"${WORKFLOW}" printf '%s\n' 'preserve this unrelated work' >"${UNRELATED}" +git -C "${FIXTURE}" add .github +git -C "${FIXTURE}" commit --quiet -m 'fixture workflow' workflow_before="$(sha256sum "${WORKFLOW}")" unrelated_before="$(sha256sum "${UNRELATED}")" +head_before="$(git -C "${FIXTURE}" rev-parse HEAD)" +status_before="$(git -C "${FIXTURE}" status --porcelain)" HOME="${FAKE_HOME}" bash "${FIXER}" "${FIXTURE}" >/dev/null [[ "$(sha256sum "${WORKFLOW}")" == "${workflow_before}" ]] [[ "$(sha256sum "${UNRELATED}")" == "${unrelated_before}" ]] +[[ "$(git -C "${FIXTURE}" rev-parse HEAD)" == "${head_before}" ]] +[[ "$(git -C "${FIXTURE}" status --porcelain)" == "${status_before}" ]] heartbeat="${FAKE_HOME}/.hypatia/kin/auto-fix.heartbeat.json" [[ -f "${heartbeat}" ]] From 8416e88c89448e911bfee435bcef09bc95c1a3c9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:29:54 +0100 Subject: [PATCH 5/6] fix(autofix): keep diagnostics outside target repo --- scripts/auto-fix-formulaic.sh | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/scripts/auto-fix-formulaic.sh b/scripts/auto-fix-formulaic.sh index 0c83d632..525a989d 100755 --- a/scripts/auto-fix-formulaic.sh +++ b/scripts/auto-fix-formulaic.sh @@ -21,8 +21,6 @@ FIXES_APPLIED=0 REPOS_SCANNED=0 WARNINGS=0 -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - log() { echo "[hypatia-autofix] $*"; } warn() { echo "[hypatia-autofix] WARNING: $*" >&2 @@ -33,12 +31,6 @@ fix() { FIXES_APPLIED=$((FIXES_APPLIED + 1)) } -# Record activity in the repo's .hypatia/ log -record() { - local repo="$1" action="$2" details="${3:-}" - bash "${SCRIPT_DIR}/bot-accountability.sh" record "$repo" "hypatia-autofix" "$action" "$details" 2>/dev/null || true -} - # --------------------------------------------------------------------------- # Diagnostic 1: Detect binary artifacts tracked by git # --------------------------------------------------------------------------- @@ -132,17 +124,10 @@ scan_repo() { log "Scanning $(basename "$repo")..." REPOS_SCANNED=$((REPOS_SCANNED + 1)) - local before=$FIXES_APPLIED fix_tracked_binaries "$repo" fix_agpl_references "$repo" check_security_md "$repo" check_editorconfig "$repo" - local after=$FIXES_APPLIED - - local fix_count=$((after - before)) - - # Record visit with fix count - record "$repo" "diagnostic_scan" "fixes=${fix_count};warnings=${WARNINGS}" } # Reject legacy mutation and incomplete-estate modes explicitly. From 1fc2fff4c4f456f317de0f6f15fb4aa695a5b96c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:46:39 +0100 Subject: [PATCH 6/6] fix(autofix): fail on incomplete file listing --- scripts/auto-fix-formulaic.sh | 15 ++++++++++++++- tests/auto-fix-formulaic-safety.sh | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/scripts/auto-fix-formulaic.sh b/scripts/auto-fix-formulaic.sh index 525a989d..6b40c7dd 100755 --- a/scripts/auto-fix-formulaic.sh +++ b/scripts/auto-fix-formulaic.sh @@ -55,6 +55,18 @@ fix_tracked_binaries() { fi done + # Materialise the listing before consuming it. Bash process substitution does + # not propagate the producer's exit status to the while loop, so reading + # directly from < <(git ls-files -z) could turn a failed listing into a + # successful empty scan. + local tracked_list + tracked_list=$(mktemp) + if ! git ls-files -z >"$tracked_list"; then + rm -f "$tracked_list" + echo "ERROR: git ls-files failed in $repo; tracked-file diagnostics are incomplete." >&2 + return 1 + fi + # Check for large tracked files (>1MB) while IFS= read -r -d '' f; do if [ -f "$f" ]; then @@ -64,7 +76,8 @@ fix_tracked_binaries() { warn "Large file tracked ($(( size / 1024 ))KB): $f (in $(basename "$repo"))" fi fi - done < <(git ls-files -z 2>/dev/null) + done <"$tracked_list" + rm -f "$tracked_list" } # --------------------------------------------------------------------------- diff --git a/tests/auto-fix-formulaic-safety.sh b/tests/auto-fix-formulaic-safety.sh index 18e58c02..2b68c243 100755 --- a/tests/auto-fix-formulaic-safety.sh +++ b/tests/auto-fix-formulaic-safety.sh @@ -62,4 +62,14 @@ if HOME="${FAKE_HOME}" bash "${FIXER}" all >/dev/null 2>&1; then exit 1 fi +# A failed tracked-file listing must fail the diagnostic instead of looking +# like a successful scan of an empty repository. +git_error_log="${TEST_ROOT}/git-ls-files-error.log" +printf '%s\n' 'deliberately invalid git index' >"${FIXTURE}/.git/index" +if HOME="${FAKE_HOME}" bash "${FIXER}" "${FIXTURE}" >/dev/null 2>"${git_error_log}"; then + echo "ERROR: failed git ls-files unexpectedly produced a successful scan" >&2 + exit 1 +fi +grep -q 'ERROR: git ls-files failed' "${git_error_log}" + echo "auto-fix-formulaic safety controls: PASS"