Skip to content

feat(release): a publish path for the web console, and a guard for the key inventory - #44

Merged
wshallwshall merged 1 commit into
mainfrom
claude/asvs-partials-pass-scores-2de207
Jul 29, 2026
Merged

feat(release): a publish path for the web console, and a guard for the key inventory#44
wshallwshall merged 1 commit into
mainfrom
claude/asvs-partials-pass-scores-2de207

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

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.yml had zero mentions of the web console: jobs for the engine and the harness, none for
messagefoundry-webconsole. So CI could never claim the name — 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, in 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.

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: build and version-check run on every
console tag so the path is exercised before it is armed.

To actually claim the name: set repo variable PUBLISH_WEBCONSOLE=true, then push a
webconsole-v0.2.15 tag. 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_step isolated "the release job" by splitting on the literal
    "\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.
  • test_both_wheel_smokes_compare_versions asserted exactly 2 PEP 440 comparisons — a number that
    needs 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 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 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_SECRETS in the rotation inventory. It also
caught two of my own exclusion reasons being too thin to be reasons.

Verification

  • 9440 passed. The single failure (test_installed_metadata_matches_dunder_version) is a local-venv
    artifact — this worktree has no .venv, so the run used a stale 0.3.0 install against 0.3.2
    source. It passes on a fresh CI install.
  • Three mutations red on the usage-scope guard: delete the audit-chain clause, delete the store-DEK
    clause, add an unclassified row.
  • Unlike the threat-model guard, ASVS-L2-PHASE0-CHANGES.md is tracked, so this guard runs where
    CI runs.

🤖 Generated with Claude Code

…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
wshallwshall enabled auto-merge July 29, 2026 16:11
@wshallwshall
wshallwshall disabled auto-merge July 29, 2026 16:13
@wshallwshall
wshallwshall enabled auto-merge (squash) July 29, 2026 16:23
@wshallwshall
wshallwshall merged commit aa837f8 into main Jul 29, 2026
33 checks passed
@wshallwshall
wshallwshall deleted the claude/asvs-partials-pass-scores-2de207 branch July 29, 2026 16:29
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.
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