fix(release): releases were disabled by the mirror-era guard; plus the leak-gate bootstrap (F.1, C.3)#7
Merged
Merged
Conversation
…synthetic token set announce itself
The forbidden-content hook passes --require-tokens and fails closed, which is correct: without it the
gate loads zero detectors in a fresh checkout and passes every commit green. But the token list is
git-ignored, so it does NOT travel with a clone or a `git worktree add`, and the setup step was
documented only inside a file you had to already know to open. Every new worktree therefore hit a hard
commit block with no documented way out -- reported from a parallel session that could not commit a
one-line docs edit. The enforcement was never the defect; the missing bootstrap was.
CONTRIBUTING.md said nothing about pre-commit or the token file at all. It does now, including the
part that matters: an OUTSIDE CONTRIBUTOR cannot ever have the real list, so the hook as shipped
blocked them permanently. That was stricter than both CI (which branches to structural-only for fork
PRs) and the scanner's own documented default. The supported answer is to copy the synthetic template.
Which creates the real hazard this change exists to close. The synthetic example POPULATES every
section, so it clears the count floor while matching nothing real -- a maintainer who copied it
instead of installing the real list would get a green gate that is blind, and no count could tell.
The scanner now compares the loaded set against the committed example (on PARSED content, not bytes,
so a reformatted copy still reads synthetic) and labels the run:
loaded names=7, estate=13, estate_file_scanned=12, site_prefixes=1
loaded names=5, estate=3, ... [SYNTHETIC EXAMPLE TOKENS - blind to real customer tokens; CI is authoritative]
loaded names=0, estate=0, ... [STRUCTURAL-ONLY: no token source configured]
Three states that exit 0 collapses into one. Only the first is evidence.
* scripts/dev/setup-leak-gate.ps1 -- -From <path> (maintainers) or -Synthetic (contributors). It
always finishes by RUNNING the scanner and printing per-section counts, and exits non-zero if
nothing loaded: a green gate is evidence only once you have confirmed it can see. It also verifies
the destination is git-ignored and deletes the file rather than risk committing the token list.
* scripts/worktree/new.ps1 -- carries the token file into a new worktree, or says plainly that
commits will fail closed. The symptom otherwise never mentions worktrees.
* scan-tokens.local.txt.example -- states that using it as-is is a supported contributor setup, that
it is labelled on every run, and that its counts are BELOW CI's per-section floor, so passing
locally with it does not predict CI.
Four tests, mutation-verified: forcing the predicate False fails exactly the two positive cases while
the negative controls (a real-shaped set, and no source at all) still hold -- "absent" and "synthetic"
are different failures with different fixes and must not be conflated.
Not changed, deliberately: --require-tokens stays. pre-commit has no per-hook env, so the flag is the
only mechanism, and relaxing it restores the blind-green failure it was added to stop.
…ra guard disabled releases entirely
Both jobs in release.yml were gated `if: github.repository != 'MEFORORG/MessageFoundry'`. That was
right when this repo was the published MIRROR: the mirror had to never release, and publish.ps1
rewrote the private slug to the public one across *.yml, so `!= public-slug` was the only rewrite-proof
form.
Both premises died at the cutover. publish.ps1 is retired (nothing rewrites anything), and MEFORORG is
now the SOURCE repo. Left inverted, the guard skips the release on the ONLY repo that can publish --
PyPI Trusted Publishing is bound to MEFORORG/MessageFoundry + release.yml (release.yml header, and the
runbook's F.1 prereq). The job guard and the OIDC binding pointed in opposite directions, so a pushed
`v*` tag would have silently SKIPPED and shipped nothing. Nobody had noticed because no tag has been
cut since the cutover -- F.1 ("validate the release under the new OIDC binding") is exactly the step
that would have found it, and it is still open.
Every other slug guard in .github/workflows -- ci.yml, codeql.yml, quality-advisory.yml, scorecard.yml
-- was already flipped to `== 'MEFORORG/MessageFoundry'` at the cutover. release.yml was missed. `==`
is also the safe form now: a fork, or the retired private vault, still cannot release.
THE TEST WAS PINNING THE BUG. tests/test_release_pipeline.py asserted the inverted guard appeared at
least twice, with a comment explaining the publish.ps1 rewrite. It was correct for the old topology and
survived it, so the broken state looked deliberate and enforced. Rewritten to assert the post-cutover
invariant, to reject the old form explicitly, and to say plainly that it used to assert the opposite --
a test can outlive the premise it encodes, and this one did. Mutation-verified: restoring the inversion
fails it.
NOT fixed here, deliberately: the pre-commit ruff/bandit hooks scan directories CI does not
(`scripts/` 1 ruff finding, `harness/` 74, `tee/` 12; plus pre-existing bandit findings in scripts/).
So touching any file there blocks your commit on errors you did not cause, invisibly to CI. I started
on the one-line UP017 in scripts/security/vuln_metrics.py and reverted it: editing that file pulled in
four unrelated bandit findings to annotate, which is scope creep on a release fix and would bury it.
It needs one decision -- clean the dirs and add them to CI's ruff/bandit scope, or narrow the hooks to
match CI -- not a drive-by.
wshallwshall
enabled auto-merge (squash)
July 27, 2026 14:35
wshallwshall
added a commit
that referenced
this pull request
Jul 27, 2026
…code before matching (#8) tests/test_store_aad_binding.py asserted `"DOE" not in on_disk` against an mfenc cell. The cell's payload is base64 of a random nonce + AES-GCM ciphertext, so matching a 3-character needle against that TEXT is a coin flip: measured ~0.12% of runs at this payload size, about 1 in 850. It red-X'd PR #7, whose diff touched release.yml and the leak-gate docs and nothing within reach of the store. The flake is the smaller half. The same check is also BLIND to the thing it exists to catch. Handed a cell that base64-encodes the plaintext verbatim, `"DOE" not in on_disk` evaluates TRUE -- "no plaintext here" -- because base64 of "DOE^JANE" contains no literal "DOE". So it fired at random on safe data and would have passed a real PHI-at-rest leak. Demonstrated both directions in the commit's test run. And the obvious fix is a trap. Reaching for a longer, more distinctive needle -- "DOE^JANE" -- makes it VACUOUS: '^' is not in the base64 alphabet, so the assertion could never match, would pass unconditionally, and would look stronger than what it replaced. So: decode first, then match the FULL plaintext against the at-rest BYTES. A false positive now needs a collision across the whole message rather than three characters, and a cell carrying its plaintext fails loudly. `test_the_plaintext_probe_can_actually_fail` pins the non-vacuity directly: it feeds the probe a plaintext-bearing cell and asserts the probe says so. Without it the round-trip test would pass no matter what the store wrote at rest -- which is exactly the state this repo keeps rediscovering. NOT changed, because not every short needle is wrong: * tests/test_support_bundle.py:81 (`"2575" not in blob`) matches PLAINTEXT json.dumps output. It is a deterministic redaction check and a short needle is exactly right there. * tests/test_uploads.py:52 (`"MRN123" not in blob`) is the same ciphertext shape as this one, but a 6-character needle is ~400,000x safer (~3e-7 %). Left alone rather than churn it; the pattern to avoid is a SHORT needle against ciphertext, not the length itself.
wshallwshall
added a commit
that referenced
this pull request
Jul 27, 2026
…olation (#10) Publishing docs/BACKLOG.md (#6) broke the ledger gate on every branch cut before that merge, including PR #7 -- whose diff touched release.yml and leak-gate docs and nothing near a ledger: OSError: git show HEAD:docs/BACKLOG.md failed (128): fatal: path 'docs/BACKLOG.md' does not exist in 'HEAD' CI's change set is `diff base HEAD`. Once origin/main gained the file, a branch that predates it lists docs/BACKLOG.md as CHANGED -- as a deletion relative to base -- despite never touching it. check_backlog then read HEAD for a copy that was never there. `base_has` (added when the file was imported) covers the opposite end: absent on BASE, being added. This is the mirror case -- present on base, absent on HEAD -- and it is the one that fires repeatedly, because it hits every open branch at once rather than the single commit that publishes the file. Both probes now share `_obj_exists`, which asks `git cat-file -e` directly rather than inferring absence from an exception: `git()` must keep raising on real failures, since an error swallowed as "empty ledger" reads as "no numbers taken" -- the false-clean this gate exists to prevent. "The path is not on that ref" is the one non-failure, so it gets its own probe instead of a broad `except`. `head_has` mirrors `head_text`'s ref exactly (HEAD in CI, the INDEX locally), so the guard cannot disagree with the read it guards. Mutation-verified: neutralise the guard and the new test fails with the same exit-128 shape seen in CI. Worth noting for whoever meets this next: the failure looked like an unrelated flake on somebody else's PR, and the first re-run "fixed" nothing -- the run before it had failed for a genuinely different, genuinely random reason (a 3-char needle colliding with base64 ciphertext, fixed separately). Two different faults on consecutive attempts of the same leg is exactly the shape that gets misread as one flaky test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two cutover-completion items. Read the first one — it means no release could ship.
1. Releases were disabled entirely (runbook F.1)
Both jobs in
release.ymlwere gated:Correct when this repo was the published mirror — the mirror must never release, and
publish.ps1rewrote the private slug to the public one across*.yml, so!= public-slugwas the only rewrite-proof form.Both premises died at the cutover.
publish.ps1is retired, and MEFORORG is now the source. Left inverted, the guard skips the release on the only repo that can publish — PyPI Trusted Publishing is bound toMEFORORG/MessageFoundry+release.yml(see therelease.ymlheader, and F.1's owner prereq). The job guard and the OIDC binding pointed in opposite directions: a pushedv*tag would have silently skipped and shipped nothing.Nobody noticed because no tag has been cut since the cutover. F.1 — "validate the release under the new OIDC binding" — is precisely the step that would have caught it.
Every other slug guard (
ci.yml,codeql.yml,quality-advisory.yml,scorecard.yml) was already flipped to==at the cutover.release.ymlwas missed.==is also the safe form now: a fork, or the retired vault, still cannot release.The test was pinning the bug
tests/test_release_pipeline.pyasserted the inverted guard appeared at least twice, with a comment explaining thepublish.ps1rewrite. It was right for the old topology and outlived it — so the broken state looked deliberate and was enforced. Rewritten to assert the post-cutover invariant, reject the old form explicitly, and state plainly that it used to assert the opposite. Mutation-verified: restoring the inversion fails it.F.1 is still open. Cutting
vX.Y.Z-rc1publishes to production PyPI — an owner action, not something to slip into a PR.2. The leak-gate bootstrap (runbook C.3)
forbidden-contentpasses--require-tokensand fails closed — correct, since without it the gate loads zero detectors and passes every commit green. The flag stays. But the token list is git-ignored, so it doesn't travel with a clone orgit worktree add, and the setup step was documented only inside a file you had to already know to open.CONTRIBUTING.mdmentioned neitherpre-commitnor the token file.Outside contributors were permanently blocked — they can never have the real list, making the hook stricter than both CI (which branches to structural-only for fork PRs) and the scanner's own default. The supported answer is to copy the synthetic template.
Which creates the hazard this actually closes
The synthetic example populates every section, so it clears the count floor while matching nothing real. A maintainer who copied it instead of installing the real list gets a green gate that is blind — and no count could tell. So the scanner now recognises its own template and labels the run:
Three states that
exit 0collapses into one. Only the first is evidence. Compared on parsed content, not bytes, so adding a comment can't silently reclassify a synthetic set as real — the direction that fails open.scripts/dev/setup-leak-gate.ps1-From <path>(maintainers) /-Synthetic(contributors). Always ends by running the scanner and printing per-section counts, exiting non-zero if nothing loaded. Verifies the destination is git-ignored, deleting it rather than risk committing the token list.CONTRIBUTING.mdscripts/worktree/new.ps1scan-tokens.local.txt.exampleFour tests, mutation-verified; the negative controls matter because "no source" and "the example" are different failures with different fixes.
C.3's ordering hazard is separately resolved and I've marked it done in the runbook:
install-git-hooks.ps1no longer writes.git/hooks/pre-commitat all (PR #4 moved the ledger gate into.pre-commit-config.yaml), so it can't collide withpre-commit installand order no longer matters.Not fixed here, deliberately
The pre-commit
ruff/bandithooks scan directories CI does not —scripts/(1 ruff finding),harness/(74),tee/(12), plus pre-existing bandit findings. Touching any file there blocks your commit on errors you didn't cause, invisibly to CI. I started on the one-lineUP017invuln_metrics.pyand reverted it: editing that file pulled in four unrelated bandit findings to annotate. That needs one decision — clean the dirs and add them to CI's scope, or narrow the hooks to match CI — not a drive-by under a release fix.🤖 Generated with Claude Code