From af0fc49aa32adf8865b9bfb7a8a241f4289a7326 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 11:35:27 +0000 Subject: [PATCH 1/4] ci: restore main's changelog entry; wire the lock SHA-ref test The merge of main into this branch (497e37f) dropped main's "minted, and the estate checks made green" entry from CHANGELOG.adoc in favour of this PR's re-pin entry. That re-pin landed on main first (9240142, #8), so the entry now describes what this PR still adds over main: - the Scorecard calling job's `actions: read` grant (43b7040); - the four SHA-keyed lock leaves recording their SHA as `ref`; - tests/workflows/actions_lock_sha_refs_test.sh (2108dd7), which checks those leaves with negative controls. That test ran nowhere, so it is now a step in Estate Rules, next to the other lock test. Locally: check-lock-sync.sh passes, the test passes (4 dependencies), and the 934b879 lock gate (gh-actions-lock v0.2.0) passes on this lock. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/estate-rules.yml | 4 +++ CHANGELOG.adoc | 42 ++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/.github/workflows/estate-rules.yml b/.github/workflows/estate-rules.yml index 2104ecb..56e089b 100644 --- a/.github/workflows/estate-rules.yml +++ b/.github/workflows/estate-rules.yml @@ -94,6 +94,10 @@ jobs: # the spine's at mint and left every Julia mint with Lock Sync red. - name: Archetype lock fragments merge into a valid lock run: bash tests/workflows/archetype_lock_merge_test.sh + # The four SHA-keyed leaves standards' reusables add keep an immutable ref, + # and the sync gate names each one if its record goes missing. + - name: actions.lock SHA-ref regression controls + run: bash tests/workflows/actions_lock_sha_refs_test.sh # The CHECK half of the template contract. repo-init has always written an # answer-file (now the child deed's (provenance) clause); nothing ever read it. # This is the missing reader, and it covers the #200/#201/#203 class: diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 744f792..29f3cc5 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,20 +1,34 @@ == Changelog -== 2026-10-14 — governance pin `da2c748` → `934b879` - -* The six standards reusables (governance, hypatia-scan, mirror, rust-ci, - scorecard, secret-scanner) move to hyperpolymath/standards `934b879`. - At `da2c748`, governance ran its lock gate from a fixed helper pin with an - unpinned gh-actions-lock. Under v0.2.0 that gate failed *Actions lockfile - verify* on every job-level reusable ref, although it had accepted each one - as coverage. The new pin runs the gate at its own commit, with v0.2.0 - pinned (standards#1250). -* `actions.lock`: the `hyperpolymath/standards` record is rewritten at the - new pin with its called workflows' `uses:` union, and closed over the four - refs that union adds (setup-bun, setup-deno, upload-artifact, - rust-toolchain). The old record and the three refs only it named are - removed. +== 2026-10-14 — Scorecard grants `actions: read`; four lock refs kept immutable (resource-algebra#7) + +* `.github/workflows/scorecard.yml`: the calling job grants `actions: read`. + Its job-level `permissions:` block replaces the workflow-level one, so the + workflow-level grant never reached scorecard-reusable.yml, whose Packaging + check needs it. +* `actions.lock`: the four leaves that the reusables at standards `934b879` add + (upload-artifact, setup-deno, rust-toolchain, setup-bun) record their SHA as + `ref`, not the tag copied from standards' lock. + `tests/workflows/actions_lock_sha_refs_test.sh` checks this, with negative + controls, and runs in Estate Rules. +* The governance re-pin this PR began with landed first, in #8. + +== 2026-10-14 — minted, and the estate checks made green + +* Minted with `just repo-init`. The repository had been created from + rsr-template-repo df09a6c with GitHub's template copy and never minted, + so it still declared itself the template. The deed is now + `resource-algebra_chora.deed` (clade `fv`), placeholders are filled, + and CLAUDE.md and the repository map describe this repository. +* `build/just/repo-init.just`: the non-interactive mint now completes; + just's own interpolation, the arrival pack, the repository map, the + state-validation detector and the OpenSSF audit table survive the mint. +* `scripts/check-agda.sh`: each expected rejection must land on the + fixture's `invalid` definition. +* `PROOF-STATUS.adoc` moved to `docs/status/PROOF-STATUS.adoc`. +* CI: the standards reusables re-pinned to standards main, with a closed + `actions.lock`; `CITATION.cff` gained its required `message`. == 2026-10-14 — MinMax and morphisms (resource-algebra#3) From 68f026d8e767607d4ef282b19eb0b0c4e7037333 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:35:44 +0000 Subject: [PATCH 2/4] test(workflows): Add regression tests for immutable action pins and Scorecard job permissions --- tests/workflows/actions_lock_pins_test.sh | 92 +++++++++++++++++++ tests/workflows/scorecard_permissions_test.sh | 70 ++++++++++++++ 2 files changed, 162 insertions(+) create mode 100755 tests/workflows/actions_lock_pins_test.sh create mode 100755 tests/workflows/scorecard_permissions_test.sh diff --git a/tests/workflows/actions_lock_pins_test.sh b/tests/workflows/actions_lock_pins_test.sh new file mode 100755 index 0000000..d024570 --- /dev/null +++ b/tests/workflows/actions_lock_pins_test.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# PR #7: the four transitive action records must retain their immutable refs. +# Run: bash tests/workflows/actions_lock_pins_test.sh +# Requires mikefarah/yq v4 and gawk (for the repository's lock-sync gate). +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +lock="$repo/.github/workflows/actions.lock" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT + +fail() { echo "FAIL: $*" >&2; exit 1; } +yq --version | grep -q 'github.com/mikefarah/yq.*version v4\.' \ + || fail "mikefarah/yq v4 is required" + +# Discover the current standards pin from the caller, so a legitimate future +# pin bump does not require copying new SHAs into this test. +caller=$(yq -r '.jobs.scorecard.uses' "$repo/.github/workflows/scorecard.yml") +standards_key="${caller%%/.github/workflows/*}@${caller##*@}" + +pin_ok() { + local key=$1 file=$2 sha=${1##*@} + [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || return 1 + DEP_KEY="$key" PIN_SHA="$sha" yq -e ' + .dependencies[strenv(DEP_KEY)] | + (.ref == strenv(PIN_SHA) and .commit == ("sha1-" + strenv(PIN_SHA))) + ' "$file" >/dev/null 2>&1 +} + +reject_mutant() { + local label=$1 key=$2 + cmp -s "$lock" "$fixture/mutant.lock" && fail "$label: mutation did not change the lock" + if pin_ok "$key" "$fixture/mutant.lock"; then + fail "$label: invalid pin accepted" + fi +} + +# Closure is essential: a valid-looking ref alone cannot start a workflow if +# the called reusable's dependency graph has a dangling edge. +bash "$repo/scripts/check-lock-sync.sh" "$repo/.github/workflows" >"$fixture/sync.log" 2>&1 \ + || { cat "$fixture/sync.log" >&2; fail "repository lock is not in sync and transitively closed"; } + +checked=0 +while read -r action old_tag; do + refs=$(STANDARDS_KEY="$standards_key" ACTION="$action" yq -r ' + .dependencies[strenv(STANDARDS_KEY)].uses[] | + select(split("@")[0] == strenv(ACTION)) + ' "$lock") + [ -n "$refs" ] || fail "$action: absent from the standards dependency graph" + [[ "$refs" != *$'\n'* ]] || fail "$action: ambiguous transitive pin" + key=$refs + pin_ok "$key" "$lock" || fail "$action: ref and commit must match the full SHA in its dependency key" + + # Reproduce the exact pre-PR metadata while leaving the SHA key and commit + # intact. Closure alone accepts this, but the immutable-ref contract must not. + cp "$lock" "$fixture/mutant.lock" + DEP_KEY="$key" OLD_TAG="$old_tag" yq -i \ + '.dependencies[strenv(DEP_KEY)].ref = strenv(OLD_TAG)' "$fixture/mutant.lock" + reject_mutant "$action: old tag $old_tag" "$key" + + cp "$lock" "$fixture/mutant.lock" + DEP_KEY="$key" yq -i '.dependencies[strenv(DEP_KEY)].ref = "main"' "$fixture/mutant.lock" + reject_mutant "$action: floating branch" "$key" + + cp "$lock" "$fixture/mutant.lock" + DEP_KEY="$key" SHORT_SHA="${key##*@}" yq -i \ + '.dependencies[strenv(DEP_KEY)].ref = (strenv(SHORT_SHA) | sub(".$"; ""))' "$fixture/mutant.lock" + reject_mutant "$action: 39-character ref" "$key" + + cp "$lock" "$fixture/mutant.lock" + DEP_KEY="$key" yq -i \ + '.dependencies[strenv(DEP_KEY)].commit = "sha1-0000000000000000000000000000000000000000"' "$fixture/mutant.lock" + reject_mutant "$action: mismatched commit" "$key" + + cp "$lock" "$fixture/mutant.lock" + DEP_KEY="$key" yq -i 'del(.dependencies[strenv(DEP_KEY)].ref)' "$fixture/mutant.lock" + reject_mutant "$action: missing ref" "$key" + + cp "$lock" "$fixture/mutant.lock" + DEP_KEY="$key" yq -i 'del(.dependencies[strenv(DEP_KEY)])' "$fixture/mutant.lock" + reject_mutant "$action: missing dependency record" "$key" + checked=$((checked + 1)) +done <<'ACTIONS' +actions/upload-artifact v7.0.2 +denoland/setup-deno v2.0.5 +dtolnay/rust-toolchain v1 +oven-sh/setup-bun v2.2.0 +ACTIONS + +echo "PASS: $checked immutable action records; 24 invalid metadata controls rejected; lock graph closed" diff --git a/tests/workflows/scorecard_permissions_test.sh b/tests/workflows/scorecard_permissions_test.sh new file mode 100755 index 0000000..7e23c59 --- /dev/null +++ b/tests/workflows/scorecard_permissions_test.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# PR #7: the caller's explicit job permission map must grant actions: read. +# Top-level grants do not supply omitted scopes in a job permission map. +# Run: bash tests/workflows/scorecard_permissions_test.sh +# Requires mikefarah/yq v4. +set -euo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) +workflow="$repo/.github/workflows/scorecard.yml" +fixture=$(mktemp -d) +trap 'rm -rf "$fixture"' EXIT + +fail() { echo "FAIL: $*" >&2; exit 1; } +yq --version | grep -q 'github.com/mikefarah/yq.*version v4\.' \ + || fail "mikefarah/yq v4 is required" + +permissions_ok() { + yq -e ' + .jobs.scorecard.permissions | + (.actions == "read" and .contents == "read" and + .["security-events"] == "write" and .["id-token"] == "write") + ' "$1" >/dev/null 2>&1 +} + +reject_mutant() { + local label=$1 expression=$2 + cp "$workflow" "$fixture/mutant.yml" + yq -i "$expression" "$fixture/mutant.yml" + cmp -s "$workflow" "$fixture/mutant.yml" && fail "$label: mutation did not change the workflow" + if permissions_ok "$fixture/mutant.yml"; then + fail "$label: invalid job permissions accepted" + fi +} + +permissions_ok "$workflow" || fail "Scorecard lacks a required caller grant" + +# The original failure: workflow-level actions: read is still present, but the +# job's explicit map omits actions. A text search would incorrectly pass this. +reject_mutant "top-level-only actions grant" 'del(.jobs.scorecard.permissions.actions)' +reject_mutant "actions disabled" '.jobs.scorecard.permissions.actions = "none"' +reject_mutant "unnecessary actions write access" '.jobs.scorecard.permissions.actions = "write"' +reject_mutant "null actions grant" '.jobs.scorecard.permissions.actions = null' +reject_mutant "empty job permission map" '.jobs.scorecard.permissions = {}' +reject_mutant "missing job permission map" 'del(.jobs.scorecard.permissions)' + +# A grant on a sibling job cannot authorize the reusable call either. +reject_mutant "actions grant on the wrong job" ' + del(.jobs.scorecard.permissions.actions) | + .jobs.other.permissions.actions = "read" +' + +# Adding the staleness-check grant must preserve the existing checkout, SARIF +# upload and OIDC grants needed by this same reusable call. +for scope in contents security-events id-token; do + SCOPE="$scope" reject_mutant "missing $scope grant" \ + 'del(.jobs.scorecard.permissions[strenv(SCOPE)])' +done + +# YAML quoting, comments and key order have no effect on permission semantics. +cp "$workflow" "$fixture/reordered.yml" +yq -i '.jobs.scorecard.permissions = { + "id-token": "write", "security-events": "write", + "contents": "read", "actions": "read" +}' "$fixture/reordered.yml" +printf '\n# actions: none (a comment, not a grant)\n' >>"$fixture/reordered.yml" +permissions_ok "$fixture/reordered.yml" || fail "equivalent YAML permissions rejected" + +echo "PASS: Scorecard caller grants; 10 invalid permission controls rejected; equivalent YAML accepted" From e35a0c5748b8068a2215fd0923fddba3b0cb92bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 11:36:18 +0000 Subject: [PATCH 3/4] docs(changelog): word the #7 entry for the merged state Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- CHANGELOG.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 29f3cc5..d729c12 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -12,7 +12,7 @@ `ref`, not the tag copied from standards' lock. `tests/workflows/actions_lock_sha_refs_test.sh` checks this, with negative controls, and runs in Estate Rules. -* The governance re-pin this PR began with landed first, in #8. +* The governance re-pin that #7 was opened for had already landed, in #8. == 2026-10-14 — minted, and the estate checks made green From a6daf2308ef72fc2044b354a296e9bc98a5b52e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 11:37:32 +0000 Subject: [PATCH 4/4] ci: run the Scorecard grant test in Estate Rules; drop the duplicate lock test 68f026d added two tests that nothing ran. scorecard_permissions_test.sh checks something new: that the Scorecard calling job's own permissions map grants actions: read, with ten mutants rejected. It is now an Estate Rules step. actions_lock_pins_test.sh checks the same four SHA-keyed lock records as actions_lock_sha_refs_test.sh, which merged with #7, so it is not added. Locally, all three passed before the duplicate was removed. The CHANGELOG entry names both tests that remain. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y6FD63UrriRBHqmhhEca9t --- .github/workflows/estate-rules.yml | 4 + CHANGELOG.adoc | 3 +- tests/workflows/actions_lock_pins_test.sh | 92 ----------------------- 3 files changed, 6 insertions(+), 93 deletions(-) delete mode 100755 tests/workflows/actions_lock_pins_test.sh diff --git a/.github/workflows/estate-rules.yml b/.github/workflows/estate-rules.yml index 56e089b..8c8f44d 100644 --- a/.github/workflows/estate-rules.yml +++ b/.github/workflows/estate-rules.yml @@ -98,6 +98,10 @@ jobs: # and the sync gate names each one if its record goes missing. - name: actions.lock SHA-ref regression controls run: bash tests/workflows/actions_lock_sha_refs_test.sh + # The Scorecard calling job's own permissions map must grant actions: read; + # the workflow-level grant does not reach a job that declares its own map. + - name: Scorecard caller grants + run: bash tests/workflows/scorecard_permissions_test.sh # The CHECK half of the template contract. repo-init has always written an # answer-file (now the child deed's (provenance) clause); nothing ever read it. # This is the missing reader, and it covers the #200/#201/#203 class: diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index d729c12..6f5d239 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -11,7 +11,8 @@ (upload-artifact, setup-deno, rust-toolchain, setup-bun) record their SHA as `ref`, not the tag copied from standards' lock. `tests/workflows/actions_lock_sha_refs_test.sh` checks this, with negative - controls, and runs in Estate Rules. + controls, and `tests/workflows/scorecard_permissions_test.sh` checks the + grant; both run in Estate Rules. * The governance re-pin that #7 was opened for had already landed, in #8. == 2026-10-14 — minted, and the estate checks made green diff --git a/tests/workflows/actions_lock_pins_test.sh b/tests/workflows/actions_lock_pins_test.sh deleted file mode 100755 index d024570..0000000 --- a/tests/workflows/actions_lock_pins_test.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# -# PR #7: the four transitive action records must retain their immutable refs. -# Run: bash tests/workflows/actions_lock_pins_test.sh -# Requires mikefarah/yq v4 and gawk (for the repository's lock-sync gate). -set -euo pipefail - -repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) -lock="$repo/.github/workflows/actions.lock" -fixture=$(mktemp -d) -trap 'rm -rf "$fixture"' EXIT - -fail() { echo "FAIL: $*" >&2; exit 1; } -yq --version | grep -q 'github.com/mikefarah/yq.*version v4\.' \ - || fail "mikefarah/yq v4 is required" - -# Discover the current standards pin from the caller, so a legitimate future -# pin bump does not require copying new SHAs into this test. -caller=$(yq -r '.jobs.scorecard.uses' "$repo/.github/workflows/scorecard.yml") -standards_key="${caller%%/.github/workflows/*}@${caller##*@}" - -pin_ok() { - local key=$1 file=$2 sha=${1##*@} - [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || return 1 - DEP_KEY="$key" PIN_SHA="$sha" yq -e ' - .dependencies[strenv(DEP_KEY)] | - (.ref == strenv(PIN_SHA) and .commit == ("sha1-" + strenv(PIN_SHA))) - ' "$file" >/dev/null 2>&1 -} - -reject_mutant() { - local label=$1 key=$2 - cmp -s "$lock" "$fixture/mutant.lock" && fail "$label: mutation did not change the lock" - if pin_ok "$key" "$fixture/mutant.lock"; then - fail "$label: invalid pin accepted" - fi -} - -# Closure is essential: a valid-looking ref alone cannot start a workflow if -# the called reusable's dependency graph has a dangling edge. -bash "$repo/scripts/check-lock-sync.sh" "$repo/.github/workflows" >"$fixture/sync.log" 2>&1 \ - || { cat "$fixture/sync.log" >&2; fail "repository lock is not in sync and transitively closed"; } - -checked=0 -while read -r action old_tag; do - refs=$(STANDARDS_KEY="$standards_key" ACTION="$action" yq -r ' - .dependencies[strenv(STANDARDS_KEY)].uses[] | - select(split("@")[0] == strenv(ACTION)) - ' "$lock") - [ -n "$refs" ] || fail "$action: absent from the standards dependency graph" - [[ "$refs" != *$'\n'* ]] || fail "$action: ambiguous transitive pin" - key=$refs - pin_ok "$key" "$lock" || fail "$action: ref and commit must match the full SHA in its dependency key" - - # Reproduce the exact pre-PR metadata while leaving the SHA key and commit - # intact. Closure alone accepts this, but the immutable-ref contract must not. - cp "$lock" "$fixture/mutant.lock" - DEP_KEY="$key" OLD_TAG="$old_tag" yq -i \ - '.dependencies[strenv(DEP_KEY)].ref = strenv(OLD_TAG)' "$fixture/mutant.lock" - reject_mutant "$action: old tag $old_tag" "$key" - - cp "$lock" "$fixture/mutant.lock" - DEP_KEY="$key" yq -i '.dependencies[strenv(DEP_KEY)].ref = "main"' "$fixture/mutant.lock" - reject_mutant "$action: floating branch" "$key" - - cp "$lock" "$fixture/mutant.lock" - DEP_KEY="$key" SHORT_SHA="${key##*@}" yq -i \ - '.dependencies[strenv(DEP_KEY)].ref = (strenv(SHORT_SHA) | sub(".$"; ""))' "$fixture/mutant.lock" - reject_mutant "$action: 39-character ref" "$key" - - cp "$lock" "$fixture/mutant.lock" - DEP_KEY="$key" yq -i \ - '.dependencies[strenv(DEP_KEY)].commit = "sha1-0000000000000000000000000000000000000000"' "$fixture/mutant.lock" - reject_mutant "$action: mismatched commit" "$key" - - cp "$lock" "$fixture/mutant.lock" - DEP_KEY="$key" yq -i 'del(.dependencies[strenv(DEP_KEY)].ref)' "$fixture/mutant.lock" - reject_mutant "$action: missing ref" "$key" - - cp "$lock" "$fixture/mutant.lock" - DEP_KEY="$key" yq -i 'del(.dependencies[strenv(DEP_KEY)])' "$fixture/mutant.lock" - reject_mutant "$action: missing dependency record" "$key" - checked=$((checked + 1)) -done <<'ACTIONS' -actions/upload-artifact v7.0.2 -denoland/setup-deno v2.0.5 -dtolnay/rust-toolchain v1 -oven-sh/setup-bun v2.2.0 -ACTIONS - -echo "PASS: $checked immutable action records; 24 invalid metadata controls rejected; lock graph closed"