From 114d015c512766f9c9e8e65a055b958329116cc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:03:41 +0000 Subject: [PATCH 01/11] fix(actions-lock): accept lockfile v0.0.3, which shares the v0.0.2 grammar gh-actions-lock v0.2.0 writes lockfile v0.0.3, and the estate gate now pins v0.2.0 (standards#1250). ActionsLock accepted only v0.0.2, so any relocked repository raised a high-severity invalid_actions_lock from workflow_audit and workflow_hardening (palimpsest-license alerts 347 and 349). Hypatia's own lock has been v0.0.3 since #926, so its own "parses the repository's authoritative generated lockfile" test failed. v0.0.3 has the v0.0.2 line grammar: same sections, same record fields, same `'': []` form (measured on this repo's lock and on rsr-template-repo's). Both versions are now parsed by the one strict grammar; every other version string is still refused, and a second version line is still a duplicate. Tests: one case for v0.0.3 (accepted; malformed v0.0.3 still fails closed), one for unsupported versions and a duplicate line. Run standalone against the module: 14/14 pass; the unpatched module fails 3 of them, including the existing own-lock test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- lib/rules/actions_lock.ex | 27 ++++++++++++++++++--------- test/rules/actions_lock_test.exs | 27 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/rules/actions_lock.ex b/lib/rules/actions_lock.ex index fce24544..79fe2998 100644 --- a/lib/rules/actions_lock.ex +++ b/lib/rules/actions_lock.ex @@ -3,7 +3,8 @@ defmodule Hypatia.Rules.ActionsLock do @moduledoc """ - Strict local validation for machine-generated `gh actions-lock` v0.0.2 files. + Strict local validation for machine-generated `gh actions-lock` v0.0.2 and + v0.0.3 files. This module validates lockfile *shape and association*. It does not replace `gh actions-lock --verify`, which remains authoritative for resolving refs @@ -13,11 +14,19 @@ defmodule Hypatia.Rules.ActionsLock do The parser is deliberately line-oriented. `actions.lock` is generated by a single tool with a versioned stable format, and Hypatia must not add a general - YAML dependency merely to consume that format. Unknown or malformed v0.0.2 - lines fail closed rather than being guessed at. + YAML dependency merely to consume that format. Unknown or malformed lines fail + closed rather than being guessed at. + + v0.0.3 is what gh-actions-lock v0.2.0 writes, and the version the estate gate + pins (standards#1250). Its line grammar is the v0.0.2 grammar: the same + `workflows:` and `dependencies:` sections, the same record fields (`ref`, + `commit`, `owner_id`, `repo_id`, `uses`) and the same `'': []` form for a + workflow with nothing to lock (measured on rsr-template-repo and this repo's + own lock, 2026-10-19). Both versions are therefore parsed by one grammar; any + other version string is still refused. """ - @supported_version "v0.0.2" + @supported_versions ["v0.0.2", "v0.0.3"] @action_ref ~r/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.\/-]+@[^\s':]+$/ @dependency_key ~r/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[^\s':]+$/ @sha_ref ~r/^[0-9A-Fa-f]{40}$/ @@ -38,7 +47,7 @@ defmodule Hypatia.Rules.ActionsLock do } @doc """ - Parse and validate a complete v0.0.2 lockfile. + Parse and validate a complete v0.0.2 or v0.0.3 lockfile. Returns `{:error, reason}` for missing, unsupported, malformed, incomplete, duplicate, or contradictory input. One bad dependency invalidates the whole @@ -136,17 +145,17 @@ defmodule Hypatia.Rules.ActionsLock do cond do match = Regex.run(~r/^version: '([^']+)'$/, line) -> case match do - [_, @supported_version] when is_nil(state.version) -> - {:ok, %{state | version: @supported_version}} + [_, version] when version in @supported_versions and is_nil(state.version) -> + {:ok, %{state | version: version}} - [_, @supported_version] -> + [_, version] when version in @supported_versions -> line_error(line_number, :duplicate_version) [_, version] -> line_error(line_number, {:unsupported_version, version}) end - line == "workflows:" and state.version == @supported_version -> + line == "workflows:" and state.version in @supported_versions -> {:ok, %{state | section: :workflows}} true -> diff --git a/test/rules/actions_lock_test.exs b/test/rules/actions_lock_test.exs index e7e9c9eb..7c1c5432 100644 --- a/test/rules/actions_lock_test.exs +++ b/test/rules/actions_lock_test.exs @@ -83,6 +83,33 @@ defmodule Hypatia.Rules.ActionsLockTest do assert {:error, {:line, _, :malformed_dependency_commit}} = ActionsLock.parse(non_hex) end + test "accepts lockfile v0.0.3, which gh-actions-lock v0.2.0 writes, with the same grammar" do + v003 = String.replace(@valid_lock, "version: 'v0.0.2'", "version: 'v0.0.3'") + + assert {:ok, lock} = ActionsLock.parse(v003) + assert lock.version == "v0.0.3" + assert ActionsLock.pinned?(lock, "ci.yml", "actions/checkout@v7.0.1") + refute ActionsLock.pinned?(lock, "other.yml", "actions/checkout@v7.0.1") + + # The grammar is shared, not loosened: a malformed v0.0.3 lock still fails closed. + malformed = String.replace(v003, ~r/sha1-[0-9a-f]{40}/, "sha1-short") + assert {:error, {:line, _, :malformed_dependency_commit}} = ActionsLock.parse(malformed) + end + + test "rejects every version outside the supported set, and a second version line" do + for version <- ["v0.0.1", "v0.0.4", "v0.1.0", "0.0.3"] do + assert {:error, {:line, 2, {:unsupported_version, ^version}}} = + @valid_lock + |> String.replace("version: 'v0.0.2'", "version: '#{version}'") + |> ActionsLock.parse() + end + + twice = + String.replace(@valid_lock, "version: 'v0.0.2'", "version: 'v0.0.2'\nversion: 'v0.0.3'") + + assert {:error, {:line, 3, :duplicate_version}} = ActionsLock.parse(twice) + end + test "rejects unsupported versions and missing dependency entries" do assert {:error, {:line, 2, {:unsupported_version, "v9"}}} = @valid_lock From e68277c8b6d87547c0768e1e3f2c3453e0031246 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:07:22 +0000 Subject: [PATCH 02/11] ci: repin standards callers to 75bd599, whose own lock GitHub accepts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since GitHub began enforcing actions.lock at job setup (~2026-10-15), every job in a workflow that calls a standards reusable dies at `Set up job` with "Unable to resolve action actions/checkout@3d3c42e…: invalid lockfile pin". GitHub checks a reusable workflow's actions against the CALLEE's lock at the called SHA. Standards' lock at the pins used here (da2c748, 571cc73, 2479cf7) gives leaf actions their own SHA as ref; standards' lock-selfcheck rates all three POISON. All five callers now pin 75bd599 (standards main; every workflow green there; lock-selfcheck SELF-CONSISTENT). The interfaces are compatible: no new required inputs or secrets. This repo's lock records these callers as `[]` and has no self-SHA leaf records, so it needs no change (relock-standards-pin.sh: nothing to relock); the governance lock gate at 75bd599 (gh-actions-lock v0.2.0) exits 0 on this tree. Same root cause and fix as hyperpolymath/palimpsest-license#187. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/mirror.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/secret-scanner.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 0bf27a56..b770367f 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -16,7 +16,7 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 permissions: actions: read contents: read diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 61c81175..62c93431 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -21,5 +21,5 @@ permissions: jobs: hypatia: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 secrets: inherit diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 0bd744f2..aaae2b43 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -41,7 +41,7 @@ permissions: jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@2479cf769ed5f0481ccf64860a2ab954514c2b59 + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 secrets: inherit permissions: actions: read diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 79a29fee..24486170 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -16,7 +16,7 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@da2c748aad55c1a1dcba00b60fe4a35017bc6540 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 permissions: actions: read contents: read diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 0dbf55c7..542c81eb 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -22,5 +22,5 @@ jobs: contents: read pull-requests: write actions: read - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 secrets: inherit From 366009ff55e2a30ff3ea641727ee3abcfd763586 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:16:21 +0000 Subject: [PATCH 03/11] test(fixtures): give the Bitbucket mock v8 placeholder UUIDs The governance "UUID v8 conformance" gate (and the legacy "UUID v7 conformance" job, which runs the same strict check at standards 75bd599) never reached this tree before: every governance job died at job setup on the callee-lock pin. With the repin they run, and both fail on the four placeholder UUIDs in integration/fixtures/mock-responses/ bitbucket-repo.json, the only non-v8 literals in the repository. The values are invented placeholders, and nothing loads the file: load_mock_response() has no callers, and the Bitbucket mock test builds its responses inline. Flip each placeholder's version nibble to 8 and its variant nibble into 8-b, keeping the recognisable digit patterns. The file stays valid JSON, and check-uuid-v8.sh --strict now passes on the whole tree. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- .../fixtures/mock-responses/bitbucket-repo.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/integration/fixtures/mock-responses/bitbucket-repo.json b/integration/fixtures/mock-responses/bitbucket-repo.json index 628c0b6a..06fcee5e 100644 --- a/integration/fixtures/mock-responses/bitbucket-repo.json +++ b/integration/fixtures/mock-responses/bitbucket-repo.json @@ -1,6 +1,6 @@ { "type": "repository", - "uuid": "{12345678-1234-1234-1234-123456789abc}", + "uuid": "{12345678-1234-8234-8234-123456789abc}", "full_name": "hyperpolymath/test-repo", "name": "test-repo", "slug": "test-repo", @@ -15,13 +15,13 @@ "has_wiki": false, "owner": { "type": "user", - "uuid": "{87654321-4321-4321-4321-cba987654321}", + "uuid": "{87654321-4321-8321-8321-cba987654321}", "username": "hyperpolymath", "display_name": "hyperpolymath", - "account_id": "123456:abcdef12-3456-7890-abcd-ef1234567890", + "account_id": "123456:abcdef12-3456-8890-abcd-ef1234567890", "links": { "self": { - "href": "https://api.bitbucket.org/2.0/users/%7B87654321-4321-4321-4321-cba987654321%7D" + "href": "https://api.bitbucket.org/2.0/users/%7B87654321-4321-8321-8321-cba987654321%7D" }, "html": { "href": "https://bitbucket.org/hyperpolymath/" @@ -33,7 +33,7 @@ }, "workspace": { "type": "workspace", - "uuid": "{87654321-4321-4321-4321-cba987654321}", + "uuid": "{87654321-4321-8321-8321-cba987654321}", "name": "hyperpolymath", "slug": "hyperpolymath", "links": { @@ -51,7 +51,7 @@ "project": { "type": "project", "key": "TEST", - "uuid": "{11111111-1111-1111-1111-111111111111}", + "uuid": "{11111111-1111-8111-8111-111111111111}", "name": "Test Project", "links": { "self": { From abbe38233cc8a647de9ef6c41376f35117967606 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:16:22 +0000 Subject: [PATCH 04/11] ci(gitleaks): ignore five historical false positives by exact fingerprint Standards 75bd599 adds a gating full-history gitleaks pass to secret-scanner-reusable.yml (fetch-depth 0, every commit). It reports five generic-api-key matches, all in files that f2fe2df removed from the tree: - deploy/k8s/secrets.yaml at 0d310fa, lines 38/40/44/50: a Secret template whose values are base64 of the placeholder CHANGE_ME_IN_PRODUCTION, as the comment above each line says. Checked by comparing each value against the encoded placeholder. - deploy/terraform/modules/monitoring/main.tf at 269dac0, line 421: a Prometheus recording-rule name, not a credential. Ignore exactly those five commit:file:rule:line fingerprints, following the shape of standards' own .gitleaksignore. They cannot suppress a match in any new commit or location. Verified locally with the pinned gitleaks 8.18.4 (sha256 as in the reusable workflow) and the estate baseline from 75bd599: - history pass: 5 leaks before, none after; - working-tree pass: still clean; - negative control: a planted random secret at the same path and line is still caught by both passes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- .gitleaksignore | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .gitleaksignore diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 00000000..4a0185fe --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Exact historical fingerprints only (commit:file:rule:line). The full-history +# gitleaks pass in hyperpolymath/standards secret-scanner-reusable.yml scans +# every commit; these five matches sit in two files that f2fe2df removed from +# the tree. Each entry names what the matched value is. None of them suppresses +# a rule in any new commit or location. +# +# deploy/k8s/secrets.yaml at 0d310fa is a Secret TEMPLATE. Each of the four +# values is base64 of the placeholder CHANGE_ME_IN_PRODUCTION, as the comment +# above each line says. Checked 2026-10-19 by comparing every value against +# the encoded placeholder. +0d310faa0d74da36faa8f5cfb5b6786ebe912c1b:deploy/k8s/secrets.yaml:generic-api-key:38 +0d310faa0d74da36faa8f5cfb5b6786ebe912c1b:deploy/k8s/secrets.yaml:generic-api-key:40 +0d310faa0d74da36faa8f5cfb5b6786ebe912c1b:deploy/k8s/secrets.yaml:generic-api-key:44 +0d310faa0d74da36faa8f5cfb5b6786ebe912c1b:deploy/k8s/secrets.yaml:generic-api-key:50 +# +# deploy/terraform/modules/monitoring/main.tf at 269dac0, line 421, is a +# Prometheus recording-rule name (record = "cicd_hyper_a:api:"). It is +# a metric identifier, not a credential. +269dac03d57835a1dfe3fb336095ffac61567ccf:deploy/terraform/modules/monitoring/main.tf:generic-api-key:421 From 61624390f11c0186c4ac3065f03f3ea7fd7e5c67 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:24:44 +0000 Subject: [PATCH 05/11] ci: pass reusable workflows only the secrets they declare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four callers used `secrets: inherit`, which forwards every repository and organisation secret to the callee (CWE-250). The callees at standards 75bd599 declare what they need, and their headers say not to inherit (standards#1108 §6b): - hypatia-scan-reusable declares HYPATIA_SCAN_PAT (optional); - mirror-reusable declares the seven forge keys (all optional); - scorecard-reusable and secret-scanner-reusable declare none. Pass exactly those. The `uses:` lines are unchanged, so actions.lock is too. This clears the four WH008 findings that the governance baseline gate reports on these files. It matches the change CodeRabbit asked for on hyperpolymath/palimpsest-license#187. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- .github/workflows/hypatia-scan.yml | 4 +++- .github/workflows/mirror.yml | 10 +++++++++- .github/workflows/scorecard.yml | 1 - .github/workflows/secret-scanner.yml | 1 - 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 62c93431..f9dfc9d7 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -22,4 +22,6 @@ permissions: jobs: hypatia: uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 - secrets: inherit + # Only the one secret the callee declares (standards#1108 §6b). + secrets: + HYPATIA_SCAN_PAT: ${{ secrets.HYPATIA_SCAN_PAT }} diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index aaae2b43..95d1c65e 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -42,7 +42,15 @@ permissions: jobs: mirror: uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 - secrets: inherit + # Only the seven forge keys the callee declares (standards#1108 §6b). + secrets: + GITLAB_SSH_KEY: ${{ secrets.GITLAB_SSH_KEY }} + BITBUCKET_SSH_KEY: ${{ secrets.BITBUCKET_SSH_KEY }} + CODEBERG_SSH_KEY: ${{ secrets.CODEBERG_SSH_KEY }} + SOURCEHUT_SSH_KEY: ${{ secrets.SOURCEHUT_SSH_KEY }} + DISROOT_SSH_KEY: ${{ secrets.DISROOT_SSH_KEY }} + GITEA_SSH_KEY: ${{ secrets.GITEA_SSH_KEY }} + RADICLE_KEY: ${{ secrets.RADICLE_KEY }} permissions: actions: read contents: read diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 24486170..3bc19ee8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -22,4 +22,3 @@ jobs: contents: read security-events: write id-token: write - secrets: inherit diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 542c81eb..36571a8b 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -23,4 +23,3 @@ jobs: pull-requests: write actions: read uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@75bd5994470661a099df562d6d5ad2fe744dbd66 - secrets: inherit From c19c465b9010f60532efba7c1fa008d957e42ad6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:24:44 +0000 Subject: [PATCH 06/11] build(cargo): keep standards' staged js-runtimes crate out of the workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At standards 75bd599, the governance "JS runtimes — environment provisions Bun first" job checks standards' tools/js-runtimes out at .standards-js/ inside the caller's checkout, then runs `cargo install --locked --path .standards-js/tools/js-runtimes`. Here that path sits under this repository's root workspace, so cargo refuses: error: current package believes it's in a workspace when it's not Exclude .standards-js from the workspace. Reproduced locally by staging the crate the same way: `cargo metadata` fails with that error before the change and passes after. The job's full `cargo install --locked` then builds, and `js-runtimes provision .` exits 0. The root fix belongs in standards (an empty [workspace] table in the crate's manifest); this line is harmless once that lands. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index edc5d904..703051e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,11 @@ members = [ "integration", "tools/cii-registrar", ] +# standards' governance "JS runtimes" job checks its js-runtimes crate out at +# .standards-js/ inside this checkout and `cargo install`s it from there. +# Without this, cargo treats that crate as an undeclared member of this +# workspace and refuses to build it. +exclude = [".standards-js"] [workspace.package] version = "0.1.0" From a50c0e5ea8ceee35b3dda3fa9978d944977615b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 12:33:46 +0000 Subject: [PATCH 07/11] fix(actions-lock): name v0.0.3's GHE.com hostname field instead of a generic error v0.0.3 is not quite the v0.0.2 grammar. gh-actions-lock v0.2.0 adds one optional per-dependency field, `hostname` (`json:"hostname,omitempty"`, confirmed in the v0.2.0 binary). It writes the field only for a GHE.com data-residency tenant, never for github.com, so neither real v0.0.3 lock in the estate (this repo's, rsr-template-repo's) carries it. Make the parser say so instead of reporting a generic :malformed_dependency_entry: - In a v0.0.3 lock, `hostname: 'github.com'` is accepted. It states the default. - In a v0.0.3 lock, any other host is refused as {:unsupported_dependency_hostname, host}. Repository identity (owner_id/repo_id uniqueness) is validated per host, and this parser models github.com only. - In a v0.0.2 lock, a hostname line is still malformed: the field did not exist then. The moduledoc now describes the restricted schema accurately. Tests: a new case covers all three behaviours and a duplicate hostname line. The real locks still parse: hypatia v0.0.3 (40 deps), rsr-template-repo v0.0.3 (37), palimpsest-license v0.0.2 (38), standards v0.0.2 (26). Addresses CodeRabbit's finding on #929. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- lib/rules/actions_lock.ex | 30 ++++++++++++++++++++++++------ test/rules/actions_lock_test.exs | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lib/rules/actions_lock.ex b/lib/rules/actions_lock.ex index 79fe2998..9fcec245 100644 --- a/lib/rules/actions_lock.ex +++ b/lib/rules/actions_lock.ex @@ -18,12 +18,20 @@ defmodule Hypatia.Rules.ActionsLock do closed rather than being guessed at. v0.0.3 is what gh-actions-lock v0.2.0 writes, and the version the estate gate - pins (standards#1250). Its line grammar is the v0.0.2 grammar: the same - `workflows:` and `dependencies:` sections, the same record fields (`ref`, - `commit`, `owner_id`, `repo_id`, `uses`) and the same `'': []` form for a - workflow with nothing to lock (measured on rsr-template-repo and this repo's - own lock, 2026-10-19). Both versions are therefore parsed by one grammar; any - other version string is still refused. + pins (standards#1250). For github.com dependencies its line grammar is the + v0.0.2 grammar: the same `workflows:` and `dependencies:` sections, the same + record fields (`ref`, `commit`, `owner_id`, `repo_id`, `uses`) and the same + `'': []` form for a workflow with nothing to lock (measured on + rsr-template-repo and this repo's own lock, 2026-10-19). Any other version + string is still refused. + + The one v0.0.3 addition is an optional per-dependency `hostname`, which + gh-actions-lock writes only for a GHE.com data-residency tenant + (`hostname,omitempty`). An explicit `hostname: 'github.com'` is accepted as the + default it states. Any other host is refused as + `{:unsupported_dependency_hostname, host}`. Repository identity is validated + per host, this parser models github.com only, and the estate has no tenant + locks. """ @supported_versions ["v0.0.2", "v0.0.3"] @@ -222,6 +230,16 @@ defmodule Hypatia.Rules.ActionsLock do Regex.match?(~r/^ (?:owner_id|repo_id):/, line) -> line_error(line_number, :malformed_dependency_identity) + # v0.0.3 only: an explicit github.com host is the default spelled out. + line == " hostname: 'github.com'" and state.version == "v0.0.3" -> + put_dependency_field(state, :hostname, "github.com", line_number) + + # A GHE.com tenant host is refused by name: owner/repo identity is + # validated per host, and this parser models github.com only. + match = state.version == "v0.0.3" && Regex.run(~r/^ hostname: '([^']+)'$/, line) -> + [_, host] = match + line_error(line_number, {:unsupported_dependency_hostname, host}) + line == " uses:" -> put_dependency_field(state, :uses, MapSet.new(), line_number) diff --git a/test/rules/actions_lock_test.exs b/test/rules/actions_lock_test.exs index 7c1c5432..78af2905 100644 --- a/test/rules/actions_lock_test.exs +++ b/test/rules/actions_lock_test.exs @@ -96,6 +96,33 @@ defmodule Hypatia.Rules.ActionsLockTest do assert {:error, {:line, _, :malformed_dependency_commit}} = ActionsLock.parse(malformed) end + test "v0.0.3 hostname: github.com is the default, a GHE.com tenant host is refused" do + v003 = String.replace(@valid_lock, "version: 'v0.0.2'", "version: 'v0.0.3'") + + with_host = fn lock, host -> + String.replace( + lock, + " repo_id: 197814629", + " repo_id: 197814629\n hostname: '#{host}'" + ) + end + + assert {:ok, lock} = ActionsLock.parse(with_host.(v003, "github.com")) + assert ActionsLock.pinned?(lock, "ci.yml", "actions/checkout@v7.0.1") + + assert {:error, {:line, 13, {:unsupported_dependency_hostname, "acme.ghe.com"}}} = + ActionsLock.parse(with_host.(v003, "acme.ghe.com")) + + # The field is new in v0.0.3; a v0.0.2 lock carrying it is still malformed. + assert {:error, {:line, 13, :malformed_dependency_entry}} = + ActionsLock.parse(with_host.(@valid_lock, "github.com")) + + duplicate = with_host.(with_host.(v003, "github.com"), "github.com") + + assert {:error, {:line, _, {:duplicate_dependency_field, _, :hostname}}} = + ActionsLock.parse(duplicate) + end + test "rejects every version outside the supported set, and a second version line" do for version <- ["v0.0.1", "v0.0.4", "v0.1.0", "0.0.3"] do assert {:error, {:line, 2, {:unsupported_version, ^version}}} = From 3e695cc02e57ceae5b4232f4393e7f292a54d037 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:11:21 +0000 Subject: [PATCH 08/11] Fix 10 failing CI check(s): GitHub Actions: ABI codegen drift / 0_abi-codegen-drift.txt, GitHub Actions: Security / 3_Rust License & Ban Check.txt, GitHub Actions: Security / 7_Secret Detection (TruffleHog).txt, GitHub Actions: Security / 8_Rust Dependency Audit.txt, GitHub Actions: Governance / 0_governance _ Validate Hypatia Baseline.txt, GitHub Actions: Governance / 8_governance _ Security policy checks.txt, GitHub Actions: Governance / 12_governance _ Well-Known (RFC 9116 + RSR).txt, GitHub Actions: Governance / 13_governance _ Actions lockfile verify.txt, GitHub Actions: Governance / 15_governance _ Code quality + docs.txt, GitHub Actions: Governance / 19_governance _ Workflow security linter.txt Co-Authored-By: CodeRabbit --- Cargo.lock | 16 ++++++------ deny.toml | 46 +++++++++++++++++++++++++++++++++ hypatia-cli-bash.sh | 2 +- scripts/hypatia-sitrep.sh | 9 +++---- scripts/ramdisk-orchestrator.sh | 7 ++--- 5 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 deny.toml diff --git a/Cargo.lock b/Cargo.lock index 9d44e46d..d01f31a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -824,9 +824,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -1445,9 +1445,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -2861,9 +2861,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" dependencies = [ "aws-lc-rs", "bytes", @@ -3213,9 +3213,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "once_cell", diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..e7b0e605 --- /dev/null +++ b/deny.toml @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: MPL-2.0 +# cargo-deny configuration for hypatia. +# +# Checked in so security-policy.yml's "Create deny.toml if missing" fallback is +# not used: that template predates the cargo-deny 0.18 schema (`vulnerability`, +# `unmaintained = "warn"`, `notice`, `unlicensed`, `copyleft`, `default` are now +# hard errors) and fails to deserialize. + +[advisories] +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +# Vulnerability advisories always fail. Unmaintained advisories fail only for +# crates the workspace depends on directly; transitive ones (unsafe-libyaml via +# serde_yaml, proc-macro-error2 via tabled_derive) are tracked upstream. +unmaintained = "workspace" +yanked = "warn" + +[licenses] +# Every licence that appears in Cargo.lock's dependency graph (measured +# 2026-10-19 against crates.io metadata for all 479 registry packages). +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "BSL-1.0", + "CC0-1.0", + "CDLA-Permissive-2.0", + "ISC", + "MPL-2.0", + "Unicode-3.0", + "Zlib", +] + +[bans] +multiple-versions = "warn" +wildcards = "warn" +highlight = "all" +skip = [] +skip-tree = [] + +[sources] +unknown-registry = "deny" +unknown-git = "warn" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/hypatia-cli-bash.sh b/hypatia-cli-bash.sh index 3bcf1cfc..4f4d400a 100755 --- a/hypatia-cli-bash.sh +++ b/hypatia-cli-bash.sh @@ -331,7 +331,7 @@ scan_code_patterns() { # the banned-language filter above). run_pattern "$dir" "*.{js,jsx,mjs,res}" '\beval\s*\(' \ "critical" "code_injection" "eval_usage" "CWE-95" \ - "Replace eval() with JSON.parse, switch, or safe alternatives" + "Replace eval() with JSON.parse, switch, or safe alternatives" # hypatia: allow content_patterns/eval_in_shell -- remediation text, not an eval call # Dynamic Function() constructor run_pattern "$dir" "*.{js,jsx,mjs,res}" '\bFunction\s*\(' \ diff --git a/scripts/hypatia-sitrep.sh b/scripts/hypatia-sitrep.sh index 14648063..7bb20e96 100755 --- a/scripts/hypatia-sitrep.sh +++ b/scripts/hypatia-sitrep.sh @@ -135,11 +135,10 @@ echo "$ALL_OUTPUT" | sed -n '/SECTION:GATE/,/SECTION:END/p' | grep -E "^\s+(Appr # --- 6. Top findings by frequency --- header "TOP PATTERNS (by repo count)" if [[ -f "$MANIFEST" ]]; then - jq -r '.pattern_id' "$MANIFEST" 2>/dev/null | sort | uniq -c | sort -rn | head -10 > /tmp/hypatia-top-patterns.txt || true - while read -r count pid; do - printf " %4d repos %s\n" "$count" "$pid" - done < /tmp/hypatia-top-patterns.txt - rm -f /tmp/hypatia-top-patterns.txt + { jq -r '.pattern_id' "$MANIFEST" 2>/dev/null || true; } | sort | uniq -c | sort -rn | head -10 | + while read -r count pid; do + printf " %4d repos %s\n" "$count" "$pid" + done || true else log " No dispatch manifest found" fi diff --git a/scripts/ramdisk-orchestrator.sh b/scripts/ramdisk-orchestrator.sh index bbf2e06f..2551cc5c 100755 --- a/scripts/ramdisk-orchestrator.sh +++ b/scripts/ramdisk-orchestrator.sh @@ -19,7 +19,7 @@ # --push Push committed fixes to remotes # --dry-run Show what would be done without doing it # --keep Do not unmount RAM disk on completion -# --mount PATH Custom mount point (default: /tmp/hypatia-workspace) +# --mount PATH Custom mount point (default: ${XDG_RUNTIME_DIR:-~/.local/state}/hypatia-workspace) # --max-repos N Limit number of repos to process # --fallback-disk Fall back to Eclipse drive if RAM is insufficient # --quiet Suppress non-essential output @@ -43,7 +43,8 @@ FARM_DIR="${FARM_DIR:-${HYPATIA_DIR}/../.git-private-farm}" ECLIPSE_REPOS="${HYPATIA_REPOS_DIR:-$(dirname "$HYPATIA_DIR")}" # Defaults -MOUNT_POINT="/tmp/hypatia-workspace" +WORKSPACE_BASE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}" +MOUNT_POINT="${WORKSPACE_BASE}/hypatia-workspace" RAMDISK_SIZE="2G" ORG="hyperpolymath" SEVERITY="high" @@ -176,7 +177,7 @@ mount_workspace() { if ! sudo -n true 2>/dev/null; then log_warn "No passwordless sudo — falling back to disk workspace" USE_TMPFS=false - MOUNT_POINT="/tmp/hypatia-workspace-$$" + MOUNT_POINT="${WORKSPACE_BASE}/hypatia-workspace-$$" fi fi From e8ed9f181ca9b29f69747cfd8c36d1a928ba4c2c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:20:24 +0000 Subject: [PATCH 09/11] docs(ramdisk): document workspace mounting and disk fallback behavior --- scripts/ramdisk-orchestrator.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/ramdisk-orchestrator.sh b/scripts/ramdisk-orchestrator.sh index 2551cc5c..81eda500 100755 --- a/scripts/ramdisk-orchestrator.sh +++ b/scripts/ramdisk-orchestrator.sh @@ -165,6 +165,13 @@ check_resources() { # ───────────────────────────────────────────────────────────────────── # Mount / unmount # ───────────────────────────────────────────────────────────────────── +# Prepare MOUNT_POINT with repos, findings, fixes and logs subdirectories, +# using tmpfs when USE_TMPFS is true and RAMDISK_SIZE as its size limit. +# Reuse writable mounts; attempt to unmount and replace unwritable mounts. +# Without passwordless sudo, set USE_TMPFS=false and change MOUNT_POINT to +# WORKSPACE_BASE/hypatia-workspace-$$ for a disk workspace. +# Dry runs only create MOUNT_POINT. Failed mounts or directory creation +# terminate the script under set -e. mount_workspace() { if [[ "$DRY_RUN" == "true" ]]; then log "[DRY-RUN] Would mount tmpfs (${RAMDISK_SIZE}) at $MOUNT_POINT" From fd2531fff8aa52d0e260ef2b486ff0f198db39f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:16:30 +0000 Subject: [PATCH 10/11] fix(ramdisk): resolve the mount point after parsing, mktemp the fallback Follow-up to 3e695cc, answering three review findings on it: - The default MOUNT_POINT expanded $HOME at load time, so with neither XDG variable nor HOME set, `set -u` aborted before --help or --mount could run. The default is now resolved after parse_args, and an environment with none of them asks for --mount instead of crashing. - The no-sudo disk fallback sat under XDG_RUNTIME_DIR, which is itself a small tmpfs. It is scratch space, so it now uses `mktemp -d -t`: the system temp dir (honouring TMPDIR) with an unpredictable name, removed by the existing EXIT trap. That is the cure hardcoded_tmp names for scratch files. - The --mount help text now names the XDG_STATE_HOME step. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 --- scripts/ramdisk-orchestrator.sh | 34 +++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/scripts/ramdisk-orchestrator.sh b/scripts/ramdisk-orchestrator.sh index 81eda500..ae17cd60 100755 --- a/scripts/ramdisk-orchestrator.sh +++ b/scripts/ramdisk-orchestrator.sh @@ -19,7 +19,9 @@ # --push Push committed fixes to remotes # --dry-run Show what would be done without doing it # --keep Do not unmount RAM disk on completion -# --mount PATH Custom mount point (default: ${XDG_RUNTIME_DIR:-~/.local/state}/hypatia-workspace) +# --mount PATH Custom mount point (default: hypatia-workspace under +# $XDG_RUNTIME_DIR, else $XDG_STATE_HOME, else +# ~/.local/state) # --max-repos N Limit number of repos to process # --fallback-disk Fall back to Eclipse drive if RAM is insufficient # --quiet Suppress non-essential output @@ -43,8 +45,9 @@ FARM_DIR="${FARM_DIR:-${HYPATIA_DIR}/../.git-private-farm}" ECLIPSE_REPOS="${HYPATIA_REPOS_DIR:-$(dirname "$HYPATIA_DIR")}" # Defaults -WORKSPACE_BASE="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}}" -MOUNT_POINT="${WORKSPACE_BASE}/hypatia-workspace" +# Empty until parse_args has run: --help and --mount must work even when +# neither XDG variable nor HOME is set. See resolve_mount_point. +MOUNT_POINT="" RAMDISK_SIZE="2G" ORG="hyperpolymath" SEVERITY="high" @@ -103,6 +106,21 @@ parse_args() { done } +# The tmpfs mount point is re-found by later runs (stale-mount reuse), so it +# lives in a per-user directory, not a mktemp one. +resolve_mount_point() { + [[ -n "$MOUNT_POINT" ]] && return 0 + local base="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-}}" + if [[ -z "$base" && -n "${HOME:-}" ]]; then + base="$HOME/.local/state" + fi + if [[ -z "$base" ]]; then + log_err "No XDG_RUNTIME_DIR, XDG_STATE_HOME or HOME; pass --mount PATH" + exit 1 + fi + MOUNT_POINT="$base/hypatia-workspace" +} + # ───────────────────────────────────────────────────────────────────── # Resource checks # ───────────────────────────────────────────────────────────────────── @@ -168,8 +186,9 @@ check_resources() { # Prepare MOUNT_POINT with repos, findings, fixes and logs subdirectories, # using tmpfs when USE_TMPFS is true and RAMDISK_SIZE as its size limit. # Reuse writable mounts; attempt to unmount and replace unwritable mounts. -# Without passwordless sudo, set USE_TMPFS=false and change MOUNT_POINT to -# WORKSPACE_BASE/hypatia-workspace-$$ for a disk workspace. +# Without passwordless sudo, set USE_TMPFS=false and change MOUNT_POINT to a +# fresh `mktemp -d` directory (under TMPDIR) for a disk workspace, removed +# by unmount_workspace on exit. # Dry runs only create MOUNT_POINT. Failed mounts or directory creation # terminate the script under set -e. mount_workspace() { @@ -184,7 +203,9 @@ mount_workspace() { if ! sudo -n true 2>/dev/null; then log_warn "No passwordless sudo — falling back to disk workspace" USE_TMPFS=false - MOUNT_POINT="${WORKSPACE_BASE}/hypatia-workspace-$$" + # Scratch space: XDG_RUNTIME_DIR is itself a small tmpfs, so + # use the system temp dir, with an unpredictable name. + MOUNT_POINT="$(mktemp -d -t hypatia-workspace.XXXXXXXX)" fi fi @@ -504,6 +525,7 @@ generate_summary() { # ───────────────────────────────────────────────────────────────────── main() { parse_args "$@" + resolve_mount_point log "${BOLD}Hypatia RAM Disk Orchestrator v${VERSION}${NC}" From 5c27a59ae7e3b2721e064e8054fec76b6c520baa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 16:35:47 +0000 Subject: [PATCH 11/11] fix(ci): give the Security Status aggregator its own workflow file The orphan-job self-check (#850) parses .github/workflows/security-policy.yml, but the security-status job had no checkout, so awk and sed found no file and the step exited 2. The verdict step after it was skipped on every run, which left the aggregator red without ever consulting its eight scans. A sparse checkout of just that file, with persist-credentials off, lets the self-check run. actions/checkout@v7.0.1 is already in actions.lock for this workflow, so the lock does not change. Checked locally: the self-check script passes against the edited file (9 jobs, all 8 non-aggregators named in needs:). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LET2eHeDbN3b4WuxYjMUX9 (cherry picked from commit 71a1af9db42fbd1d61696c3cfd7409d68b988d72) --- .github/workflows/security-policy.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/security-policy.yml b/.github/workflows/security-policy.yml index 82785297..6157e107 100644 --- a/.github/workflows/security-policy.yml +++ b/.github/workflows/security-policy.yml @@ -755,6 +755,17 @@ jobs: - sbom if: always() steps: + # The self-check below parses this very file, so the file has to be on + # disk. Without this step it reads nothing, exits 2, and the verdict + # step never runs. Only the one file: this job builds nothing. + - name: Checkout this workflow file + uses: actions/checkout@v7.0.1 + with: + sparse-checkout: | + .github/workflows/security-policy.yml + sparse-checkout-cone-mode: false + persist-credentials: false + # #850 AC4: every job in this workflow except the aggregators must be # named in some aggregator's `needs:`. Parse the job keys out of this # very file and compare; a future orphan job fails HERE rather than