From fa550d19561bc2df37313319c9b764adbdf7c937 Mon Sep 17 00:00:00 2001 From: Jake Fineman Date: Wed, 5 Aug 2026 09:42:03 -0400 Subject: [PATCH 1/3] =?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..d0fc03d --- /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 — this one, sdk, adk, mcp-server, workflow-sdk — were the ones running with no +# A_BLOCK secrets scan at all. Copied verbatim from wave-moq-edge, which is public and 12/12 green, +# so the shape is proven on a public repo rather than assumed from a private one. +# +# 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 36142007f32e57802c4ea34b486763ca61475cdf Mon Sep 17 00:00:00 2001 From: yakimoto <66892052+yakimoto@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:16:30 -0400 Subject: [PATCH 2/3] 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 | 58 +++++++++++++++++++----- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml index d0fc03d..6e9d0ae 100644 --- a/.github/workflows/governance-enforce.yml +++ b/.github/workflows/governance-enforce.yml @@ -8,9 +8,12 @@ name: governance-enforce # 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 — this one, sdk, adk, mcp-server, workflow-sdk — were the ones running with no -# A_BLOCK secrets scan at all. Copied verbatim from wave-moq-edge, which is public and 12/12 green, -# so the shape is proven on a public repo rather than assumed from a private one. +# our npm packages — cli, sdk, adk, mcp-server, workflow-sdk — were the ones running with no +# 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 a3dbfa99ccde691d068188f200e94b23390e8e5b Mon Sep 17 00:00:00 2001 From: yakimoto <66892052+yakimoto@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:29:31 -0400 Subject: [PATCH 3/3] =?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 | 38 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/governance-enforce.yml b/.github/workflows/governance-enforce.yml index 6e9d0ae..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,27 +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, 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 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 "::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"