Skip to content

test(crypto)+ci: sweep the at-rest assertions off the frozen v1 prefix, and pin the release toolchain - #48

Merged
wshallwshall merged 6 commits into
mainfrom
sweep
Jul 29, 2026
Merged

test(crypto)+ci: sweep the at-rest assertions off the frozen v1 prefix, and pin the release toolchain#48
wshallwshall merged 6 commits into
mainfrom
sweep

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Closes the two deferred follow-ups from #37/#38.

1. The at-rest assertion sweep (41 sites)

aad_bind now defaults on, so the default at-rest format is mfenc:v2 — but 41 assertions checked startswith(PREFIX) (v1-only) to mean "is this ciphertext at rest". Each is now MARKER_PREFIX, which matches both versions.

The risk here was asymmetric, and it drove the whole approach: sweeping a site that deliberately pins v1 would silently delete CRYPTO-1 frozen-writer coverage. 13 sites were left as PREFIX on purpose — the frozen-writer gate itself, three that pin the key_id's field position (widening makes the positive unsatisfiable and the negative unfailable), five premise assertions whose dual-read proofs collapse into tautologies under v2, and two poison injections that need a version the cipher dispatches on. 0 sites were ambiguous.

Three of the swept sites are negatives (not startswith) — as v1-only they would have passed silently on a leaked or unmigrated v2 value. Widening those is strictly stronger.

Vacuity proven first-hand, not asserted: patching AesGcmCipher.encrypt to identity kills 12/12 of the SQLite-runnable swept-site tests (control 12 passed → mutated 12 failed). Stated limitation: the migrating half can't be probed that way — _encrypt_existing_rows doesn't converge against a no-op writer, so those hang rather than fail.

2. Release-toolchain pins — and a guard that could not fail

Pinned pip==26.1.2, build==1.5.0, sigstore==4.4.0, cyclonedx-bom~=7.3.1, packaging==26.2; every version verified live against PyPI.

sigstore is pinned to 4.4.0, not the latest 4.5.0, deliberately. 4.5.0 is one day old against this repo's own dependabot.yml cooldown.default-days: 5 — pinning the signing toolchain fresher than policy allows would invert that policy at the highest-privilege point in the build. The comment also records that this pins the top only; ~30 transitive deps still float at signing time.

The guard had a real hole. _VERSION_OPS accepted >=, <=, != as pins — so sigstore>=4.4.0, which resolves whatever PyPI serves at tag time, passed green under a test literally named test_release_toolchain_pin_is_present. Split into _PIN_OPS = ("==", "~=") for the decision and _SPEC_OPS for name-matching. A 13-case mutation matrix (run against a temp copy; the repo was never modified) now kills every injected regression — including pip install https://…/evil.whl, which scored 0 before this commit.

Verification

messagefoundry/ untouched (0 files). ruff clean · full suite 9145 passed, 816 skipped, 0 failed · both workflows parse, and permissions/triggers/job lists compared against origin/main as parsed structures rather than eyeballed — identical.

… frozen v1 prefix

`[store].aad_bind` now defaults True (#38), so the default at-rest format is mfenc:v2. 41 test
assertions anchored on `PREFIX` ("mfenc:v1:") whose actual claim is "this value is enciphered at
rest at all" — a v1-only spelling that a v2 value silently fails. They move to the version-agnostic
`MARKER_PREFIX` ("mfenc:"), which is the same anchor the store's own find-all/migration `mfenc:%`
LIKE patterns and `is_encrypted()` already use.

Evidence, not assertion. Under the ASVS correctness net `MEFOR_TEST_FORCE_AAD_BIND=1` (conftest
forces every AesGcmCipher to the v2 writer), the swept files went 9 failed -> 1 failed. The one
remaining failure is `test_store_aad_binding::test_v1_rows_still_read_under_aad_bind`, a site this
commit deliberately does NOT touch: it fails on both branches because forcing v2 destroys its v1
premise, which is exactly what makes it the liveness receipt that the flag is live.

DELIBERATELY LEFT as v1 (CRYPTO-1 / ADR 0019 coverage a sweep would DELETE), each now carrying a
comment saying so — their absence is what made this dangerous to review:
  - the frozen-writer set (test_store_encryption: the _v1_blob oracle, the byte-identity gate,
    test_default_writer_is_v1_not_v2)
  - premise assertions whose point is "the row really IS v1" so a dual-read/no-rotation proof
    cannot go vacuous (test_v2_active_decrypts_v1_without_rotation, test_store_aad_binding,
    test_keyprovider x2, test_keyprovider_vault)
  - field-POSITION pins, where "mfenc:<fp>:" is a string no writer emits, so widening makes the
    positive unsatisfiable and the negative unfailable (test_key_id_is_a_fingerprint_not_zero,
    the legacy key_id='0' fixture)
  - undecryptable-payload injections (test_batch_claim_fifo, test_claim_fifo_heads): the marker
    version must be one the cipher DISPATCHES on, or the poison takes the unknown-version branch
    instead of the base64 failure the test is about

Three checks got strictly stronger rather than merely wider:
  - the rotation assertion now anchors on `cipher.active_marker_prefix`, keeping the "re-encrypted
    under the ACTIVE key" half of the proof that a bare MARKER_PREFIX would have dropped
  - three NEGATIVE leak/plaintext checks (`not startswith` / `not in`) now exclude EVERY marker
    version; as v1-only they would have passed silently on a leaked or already-encrypted v2 value
  - test_ack_sent_store's manual decrypt now passes the cell AAD the store wrote with. Pre-existing
    and previously MASKED by the v1 assertion failing first; unmasked once the sweep let the test
    reach it. Harmless on v1 (that reader ignores the caller's aad by design).

Comments naming "v1"/"the v1 marker" at swept sites were updated so they no longer lie.

Verification (SQLite leg, from the worktree): 9135 passed, 816 skipped, 0 failed; ruff check +
ruff format --check clean. Non-vacuity proven by injecting one regression (AesGcmCipher.encrypt
returns plaintext): two swept assertions FAIL on a plaintext at-rest value and pass without it.
…inst silent rot

Five installs on the release path resolved whatever PyPI served at tag time (Scorecard
PinnedDependenciesID; ADR 0034 §3). The `release` job holds contents/id-token/attestations: write
and `release-harness` holds contents/id-token: write, so these ran with the OIDC identity that
signs and publishes.

The sharp one is `sigstore`: its step carries no `if:`, so it runs on every tag AND every dispatch,
and the very next command signs the wheel, sdist, SBOM and VEX with that identity.

  release.yml  sigstore                -> ==4.4.0
  release.yml  pip + build (x2: engine and harness jobs)  -> ==26.1.2 / ==1.5.0
  release.yml  cyclonedx-bom           -> ~=7.3.1
  release.yml  packaging (x2)          -> ==$PKG_PIN, derived from constraints.lock
  security.yml pip + cyclonedx-bom     -> ==26.1.2 / ~=7.3.1   (keeps it the true SBOM twin)

Three judgement calls, none of them the obvious choice:

1. sigstore 4.4.0, NOT the newer 4.5.0. .github/dependabot.yml sets a 5-day supply-chain cooldown
   whose stated purpose is dodging a package compromised shortly after publish; 4.5.0 is <48h old.
   Hard-pinning the SIGNING toolchain to an artifact fresher than the repo's own routine-update
   policy allows would invert that policy at the highest-privilege point in the pipeline. 4.4.0 has
   aged 23 days. Re-evaluate once 4.5.0 clears the window.

2. `packaging` is DERIVED from constraints.lock, not hardcoded. It IS a DEP-1 dependency
   (requirements.lock + constraints.lock both pin it at 26.2), so a literal would drift on the next
   Dependabot bump. Same run-time-read pattern as quality-advisory.yml's ruff pin, but FAIL-CLOSED
   instead of falling back to an unpinned fetch — a soft fallback on the release path defeats the
   pin exactly where it matters. Both installs also move ABOVE their `GITHUB_REF_TYPE = tag` guard
   so a workflow_dispatch dry-run exercises them; they were the only two of the five a dispatch
   could not reach, and an install that never runs before the tag cannot be validated before it.

3. ~=7.3.1 rather than ~=7.3 for cyclonedx-bom: the looser form floats the whole 7.x minor range,
   and a 7.4 could change the JSON shape scripts/security/sbom_finalize.py parses — which exits
   non-zero and FAILS the release. ~=7.3.1 still takes patch fixes.

This is residual-risk reduction, NOT an alert closure: ADR 0034 §3 shows from this repo's own data
that exactly-pinned tools (bandit==1.9.4, zizmor==1.5.2) are still flagged while --require-hashes
installs are not. Pinning `sigstore` also pins the TOP only — its ~30 transitive deps still float
at signing time. Closing it needs the hashed release-tools lock (ADR 0034 option B), which is an
owner call and is deliberately NOT built here.

GUARD: a pin nothing watches rots back to unpinned, and Dependabot has no updater for an inline
`pip install X==Y` in a workflow (its `uv` ecosystem reads only pyproject.toml + uv.lock), so both
a stale pin and a DELETED pin are invisible. tests/test_ci_venv_pinning.py gains two tests: a
BLANKET scan of every pip-install target in release.yml (so a NEW unpinned install added tomorrow
fails too — the case a fixed name list cannot see), plus a non-vacuity table asserting each tool is
still installed and still pinned at EVERY occurrence.

Guard proven by injecting one regression at a time rather than trusting a green run:
  un-pin sigstore        -> 2 failed (blanket scan + the named backstop)
  DELETE the install     -> 1 failed (only the backstop can see this, as designed)
  un-pin 1 of 2 `build`  -> 3 failed (proves every occurrence is checked, not just the first)
  restored               -> 12 passed
Both workflows re-parsed as YAML. 192 passed across every test that reads .github/workflows.

Line-neutral where ADR 0034 requires it: the scanner re-fires a dismissed alert under a NEW number
when a line moves, so each pin was made in place. Not done: security.yml's `pip uv` (line 55) and
`pip-audit` (81) stay unpinned — the DEP-1 job is contents: read and outside the release path.
… sweep recorded

Follow-up to 09767bf. The sweep widened the "encrypted at rest" assertions correctly, but
review found five things it got wrong or left half-done.

1. Five more bare `decrypt()` calls on cell-bound columns (the defect the sweep fixed once,
   in test_ack_sent_store). Each fails closed on an mfenc:v2 value and was only invisible
   because the v1 prefix assertion above it failed first:
     - test_store_encryption   x3, queue.last_error   (store.py binds cell_aad("queue","last_error", <row id>))
     - test_sqlserver_store    x1, response.body      (sqlserver.py:4326)
     - test_postgres_store     x1, response.body      (postgres.py:3668)
   The SS/PG pair skip without a live DB, so they surface only in CI. Both now read
   destination_name back from the row rather than rebuilding the "\x1fack:" sentinel.

2. Seven new comments asserted a causal link that does not exist: "the at-rest format follows
   [store].aad_bind (v2 by default), so a v1-only prefix would fail here". aad_bind is read in
   exactly one place -- base.py:1736, make_cipher(..., write_v2=settings.aad_bind), reachable
   only via build_cipher/open_store. Every one of these tests builds its own cipher, and
   make_cipher's write_v2 parameter still defaults False, so they observe mfenc:v1: in a normal
   run and the setting has no influence on them. Reworded to the true reason: the marker version
   is the cipher's business, not the assertion's.

3. Two poison-injection comments named the wrong failure mechanism. Measured:
     'mfenc:v1:not-base64-$$$' -> ValueError: Nonce must be between 8 and 128 bytes
     'mfenc:not-base64-$$$'    -> CipherError: unknown at-rest marker version
   `_parse` splits on the second colon, which is absent, so the blob is empty and b64decode("")
   succeeds -- the raise comes out of AESGCM, not the base64 path. The decision to keep these
   two at v1 was right; only the stated mechanism was wrong.

4. test_store_encryption's M9 section claimed "WRITES v1 BY DEFAULT" / "the shipping default
   never emits a v2 marker" in the same file where the sweep added six "v2 by default" comments.
   ADR 0148 falsified the prose, not the tests: they pin make_cipher's PARAMETER default (still
   False), while the shipped store writes v2 via build_cipher. Both defaults now named separately.

5. test_bytes_per_message_amplification documented the at-rest shape as mfenc:v1:<key_id>:<b64>.

Receipt (real numbers, run from this worktree). Default mode over the 11 edited modules:
166 passed, 351 skipped. Forced-v2 leg (MEFOR_TEST_FORCE_AAD_BIND=1) over all 60 test modules
that import store.crypto -- the scanned set, not a 12-file window: 10 failed, 1159 passed,
359 skipped, down from 13 failed. The 3 that stopped failing are exactly the last_error sites
in (1). All 10 remaining are deliberate-v1 sites that MUST red under a forced v2 writer:
the four CRYPTO-1 frozen-writer pins, test_active_marker_prefix_v1_and_v2,
test_key_id_is_a_fingerprint_not_zero, and the four v1-premise dual-read tests.

No messagefoundry/ change. ruff check + ruff format --check clean over tests/.
…inish the ADR 0034 rows

Follow-up to 9bf7178. The pins were right; the guard protecting them was not, and two of the
four ADR 0034 rows were only half-applied.

THE GUARD ACCEPTED `>=` AS A PIN. `_VERSION_OPS` listed ("==", "~=", ">=", "<=", "!="), so
`pip install "sigstore>=4.4.0"` — which resolves whatever PyPI serves at tag time, byte-for-byte
the exposure ADR 0034:190 calls "the highest residual in the group … runs with the OIDC identity
used to publish" — passed green under a test named test_release_toolchain_pin_is_present, and its
own failure text never fired. Split into _PIN_OPS ("==", "~=") for the pin decision and _SPEC_OPS
(every PEP 440 operator) for matching a token to a package NAME, so an unpinned `sigstore>=…` is
reported as unpinned rather than as a missing step.

Three more holes in the same scan:
  - `"/" not in target` exempted every URL and git+ target — the one class that cannot carry a pin
    at all. `pip install https://…/evil.whl` inserted before the Sigstore step scored ZERO failures.
    Remote is now tested FIRST and always reported; only local path shapes are exempt.
  - `line.split(" install ", 1)[1]` disagreed with the `\bpip\s+install\b` regex that selected the
    line, so `pip\tinstall` raised IndexError instead of asserting. Both now anchor on one regex,
    which also picks up `pip3 install` and `pip --quiet install`.
  - The vacuity floor said >= 6 while the file has 8 pip installs — two steps could be deleted
    before the scan noticed. Floor is now the real count.

ADR 0034 ROWS FINISHED:
  - The harness `packaging` install got the pin but not the venv. ADR 0034:192 prescribes both, and
    the venv is the half carrying the risk: it landed in release-harness's MAIN interpreter, and the
    steps after it attach the wheel to the release and publish to PyPI. Now /tmp/harnesssmoke,
    mirroring the engine job's /tmp/relsmoke.
  - The two SBOM installs had drifted apart, which quietly retired ADR 0034:205's pre-tag validation
    route ("run security.yml's sbom job … the install command there is byte-identical"). Nothing in
    PR CI executes release.yml, so that dispatch was its only dry-run proxy. Both lines are now the
    same command and a test keeps them that way.

The register itself said "Recommended hardening — identified, NOT done" over four rows this branch
had already done, and called them "an owner decision, not a drive-by". Rewritten with per-row status
and the reason the owner gate is retired for them (they are PR-visible now). What is NOT closed is
stated plainly: these pin only the top of each install, sigstore's ~30 transitive deps still float,
and §3's own data (bandit==1.9.4 pinned and still flagged) says no pin moves the Scorecard finding —
option B remains the only thing that does, and remains an owner call.

New guards: security.yml's unpinned installs are a REGISTERED set (pip/uv/pip-audit — contents:read,
schedule-only, nothing anyone installs) so a new one there still reds; the SBOM twin-identity check;
and a PR-time canary that constraints.lock still carries exactly one `packaging==` line, because
release.yml derives that pin at run time and `exit 1`s without it — on the tag push.

RECEIPTS. Guard mutation-tested against a TEMP COPY of the workflows (repo never modified), one
regression at a time — control 0 failures, no crashes:
  sigstore == -> >=                     2   blanket_scan + named_backstop
  build == -> >= (1 of the 2 installs)  2   blanket_scan + named_backstop[build]
  sigstore install deleted              2   blanket_scan + named_backstop
  new unpinned named install            1   blanket_scan
  new https://…whl install              1   blanket_scan   (scored 0 before this commit)
  new git+ install                      1   blanket_scan
  packaging pin -> bare                 2   blanket_scan + named_backstop[packaging]
  TAB / pip3 / flag-before-subcommand   2   each (was an IndexError crash)
  SBOM twin drift                       1   sbom_twin
  security.yml gains an unpinned target 1   security_registry
  --upgrade-deps / lost --require-hashes 2 / 1
  constraints.lock loses packaging==    1   packaging_canary

Both workflows re-parse as YAML; permissions, triggers and job lists are byte-identical to
origin/main (checked, not assumed). 195 passed / 10 skipped across all 14 test modules that read
.github/. No messagefoundry/ change.
@wshallwshall
wshallwshall enabled auto-merge (squash) July 29, 2026 17:16
The console publish job landed while this branch was open and installs pip,
build and packaging unpinned. The guard added earlier on this branch rejects
exactly that, so updating the branch against main turned all three test legs
red -- the guard catching a real regression written after it was, which is the
best evidence it works that this branch could have produced.

pip and build take the same explicit pins the sibling build jobs use. packaging
is derived from constraints.lock the way relsmoke and harnesssmoke already do,
so one lock bump moves every release-path packaging install together instead of
letting them drift apart.

Also adds the check the pin guard structurally cannot make. Getting here, an
edit put a literal 0x01 byte in release.yml -- a sed backreference written into
a non-raw Python string -- and the file became unparseable YAML that GitHub
Actions could never have run. The pin guard passed green through it, because it
greps lines and never parses the document. So the file is now verified by
parsing it, and its permissions blocks and triggers are compared against
origin/main as parsed structures rather than eyeballed: jobs release,
release-webconsole, release-harness; permissions identical; triggers identical.

Verification: pin guard 15 passed; full suite 9331 passed, 818 skipped, 1
failed. That failure is tests/test_gate_installed_parity.py, which is
PRE-EXISTING and unrelated -- it reproduces identically on clean main, this
branch touches neither the gate source nor that test, and the test is
LOCAL-MACHINE only (it skips on CI, where no installed gate exists). It reports
that this workstation's installed ~/.claude/hooks copy is older than the source
that PR #36 merged; re-running install-gate.ps1 is the fix, and that is
deliberately an owner action because it activates the EnterWorktree rule for
every session on the box.
@wshallwshall
wshallwshall merged commit faefa27 into main Jul 29, 2026
34 checks passed
@wshallwshall
wshallwshall deleted the sweep branch July 29, 2026 19:17
wshallwshall added a commit that referenced this pull request Jul 29, 2026
main's #48 pinned the RELEASE path (sigstore, build, cyclonedx-bom, packaging,
pip) and built a blanket-scan + registry to keep it that way. This branch pinned
the BLOCKING SCANNERS in security.yml (pip-audit, uv, semgrep) and checksum-verified
the gitleaks download. Same subject, opposite halves, so they conflicted in
tests/test_ci_venv_pinning.py.

Resolved toward main's machinery, which is the better base: its _PIP_INSTALL scan
catches a NEW unpinned install anywhere on the release path, which a fixed job list
cannot. My test_blocking_jobs_pin_the_tools_they_install is DROPPED rather than
merged -- it duplicated that scan for three named jobs and would have been a second
gate on the same property.

Two substantive corrections to main's side, both load-bearing:

1. SECURITY_YML_ACCEPTED_UNPINNED registered `uv` and `pip-audit` as accepted
   unpinned, reasoning that security.yml's jobs are "schedule/dispatch-only" and
   "produce nothing anyone installs". The first half is factually wrong: that file
   triggers on `pull_request` and SEVEN of its jobs are required contexts,
   pip-audit among them. So those were unpinned dependency intake on every PR,
   inside the gate whose stated purpose is proving nothing unpinned enters the
   tree -- and `uv` is the resolver producing every lockfile that job then audits,
   so an implicit upgrade can change the exported set and red the
   `git diff --exit-code` for a reason unrelated to the change. Both are now pinned
   and out of the registry, which now holds only the `pip` bootstraps. The
   docstring records the correction rather than quietly rewriting the rationale.

2. RELEASE_PINNED_TOOLS gains the four security.yml scanners, so a pin that
   vanishes WITH its step is caught -- the direction the blanket scan structurally
   cannot see.

test_release_asset_downloads_in_blocking_jobs_are_checksum_verified is kept: it is
the non-pip half of the same intake and main has no equivalent. It excludes trivy
(continue-on-error, so it cannot turn a required context green) and strips comments
before matching, because the gitleaks step's rationale comment QUOTES the
`curl | tar` it replaced -- a detector that would otherwise count itself.

security.yml auto-merged clean: main touched only the SBOM step, this branch only
the pip-audit/semgrep/gitleaks steps.

Verified after resolution: 154 tests pass across every affected suite (including
main's byte-identical-SBOM twin check and the release-pipeline structure pins),
actionlint clean.
wshallwshall added a commit that referenced this pull request Jul 29, 2026
#57)

release.yml's webconsole job carried

    python -m pip install --quiet "packaging==$PKG_PIN\"

A stray backslash before the closing quote left the shell string
unterminated, so that job would have died the first time anyone cut a
webconsole tag. I introduced it in #48 while fixing a DIFFERENT escape bug:
a sed backreference written into a non-raw Python string became chr(1), I
switched to raw strings, and a raw string then preserved the \" I did not
want. Two escape failures from opposite directions in one edit.

Nothing could have caught it, which is the more interesting half:

- the YAML parsed, because a broken shell string is still a valid YAML
  scalar;
- the pin guard passed, because it greps for pin syntax and the line does
  contain packaging==;
- no CI leg executes it -- the step is tag-only and webconsole-gated, and
  no pull request runs a tag-only release job.

So the first execution would have been a release. A one-line fix leaves that
hole open for the next edit, so this adds tests/test_workflow_shell_syntax.py:
parse every workflow, extract every shell run: block, and ask bash -n whether
it is syntactically valid. It resolves the shell the way Actions does (step >
job defaults > workflow defaults), skips pwsh blocks and Windows runners with
an unspecified shell, and substitutes ${{ ... }} first since Actions templating
is not shell.

126 blocks across 12 workflows check clean. Two liveness tests sit in front of
the check because a glob or extractor that silently found nothing would make it
vacuous -- it asserts the block count instead of trusting a green.

Proven to kill the real bug, mutation verified as APPLIED first: reintroducing
the backslash exits 1, restoring exits 0. An unapplied mutation reads as a pass,
so the injection is byte-checked before the result is believed.

One implementation note worth keeping: bash -n must take a FILE, not stdin. A
script containing a heredoc makes bash read the heredoc body from the same
stream it is reading the script from, and it blocks for a terminator that never
arrives -- a 30s timeout, not a syntax error. The timeout is caught and named
per block rather than failing the run blind.

Scope stated in the docstring: this checks syntax, not behaviour. It cannot
catch a command that parses and then does the wrong thing.
wshallwshall added a commit that referenced this pull request Jul 29, 2026
…ect the provenance claims (#54)

From an audit of this repo's controls against an external "reviewing AI-generated code" guide. Most of that guide's advice presupposes a second human reviewer; what survived translation to a solo, auto-merge, AI-authored repo is here.

THE MERGE GATE IS NOW KNOWABLE, AND NOT NEUTERABLE IN ONE LINE.

security.yml documented the downgrade verbatim: "To temporarily downgrade one, add `continue-on-error: true` back to its job." A job-level continue-on-error makes the job report SUCCESS, so the context branch protection requires stays green while the scanner's findings are discarded -- and with zero required approvals and auto-merge armed, the PR merges unread. Nothing guarded that.

"Is this check blocking?" could not be answered from a clone either. Five in-repo statements disagreed with each other and with the server: docs/CI.md said 8, manifest-lint.yml said 4, docs/design/freethread.md said 7, tests/test_push_guard.py said 12. docs/CI.md also called zizmor "Blocking" (it is not), said Scorecard runs on PRs (no pull_request trigger), and named the CLA context "CLA Assistant" when the real string is `cla`.

  * .github/required-contexts.txt -- the checked-in claim, with the required-but-absent trap documented for anyone adding one.
  * tests/test_required_contexts.py -- reconciles every prose statement against it.
  * tests/test_security_posture.py -- refuses continue-on-error, `|| true`/--exit-zero, and a skippable job-level `if:` on any job backing a required context; pins the advisory jobs the OTHER way so an accidental promotion is equally deliberate.
  * actionlint -- pre-commit hook + a checksum-verified step in zizmor.yml, for the invalid-expression class that aborts workflow COMPILATION so required contexts silently never appear. docs/CI.md asked for actionlint; nothing ran it.

A DEPENDENCY NAME PYPI CANNOT VOUCH FOR IS NOW REJECTED.

pip-audit asks "does this pinned version have a known CVE". A freshly registered hallucinated name has no advisory, so it resolves through `uv lock`, hashes into requirements.lock and installs under --require-hashes: every DEP-1 control passes it. The only thing in the way was that `uv lock` cannot resolve a name PyPI does not serve -- a resolver side effect, not a control, and exactly what slopsquatting defeats.

scripts/security/new_dependency_check.py sweeps every distribution in pyproject.toml, core and every extra, and fails on a name that does not exist, publishes no files, was first released inside a 90-day floor, or is served under another project's canonical name. Age comes from the EARLIEST release across all versions, so a squatter who registers then publishes later does not look established. It runs as a step in the already-REQUIRED pip-audit job, so it blocks today with no branch-protection change. Stdlib-only, and fails CLOSED: unreachable PyPI exits 2, and examining zero distributions exits 2 so a schema change cannot read as a clean sweep.

Verified against live PyPI: 41 distributions clean, an injected `hl7-dicom-bridge>=1.0` rejected. Its blind spot is documented and pinned by a named test rather than glossed: it cannot tell a REAL package is the WRONG one -- measured, `py-webauthn` exists, publishes, is years old and is canonically named, yet is AS207960's project rather than the intended `webauthn`.

THE PROVENANCE CLAIMS NOW MATCH REALITY.

The standard prescribed a `Co-Authored-By` + `Tier:` trailer, listed it under "Built (in code today)", put it in the retained-evidence set, and cited it for two claims in the register aimed at "adopters and auditors". Measured: 0 trailers and 0 `Tier:` lines in 300 commits, while 81 tracked files instruct omitting it -- because cla.yml allowlists three identities, a trailer co-author reads as an unsigned contributor, and `cla` is required. The prescription and the prohibition were both in the tree, neither referencing the other.

Fixed by making the documents honest rather than by building a hook. The claim "Provenance recorded at commit granularity by convention" is WITHDRAWN with a replacement wording, and the deviation corrected to the broader truth: no per-commit model/version record at any granularity. tests/test_ai_provenance_claims.py asserts CONSISTENCY rather than counting git log, because CI checks out shallow and "0 of 0 commits" is a gate measuring nothing.

THE GATES' OWN TOOLCHAIN IS PINNED.

The pip-audit job -- whose purpose is proving nothing unpinned enters the tree -- installed its auditor with a bare `pip install pip-audit`. DEP-1 bootstrapped `uv`, the resolver producing every lockfile it then audits, unpinned. semgrep rode `~=1.90`. gitleaks was `curl | tar` with no integrity check while the sbomqs step in the same file already verified a checksum. Now pinned (each to what the prior spec already resolved to, so no behaviour change) and checksum-verified.

This also reconciles with #48's release-path pinning, which registered `uv` and `pip-audit` as accepted-unpinned on the reasoning that security.yml's jobs are "schedule/dispatch-only". That half was wrong: it triggers on pull_request and seven of its jobs are required contexts.

VERIFICATION.

Nothing was accepted for being green. 7/7 mutations into security.yml were caught by the intended assertion; reverting the doc fixes reproduced 5/8 required-context and 4/6 provenance failures; actionlint catches an invalid expression inside a comment -- the exact class docs/CI.md describes.

Bugs that discipline found in this work: an invariant asserting 12 contexts == 12 jobs (they are 10 -- the test legs share a matrix job); `find("**Built")` slicing a section 400 lines off target so a guard checked nothing; a detector flagging `-r requirements.lock`, the most pinned install in the file; a detector matching `| tar` inside its own comment explaining the pipe it replaced; and an attempt to patch pre-commit's generated shim, which tests/test_ledger_check.py correctly refused -- `pre-commit install` rewrites that file, so the patch was futile as well as forbidden.

Owner action still needed: branch protection is server-side and unchanged by this PR. required-contexts.txt deliberately mirrors the LIVE server, so backlog-hygiene is recorded as pending promotion rather than added -- adding it first would make the file lie in the one direction it exists to prevent.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…ones (#163)

* docs(backlog): close BACKLOG #226 — the estate Hybrid-layout sweep is done, off-repo

The per-feed Hybrid split (connections.toml / <INBOUND>_router.py /
<INBOUND>_handler.py / _<feed>_transforms.py) landed across the ported estate in
the maintainer-internal migration repository. Owner-attested; nothing in this
repository changes, which is also why leaving the item open could never have
closed it.

Both "Also" clauses are recorded as NOT delivered, with the reason each is not a
residual of this item:

  - "align the IDE Corepoint-import / scaffold path to emit the Hybrid layout" —
    there is no Corepoint-import path in ide/ to align. That tooling is #105,
    still open, so the clause is a constraint on #105's design rather than work
    #226 can perform. The scaffold half is misaddressed too: Insert Element (#48)
    drops per-file idioms into the current buffer (ide/src/insertElement.ts:1-5)
    and emits no multi-file feed layout.

  - "consider a recursive-glob / folder-per-feed loader enhancement" — filed as a
    consider, and not taken: load_config still globs *.py non-recursively
    (config/wiring.py:4162), the flat-merge behaviour the Hybrid layout is built
    around.

Follows the #227 precedent: close the primary, state the off-repo/misaddressed
residuals explicitly so the item is not re-opened for them.

backlog_status_check.py: OK — 277 items, each declaring exactly one status.

* fix(ledger): teach the number-space gates to span an archive, and fix two holes found proving it

Prerequisite for moving the 185 closed BACKLOG items into docs/archive/backlog/.
No item has moved yet; this only makes the guards able to see one when it does.

The item namespace will span two paths, so every guard now reads their UNION:

  - backlog_status_check.py: scan() takes (label, text) pairs and parses them as ONE
    namespace. A number re-used across BACKLOG.md and the archive was structurally
    undetectable before -- `seen` was per-parse -- which is the erratum's own shape.
  - ledger_check.py: triggers on any backlog-bearing path, not the one literal, and
    builds head/base as the union. Reading the union on both sides also removes a
    false positive: the move relocates 185 items, so head-union == base-union and
    `head - base` stays empty, where a per-file view would report 185 vanished
    numbers with a remedy that renumbers cited items.
  - alloc.ps1: sweeps both paths in the all-refs term and the working-tree term.
  - backlog-hygiene.yml: accepts a banner updated in either location.

Two pre-existing defects surfaced only because the gates were made to fail on
purpose first, neither of which is about the archive:

  1. alloc.ps1's working-tree term has NEVER worked. `[regex]'^...'` anchors at the
     start of the STRING; the term feeds it `Get-Content -Raw`, one string starting
     "# Backlog". Measured: 0 of 277 headings matched without Multiline, 277 with.
     The all-refs term hid it by covering every number committed somewhere -- i.e.
     every case except the uncommitted one this term exists for.
  2. backlog-hygiene.yml diffed BASE_SHA..HEAD_SHA (two-dot), which credits a PR for
     main-side changes to paths it never touched. One main-side edit to BACKLOG.md
     -- the move being a large one -- would let every PR with an older base pass the
     "must update BACKLOG.md" required check while enforcing nothing. Now three-dot,
     matching ci.yml's form for the same question.

Anti-narrowing, because a green gate over a shrunken corpus is the failure mode:
  - `--min-items N` fails when fewer items are found than required, and CI pins 277.
    Without it, 277 -> 92 fails nothing.
  - The scanned files are always printed with the count; a bare integer cannot
    distinguish "items closed" from "a file stopped being read".
  - A liveness receipt in the test suite asserts the same floor.
  - An explicitly-named --backlog path that does not exist is an error, not a skip.

alloc.ps1 gains `-ShowFloor`: print the floor and the swept paths, allocate nothing.
Allocation is a one-way door, so before this the only way to ask what the floor could
see was to spend a number on the question -- which is how it ran a whole release
reading two refs while its header promised all of them. Get-Floor takes -Peek so the
inspection cannot advance the high-water ratchet; the first -ShowFloor run against a
planted number moved this clone's watermark 316 -> 990 before that was fixed.

Proofs run, each observed failing BEFORE the fix:
  - archive-only unallocated #1007 staged: old gate rc=0, new gate BLOCKED.
  - #990 planted in the archive: old sweep floor 353 (blind), new sweep 990.
  - cross-file duplicate #118: detected, naming the other file.
  - banner violations inside the archive only: detected.
  - --min-items over a narrowed corpus: rc=1 with the scanned-file list.
  - -ShowFloor twice against a plant: watermark unchanged at 316.

ruff + mypy --strict clean; 43 gate tests pass.

* docs(backlog): move the 185 closed items into docs/archive/backlog/BACKLOG-CLOSED.md

docs/BACKLOG.md becomes the ~92 items someone can act on: 8,742 -> 3,648 lines.
The closed items are not deleted, summarised, or rewritten -- they are relocated
verbatim, so the file that gets opened, grepped and edited daily is the open set.

MOVED, NOT REWRITTEN. Every relocated block is byte-identical to the one that left
BACKLOG.md, headings included. Verified mechanically against a pre-move copy:

  - 277 items before = 92 after + 185 archived, no overlap, union identical
  - every OPEN block byte-identical to its source
  - every ARCHIVED block byte-identical to its source
  - all non-item prose in BACKLOG.md preserved verbatim

Byte-identical headings are load-bearing, not tidiness: GitHub derives anchor slugs
from heading text, so all 64 archived->archived cross-references keep resolving with
no edit at all. That is the whole argument for one archive file rather than a split
by status, year, or cluster -- #52 alone receives 99 of the 110 in-file anchors, and
its citers span #65 to #184, so no cut isolates them.

Cutting item blocks at the next '## ' heading of EITHER kind, not the next numbered
item: 4 blocks in this file are followed by a section header, which a naive cut would
have dragged into the archive along with the prose beneath it.

Anchors, all 127 re-resolved against real headings after the edit:
  - 44 rewritten in BACKLOG.md   -> archive/backlog/BACKLOG-CLOSED.md#<same-slug>
  -  1 rewritten in the archive  -> ../../BACKLOG.md#<same-slug>  (#226 -> #105)
  -  3 cross-file links repointed: AOAG-DEPLOYMENT.md (#100, #101), ADR 0026 (#30)
  - 64 archived->archived untouched, by design

13 anchors still do not resolve, and ALL 13 WERE ALREADY DEAD BEFORE THIS COMMIT --
confirmed by running the same check over the pre-move file, which returns the
identical multiset (11 bare-number self-anchors: #40 x4, #323 x3, #28, #29, #329,
#333; plus 2 links to #13 in COUNSEL-ENGAGEMENT-BRIEF.md, a number this sequence
never had). They are left dead and documented in the archive header rather than
repointed at a plausible neighbour: a citation resolving to the WRONG item is the
erratum's failure mode, and unlike a dead link it looks like success.

The archive carries its retirement banner inline rather than in a sibling README --
docs/archive/throughput/ needs a README because it indexes five documents; one file
does not, and two documents that must agree is a drift surface. It states the rules
that keep the namespace honest: never renumber, re-open by moving the block back
(never by copying, which creates the cross-file duplicate the status check now
fails), and add any future archive file to alloc.ps1's $backlogPaths AND
backlog_status_check.py's DEFAULT_SOURCES in the same commit -- a file named in
neither is policed by nothing.

Gates verified post-move:
  - backlog_status_check.py --min-items 277: OK, 277 items, and it now PRINTS
    "scanned: docs/BACKLOG.md (92), docs/archive/backlog/BACKLOG-CLOSED.md (185)"
  - ledger_check.py on the staged move: rc=0 (relocation adds no numbers, because
    head-union == base-union -- the exact false positive the union view removes)
  - alloc.ps1 -ShowFloor: floor 353 across both paths, next 1000
  - 43 gate tests pass

Note the floor is unchanged at 353 because the highest item (#353) is open and stays
in BACKLOG.md. The archive-sweep fix is therefore PROSPECTIVE, not a save: it starts
mattering the first time a top-of-range item closes and moves.

* docs(backlog): re-score all 92 open items on the ten-level scale (2026-08-03)

Every open item now carries a current value x difficulty score. Before this, 23 had
none at all and the other 69 were from the frozen 2026-07-10 pass, which predates the
2026-07-28 reconcile that closed 31 items -- and a stale score reads exactly like a
fresh one.

Method, unchanged from the pass it supersedes: scored from each item's own Scope /
Why / Trigger / Nearest-existing-mechanism text rather than rescaled from the old
number, then adversarially verified against the code -- a second reader per batch
attacking build state first, then verdict/tier, then value and difficulty. 26 of 92
scores were overturned by that pass and carry the refuter's number.

The banner is the live record and the table is a view of it; both are written here and
a mechanical check confirms 92 banners and 92 rows agree on every triple.

THE RATIONALE IS REPLACED, NOT JUST THE NUMBERS. Carrying an old justification under a
new score is how a banner comes to argue against itself:
  - #114's surviving "clean workaround via the on-demand test probe" is a claim PR #162
    explicitly retracted -- both destinations' test_connection CREATE the target dir, so
    the probe cannot answer the question the toggle asks. That is what lifts it off the
    parity-with-a-workaround band to 6/3. Its replacement rationale was ALSO stale (it
    described the silent-ignore #162 had just fixed) and is hand-corrected.
  - #105's "large greenfield 71-action mapper needing its own ADR" describes an importer
    that has since shipped under ADR 0086.

Scheduling barely moved, which is the reassuring result: only TWO tiers changed --
#64 DEMAND-GATE -> P3 (an index over levers that live in #62/#63/#47/#34, so it ships
nothing runnable of its own) and #105 P3 -> DEMAND-GATE. Neither contradicts an
explicit demand-gate/on-trigger ruling in its own body; that was checked for all 51
items carrying a prior tier.

Distribution is RECOMPUTED with the table rather than carried forward, and all four
lines sum to 92. The superseded table keeps its own frozen lines and now says so.

  Tiers: P1 4, P2 19, P3 17, DEMAND-GATE 52
  Quadrants: quick win 22, big bet 5, fill-in 56, money pit 9

The four P1s: #341 (9/3, a handler returning a tuple/set of Sends delivers nothing
silently -- an accept-and-drop CLAUDE.md §12 forbids), #324 (7/2), #325 (6/2), #327 (6/2).

NOT in this commit: 24 items were found to misdescribe their own build state -- prose
asserting a gap that has since shipped, or citing messagefoundry/console/, a package
retired with #103. Those are banner corrections and land separately; the scores here
already price the remainder rather than the original scope.

Two mechanical faults were caught by reading the output rather than trusting the run:
the quadrant regex omitted the hyphen in "fill-in", so 57 of 69 items took the fallback
branch and got a SECOND score inserted beside the first; and the synthesizer's own
distribution lines did not follow from its own table (11 quadrant mismatches, 8
ordering violations, difficulty summing to 95 of 92). The script now refuses to write
when any line carries two score spans or the scored count is not 92.

backlog_status_check.py --min-items 277: OK, 277 items across both files.

* docs(backlog): correct 10 items whose own prose misdescribed build state

The 2026-08-03 re-score flagged 24 open items as misdescribing what the code does.
Re-verified each against the tree as it stands -- after the archive move and after
PR #162, both of which post-date the findings -- and 10 survived. The other 14 did
not, and are recorded here rather than silently dropped:

  #84 #95 #99 #105 #114 #124 #125 #127 #133 #137 #167 #169 #214 #228

Most of those already carry an amendment that covers the stale sentence (#95, #99,
#105, #114, #124, #125, #127, #133, #228), and stacking a second ruling saying the
same thing is noise. The rest did not survive verification: the finding was itself
wrong or overstated, and a wrong correction in a ledger is worse than a stale one.

CORRECTIONS ARE ADDED AS DATED AMENDMENTS, NOT PROSE REWRITES. This file's convention
is to leave the original claim standing and rule against it, so the record shows what
was believed and what replaced it. Silently editing the stale sentence would destroy
the evidence that makes the correction checkable.

Applied to #62 #64 #131 #166 #179 #182 #237 #321 #329 #336. Representative:

  - #329 "Five MEFOR_ALLOW_INSECURE_TLS cells": the census is FOUR. #323 landed and
    routed transports/direct.py through the clamp; it now holds no call to the raw
    predicate at all (:63, :197, :215).
  - #321 "no test asserts the detectors can see a site code": false --
    tests/test_scan_forbidden.py has per-class hit tests for at least the site code
    (:126), a customer name (:83), a case-sensitive code (:91) and a routable IP
    (:107). The detector-coverage half of its Proposed 2 is already in the tree.
  - #62 plans a dual-read over "existing mfenc:v1 rows", but cell-bound mfenc:v2 is
    the default writer (settings.py:383 -> base.py:1841; crypto.py:36), and v2 folds
    (table, column, pk) into the GCM tag -- so a body landing under a different column
    must be RE-ENCRYPTED, not merely re-encoded. That tightens the catch.
  - #64's ordered plan still reads live ("Nothing builds before it"), but the
    measure-first phase completed 2026-07-12 (ADR 0051) and its step-2 lever is
    refused outright (ADR 0055 withdrawn; ADR 0107 "Do not build F2 or F3").

The refuters removed two overclaims before they landed: #62's draft asserted a live
store holds both mfenc markers (a fresh store under the shipped default holds only
v2 -- the defensible claim is that a MIGRATION must expect both), and #64's asserted
the multi-DB log split still remains, which could not be verified against ADR 0098 and
would have been a fresh false claim.

No item closes here: in every case the correction narrows the remainder rather than
discharging it, and the 2026-08-03 scores already price the remainder.

backlog_status_check.py --min-items 277: OK, 277 items, one status banner each.

* docs(backlog): file BACKLOG #1000 — prove each required merge context can fail

Escalated by the coordinator on the ground that it outlives the PR that fixed it.
Deliberately NOT filed as "fix the two-dot diff": that instance already landed in
39b62bf, and filing shipped work is the rot the hygiene gate exists to prevent.

The item is the CLASS. `.github/required-contexts.txt` names 13 contexts that block
merge, and not one of them is proven able to go red. The deliverable is a negative
control per context -- a fixture carrying the exact violation that context exists to
catch -- plus a CI job that fails when a required context has none, so the coverage
cannot silently decay as contexts are added.

Scoped narrower than "test the gates" on purpose: it does not re-test what each gate
checks, since the gates' own suites do that. It asserts one property per context --
this gate is capable of failing.

The argument is that the class has now fired at least four times here, each found by
hand and none by CI:

  #334  semgrep, required and blocking, scans a two-directory allow-list
  #327  six .gitignore rules are the sole control over maintainer-internal docs, and
        nothing asserts they still match anything
  #321  the forbidden-content gate exited 0 on a real site code and partner product
  #325  the same gate's home-path detector misses 1 of 4 spellings of a Windows path

Each is correctly filed as its own defect. None of them establishes the property that
would have caught all four before they shipped, and that property is a different
artifact from any of the individual fixes.

Value 7 / Difficulty 3, quick win, P1 -- not demand-gated; the trigger fired four
times. Ranked table and all four distribution lines recomputed to 93 open items; a
mechanical check confirms 93 banners and 93 rows agree on every triple.

Number allocated atomically via scripts/coord/alloc.ps1 (#1000 -- the first in the
post-partition public sequence, clamped to >= PUBLIC_BACKLOG_FLOOR), never grepped.

backlog_status_check.py --min-items 277: OK, 278 items across both files. The floor is
a floor, so growth passes it; it is there to catch shrinkage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant