From 85ec9e166d0b78d91ed77689dfae4ffd6667c01f Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Wed, 5 Aug 2026 15:10:35 -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 a683baecb6437c2a8716edaa3c6676791248a8ea Mon Sep 17 00:00:00 2001 From: yakimoto <66892052+yakimoto@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:16:28 -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 5f14179ed40fff38a46d34b38ad8c6ac1d646b46 Mon Sep 17 00:00:00 2001 From: yakimoto Date: Thu, 6 Aug 2026 00:19:36 +0000 Subject: [PATCH 3/4] fix(ci): an unreachable force-push base now scans the whole tree, not just HEAD~1 Co-authored-by: Codesmith --- .github/workflows/governance-enforce.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml index 6e9d0ae..cc0a86e 100644 --- a/.github/workflows/governance-enforce.yml +++ b/.github/workflows/governance-enforce.yml @@ -74,10 +74,13 @@ jobs: 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. + # pointing at a commit this checkout no longer contains. Do NOT fall back to HEAD~1 + # here: that diffs only the newest commit, so earlier commits of a multi-commit + # force-push would reach main with a green check and zero examination. Clear the base + # and let the empty-tree fallback below scan the whole tree instead. if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] \ || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then - BASE="$(git rev-parse --verify --quiet 'HEAD~1' || true)" + BASE="" 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 From 5886e81da8a7746729211d8fd8e51625da41a025 Mon Sep 17 00:00:00 2001 From: yakimoto <66892052+yakimoto@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:29:30 -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 | 41 +++++++++++++++--------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml index cc0a86e..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,30 +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. Do NOT fall back to HEAD~1 - # here: that diffs only the newest commit, so earlier commits of a multi-commit - # force-push would reach main with a green check and zero examination. Clear the base - # and let the empty-tree fallback below scan the whole tree instead. + # 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="" - 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 "::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"