From b3ddba6426919db895b3ecbf01b77c2a3a264126 Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Wed, 5 Aug 2026 15:10:58 -0400 Subject: [PATCH 1/4] =?UTF-8?q?ci:=20vendor=20governance-enforce=20(A=5FBL?= =?UTF-8?q?OCK=20secrets=20scan)=20=E2=80=94=20this=20repo=20was=20never?= =?UTF-8?q?=20in=20the=20ruleset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/governance-enforce.yml | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/governance-enforce.yml diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml new file mode 100644 index 0000000..4263300 --- /dev/null +++ b/.github/workflows/governance-enforce.yml @@ -0,0 +1,59 @@ +name: governance-enforce + +# A_BLOCK gate: no-secrets-in-git / secrets-from-doppler / no-hardcoded-paths, enforced on the +# PR diff via the @wave-av/governance package (the org fan-out channel). Diff-scoped: blocks NEW +# violations without failing on legacy debt. Isolated install bypasses any min-release-age policy. +# The org ruleset `governance-a-block-enforce` requires this job's `enforce` check. +# +# VENDORED 2026-08-05 (claude-workstation#1624, E4 T4.9a). This repo was never in that ruleset's +# include list, because the list is 112 hand-maintained names and every one of them matches +# `wave-*`. A naming convention had silently become a security boundary: the repos that publish +# our npm packages — cli, sdk, adk, mcp-server, workflow-sdk — were the ones running with no +# A_BLOCK secrets scan at all. Copied verbatim from wave-moq-edge, and proven on a PUBLIC repo +# before fan-out: wave-av/cli#20, run 31011943790, conclusion success. +# +# DO NOT add this repo to `governance-a-block-enforce` until this check is observed green here. +# A required status check that never reports is a permanent deadlock, not a stricter gate. + +on: + pull_request: + push: + branches: [main, master] + +permissions: + contents: read + packages: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + enforce: + runs-on: ubuntu-latest + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + - name: fetch governance enforcer (isolated install) + run: | + mkdir -p "$RUNNER_TEMP/gov" && cd "$RUNNER_TEMP/gov" + printf '@wave-av:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}\n' > .npmrc + npm install @wave-av/governance@^0.4.4 --no-save --no-audit --no-fund + - name: A_BLOCK enforce (secrets + hardcoded paths on the diff) + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PUSH_BEFORE_SHA: ${{ github.event.before }} + run: | + BASE="${PR_BASE_SHA:-$PUSH_BEFORE_SHA}" + if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then + BASE=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD) + fi + echo "diffing against $BASE" + node "$RUNNER_TEMP/gov/node_modules/@wave-av/governance/bin/enforce.mjs" --changed "$BASE" From 31eeb1c7acb4cc9afab3b4e3afd7c7cd33bdded6 Mon Sep 17 00:00:00 2001 From: yakimoto <66892052+yakimoto@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:16:33 -0400 Subject: [PATCH 2/4] fix(ci): the vendored A_BLOCK gate could report PASS having scanned nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five defects, none of them cosmetic. Refs wave-av/claude-workstation#1747. 1. FAIL-OPEN DIFF BASE. `BASE=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)` — on a root commit `git rev-parse HEAD~1` prints its unresolved argument to stdout AND fails, so the `||` branch appends and BASE becomes a two-line string. `git diff` then exits 128, and the pinned enforcer turned that into zero files and a green check. Now: a reachability-checked base (a force-push can leave `github.event.before` pointing at a commit this checkout does not have), and with no resolvable base at all it diffs against the EMPTY TREE so the whole repo is scanned rather than nothing. 2. THE PINNED ENFORCER ITSELF FAILED OPEN. `^0.4.4` resolved to 0.4.4, whose file lister is `catch { return []; }` — any git error became zero files and rendered as `OK[enforce]: 0 changed file(s) scanned — 0 A_BLOCK violations`. A git error and a clean diff were byte-identical in the output. The fix had sat unreleased on claude-workstation main since 2026-07-29 because no `governance-v*` tag was ever pushed. Released now as 0.4.6 and pinned exactly here. 3. TOKEN IN SCOPE FOR THE WRONG STEPS. `NODE_AUTH_TOKEN` was job-level, so it was also in the environment of the step that executes the downloaded package. Now step-scoped, and the .npmrc holding it is removed on exit. 4. INSTALL SCRIPTS RAN WITH THAT TOKEN. `npm install` runs preinstall/postinstall by default. Added `--ignore-scripts`. 5. CANCELLED PUSH RUNS WERE SCANNED BY NOBODY. `cancel-in-progress: true` applied to push runs, and each push run only diffs its own before..HEAD range — so a cancelled run's commits were never examined by anything. Now PR-only. Also: `timeout-minutes: 10` and `set -euo pipefail`. Receipt, against a scratch repo whose root commit carries a no-hardcoded-paths violation, simulating a branch-creation push (`before` = all zeros): old logic -> malformed base -> caught error -> [] -> OK, 0 files scanned, PASS new logic -> "no diff base resolved ... scanning the whole tree" -> BLOCK, exit 1 Credit where it is due: several of these were found by the review bots on the sibling vendoring PRs and are folded in here — the step-scoped token, the .npmrc cleanup, the exact pin, `--ignore-scripts`, the force-push reachability check, `timeout-minutes`, and the concurrency hole (5), which was crest-console#7's catch and which I had missed entirely. --- .github/workflows/governance-enforce.yml | 56 +++++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml index 4263300..6e9d0ae 100644 --- a/.github/workflows/governance-enforce.yml +++ b/.github/workflows/governance-enforce.yml @@ -9,8 +9,11 @@ name: governance-enforce # include list, because the list is 112 hand-maintained names and every one of them matches # `wave-*`. A naming convention had silently become a security boundary: the repos that publish # our npm packages — cli, sdk, adk, mcp-server, workflow-sdk — were the ones running with no -# A_BLOCK secrets scan at all. Copied verbatim from wave-moq-edge, and proven on a PUBLIC repo -# before fan-out: wave-av/cli#20, run 31011943790, conclusion success. +# A_BLOCK secrets scan at all. +# +# HARDENED 2026-08-05 (claude-workstation#1747), before any of the fan-out merged. The copy first +# vendored here could report PASS having examined nothing. Five fixes, each marked at its site +# below. A gate may not return a passing value for input it did not examine. # # DO NOT add this repo to `governance-a-block-enforce` until this check is observed green here. # A required status check that never reports is a permanent deadlock, not a stricter gate. @@ -24,15 +27,17 @@ permissions: contents: read packages: read +# FIX 4 — a cancelled push run's commits were scanned by NOBODY. Every push run diffs only its +# own before..HEAD range, so cancelling run N when run N+1 starts leaves N's commits permanently +# unexamined. PR runs are safe to supersede: each one re-diffs the whole branch against its base. concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: enforce: runs-on: ubuntu-latest - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + timeout-minutes: 10 steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -42,18 +47,47 @@ jobs: with: node-version: "22" - name: fetch governance enforcer (isolated install) + # FIX 1 — token scoped to THIS STEP. At job level it was also in scope for the step that + # executes the downloaded package, and for anything else the job ever grows. + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + set -euo pipefail mkdir -p "$RUNNER_TEMP/gov" && cd "$RUNNER_TEMP/gov" + trap 'rm -f "$RUNNER_TEMP/gov/.npmrc"' EXIT printf '@wave-av:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}\n' > .npmrc - npm install @wave-av/governance@^0.4.4 --no-save --no-audit --no-fund + # FIX 2 — --ignore-scripts. npm runs preinstall/install/postinstall by default, so this + # step would execute dependency-authored code with the registry token in its environment. + # FIX 3 — exact pin, and 0.4.6 specifically. `^0.4.4` resolved to 0.4.4, whose file lister + # is `catch { return []; }` — ANY git error became zero files and rendered as + # `OK[enforce]: 0 changed file(s) scanned`. 0.4.6 fails closed on a git error instead. + # A caret is also a standing authorization for whatever is published next; a bump is now + # a visible commit in this file. + npm install @wave-av/governance@0.4.6 --no-save --no-audit --no-fund --ignore-scripts - name: A_BLOCK enforce (secrets + hardcoded paths on the diff) env: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | - BASE="${PR_BASE_SHA:-$PUSH_BEFORE_SHA}" - if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then - BASE=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD) + set -euo pipefail + ENFORCE="$RUNNER_TEMP/gov/node_modules/@wave-av/governance/bin/enforce.mjs" + BASE="${PR_BASE_SHA:-}" + [ -n "$BASE" ] || BASE="${PUSH_BEFORE_SHA:-}" + # A base can be PRESENT and still unusable: a force-push leaves `github.event.before` + # pointing at a commit this checkout no longer contains. + if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \ + || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then + BASE="$(git rev-parse --verify --quiet 'HEAD~1' || true)" + fi + # FIX 5 — fail CLOSED. This previously fell back to `BASE=HEAD`, and `--changed HEAD` + # diffs HEAD against itself: an empty diff, zero files scanned, job green. With no + # resolvable base, diff against the EMPTY TREE so every tracked file reads as added and + # the whole repo is scanned. (`--all` also exists in 0.4.6 and would do most of this, but + # it is documented as NOT covering the diff-scoped over-grant detectors. Routing through + # the diff path with an empty base keeps every detector in play.) + if [ -z "$BASE" ]; then + BASE="$(git hash-object -t tree /dev/null)" + echo "::notice::no diff base resolved (root commit or unreachable before-sha) — scanning the whole tree against the empty tree" fi echo "diffing against $BASE" - node "$RUNNER_TEMP/gov/node_modules/@wave-av/governance/bin/enforce.mjs" --changed "$BASE" + exec node "$ENFORCE" --changed "$BASE" From b7f2357b0cb4a8776ce8c9970174c64cb9aa6315 Mon Sep 17 00:00:00 2001 From: yakimoto Date: Thu, 6 Aug 2026 00:19:34 +0000 Subject: [PATCH 3/4] fix(ci): fail closed on unverifiable diff base instead of guessing HEAD~1 Co-authored-by: Codesmith --- .github/workflows/governance-enforce.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml index 6e9d0ae..41b5ab4 100644 --- a/.github/workflows/governance-enforce.yml +++ b/.github/workflows/governance-enforce.yml @@ -75,19 +75,18 @@ jobs: [ -n "$BASE" ] || BASE="${PUSH_BEFORE_SHA:-}" # A base can be PRESENT and still unusable: a force-push leaves `github.event.before` # pointing at a commit this checkout no longer contains. + # FIX 5 — fail CLOSED. This previously fell back to `BASE=HEAD` (an empty diff, zero + # files scanned, job green), then to `HEAD~1` — which scans only the newest commit, so + # a force-push that rewrites history with several commits hides every commit but the + # last from the gate. With no VERIFIABLE base, guessing any commit under-scans; diff + # against the EMPTY TREE instead, so every tracked file reads as added and the whole + # repo is scanned. (`--all` also exists in 0.4.6 and would do most of this, but it is + # documented as NOT covering the diff-scoped over-grant detectors. Routing through the + # diff path with an empty base keeps every detector in play.) if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \ || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then - BASE="$(git rev-parse --verify --quiet 'HEAD~1' || true)" - fi - # FIX 5 — fail CLOSED. This previously fell back to `BASE=HEAD`, and `--changed HEAD` - # diffs HEAD against itself: an empty diff, zero files scanned, job green. With no - # resolvable base, diff against the EMPTY TREE so every tracked file reads as added and - # the whole repo is scanned. (`--all` also exists in 0.4.6 and would do most of this, but - # it is documented as NOT covering the diff-scoped over-grant detectors. Routing through - # the diff path with an empty base keeps every detector in play.) - if [ -z "$BASE" ]; then BASE="$(git hash-object -t tree /dev/null)" - echo "::notice::no diff base resolved (root commit or unreachable before-sha) — scanning the whole tree against the empty tree" + echo "::notice::no verifiable diff base (root commit, branch creation, or force-push with unreachable before-sha) — scanning the whole tree against the empty tree" fi echo "diffing against $BASE" exec node "$ENFORCE" --changed "$BASE" From d0277f07fc4a476651d7541c72104f4842f7c100 Mon Sep 17 00:00:00 2001 From: yakimoto <66892052+yakimoto@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:29:34 -0400 Subject: [PATCH 4/4] =?UTF-8?q?fix(ci):=20the=20empty-tree=20fallback,=20n?= =?UTF-8?q?ot=20HEAD~1=20=E2=80=94=20a=20partial=20scan=20is=20still=20a?= =?UTF-8?q?=20false=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correction to the previous commit on this branch. Refs wave-av/claude-workstation#1747. That commit replaced the fail-open `BASE=HEAD` with a fallback to `HEAD~1`. `HEAD~1` is also wrong: it scans exactly ONE commit, so a five-commit push whose base is indeterminate (branch creation, force-push, unreachable `github.event.before`) examines the last commit and reports a confident pass on the other four. A narrowed scan reported as a full pass is the same defect in a quieter costume. Receipt — scratch repo, five-commit push, violation planted in commit 1: HEAD~1 base -> OK[enforce]: 1 changed file(s) scanned -> PASS (never saw it) empty-tree base -> 5 changed file(s) scanned -> BLOCK[enforce]: no-hardcoded-paths, exit 1 Now: with no resolvable base of any kind, diff against git's empty-tree object so every tracked file reads as added and the whole repo is scanned. Loud, never partial, never empty. Credit: wave-av/wave-rig's copy on main already had this right, with the reasoning in a comment ("HEAD~1 would skip earlier commits in a multi-commit push and let a violation through"). The fan-out copied the broken shape from elsewhere and I did not check the one repo that had already solved it. Also from wave-rig: `merge_group` is now a declared trigger and `github.event.merge_group. base_sha` joins the base chain. None of these repos runs a merge queue today, so the trigger is inert — but a required check that never reports on an event the repo actually uses is a permanent deadlock, and this closes that in advance rather than after someone hits it. --- .github/workflows/governance-enforce.yml | 37 +++++++++++++++++------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml index 41b5ab4..98dee26 100644 --- a/.github/workflows/governance-enforce.yml +++ b/.github/workflows/governance-enforce.yml @@ -20,6 +20,10 @@ name: governance-enforce on: pull_request: + # A required check that never reports on an event the repo actually uses is a permanent + # deadlock, not a stricter gate. None of these repos runs a merge queue today; declaring + # `merge_group` costs nothing until one does, and closes that hole in advance. + merge_group: push: branches: [main, master] @@ -67,26 +71,37 @@ jobs: - name: A_BLOCK enforce (secrets + hardcoded paths on the diff) env: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + MERGE_BASE_SHA: ${{ github.event.merge_group.base_sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | set -euo pipefail ENFORCE="$RUNNER_TEMP/gov/node_modules/@wave-av/governance/bin/enforce.mjs" BASE="${PR_BASE_SHA:-}" + [ -n "$BASE" ] || BASE="${MERGE_BASE_SHA:-}" [ -n "$BASE" ] || BASE="${PUSH_BEFORE_SHA:-}" - # A base can be PRESENT and still unusable: a force-push leaves `github.event.before` - # pointing at a commit this checkout no longer contains. - # FIX 5 — fail CLOSED. This previously fell back to `BASE=HEAD` (an empty diff, zero - # files scanned, job green), then to `HEAD~1` — which scans only the newest commit, so - # a force-push that rewrites history with several commits hides every commit but the - # last from the gate. With no VERIFIABLE base, guessing any commit under-scans; diff - # against the EMPTY TREE instead, so every tracked file reads as added and the whole - # repo is scanned. (`--all` also exists in 0.4.6 and would do most of this, but it is - # documented as NOT covering the diff-scoped over-grant detectors. Routing through the - # diff path with an empty base keeps every detector in play.) + # FIX 5 — fail CLOSED, and do not settle for a PARTIAL range either. + # + # This previously fell back to `BASE=HEAD`, and `--changed HEAD` diffs HEAD against + # itself: an empty diff, zero files scanned, job green. + # + # `HEAD~1` is the obvious replacement and is ALSO wrong — it scans exactly one commit, + # so a five-commit push whose base is indeterminate would examine the last one and + # report a confident pass on the other four. A narrowed scan reported as a full pass is + # the same defect in a quieter costume. (This is wave-av/wave-rig's reasoning, already + # correct on its main; the fan-out copied the broken shape from elsewhere.) + # + # A base can also be PRESENT and still unusable: a force-push leaves + # `github.event.before` pointing at a commit this checkout no longer contains. + # + # So: no resolvable base of any kind → diff against the EMPTY TREE, which makes every + # tracked file read as added and scans the whole repo. Loud, never partial, never empty. + # (`--all` also exists in 0.4.6 and would do most of this, but it is documented as NOT + # covering the diff-scoped over-grant detectors. Routing through the diff path with an + # empty base keeps every detector in play.) if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \ || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then BASE="$(git hash-object -t tree /dev/null)" - echo "::notice::no verifiable diff base (root commit, branch creation, or force-push with unreachable before-sha) — scanning the whole tree against the empty tree" + echo "::warning::indeterminate diff base (root commit, branch creation, or unreachable before-sha) — scanning the full tree against the empty-tree object so no commit is skipped" fi echo "diffing against $BASE" exec node "$ENFORCE" --changed "$BASE"