From 9aae84e9fc4706f915213c4f3d3a25a50f7c9290 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 27 Jul 2026 12:08:46 -0500 Subject: [PATCH 1/3] fix(scaffold): point the adopter provenance gate at the repo that mints attestations `messagefoundry init` scaffolds a fail-closed CI gate that runs `gh attestation verify --repo ...` before installing the pinned engine wheel. It named `wshallwshall/MessageFoundry` -- the retired private vault, which no adopter can read and which no longer builds releases. Every project scaffolded by the next release would ship a supply-chain gate that fails for its own owner. Every other reference in the repo already names MEFORORG/MessageFoundry (release.yml, README, INSTALL-GUIDE, EARLY-ADOPTER-GUIDE, SUPPLY-CHAIN, MENTAL-MODEL); scaffold.py was the lone outlier, so this is a correction to it rather than a change of policy. The test pinned the wrong slug, which meant the broken state was actively enforced. It now pins the correct one and the negative -- the retired slug must never reappear in a scaffolded gate. Verified by mutation: reverting the source string fails the test, so the assertion is not green for free. --- messagefoundry/scaffold.py | 2 +- tests/test_scaffold.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/messagefoundry/scaffold.py b/messagefoundry/scaffold.py index aad1c83..d2ead02 100644 --- a/messagefoundry/scaffold.py +++ b/messagefoundry/scaffold.py @@ -162,7 +162,7 @@ def archive(msg): # type: ignore[no-untyped-def] - name: Verify SLSA build provenance before install env: GH_TOKEN: ${{ github.token }} - run: gh attestation verify dist-verify/messagefoundry-*.whl --repo wshallwshall/MessageFoundry + run: gh attestation verify dist-verify/messagefoundry-*.whl --repo MEFORORG/MessageFoundry check: needs: verify-engine diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py index 396a2dd..5270161 100644 --- a/tests/test_scaffold.py +++ b/tests/test_scaffold.py @@ -65,9 +65,13 @@ def test_scaffold_writes_the_skeleton(tmp_path: Path) -> None: # repo variable for indexes that strip attestations; the check job gates on it (never on verify failure) assert "verify-engine:" in ci assert ( - "gh attestation verify dist-verify/messagefoundry-*.whl --repo wshallwshall/MessageFoundry" + "gh attestation verify dist-verify/messagefoundry-*.whl --repo MEFORORG/MessageFoundry" in ci ) + # The scaffolded gate must name the repo that BUILDS the release — attestations are minted by the + # public repo's release workflow, so a private-vault slug here verifies against something no + # adopter can read. Pin the negative too: the retired slug must never creep back in. + assert "wshallwshall" not in ci assert "vars.MEFOR_VERIFY_ENGINE != 'off'" in ci assert "needs: verify-engine" in ci assert "needs.verify-engine.result != 'failure'" in ci From bb1c873a618da36ed73b1a5d95eea5dddb218d04 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 27 Jul 2026 12:09:03 -0500 Subject: [PATCH 2/3] fix(security): vuln-metrics measured the retired vault, not this repo vuln-metrics.yml invokes vuln_metrics.py with no --repo, so the argparse default silently decided what the weekly KPI run measured -- and it named the private vault. A public GITHUB_TOKEN cannot read that repo's Dependabot PRs, so the seven KPIs were computed over an empty window instead of failing loudly. Default to $GITHUB_REPOSITORY so the job always measures the repo it runs in; the literal slug is now only a local-run fallback. Still stdlib-only (os), which the workflow requires -- it runs the script with no install. --- scripts/security/vuln_metrics.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/security/vuln_metrics.py b/scripts/security/vuln_metrics.py index 80fbf4b..41cd94a 100644 --- a/scripts/security/vuln_metrics.py +++ b/scripts/security/vuln_metrics.py @@ -27,6 +27,7 @@ import base64 import csv import json +import os import re import statistics import subprocess @@ -264,7 +265,14 @@ def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter ) - p.add_argument("--repo", default="wshallwshall/MessageFoundry") + # Measure the repo the workflow actually runs in. vuln-metrics.yml invokes this with no --repo, so a + # hardcoded default silently decides what gets measured: it named the private vault, whose Dependabot + # PRs a public GITHUB_TOKEN cannot read — every KPI would have been computed over an empty window. + # $GITHUB_REPOSITORY makes it self-correcting; the literal is only for a local run outside Actions. + p.add_argument( + "--repo", + default=os.environ.get("GITHUB_REPOSITORY") or "MEFORORG/MessageFoundry", + ) p.add_argument("--limit", type=int, default=200, help="Dependabot PRs to scan (the window)") p.add_argument("--timeout", type=float, default=20.0) p.add_argument("--csv", default="docs/security/metrics/vuln-metrics.csv") From d79c5aafc4eafbd1d2be68b813aa682f95ffba9d Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 27 Jul 2026 12:09:05 -0500 Subject: [PATCH 3/3] docs(ci): describe the fork/canonical matrix split, not the retired mirror The test matrix branches on $GITHUB_REPOSITORY == MEFORORG/MessageFoundry. The logic is correct after the cutover, but the comments still described a private-repo / public-mirror split that no longer exists -- so the branch reads as a stale guard someone will eventually "fix" in the wrong direction. Restate what the branch actually does, and record why a fork PR cannot wedge the required Windows contexts: a fork PR runs in this repo's context, so it gets the full matrix and the required contexts are always present. Comments only -- no logic change. --- .github/workflows/ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d95d26..f50f4f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,11 +47,11 @@ jobs: # edge can't wedge the required context.) needs: changes # Every leg runs on GitHub-HOSTED runners now — the self-hosted NucBox Windows runners are RETIRED. - # The PRIVATE repo (wshallwshall) runs the UBUNTU leg only (its remaining Actions cost); the mirror - # (MEFORORG) runs the full ubuntu + windows-2022 + windows-2025 matrix, all FREE (hosted minutes are - # free on the public mirror). Hosted-everywhere is also safer: a self-hosted runner must never be - # reachable by a public repo (fork PR = RCE on the owner's LAN). `matrix.hosted` is the runner label - # set; the per-repo matrix (which drops the Windows legs on the private repo) is built in `changes`. + # This repo (MEFORORG) runs the full ubuntu + windows-2022 + windows-2025 matrix, all FREE (hosted + # minutes are free on a public repo); a FORK building its own pushes gets the UBUNTU leg only, to + # keep a contributor's personal CI cheap. Hosted-everywhere is also safer: a self-hosted runner must + # never be reachable by a public repo (fork PR = RCE on the owner's LAN). `matrix.hosted` is the + # runner label set; the per-repo matrix is built in `changes`. runs-on: ${{ matrix.hosted }} # Wall-clock backstop: bound each leg so a hung test fails in minutes, not the 6h default. Three # nested watchdogs catch a hang at increasing scope (#55): the pytest-timeout per-test cap (60s, @@ -61,11 +61,11 @@ jobs: timeout-minutes: ${{ matrix.job_timeout }} strategy: fail-fast: false - # Per-repo matrix from the `changes` job: ubuntu-only on the private repo (Windows runners retired), - # full ubuntu+windows matrix on the mirror. Each leg carries os / python-version / hosted (the - # runner labels, an array) + the timeout knobs. NB: the Windows legs no longer EXIST on the private - # repo, so drop `test (windows-2022/2025, py3.14)` from wshallwshall's REQUIRED checks — a - # required-but-absent context blocks the PR forever. + # Per-repo matrix from the `changes` job: the full ubuntu+windows matrix here, ubuntu-only on a + # fork's own pushes. Each leg carries os / python-version / hosted (the runner labels, an array) + + # the timeout knobs. NB: a fork *PR* against this repo still runs in THIS repo's context, so it + # gets the full matrix — the required `test (windows-2022/2025, py3.14)` contexts are always + # present on a PR here, and a required-but-absent context (which blocks a PR forever) can't occur. matrix: ${{ fromJSON(needs.changes.outputs.matrix) }} defaults: run: