feat(release): a publish path for the web console, and a guard for the key inventory - #44
Merged
Merged
Conversation
…y inventory Two ASVS threads, both about a claim nothing was checking. ## ASVS 15.2.4 — the console has no publish path, so its PyPI name stays free release.yml had zero mentions of the web console: jobs for the engine and the harness, none for messagefoundry-webconsole. So the name could never be claimed by CI, and PyPI confirms it is UNCLAIMED today while messagefoundry and messagefoundry-harness are both taken. A pending Trusted Publisher is now registered against this workflow, which grants permission to publish but -- PyPI's own words -- "does not reserve that name". release-webconsole is a close sibling of release-harness with one deliberate difference: it is NOT lockstep. The console is separately versioned (0.2.15 vs the engine's 0.3.2, its own __version__ root, changelog and cadence per docs/WEBCONSOLE-PACKAGE.md), so it fires on its own `webconsole-v*` tag and carries no `needs: release`. An engine release must not drag it along and a console release must not wait on one. It lives in release.yml rather than its own file because the Trusted Publisher is registered against `release.yml`; a separate workflow would not match it. The engine job is now guarded with `!startsWith(github.ref_name, 'webconsole-')`. Without that a console tag would have run the engine release AT THE CONSOLE'S VERSION -- and neither job's version check could catch it, since each compares its own wheel against the same tag. Publishing stays gated on PUBLISH_WEBCONSOLE, like the harness: the build and version-check run on every console tag so the path is exercised before it is armed. Flipping the variable is what actually creates the project and claims the name. ## Two existing release guards were brittle, and this proved it Adding the job reds two tests that had nothing to do with it. Both were pinning an incidental shape rather than the property, so both are de-hardcoded rather than renumbered: - test_release_pypi_publish_is_last_step: isolated "the release job" by splitting on the literal string "\n release-harness:". Inserting a job between the two silently widened that slice, so the assertion measured the wrong job's last step. The boundary is now derived from the next top-level job. - test_both_wheel_smokes_compare_versions: asserted exactly 2 PEP 440 comparisons. That number needs bumping every time a wheel job is added, which is how it gets bumped without thought. It is now derived from the count of jobs that actually build a wheel, so a NEW wheel job carrying a string compare fails there instead of lowering the bar. ## ASVS 11.1.2 — the usage-scope clauses were scored Pass and nothing pinned them `grep -rn 'Usage scope' tests/` returned nothing. The cell was scored Pass on the **Usage scope:** clauses in ASVS-L2-PHASE0-CHANGES.md section 4, and a new key-material row with no clause -- or a deleted clause -- would have shipped green. Writing the guard found a live gap: the Audit chain row keys its HMAC on an HKDF-derived subkey of the store DEK (and a named Transit audit key under vault_transit) and documented no usage scope, despite being exactly 11.1.2's case -- a key derived from a confidentiality key but used for a different property, where the separation is the thing worth recording. Clause added. Classification is explicit rather than a heuristic. A "does this look like key material" regex mis-sorted 2 of 20 rows on first contact, and a guard with a non-zero false-positive rate gets suppressed. So every row is classified and an UNCLASSIFIED row fails -- a new row cannot be added without someone deciding which side it is on. Same shape as CRITICAL_SECRETS in the rotation inventory. It also caught two of my own exclusion reasons being too thin to be reasons. Three mutations red: delete the audit-chain clause, delete the store-DEK clause, add an unclassified row. Unlike the threat-model guard this document is TRACKED, so this one runs where CI runs.
wshallwshall
enabled auto-merge
July 29, 2026 16:11
wshallwshall
disabled auto-merge
July 29, 2026 16:13
wshallwshall
enabled auto-merge (squash)
July 29, 2026 16:23
wshallwshall
added a commit
that referenced
this pull request
Jul 29, 2026
PR #44 was SQUASH-merged, so its content reached main as a new sha (aa837f8) while c27d4c4 stayed unmerged on this branch. git therefore saw the same change twice and PR #45 came up CONFLICTING. Resolution: both conflicting files on main are BYTE-IDENTICAL to this branch's pre-fix commit (verified by comparison, not assumed), so this branch is a clean superset and taking its side loses nothing from main. Worth recording because it caught me twice today: `git merge-base --is-ancestor` reports NOT-MERGED after a squash, because the sha differs. Content comparison is authoritative; ancestry is not.
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.
Follow-up to #39, which auto-merged while this commit was still local. Two ASVS threads, both about a
claim nothing was checking.
ASVS 15.2.4 — the console had no publish path, so its PyPI name stays free
release.ymlhad zero mentions of the web console: jobs for the engine and the harness, none formessagefoundry-webconsole. So CI could never claim the name — and PyPI confirms it is unclaimedtoday, while
messagefoundryandmessagefoundry-harnessare both taken. A pending TrustedPublisher is now registered against this workflow, which grants permission to publish but, in PyPI's
own words, "does not reserve that name".
release-webconsoleis a close sibling ofrelease-harnesswith one deliberate difference: it isnot lockstep. The console is separately versioned (0.2.15 vs the engine's 0.3.2 — its own
__version__root, changelog and cadence perdocs/WEBCONSOLE-PACKAGE.md), so it fires on its ownwebconsole-v*tag and carries noneeds: release.It lives in
release.ymlrather than its own file because the Trusted Publisher is registered againstrelease.yml; a separate workflow would not match it.The engine job is now guarded with
!startsWith(github.ref_name, 'webconsole-'). Without that, aconsole tag would have run the engine release at the console's version — and neither job's version
check could catch it, since each compares its own wheel against the same tag.
Publishing stays gated on
PUBLISH_WEBCONSOLE, like the harness: build and version-check run on everyconsole tag so the path is exercised before it is armed.
To actually claim the name: set repo variable
PUBLISH_WEBCONSOLE=true, then push awebconsole-v0.2.15tag. Until then the name is claimable by anyone.Two existing release guards were brittle, and adding the job proved it
Both pinned an incidental shape rather than the property, so both are de-hardcoded rather than
renumbered:
test_release_pypi_publish_is_last_stepisolated "the release job" by splitting on the literal"\n release-harness:". Inserting a job between the two silently widened that slice, so theassertion measured the wrong job's last step. The boundary is now derived.
test_both_wheel_smokes_compare_versionsasserted exactly 2 PEP 440 comparisons — a number thatneeds bumping every time a wheel job is added, which is how it gets bumped without thought. Now
derived from the count of jobs that build a wheel, so a new wheel job carrying a string compare
fails there instead of lowering the bar.
ASVS 11.1.2 — the usage-scope clauses were scored Pass and nothing pinned them
grep -rn 'Usage scope' tests/returned nothing. The cell is scored Pass on the**Usage scope:**clauses in
ASVS-L2-PHASE0-CHANGES.md§4, and a new key-material row with no clause — or a deletedclause — would have shipped green.
Writing the guard found a live gap: the Audit chain row keys its HMAC on an HKDF-derived subkey of
the store DEK (and a named Transit audit key under
vault_transit) and documented no usage scope,despite being exactly 11.1.2's case — a key derived from a confidentiality key but used for a different
property, where the separation is the thing worth recording. Clause added.
Classification is explicit rather than heuristic: a "does this look like key material" regex mis-sorted
2 of 20 rows on first contact, and a guard with a non-zero false-positive rate gets suppressed. Every
row is classified and an unclassified row fails, so a new row cannot be added without someone
deciding which side it is on. Same shape as
CRITICAL_SECRETSin the rotation inventory. It alsocaught two of my own exclusion reasons being too thin to be reasons.
Verification
test_installed_metadata_matches_dunder_version) is a local-venvartifact — this worktree has no
.venv, so the run used a stale0.3.0install against0.3.2source. It passes on a fresh CI install.
clause, add an unclassified row.
ASVS-L2-PHASE0-CHANGES.mdis tracked, so this guard runs whereCI runs.
🤖 Generated with Claude Code