Skip to content

b1 lane (PARTIAL, not a supersession of #432): interpreter proof, ASVS payload types, auth+store, doc-lane gates - #487

Draft
wshallwshall wants to merge 45 commits into
mainfrom
lander/432-resolved
Draft

b1 lane (PARTIAL, not a supersession of #432): interpreter proof, ASVS payload types, auth+store, doc-lane gates#487
wshallwshall wants to merge 45 commits into
mainfrom
lander/432-resolved

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

DRAFT ON PURPOSE. DO NOT UNDRAFT WITHOUT READING THE TWO BLOCKERS BELOW. This is opened to get
CI data, not to land.

This branch is the conflict-resolved rebuild of PR #432, whose own head is DIRTY and conflicts on
two files. No PR has ever pointed at this branch -- it sat on origin and was invisible to
gh pr list, which is how it went unreviewed.

41 commits, 26 behind main. Re-derived against 90d6f180 at open time: still merges clean.


BLOCKER 1 -- merging this AS-IS would silently regress main

main has already evolved past this branch on the guard comment in scripts/asvs/apply.py, and
in the right direction. main's version was reworded to drop a vault-derived cell count; this
branch carries the older draft that still states the figure.

origin/main            count-bearing line:  0 matches
computed merge tree    count-bearing line:  1 match     <- the branch's version wins

git merge-tree reports NO CONFLICT, because the two sides edited different line ranges of the
same comment. A clean merge is not a safe merge here. Before this leaves draft, that block must
be resolved to main's version -- a semantic resolution, not a textual one. That figure is
vault-derived and a coverage count over a closed public requirement set discloses the uncovered set
by subtraction, so it must not return to a file that ships to PyPI.

This is the same hazard recorded against the original #432 -- "do not resolve textually, that picks
a winner by accident"
-- arriving on the resolved branch rather than the original.

BLOCKER 2 -- a tooling-partition failure that was SIMULATED, never executed

tests/test_bash_support.py is expected to need adding to tests/tooling_manifest.txt, or three
required test (...) contexts go red. That conclusion was reached by simulating the check in
Python, not by running it
, and main changed tests/test_tooling_partition.py after this
branch's merge base -- so a merge takes main's newer rule file while the simulation used the
branch's older copy.

This draft exists to settle that in one CI cycle rather than by more reading. Read the real
result, not the simulation.

What is in it

#1216/#1272 prove the interpreter actually in use and make the promoted bash resolver a single
source; #1242 two ASVS writer fixes (compare the type the payload STATED, and carry non-scalar
values); #1245/#1233/#1256 bootstrap-credential expiry, preset purge on delete, and one
verified subject binding one account; #1020/#1022 PHI security-notice refusal and last-factor
protection; #1262 the two doc-subject gates the docs lane was missing; ADR 0164.

Also outstanding

No test suite has been run against this branch by any seat. That is stated rather than implied,
and it is the second reason this is a draft.

Per ADR 0165, any ledger disposition for the numbered items above must be authored by a Dispatcher
or Lander and carried on this branch -- not by the builder whose work this is.

…CKLOG #1245)

Bootstrap auto-retirement reads must_change_password as "never claimed". That
holds only while the flag has one writer; admin_reset_password re-raises it, so
a reset makes a claimed account look unclaimed and the next trigger disables it.

"Never claimed" is monotonic. must_change_password is not -- rotation clears it,
a reset re-sets it -- and no non-monotonic bit can encode a monotonic predicate
across a re-set. So record the fact instead of inferring it.

password_claim_set() states the single-writer rule once for all three backends:
a set_password carrying must_change_password False is a credential the holder
chose, and only that call records the claim. A caller passing True gets an EMPTY
term, so the column is absent from the SET list and can be neither stamped nor
cleared. COALESCE makes the claim write-once.

The read is a hard subscript, deliberately unlike its .get() neighbours: a
missing key would decode as None, None means "never claimed", and that would
retire a claimed account -- this defect, re-introduced by its own fix.

Backfill sits INSIDE each column-creation guard. Hoisted out it becomes a
permanent second writer, which is exactly what #1245 documents. Three guards,
three mechanisms: PRAGMA check (SQLite), users_cols plus a _MIGRATION_REV bump
(Postgres -- without it an open DB takes the hash fast path, never gains the
column, and every user read then raises), COL_LENGTH with the backfill deferred
through EXEC (SQL Server -- a statement naming a column added earlier in the
same batch fails to compile).

Verified: ruff format --check 265 files, ruff check messagefoundry, mypy strict
265 files, and 152 passed across test_auth_service/test_store/test_last_admin_guard/
test_api_auth. Those four paths were NAMED, which overrides testpaths and does
not run the console suite. Full suite not yet run.
…lag (BACKLOG #1245)

Both WP-3 gates open-coded the same claimed-ness test against
must_change_password. Route both through one _unclaimed_bootstrap() predicate
that reads the recorded password_claimed_at instead.

Two copies of one lifecycle test is how bootstrap_expiry_warning silently
inherited this defect: a fix scoped to _retire_superseded_bootstrap alone would
have greened every test while still telling an operator that a claimed, in-use
admin account expires in N hours.

admin_reset_password is untouched and byte-identical. That is a constraint, not
an omission -- suppressing the flag on the reset path would mean an admin reset
no longer forces rotation, breaking the ASVS 6.4.6 property that path exists to
provide. The defect's own wording most naturally suggests exactly that fix, and
it is the wrong one.

The docstring at :579-581 asserted the property this defect defeats ("this can't
lock out a legitimate single-admin deployment") and is corrected here rather
than in a follow-up: it is the reason nobody looked.

Verified: ruff format --check 265 files, ruff check messagefoundry, mypy strict
265 files, 152 passed across four NAMED test paths (which overrides testpaths
and does not run the console suite). Full suite not yet run.
…ACKLOG #1245)

SECURITY.md:58-59 and :1164 restated the same false safety property as the code
docstring, in the document operators actually read. The item scoped the fix to
the docstring; that fixes the weakest instance and leaves the strongest.

Both now say the claim is RECORDED (users.password_claimed_at, stamped by
self-service rotation and never cleared) rather than inferred from current
credential state, and that an admin reset does not un-claim.

OUT OF SCOPE AND DELIBERATELY UNTOUCHED: :1828-1829, the HIPAA 164.312
emergency-access nomination. It does not restate the false property -- it
nominates this account as a required compliance control, which is a product and
compliance judgement with two defensible answers, not a factual fix. Routed to
the owner via the Liaison seat. Correcting :58-59 makes :1828 read differently
without making it right.

Zero deployments (CLAUDE.md section 0), so this describes what a first
deployment would hit, not a live exposure.
… it (BACKLOG #1245)

The durable output of #1245 is not the column, it is the invariant: a monotonic
lifecycle fact must be RECORDED with exactly one structurally-constrained
writer, never INFERRED from mutable credential state.

The argument for an ADR rather than a comment is failure history. The property
was already asserted in prose in two places -- the docstring and SECURITY.md --
and prose is exactly what failed. Bootstrap retirement (WP-3) had no ADR; it
lived only in SECURITY.md, which is how the guarantee drifted from the code with
nothing reporting it.

Records both rejected alternatives with the measurement that killed them,
because both are re-proposable and both fail non-obviously:

- last_login_at is genuinely unforgeable by the reset, but is written AFTER the
  credential check, so a bootstrap that logs in once with the printed one-time
  password and never rotates becomes permanently non-retirable -- silently
  deleting the ASVS 6.4.5 expiry arm for exactly the case it exists to cover.
  Unrepairable within its own inputs: a reset re-arms BOTH of them.
- password_changed_at != created_at fails because the reset refreshes it.

Also records what this does NOT fix: the stacked username-as-identity proxy, so
any local account named "admin" remains subject to retirement and
delete-and-recreate still mints a second silently-dead account. Narrowed, not
removed. Filed separately as content; no number allocated by me for it.

Number allocated via scripts/coord/alloc.ps1 to this worktree; index row in this
same commit, as the ledger gate requires.
…shortcut (BACKLOG #1245)

Four new tests plus a shared _claim_bootstrap() helper, covering all three
retirement triggers after an admin reset of a CLAIMED bootstrap -- the victim's
own next login, an engine restart, and create_local_user -- plus the warn path,
which is the second reader of the same lifecycle test and would otherwise stay
blind to a fix scoped to the retirement gate alone.

The load-bearing rule: every one of them claims the bootstrap by LOGGING IN AND
ROTATING through the service, never by a direct store.set_password. The existing
tests faked the claim with a direct store write, and that shortcut is exactly why
a second writer of must_change_password was invisible to this suite.

test_unclaimed_bootstrap_is_still_retired_after_an_admin_reset is the negative
control and is deliberately NOT an xfail: it reds if anyone "fixes" this by
special-casing the reset path, which is the wrong fix the item's own wording
suggests.

NO strict-xfail marker anywhere. The item specified one because it assumed the
test would land before the fix. Both are in this branch, so the tests PASS, and
a strict xfail on a passing test XPASSes and reds the suite. Recording the
deviation rather than carrying a marker that would be wrong on arrival.

Verified: 34 passed in tests/test_auth_service.py, and 152 passed across four
NAMED paths (test_auth_service, test_store, test_last_admin_guard, test_api_auth)
-- naming paths overrides testpaths, so the console suite did NOT run. mypy
strict 265 files clean, ruff clean.

NOT YET RUN: the full suite, the controlled revert for attribution, and the
adversarial review of this diff. Do not price #1245 as fully verified.
…gine (BACKLOG #1245)

Owner ruling, 2026-08-13, routed via the Liaison seat: "I don't think interface
engines need break the glass. That is for EHRs."

The HIPAA 164.312 row nominated the bootstrap admin as the emergency-access
control. That nomination is what made #1245 sharp -- a sealed break-glass
credential is precisely the one an administrator later resets, and the documented
remedy applied to the documented break-glass account would have destroyed it on a
first deployment. A compensating control resting on a false premise.

The owner rejected the premise both options shared rather than choosing between
them: no mechanism is nominated, because this component does not need one. Break-
glass exists so a clinician can reach a patient record when normal authorisation
would refuse. This engine holds no point-of-care record; the record of authority
lives in the systems on either side, and that is where the path belongs.

Marked not-applicable WITH the reason rather than dropped, so the row still
answers the requirement it sits under.

SCOPE, and deliberately narrow: this is a ruling about break-glass in an
interface engine and about THIS row. It is NOT a precedent for any other HIPAA
row or compliance nomination. The same shape found elsewhere is a new item.

One question this removes rather than answers: whether any alternative break-
glass mechanism ships in the code. Nobody measured it and nobody now needs to.

:58-59 and :1164 are unchanged by this commit -- they were the separate factual
fix and landed in 711a9b5.
…irectory admin (BACKLOG #1245)

Blocker found by the adversarial review of my own diff, and it is the
change-that-reads-as-hardening-but-deletes-a-control shape.

The retired must_change_password test excluded a directory-provisioned account BY
ACCIDENT: such a row has no password, so it carries the flag False, and the old
predicate returned early on exactly that. password_claimed_at reproduces no such
side effect -- a federated row has no stamp either, which reads as "never
claimed". So replacing the flag without this guard would newly auto-disable a
real AD administrator named "admin", revoke its sessions, and audit it as
auth.bootstrap_admin_retired -- an account the directory still considers valid,
disabled by the engine with no directory-side signal.

That is the same lockout class WP-3's own fix exists to prevent, re-opened on a
different row shape by the fix itself.

Reachable because the bootstrap is minted only when the store is EMPTY: a
directory-first install or a DR restore leaves the username free.

The test pins it through the public path -- start the engine against a non-empty
store (seeds roles, mints no bootstrap, fires the :518 retirement trigger), then
add a second administrator for the supersession arm. It carries a positive
control asserting the row really is in the shape that used to be protected by
accident (provider ad, no password_hash, flag False, no stamp), so a green cannot
mean the fixture built the wrong row. It also asserts the advisory warner stays
silent, because that path asks the same question.

RED-FIRST, verified rather than assumed: with the guard, 35 passed. Removing ONLY
the guard (needle confirmed absent, 85 bytes) reds this test alone. Restored
byte-identical, 35 passed again.

ruff and mypy strict clean. Three blockers from that review remain open and are
recorded with fix directions in the shared episode note; this branch is still NOT
landable.
… (BACKLOG #1245)

Blocker from the adversarial review. The one-time backfill is the arm that can
disable the only administrator of an UPGRADED database, and it was executed by no
test on any backend: every test opens ":memory:", which creates users WITH the
column from _SCHEMA, so the guarded migration branch is never entered. A grep for
password_claimed_at over tests/ returned one hit, a docstring. Deleting the
backfill outright reddened nothing.

This drives the real path: claim the bootstrap through the service, manufacture a
pre-#1245 database, reopen, and assert the claim came back.

Three things the test does deliberately:

- REBUILDS the table rather than ALTER ... DROP COLUMN. SQLite re-parses the
  stored CREATE TABLE text after a drop, and the trailing comment this change puts
  on the final column makes that reconstruction fail with "incomplete input".
  Measured, not anticipated.
- DERIVES the retained column list from the live table, so it does not hard-code a
  schema that will drift.
- Asserts the legacy shape was really manufactured before relying on it. Without
  that, a green is equally consistent with the column never having been dropped.

It also asserts the backfill restored the ORIGINAL claim instant rather than any
non-NULL value: the stamp is evidence about WHEN the holder took the account, so a
backfill writing "now" would satisfy a not-None assertion while destroying the
fact.

SCOPE, stated in the test rather than implied: it pins that the stamp is RESTORED.
The consequence of a restored stamp is pinned by the sibling tests, and is not
re-tested here because the rebuilt legacy table has no primary key and cannot
carry the user_roles foreign key a second administrator needs.

MUTATION-VERIFIED both directions: 36 passed with the backfill; deleting the
UPDATE (191 bytes, needle confirmed absent) reds this test ALONE; restored
byte-identical, 36 passed again.

Two blockers remain open (the ADR's false "structural" claim, and the unbounded
reset temp for a claimed bootstrap). This branch is still NOT landable.
…e claim (BACKLOG #1245)

Blocker from the adversarial review, and the sharpest one: ADR 0164 asserted the
single-writer property was "structural, not conventional" and that "you cannot
record a claim without being the authenticated holder". Both false, both measured.

set_password declared the CLAIMING value as its default, so a caller that merely
OMITTED the keyword recorded a claim -- demonstrated against a live store. And
scripts/security/dast_target.py already passes False outside self-service
rotation. Asserting a structural guarantee in the same paragraph that says a
conventional one is not good enough is SDS-3.7 inside the document written to
prevent SDS-3.7.

Fixed both halves rather than picking one, because either alone leaves a false
sentence standing:

- STRUCTURAL, for the accidental path: set_password's default flips to True
  across the protocol and all three backends, so omission is now the SAFE branch
  and a forgotten keyword can no longer stamp a claim. Verified first that all
  four callers pass the argument explicitly, so this changes NO current behaviour
  -- it bounds the next caller. create_user keeps False deliberately: it never
  writes this column and AD provisioning depends on it.
- HONEST, for the deliberate path: the ADR now states that a caller can still pass
  False, that the real invariant is "every caller passing False has just
  authenticated the holder", and that what enforces it is review rather than the
  type system. The retraction is KEPT rather than deleted, because the deleted
  version is what a later reader would re-derive.

Also corrects password_claim_set's docstring from a closed enumeration ("and only
that call") to an "at least" floor, per SDS-3.6: the argon2 rehash-on-login path
is a second caller passing False. It is safe, and the docstring now says WHY
rather than denying it exists -- reaching it needs a verified password, and on any
row where the flag is False with a password set the stamp is already present.

mypy strict clean. A broad auth/store/migration slice was still running at commit
time and its result is NOT included here; the default flip is expected to be
behaviour-neutral by the caller audit above, and that expectation is not yet
confirmed by the slice.

B3 remains open: a claimed bootstrap's admin-reset temp has no expiry path. NOT
landable.
…G #1245)

Last of the four blockers from the adversarial review, and the subtlest: this
change CREATED the exposure by removing an accidental bound.

The ASVS 6.4.1 initial-password expiry carves the bootstrap out entirely, on the
stated premise that WP-3 gives that account its own deadline. That premise holds
only while the account is UNCLAIMED. Once claimed, WP-3 deliberately stops
covering it -- which is the whole point of the claimed-ness fix -- so after it,
nothing bounded an admin-issued temp on the highest-privilege account name in the
system.

THE GAP WAS MASKED BY THE DEFECT ITSELF. Pre-#1245 the reset re-armed retirement
and the account got disabled, so nobody noticed the temp had no deadline. Fixing
#1245 removed that accidental bound, which is why the bound has to be put back
deliberately rather than treated as a pre-existing gap. settings.py states the
harm in the repo's own words: such a session's one permitted action is to set the
password, i.e. account takeover.

The carve-out is NARROWED, not deleted: an unclaimed bootstrap keeps it, because
WP-3 really does own that account's lifecycle and a second deadline would be
redundant. A claimed bootstrap is gated exactly like any other account.

The comment that asserted the removed control is corrected in the same change --
it is the sentence that would otherwise keep the false premise alive.

MUTATION-VERIFIED, AND THE RESULT IS ASYMMETRIC, which is what distinguishes a
correction from a widening: 38 passed with the narrowing; restoring the OLD wide
carve-out reds the claimed-bootstrap test while the unclaimed-carve-out test STILL
PASSES (1 failed, 1 passed). A control that failed on both would not have told me
which layer does the work. Restored byte-identical, 38 passed again.

Both tests carry positive controls: the claimed case asserts the temp WORKS inside
the window before asserting it fails outside, so the refusal cannot be an unusable
credential; the unclaimed case asserts the account was never claimed before
relying on the carve-out.

ruff and mypy strict clean. ALL FOUR review blockers are now fixed.
… bound a credential (BACKLOG #1245)

BLOCKER found by an adversarial review OF MY OWN BLOCKER FIX, and it is the same
defect class I was fixing, one layer down.

a6ea82b narrowed the ASVS 6.4.1 carve-out so a CLAIMED bootstrap is gated, and
KEPT it for an unclaimed one on a premise I wrote in that commit: "WP-3 retires an
unclaimed bootstrap on its own timer, so a second deadline here would be
redundant."

THAT PREMISE IS FALSE, and my own test proved it while asserting the opposite.
service.py gates the WP-3 time arm on `expiry_hours > 0`, and
`bootstrap_expiry_hours = 0` is a DOCUMENTED supported value. Measured before this
fix, at bootstrap_expiry_hours=0 with a 72-hour 6.4.1 policy: the printed
first-run administrator credential still logged in at 73 hours, at 8760 hours, and
at 87600 hours -- ten years. A second reachable misconfiguration, expiry 8760
against a 72-hour policy, bounded it at 100x the stated policy. Neither is
reported by security_loosenings(), so both were silent.

WORSE, AND THE PART I WANT ON THE RECORD: the test I added in that same commit,
test_an_unclaimed_bootstrap_temp_keeps_its_carve_out, set bootstrap_expiry_hours=0
IN ITS OWN FIXTURE -- disabling the very deadline its comment invoked as the
reason the carve-out was safe -- and its stated purpose was to red if anyone
removed the carve-out. That is a test locking a hole in as intended behaviour,
which is the worst shape available, written into the commit whose purpose was
closing a carve-out.

THE FIX: remove the carve-out entirely. It conflated TWO CONTROLS -- WP-3 retires
an ACCOUNT, 6.4.1 expires a CREDENTIAL, different triggers and different outcomes,
and one is not a substitute for the other. Dropping it is near-behaviour-neutral
at the stock 72/72 defaults, because a freshly minted bootstrap has created_at and
password_changed_at within milliseconds, so 6.4.1 comes due at the same instant
WP-3 retirement already does -- while closing both misconfigurations.

RED-FIRST: the replacement test is parametrized over both reachable
misconfigurations (0 and 8760) and BOTH params red against the pre-fix code. After
the fix the original probe inverts at all three ages: 73h, 8760h, 87600h now
refuse.

ALSO REWRITTEN, not deleted: test_bootstrap_admin_is_not_gated_by_initial_password_
expiry, PRE-EXISTING on origin/main since 2026-07-30, asserted a 500-hour-old
credential against a 1-HOUR policy as correct. It pinned the hole. Inverted with
the retraction recorded in place, because the old assertion is what a later reader
would re-derive.

AND THE TWO OPERATOR-FACING SITES the previous commit missed, both still asserting
the blanket exemption: settings.py's initial_password_expiry_hours docstring and
docs/CONFIGURATION.md. My earlier commit message claimed "the comment" singular;
there were three, and the one I corrected was the inline one an operator never
reads.

Verified: ruff, ruff format, mypy strict 265 files, 39 passed in
tests/test_auth_service.py, and 1010 passed / 333 skipped / 0 failed across an
auth/store/bootstrap/password/login slice.
…ACKLOG #1245)

BLOCKER from an adversarial re-review of ef51a9d, and it is the same
account-versus-credential conflation that commit says it removed -- surviving in
the ONE field whose entire job is telling the operator when the credential dies.

BootstrapAdmin.expires_at was computed from bootstrap_expiry_hours alone, and
api/app.py writes it verbatim into bootstrap-admin.txt as
"expires: <ISO> - sign in and change this password before then". Once ef51a9d
stopped 6.4.1 carving the bootstrap out, that made the file LIE IN BOTH
DIRECTIONS. Measured at HEAD before this commit:

  bootstrap_expiry_hours=0     file stated NO deadline, credential died at 72h
  bootstrap_expiry_hours=8760  file stated +8760h, credential died at 72h (121x)

A file that OVERSTATES a deadline is worse than one that omits it: the operator
plans around a date that is not real. Compounding it, the ASVS 6.4.5 warn window
gated on bootstrap_expiry_hours <= 0 and so never fired at all for the deadline
that now actually ends the credential.

FIX: derive both the issued deadline and the warn window as the MINIMUM of the two
bounds -- WP-3 retiring the ACCOUNT at created_at + bootstrap_expiry_hours, and
ASVS 6.4.1 expiring the CREDENTIAL at password_changed_at +
initial_password_expiry_hours. Each arm contributes nothing when its own setting
is 0, so None survives as correct only when BOTH are off. Keying the warning off
the same minimum means the two can never disagree.

At stock 72/72 the surfaced value is UNCHANGED: a freshly minted bootstrap takes
created_at and password_changed_at from one clock read, so the minimum is that
shared instant. The reviewer measured them identical, delta exactly 0.0 in twelve
probe rows -- the previous comment called that "within milliseconds", which
understated a property the code guarantees exactly.

ALSO REWRITTEN, not deleted, both with the retraction recorded in place:
- test_bootstrap_admin_deadline_is_none_when_expiry_off, PRE-EXISTING on
  origin/main, set ONLY bootstrap_expiry_hours=0 and asserted no deadline while
  6.4.1 sat at its default 72. It pinned the lie. Now asserts None only when BOTH
  bounds are off, plus the WP-3-off case where a deadline exists and must show.
- A comment 45 lines above ef51a9d's own replacement test still described the
  carve-out premise as merely NARROW rather than false. That commit ran a hunt for
  surviving statements of the premise and missed one in the file it was rewriting.

New test pins all four combinations and asserts the stated instant is never later
than the credential actually survives.

Verified: ruff, ruff format, mypy strict 265 files, 48 passed across
test_auth_service + test_search_presets_api, and a four-configuration probe
showing stated == earliest-real in every case.
…sets (BACKLOG #1233)

delete_user removed user_roles, sessions, webauthn_credentials and the user row,
but never search_presets. There is no FK cascade on that table, so the rows
outlive the account -- carrying PHI-shaped `criteria` (ADR 0136) that no owner can
reach, list or purge, and that nothing counts.

Fixed in ALL THREE backends with the identical keyed DELETE. The item understated
its own scope by two thirds: the four-DELETE body is verbatim in store.py,
postgres.py and sqlserver.py, and the two server legs are CI-only, so a divergence
between them surfaces first in CI rather than locally. Parity asserted by
measurement -- one occurrence in each of the three, against a positive control
(the webauthn DELETE) reading three in each.

`owner` already holds Identity.user_id rather than the username (#1225, landed),
which is what makes this a single keyed DELETE instead of a name lookup -- and a
name lookup would have keyed a deletion on the reassignable value #1225 exists to
stop trusting.

THE ITEM NAMES THE WRONG BLOCKING TEST. It cites tests/test_retention.py, which
mentions delete_user ZERO times (positive control: test_search_presets_api.py
mentions it 3 times). The test that actually reds is
test_a_recreated_username_does_not_inherit_the_departed_operators_presets.

THAT TEST NEEDED CARE, NOT JUST A FLIPPED ASSERTION. Its precondition asserted
"delete_user does not purge preset rows" -- the defect -- as the ENABLING HALF of
#1225's scenario. After this fix its substantive assertions still pass, but
TRIVIALLY: with the row purged there is nothing left to inherit, so it would have
stopped being evidence about the KEY at all. #1233 would have silently retired
#1225's only end-to-end coverage while looking green.

So the test now asserts the purge (replacing the retracted precondition, recorded
in place), then RESTORES the captured row and continues. That is not artificial:
it is exactly the state of a database upgraded from before #1233, where orphan
rows already exist and only the key protects them. Both controls stay pinned --
the row is gone, AND if it were not, the key would not match.

MUTATION-VERIFIED: removing the SQLite purge (96 bytes) reds that test alone;
restored byte-identical, 4 passed. The Postgres and SQL Server arms are UNVERIFIED
locally -- those legs skip without MEFOR_TEST_POSTGRES / MEFOR_TEST_SQLSERVER.

Verified: ruff, ruff format, mypy strict 265 files, 48 passed across
test_search_presets_api + test_auth_service.

NOT REVIEWED BY ANYONE BUT ME -- I wrote the fix and its test, which is the same
channel. Wants an outside adversarial read before it lands.
…e ADR's backfill claim (BACKLOG #1245)

Two residuals from the first adversarial review.

SERVER LEGS. Neither test_postgres_store.py nor test_sqlserver_store.py called
set_password at all -- confirmed by grep with a positive control (the same pattern
returns a hit in test_store_backend.py, and create_user returns 2 in each suite).
So the Postgres and SQL Server claim terms were executed by no test on any leg.
That matters most on SQL Server, where the term splices a positional argument into
the MIDDLE of a "?" parameter list and an off-by-one would silently write the
claim timestamp into failed_attempts or updated_at. The shared password_claim_set
helper covers the SQL TEXT; the argument binding is per-backend and hand-written.
Both suites now round-trip set_password and assert the neighbouring columns held
their values. THEY SKIP LOCALLY -- measured 300 skipped, 0 executed, without
MEFOR_TEST_POSTGRES / MEFOR_TEST_SQLSERVER -- so their correctness is UNVERIFIED
until a CI DB leg runs them.

ADR 0164 BACKFILL BOUNDARY. The ADR presented the migration backfill without
naming the case it cannot cover: its predicate is
`must_change_password = 0 AND password_hash IS NOT NULL`, so it reconstructs a
claim from the same mutable flag the ADR exists to stop trusting. A pre-upgrade
bootstrap that was claimed and THEN admin-reset has the flag back to 1, is
excluded, and reads unclaimed after upgrade -- pre-fix behaviour surviving the fix
on that row. Inherent: the claim left no durable trace before the column existed,
and password_changed_at is refreshed by every set_password, which is the same
measurement that killed rejected alternative 2. Affected population is developer
databases; a first deployment creates the column at mint.

TWO LAYERING SLIPS OF MINE, recorded rather than rewritten. Four agents wrote this
worktree concurrently and I staged whole files, so two commits carry work that
belonged elsewhere: f414884 (#1233) also carries the postgres.py _MIGRATION_REV
comment correction, and 5ed8ceb (#1245 deadline) also carries the COALESCE
write-once test. Both are the same feature family and both are tested, but neither
is one coherent layer. Amending would rewrite SHAs a review may already reference,
which is the worse trade.

Verified: 44 passed, 300 skipped across test_auth_service + the two DB suites;
ruff, ruff format and mypy strict 265 files clean at the previous commit and
unchanged by this one (docs and tests only).
…EMANTICALLY

ONE CONFLICT, docs/SECURITY.md, and it was an INVALIDATED CLAIM rather than a
competing edit. main carried PR #361 (the previous Builder 1's #1131 work), which
documents #1245's defect as shipped behaviour -- correct for the tree it was
written against:

  "both the expiry timer and the supersession check fire only while the bootstrap
   is still unclaimed, which is carried by the must_change_password flag -- so an
   administrative password reset, which re-sets that flag, RE-ARMS them."

This branch makes that sentence FALSE. A mechanical keep-both-sides resolution
would have shipped a security document asserting BOTH "a reset re-arms retirement"
AND "a reset does not un-claim it", about the same defect, in the document
operators read.

NEITHER SIDE WAS RIGHT AS A WHOLE, which is why this was resolved by intent:
  - TAKEN  (ours):   the recorded-claim mechanism and the reset-does-not-un-claim
                     consequence -- true of the tree after this branch.
  - DROPPED (theirs): "an administrative password reset ... re-arms them" -- this
                     branch falsifies it.
  - KEPT   (theirs): "auto-retirement is not the only way to lose an
                     administrator: the failed-attempt lockout is a separate
                     mechanism and it does reach a claimed sole administrator" --
                     #1131's contribution, orthogonal to #1245, still true. Taking
                     "ours" wholesale would have silently dropped a true sentence
                     another item paid for.

L30 CONTROL RUN, with the expectation stated rather than assumed. docs/BACKLOG.md
AUTO-merged (no conflict), and this branch touches it ZERO times across sixteen
commits -- positive control: docs/SECURITY.md appears twice. So the correct
expectation is not 0/0/0 but EQUALITY WITH MAIN, and parse_items gives
281 items / 206 open / 75 closed on the merged tree and 281/206/75 on
origin/main's own copy. Identical.

L30 SECOND CLAUSE: the merged paragraph was re-read end to end for what it now
CLAIMS, not merely for absent markers. It is coherent -- recorded claim, reset
does not un-claim, and the lockout named as a separate mechanism that does reach a
claimed sole administrator.
…edential (BACKLOG #1245)

The last three residuals from the adversarial reviews, all the same shape: sites
that are true about the ACCOUNT and incomplete about the CREDENTIAL, which is the
distinction #1245 turned on.

settings.py bootstrap_expiry_hours and docs/CONFIGURATION.md both said "0 = no
time expiry" without qualification. Since #1245 removed the bootstrap's carve-out
from the ASVS 6.4.1 gate that is only true of the account: at 0, or at any value
longer than initial_password_expiry_hours, the account survives while the printed
credential still dies on the other clock. Both now say which of the two they
bound, and name bootstrap-admin.txt as stating the earlier.

security_loosenings() -- WRITTEN DECISION, not a fix. Its own docstring says the
unreported set is "enumerated in the floor test's exemption set so the gap is a
written decision that a new switch cannot silently join".
initial_password_expiry_hours was unreported and #1245 made it LOAD-BEARING: it is
now the only bound on the first-run administrator credential whenever
bootstrap_expiry_hours is 0 or longer, so setting it to 0 unbounds that credential
and nothing in the registry says so.

Recorded rather than fixed, with the reason. It is not a [security] field, so the
completeness floor (which iterates SecuritySettings.model_fields) never covered it
and this is not a floor-test gap. Reporting it needs a new REQUIRED parameter --
every parameter there is required BY DESIGN, precisely so an optional detector
cannot be added quietly -- which is a larger change than the item that exposed it.
Handed to the dispatcher as content.

This is the registry's own standard applied to itself: the gap was already there,
#1245 changed what it costs, and the docstring demands that be written down rather
than implied.

Verified: ruff, ruff format, mypy strict 265 files, 187 passed across
test_security_posture_defaults + test_settings + test_auth_service (the floor test
gates this registry and still passes -- the change is docstring-only), and
link_check.py "every relative link resolves".
…(BACKLOG #1020)

Owner ruling 2026-08-13, option (b) -- gate startup on a DELIVERABLE channel --
recorded by the dispatcher at 30d5358.

The serve gate in __main__.py computes readiness as notify_security_events +
email_smtp_host + email_from: SMTP WIRING ALONE. It asks "is a transport
configured" and never "can the account that matters actually receive" -- the
instrument answering the adjacent question, SDS-3.8. On a first run the only
account that exists is the bootstrap administrator, created with no address, so
the gate reports a healthy channel while all ten notice types about the account
holding frozenset(Permission) silently no-op, including LOGIN_AFTER_FAILURES.

THE CHECK MUST LIVE IN THE LIFESPAN, and the item's own stated location was wrong.
It pointed at __main__.py:2259; _serve is SYNCHRONOUS and opens no store, so there
is no user table to ask. The ASGI lifespan is the only place the store and the
freshly minted bootstrap admin are both in hand. The owner's ruling corrected this
and it is why the fix is here.

DELIVERABILITY, NOT "REQUIRE AN EMAIL AT CREATION". update_user_profile issues
UPDATE users SET display_name=?, email=? unconditionally on every directory login,
so any address a human sets on an AD or OIDC account is overwritten at that
holder's next sign-in. A fix resting on an OPERATOR ACTION cannot cover that
population; an assertion about the state of the table can.

Deliberately narrow: it asks whether SOME enabled Administrator carries an
address, not whether mail would arrive. Proving delivery needs an SMTP round trip
at startup, which is a different and much larger change.

Eight tests, including a POSITIVE CONTROL (a reachable admin must still start --
without it, a predicate that refused unconditionally would pass the refusal test
and break every deployment), a disabled admin and an addressable non-admin (so the
count cannot be satisfied by the wrong row), warn-enforcement parity with the
transport gate, and each of the three preconditions asserted separately because
one combined case cannot show which arm did the work.

A TRAP THE FIXTURE HIT, WORTH THE COMMENT IT NOW CARRIES: set_user_roles on a bare
store raises FOREIGN KEY constraint failed, because the roles table is seeded by
AuthService.initialize() and not by MessageStore.open(). That failure HANGS rather
than reporting -- the exception escapes before store.close(), aiosqlite's
non-daemon worker thread stays alive, and the process never exits. pytest produced
ZERO output for five minutes with no traceback; it was only legible when driven
outside the runner. The fixture now builds through initialize() and closes the
store on every error path.

GAP, STATED RATHER THAN PAPERED OVER: these tests exercise the PREDICATE, not the
WIRING. Removing the lifespan call site would not red any of them, by design.
There is one call site and one definition (asserted), but nothing proves the
lifespan invokes it. Tests that build a managed app exist (test_api.py and
siblings), so an integration test asserting startup actually refuses is the next
step -- NOT done here.

Verified: ruff, ruff format, mypy strict 265 files, 8 passed in 1.30s.
…NGS (BACKLOG #1020)

The wiring test that belongs here was written, run, and REMOVED. It hangs, and
what it found matters more than the coverage it would have added.

MEASURED, WITH A CONTROL:
  sibling lifespan test that does NOT raise
    (test_security_posture_defaults::test_managed_app_stashes_auth_settings_for_the_registry)
                                                  -> 1 passed in 1.13s
  raising from the lifespan BODY, after `yield`   -> exits cleanly
  raising during lifespan STARTUP, before `yield` -> HANGS, pytest emits zero output

So an exception raised during lifespan startup does not unwind. The gate sits
after engine.start(), the upload-retention runner and the security notifier are
all running, and before the three asyncio.create_task handles the teardown
expects. The finally guards every handle with `is not None`, so this is not an
unbound name -- it is teardown of a PARTIALLY-STARTED lifespan not completing.
Pre-existing; the gate is merely the first thing to raise in that window.

WHAT THIS DOES NOT ESTABLISH, kept separate on purpose: what uvicorn does.
Production startup failure goes through uvicorn's own path, which is UNMEASURED.
The refusal may exit cleanly in `serve`.

CONSEQUENCE, STATED PLAINLY: #1020's refusal path is UNVERIFIED under the runner
that actually ships, and a hang there would be WORSE than the defect it fixes --
a silent notice at least leaves a running, legible server. **This is why the gate
should not be relied on until someone drives it under uvicorn.**

I nearly reported the opposite. The first run of the hanging test returned exit
code 0 and I almost read that as a pass -- it was my own wrapper's exit after a
Wait-Job timeout, printing "HUNG". A success exit code that is a fact about the
wrapper, not the subject.

The eight predicate tests are unaffected: 8 passed in 1.26s. ruff and mypy clean.
The gap they leave -- deleting the lifespan call site reds nothing -- is now
documented in the file rather than implied by its absence.
One conflict, docs/adr/README.md, and it is the APPEND/APPEND case: my ADR 0164
row against main's 0165. Both belong. Neither side is wrong and picking one would
silently delete a landed ADR's index row -- which the ledger gate would not catch,
because it checks that an ADDED heading owns its number, not that an existing row
survived a merge.

Resolved by keeping both, ours first so the table stays in number order, stripping
only the three marker lines. Asserted after: 0 markers, both rows present, 157
rows with ZERO duplicated ADR numbers -- the specific hazard of a keep-both
resolution is a row appearing twice, so it is checked rather than assumed. Both
referenced files exist, and link_check.py resolves every relative link.

docs/SECURITY.md auto-merged cleanly this time; the semantic resolution it needed
landed earlier at 93ccb3b.

L30 CONTROL, expectation stated rather than assumed: this branch touches
docs/BACKLOG.md zero times, so a merge that brings main's ledger forward should
land on MAIN'S counts, not 0/0/0. parse_items before 281/206/75, after
284/205/79 -- matching origin/main's own 284/205/79, measured independently.
…1256)

My own #1143 research finding, filed by the dispatcher at f12a1f5 and built here.

#1015's guard resolves the account by USERNAME and asks whether THIS ACCOUNT
carries a different subject. So it constrains WHICH subject may bind to a given
account, and is structurally incapable of seeing a SECOND ACCOUNT already bound to
the subject now presenting. Nothing else sees it either: measured, no UNIQUE
constraint names the federated columns on any backend -- 0/0/0, against 13/8/10
total UNIQUE declarations as the positive control.

Without this, one verified identity could own two accounts: bind as `jdoe`, have
the IdP later resolve you to a different on-prem object, and both rows carry your
subject while #1015 refuses neither, because each account's own binding is
self-consistent.

REFUSED, NOT RE-POINTED. Silently moving the binding would hand the subject the
newer account and strand the older one -- the account-takeover-without-credential-
compromise shape #1015 exists to prevent, arriving from the other side.

New store method `get_user_by_federated_subject` on the protocol and all three
backends, mirroring `get_user_by_username`. A lookup rather than a `list_users()`
scan because it sits on the login path. Both columns are compared, never `subject`
alone: a subject is unique only WITHIN its issuer, so a subject-only match would
refuse two unrelated people sharing an opaque identifier at different IdPs.

SCOPE, AND WHY THE SCHEMA HALF IS NOT HERE. The item's own caution is that SQL
Server types these NVARCHAR(MAX), which cannot be an index key, so a UNIQUE
constraint there needs a RE-TYPE and not merely a constraint. Measured: the file
has a house convention for exactly this (NVARCHAR(450) + COLLATE
Latin1_General_100_BIN2 + a runtime length check, used for reference_sets.[key],
a documented schema divergence) -- but it BOUNDS AT CREATION and there are ZERO
`ALTER COLUMN` statements anywhere in the file. Re-typing a live column with data
would be the first, on the backend whose suites skip locally.

So this closes the defect BEHAVIOURALLY on all three backends today, and the
structural constraint stays open with its cost measured rather than being
half-built. I am not pricing that migration; per the dispatcher's instruction it
goes back if it becomes an owner question, and the length choice for existing
data is one.

I am also NOT calling this structural. The check is a code-level refusal -- a
convention, enforced by review. Claiming otherwise is exactly the error a review
caught in ADR 0164 earlier tonight.

RED-FIRST, ASYMMETRIC: 24 passed with the guard; removing ONLY the guard (512
bytes, absence verified) reds exactly ONE test -- the new one -- while the other
23 stay green. A control that reddened everything could not show which layer does
the work. Restored byte-identical, 24 passed again.

The new test is deliberately distinguished from
`test_same_subject_changed_username_is_same_account`, which passes on the
DEFECTIVE code and is not evidence about it: there both usernames resolve through
AD to the SAME object, so only one account ever exists. This one resolves them to
genuinely different objects, with a positive control asserting that through the
same seam the service uses rather than the fake's internals.

Verified: ruff, ruff format, mypy strict 265 files, 24 passed in
tests/test_auth_oidc_service.py. The Postgres and SQL Server lookups are
UNEXERCISED locally -- those suites skip without their env vars -- so their
correctness waits on CI, which is the item's stated proof condition.
… exits (BACKLOG #1020)

Closes the question my own earlier note left open, and the answer BLOCKS the item
from landing as written.

That note said the harness hang did not establish what uvicorn does, and that
production startup failure goes through uvicorn's own path which was UNMEASURED.
Measured now, and it is both halves:

  REFUSES CORRECTLY AND LEGIBLY -- uvicorn prints the full RuntimeError including
  the operator-facing remedy, then "ERROR: Application startup failed. Exiting."
  and raises SystemExit(3). No socket is served. The gate does what #1020 asks.

  THE PROCESS THEN DOES NOT EXIT -- still alive 90 seconds after printing
  "Exiting.", and had to be killed. Timed directly on the process rather than
  through a wrapper.

THE DISTINCTION DECIDES SEVERITY, and it is not the one I expected. This is a HUNG
refusal, not a silent one. An operator watching a console sees exactly the right
error. A SUPERVISOR does not: NSSM, systemd or a container runtime sees a process
that started and never exited -- running and dead, which is precisely the state a
restart policy cannot detect and will not recover. Worse than the mis-report #1020
fixes, because a wrong readiness answer is at least visible to the thing watching.

So #1020 is NOT landable as written: the refusal must TERMINATE the process, not
merely decline to serve. The underlying teardown behaviour is pre-existing --
engine.start(), the upload-retention runner and the security notifier are all
running when the gate raises -- and is filed separately rather than fixed here.

METHOD NOTE, because it nearly cost the finding: my first probe reported "HUNG --
the refusal does NOT terminate under uvicorn either". That was my PowerShell
wrapper's Wait-Job timeout branch, not the subject; the captured output underneath
it showed uvicorn exiting via SystemExit(3). Third time tonight a wrapper's exit
path has manufactured a verdict about the thing it wrapped. The measurement that
settled it timed the PROCESS with WaitForExit and read the real exit code.
… (#1257)

The teardown was never incomplete -- it stops the engine, both notifiers, the
upload-retention runner and all three background tasks. It was UNREACHABLE. Its
try: opened at the yield, so it guarded the RUNNING phase and never the STARTUP
phase, and everything brought up between engine.start() and the yield was
abandoned in place on a startup failure.

What that costs is the process, not tidiness. engine.stop() ends in store.close(),
and aiosqlite's connection worker is a NON-DAEMON thread, so skipping it blocks
interpreter shutdown forever. Under uvicorn it reads as a HUNG REFUSAL: the correct
error prints, then "Application startup failed. Exiting.", then SystemExit(3) --
and the process stays alive. An operator watching a console sees the right message.
A supervisor does not: NSSM, systemd or a container runtime sees a service that
started and never exited, which a restart policy cannot detect or recover.

No teardown ORDER decision was needed and none was made. The existing order is
reused verbatim: hoist the five teardown handles above the new try, open try:
immediately before await engine.start(), re-indent the span, and delete the inner
try: that used to begin at the yield, so the EXISTING finally -- body byte-identical
-- now pairs with the new outer one. Control for the re-indent: git diff -w shows
23 lines of real change, being the comments plus five hoisted inits plus one try
against five relocated inits and one deleted try. A whitespace-blind diff is the
right instrument for a whitespace-only transform.

engine.stop() is safe on a partial start (every subsystem is is-not-None guarded,
and engine.py states the property outright), so wrapping engine.start() itself is
sound and fixes a start() failure too, which hung identically before.

The hoist is part of the fix, not tidiness. Deleting one hoisted init showed the
finally reaches those names BEFORE engine.stop(), so an unbound one raises
UnboundLocalError, aborts the teardown early, and the hang comes straight back
with the real startup error replaced.

Measured: at HEAD the regression test is killed by the 60s watchdog with the
process still alive; with the fix it passes in 1.8s. The assertion is deliberately
that THE PROCESS EXITS -- not that the error prints, not that SystemExit is raised.
Both of those were measured true while the process went on living, so neither
discriminates, and a test asserting either would have passed against the defect.
Every test in this file drove the predicate directly, so none of them would fail
if the lifespan's call site were deleted. That closure was written and removed
once because driving a startup failure through lifespan_context hung with zero
output. The cause was never this gate -- it was the lifespan not unwinding, fixed
as #1257 -- so the wiring assertion is now possible.

Proven to have teeth by mutation: with the lifespan's call to the gate removed,
the wiring test fails with DID NOT RAISE while all nine other tests still pass.
That is also the clearest statement of the gap it closes.

Adds a positive control alongside it: the identical app under enforcement=warn
starts and unwinds cleanly, so the refusal above cannot be confused with an app
that fails to start for an unrelated reason.

The long comment block recording the hang is replaced. It asserted "THIS BLOCKS
#1020 FROM LANDING AS WRITTEN", which the fix has made false.
…ers (#1259)

A conflicted ledger censused CLEANLY, and the counts AGREEING is the finding.
`>>>>>>> branch` starts with ">", so the banner-block scanner treated it as a
blockquote line and kept scanning instead of ending the block; items from BOTH
sides were then counted and the total looked plausible. Measured: the live ledger
and a marker-poisoned copy of it produce identical counts with no exception.

The refusal goes in the READER rather than in a pre-commit hook because of HOW it
bit -- a landing seat ran this on a merge-tree blob before checking the merge's
exit status. A hook on the working copy would not have been running at all. Three
gates inherit it from the one function: backlog_status_check,
backlog_citation_check and dangling_citation_check.

`=======` is deliberately NOT detected. A Markdown setext H1 underline is a run of
"=" and can be exactly seven, so matching it could refuse a legitimate document;
every real conflict carries the other two markers, so the exclusion costs no
detection while removing a false positive. A test pins the exclusion, because a
merge-integrity gate that refuses the real ledger is not a small failure.

The proof condition is the item's own: the poison is applied to a COPY OF THE REAL
LEDGER rather than a synthetic fixture, since the finding was that the real file
parses identically poisoned and clean.

Mutation-proven. With the detector disabled the refusal test fails DID NOT RAISE
while the setext test still passes, so the two are demonstrably testing different
things. The positive control that the live ledger still parses is load-bearing,
not decoration: without it, a reader that refused every input would satisfy the
raises-assertion on its own.

End-to-end, the gate still reports 520 items (284 live + 236 archived), exit 0.
…guard see it (#1242 limb 4)

RESTORING SPECIFIED BEHAVIOUR, not adding a requirement. The promotion of this
writer was specified to carry the union of live and payload keys so the schema
could grow without hand-editing the record. That was delivered for TOP-LEVEL
scalars (#382, limb 3) and silently not for sub-table entries: evidence was
re-emitted as exactly path/line/expect and absence as exactly
pattern/positive_control/mutation, so any other field inside an entry vanished on
every rewrite. The module header already states the governing rule -- enumerate
what you ORDER, never what you KEEP -- and those two emissions were exactly the
enumeration it forbids.

TWO HALVES, because the writer was not the only thing blind here.

WRITER. The explicit emissions stay, as an ORDERING, with a carry-through tail
appended. They are left spelled out rather than generated so the ordered keys keep
their exact typing -- `line` stays a bare int through int(), the rest stay TOML
basic strings -- which keeps every byte of today's output identical. Carrying
unknown fields through would be worthless if it re-typed the known ones on the way
past, and a test asserts that too.

GUARD. The field-preservation invariant compared TOP-LEVEL keys and, for
sub-tables, only the ENTRY COUNT. Counting entries cannot see a field vanish from
inside one, so writer and guard were blind in the same place: a rewrite could drop
a field from every evidence entry, keep the count, and report green. The invariant
now compares keys one level down and names the entry it would lose them from.

NOT KEYED ON THE FIELD NAMES THAT EXIST TODAY. A name-keyed fix satisfies the
symptom and drops the next field anyone adds, which is the defect again with a
longer list. The test therefore uses a key the writer has never heard of; a test
naming a field that exists today would pass against the lengthened-list fix.

MUTATION-PROVEN, BOTH HALVES, each failing exactly one test with the other 17 green:
  carry-through disabled          -> the survival test fails
  sub-table key comparison off    -> the backstop test fails, i.e. a field dropped
                                     from EVERY evidence entry passes the guard.
                                     That is the blindness this closes, measured.

BASE: `or key in cell` returns 0 in this diff's own copy of apply.py, so #382 is in
and this does not carry limb 3's reversal in behind it.

THIS DOES NOT CLOSE #1242. A fourth limb class -- live-only top-level TABLE values
type-mangled through _scalar() -- is untouched here. The banner is not mine to write.
…022)

delete_webauthn_credential refuses when the credential being removed is the last
second factor and MFA is still required (ADR 0068 decision 5). disable_mfa never
asked. Both are SELF-SERVICE, both sit behind the same step-up gate, and both can
take an account to zero second factors -- so the account's MFA requirement was
enforced on one route and not the other.

require_mfa defaults to TRUE with scope every_local_account, so this was the
DEFAULT posture, not an exotic configuration.

THIS IS NOT THE ADMIN ESCAPE HATCH AND DOES NOT NARROW IT. The "always-available
recovery for a locked-out passkey user" is admin_reset_mfa -- a different method,
which clears TOTP and every passkey and stays deliberately unguarded. A test now
pins that: under exactly the conditions that make the self-service path refuse
(MFA required, TOTP the only factor), the admin path still clears.

WHY BOTH PATHS SHOULD REFUSE, on the merits rather than by symmetry: with
require_mfa on, dropping to zero factors is NOT a lockout -- it lands the user in
the enroll-required flow. Neither self-service path is a rescue path, so neither
has a reason to be the exception.

AN EXISTING GREEN TEST CHANGES, AND IT IS NOT A COSMETIC CHANGE.
tests/test_mfa.py::test_disable_and_admin_reset_clear_mfa now runs with
require_mfa=False. Its subject is that disable and admin-reset CLEAR MFA; the
self-service disable in its setup was incidentally exercising the defect, because
the bootstrap admin it builds holds TOTP as its only factor. Turning the
requirement off keeps that test on its own subject. Stated here rather than
quietly adjusted: a green test that flips is exactly where a real regression
hides, and a reviewer cannot otherwise tell which kind this was.

Mutation-proven: with the guard disabled the refusal test fails DID NOT RAISE
while the positive control (disable IS allowed when MFA is not required) and the
admin-recovery test both stay green -- so the three are demonstrably testing
different things. The refusal is also asserted not to have mutated the store on
its way out; a refusal that already disabled MFA would be worse than none, since
the error would claim a state it had not preserved.

AND THE ROUTE MUST MAP IT. Adding the guard without touching DELETE /me/mfa would
have turned the refusal into a 500: auth_routes.py called disable_mfa with no
try/except, so the ValueError escaped uncaught. That reports a user-correctable
condition as a server fault AND swallows the remedy the message carries. The
neighbouring /me/mfa/confirm route already mapped ValueError to 400; this one did
not. Found by grepping the callers of the method being changed -- the guard is
only finished when its refusal reaches the caller in a form they can act on.

Pinned end-to-end: the API test drives the real step-up ceremony (enroll, confirm,
mfa-verify in a LATER TOTP step because the activating code is single-use, then a
disable-bound reauth) and asserts 400 with the remedy text, and that MFA is STILL
ON afterwards -- a 400 whose side effect already happened would be worse than the
500. Mutation-proven: drop the route mapping and the ValueError escapes uncaught.
…t (ADR 0015)

Owner-ruled: reword the docstring rather than lift the ADR block to the vault. Lifting
would have closed nothing -- the sentence already ships in the wheel AND the sdist
(pyproject only-include carries messagefoundry/), so it reaches PyPI either way.

WHAT CHANGED, and only this: the docstring named three evasions concretely -- "a
base64'd / hashed / re-encoded echo defeats it". That is a ready-made list to try. It
now states the CLASS instead: the match is literal, so any transformed echo passes
through unmasked. A reader of the code learns that from `form in text` plus .replace()
anyway, so the general statement discloses nothing the source does not; the enumeration
did.

KEPT DELIBERATELY, because removing it would be the worse defect: "Defence in depth,
not a seal". A docstring implying the scrubber is a seal misleads the next maintainer --
the same false-premise shape, pointed inward. The candour stays and gains an
operator-facing consequence: treat a captured reply as potentially secret-bearing
regardless, which is what an operator should DO and is true whatever the transformation.

Also kept: the _SCRUB_MIN_LEN rationale. The constant is visible at :269 either way, and
the docstring explains why a short low-entropy value (a facility code a CDC-IIS ACK
echoes) must survive rather than be shredded.

BEHAVIOUR UNTOUCHED, asserted rather than asserted-of: the diff is inside the docstring,
_SCRUB_MIN_LEN = 8 at :269 is unchanged, and the scrub loop is byte-identical.
52 passed in test_security_static.py, 149 passed across -k soap, ruff + mypy clean.

THE HALF THIS DOES NOT CLOSE, reported rather than silently widened: the IDENTICAL
sentence is published at docs/adr/0015-...md:462 in this public repo. Rewording only the
docstring leaves the recipe in the tree -- the mirror image of the argument that vaulting
the ADR closes nothing while the docstring ships. Whether the ADR's accepted-residual
text should be reworded too is a scope question I am handing back rather than deciding,
because editing a decision record is a different act from editing a docstring.
…ON (#1022)

THE SAME DEFECT AS THE JSON ROUTE, ONE LAYER UP, AND MY OWN FIX CREATED IT. b680ee0 made
the service refuse and mapped that refusal to HTTPException(400) on DELETE /me/mfa. This
route DELEGATES to that same handler, so from that commit onward a console user clicking
"Disable MFA" on their last factor got FastAPI's bare {"detail": ...} rendered into a
browser form navigation.

Measured, not argued -- with the translation removed the response body is literally:
  {"detail":"this is your last second factor and MFA is required for your account - enroll
  another factor first"}

Every sibling refusal in this module already answers with _account_response(error=...) --
the AD/already-enrolled case at :223, the no-enrollment-staged case at :264. This route was
the only one that did not, so the guard landed and the console got worse at precisely the
moment it started refusing.

429 is re-raised rather than translated, following the ui_reauth precedent at :201, so the
Retry-After semantics survive.

THE TEST ASSERTS THE BODY IS A PAGE, NOT THE STATUS. The mutation shows why: without the
fix the response is STILL a 400, so a status-only assertion passes against the defect. What
discriminates is "<html" in the body. It also asserts totp_enabled is unchanged -- a refusal
whose side effect already happened would be worse than the bad rendering.

248 passed in the console suite, 77 across the JSON auth suite, ruff + mypy clean.

FOUND BY A TEMP REPRO I HAD LEFT IN THE TREE from before a context compaction, asking exactly
this question and never answered. The lesson is the one b680ee0 already taught and I applied
too narrowly: a refusal is only finished when it reaches the caller in a form they can act on
-- and disable_mfa has TWO callers. I checked one, fixed it, and wrote that the guard was
complete.
My own adversarial review confirmed this one, and it is the sharpest kind: an assertion
captioned as a guard that could not fail.

WHAT WAS WRONG. The test asserted the original error message was PRESENT in the child's
output, captioned "THE TEARDOWN MUST NOT MASK THE STARTUP ERROR". CPython chains: an
exception raised inside the `finally` carries the original as __context__, and Starlette's
lifespan handler formats the WHOLE chain before uvicorn logs it. So the original text is
present EVEN WHEN IT WAS REPLACED -- the assertion is true in exactly the case it was
written to catch.

MEASURED WITH A PAIRED CONTROL rather than reasoned about. Engine.stop patched to raise
AFTER a real stop (so store.close() still runs and the process still exits promptly, which
is what makes this distinct from the hang):

    clean run   "deliberate failure..." PRESENT   chaining banner ABSENT
    masked run  "deliberate failure..." PRESENT   chaining banner PRESENT

So presence discriminates nothing and the banner separates them exactly.

THE FIX IS TWO ASSERTIONS, NOT A REWORDING. The presence check stays but is relabelled as
what it actually establishes -- the refusal was reported at all -- and marked weak on
purpose. The new check asserts the chaining banner is ABSENT, which is the masking property.

MUTATION-PROVEN IN BOTH DIRECTIONS, which the original never was: with the injection the new
assertion FAILS naming the teardown; without it, 11 passed across this file and
test_security_notice_deliverability.py. The original test's cited mutation (un-hoisting a
teardown handle) fails via the 30-SECOND TIMEOUT and never evaluates these assertions at all
-- so that evidence, which I quoted in 0e9c104's commit message, never exercised them.

WHY IT MATTERS BEYOND THIS FILE: the module docstring sets the standard -- "not that the
error prints... neither discriminates" -- and the assertion asserted precisely that the error
prints. A control resting on a false premise, whose harm is that a reader believes
post-engine.stop() masking is guarded and does not add the guard.

The #1257 FIX itself is unaffected: process-exits, the hoist, and the 1.8s-vs-61.5s pair all
stand. What was defective was one assertion in its test.
wshallwshall and others added 11 commits August 14, 2026 14:26
…guard see it (#1242)

THREE LABELS, ONE DEFECT. Builder 2's "unknown top-level non-scalars", the Dispatcher's
"non-scalar values stringified", and the inheritance in my own _carried() path all route
through _scalar(), which branched on bool and int then fell through to
toml_str(str(value)). A table became a quoted PYTHON REPR -- sym_table = "{'a': 1}" -- which
PARSES, so nothing went red, and re-reading returned the STRING. The value was not
recoverable from the file. One dict/list branch closes all three; measured across all three
call sites by round-tripping through tomllib, not by reading the emitted text.

THE GUARD IS THE HALF THAT MATTERS, and it was blind here BY CONSTRUCTION. `lost = set(was)
- set(now)` is a pure key-set difference; a type-mangled field KEEPS ITS KEY and passes it.
That check was written to catch DROPPED keys and it does -- but a rewrite could corrupt
every value while preserving every key and report green. Now type-compared as well.

SCOPED TO KEYS THE PAYLOAD DID NOT TOUCH, deliberately. The corruption is the WRITER
retyping something nobody asked it to. A payload that intentionally retypes a field is an
EDIT, and an unscoped check would refuse it -- a guard that refuses legitimate edits is a
guard someone disables, which is how you arrive back at no guard.

DOTTED KEYS ARE QUOTED, AND THIS IS THE SUBTLE ONE. A dotted key is not a syntax error in
TOML, it is a NESTING OPERATOR: {1.2.2 = "x"} is VALID and reads back as
{'1': {'2': {'2': 'x'}}}. File loads, gate green, structure different. Spaces and quotes
fail LOUDLY and are therefore safe; the dot is the only key that corrupts quietly -- and
ASVS requirement ids ARE that shape. Measured against my own first emitter, which would
have shipped it. The rule is unconditional (quote unless ^[A-Za-z0-9_-]+$) because
"quote the odd-looking ones" fails when 1.2.2 does not look odd.

NOT json.dumps: {"a": 1} is JSON, not TOML. Arrays coincide between the two, tables do not,
so a serializer that looks right on arrays emits a file that will not parse.

MUTATION-PROVEN: remove the dict branch and all four new tests fail while all 18 existing
ones pass. A guard nobody has watched fire is indistinguishable from one that cannot.
Quoting is verified at depth 1, 2 and 3 and inside arrays of tables, with plain-key and
float/bool controls that pass in BOTH modes so the result is not blanket-passing.

22 passed in this file, 410 passed across -k asvs, ruff + mypy clean. Base carries limb 3
(union walk present, `or key in cell` absent) per the stale-base check.
Owner DELEGATED the choice ("do what you sessions judge best") -- recorded as delegated, not
as approval of a particular option, because those are different sentences and only the first
is supported. The Dispatcher then decided the shape; this implements it.

WHAT CHANGED: accepted-residual 2 named three evasions concretely. It now states the CLASS --
the match is literal, so any transformed echo passes through unmasked -- plus the
operator-facing consequence: treat a captured reply as potentially secret-bearing regardless.
A reader learns the limitation either way; only the ready-made list is gone.

KEPT DELIBERATELY: "defence in depth, not a seal". Removing the limitation would be the worse
defect -- a record implying the scrubber is a seal misleads the next maintainer, which is the
same false-premise shape pointed inward.

MARKED IN PLACE: "wording narrowed 2026-08-14, THE DECISION IS UNCHANGED". An ADR is a
decision record; editing its text without saying which part moved would make a later reader
unable to tell a narrowed WORDING from a revised DECISION.

THIS MUST LAND WITH 91b3cc3, and that is why the sequencing changed after the first design.
Neither the docstring reword nor this one is on origin/main, so BOTH locations currently
publish the identical sentence. Narrowing either alone is a remediation that is not one: it
strips the recipe from one artifact while the other keeps publishing it, and reads as
withdrawn when it is not. That is the same argument that ruled OUT vaulting the ADR, pointed
the other way.

VERIFIED WITH A CONTROL, because two zeros are what a blind scan prints too:
  recipe in docs/adr/0015 : 0     recipe in transports/soap.py : 0
  recipe in origin/main's copy of the ADR : 1   <- the pattern CAN hit
  "defence in depth, not a seal" still present in both : 1 and 1

99 passed across the seven test files that read docs/adr from disk.
…262)

THE LANE ALREADY EXISTED and is well built -- one list used twice to defeat drift, an
existence check per module, -rs so a skip cannot read as a pass. The defect was its
MEMBERSHIP, which is the part the item calls the real work.

ADDED, both measured by TRACING WHAT THEY OPEN AT RUNTIME:
  test_cutover_slug_rot.py       153 allowlisted paths read
  test_backlog_citation_check.py 348

test_cutover_slug_rot holds test_present_tense_mirror_prose_does_not_grow -- THE TOPOLOGY
RATCHET THAT REDDENED main IN THE ITEM'S OWN MEASURED INSTANCE. The gate that caused the
filing was absent from the lane that exists to run it.

THE MEMBERSHIP RULE IS NOW WRITTEN BESIDE THE LIST, with the evidence that no static
criterion can re-derive it: "mentions a docs-ish token" gives 135 candidates; "quotes a real
allowlisted path" MISSES 8 OF THE 16 already listed, because those gates build paths rather
than writing one literal. A criterion that cannot re-derive the KNOWN members cannot find
unknown ones -- so the rule records the tracing method instead.

CARRIED AS A LIMIT RATHER THAN IMPLIED COMPLETE: the trace ran without five CI extras, 89
tests SKIPPED, and a skipped test reads nothing. THE LIST IS A FLOOR, NOT A CENSUS. More
members may be missing; re-derive with full extras before believing otherwise.

test_doc_ref_handle READS NO DOCUMENTATION -- it tests mfdoc:v1:ref: handles in the store and
is in the lane because its NAME reads like "documentation reference". KEPT, per dispatcher
ruling: removing a guard is a different act from adding one, and dropping it on a reading of
its name is the same move that put it there. Noted in place so the next reader is not misled.

NOT DONE: the item's proof condition is a Markdown-only PR carrying a deliberate violation of
each newly-added gate, asserting THE PR goes red. That needs a PR, not a local run.
…ot mention (BACKLOG #1242)

RETRACTING MY OWN SCOPING, on someone else's measurement. I wrote the type guard's
`k not in c` clause and flagged it as the part my own judgment could not check --
a mutation test by the author proves a guard is CONNECTED, never that it is
connected to the right thing. The ASVS Tracker measured it and the hole is real.

THE HOLE. With the writer's dict branch disabled, a payload OMITTING the key was
refused while a payload CARRYING it exited 0 and wrote a Python repr into a TOML
string. So the guard stopped looking at the exact moment a cell is rewritten.

NOT A CORNER, and this is why it outranks a tidier fix: of 345 cells in the record
exactly ONE holds a top-level non-scalar, and the natural payload for rewriting
that cell ECHOES the key. The guard covered every cell that cannot be hurt and
skipped the one that can.

THE FIX KEEPS THE PROPERTY THE SCOPING WAS FOR. The intent was right -- a payload
that intentionally retypes a field is an EDIT, and a guard refusing legitimate
writes is a guard someone disables. The payload STATES a type, so compare against
it rather than declining to look: an intentional retype agrees with its own
payload and passes, a writer corruption disagrees in BOTH zones.

_ORDERED is excluded because render() coerces those by design (int(cell['level']),
the quoted emissions), so a payload stating another type there is NORMALISED, not
corrupted -- refusing it would be the cry-wolf failure the scoping exists to avoid.

MUTATION-PROVEN, and it changed the change. Four mutants:
  revert to `k not in c`            -> killed
  always use the LIVE type          -> killed
  drop the _ORDERED exclusion       -> SURVIVED, so I wrote the test that kills it
  drop the _SUBTABLES exclusion     -> SURVIVES, and is documented as such
The third is the point: 23 tests stayed green while that clause did nothing, which
is this item's own defect one level up. _SUBTABLES is left in as belt-and-braces
with the reason written down rather than claimed as covered -- evidence and absence
render as arrays of tables on both sides, so it cannot fire today, and that is a
property of the current writer rather than an invariant.

VERIFIED: ruff format + check clean (0.15.22, matching constraints.lock), mypy
clean on the changed module, 24 passed in tests/test_asvs_apply.py and 411 passed
/ 23 skipped across -k asvs. Interpreter resolves messagefoundry to THIS worktree,
checked by printing __file__. The venv lacks the x12/xml extras, so pytest printed
INCOMPLETE RUN -- none of these is a full-suite claim.

THIS DOES NOT CLOSE #1242, and the banner is not mine to write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…LOG #1272)

THE GUARD ASKED THE WRONG QUESTION. `skipif(shutil.which("bash") is None)` asks
*is bash PRESENT*. What matters is *is the bash I found USABLE FOR WHAT I AM ABOUT
TO DO*. On Windows the WSL launcher is present, passes that guard, and then cannot
resolve any path this process wrote -- so every block failed identically regardless
of its content. SDS-3.8 in a gate's own guard clause.

MEASURED 2026-08-14, single-variable control. Same tree, same commit, same
interpreter, same three modules; only the PATH order changed:
  WSL launcher first   19 failed, 28 passed, 7 skipped
  Git Bash first       47 passed,  7 skipped
19 + 28 = 47, so the nineteen that failed are exactly the nineteen that passed.
Both bashes exist here and NOTHING PINNED WHICH ONE WAS FOUND -- which is worse
than "broken on Windows", because two seats get opposite verdicts from two honest
measurements and neither reading generalises.

THE SOLUTION ALREADY EXISTED AND DID NOT PROPAGATE. test_merge_gate_controls
solved this on 2026-08-10, carries the reasoning, and passes 23/23 under the WSL
resolution. Three sibling modules kept shutil.which. So this commit MOVES that
cluster to tests/_bash_support.py rather than inventing one -- git-derived
candidates first (git ships bash beside it), plus a LIVE NAMESPACE PROBE that
writes a token and requires the candidate to read it back. Rejecting "system32" by
name would be matching a spelling; reading a file tests the actual namespace.

A SKIP WAS REJECTED, NOT MERELY NOT CHOSEN. ci.yml sets defaults.run.shell: bash on
every OS, so a leg without a usable bash could not run the gate at all -- and
silence is also what a genuinely broken workflow block looks like. Loud failure
names the real condition. I proposed a skip first; the existing module's reasoning
is better and I withdrew it.

126/127 SEPARATED FROM 2, AND THIS IS THE HALF THAT SURVIVES A RESOLUTION
REGRESSION. bash exits 126/127 when it cannot RUN what it was handed and 2 for a
genuine syntax error; testing only `returncode != 0` conflates them, which is how
one unresolvable path presented as 160 syntax errors that did not exist. The
unrunnable list is asserted FIRST and separately, because if the checker could not
run, the other list is uninformative.

DISCRIMINATION PROVEN, not assumed -- three classes against the resolved bash:
  valid script        exit 0     neither list
  real syntax error   exit 2     -> failures    "syntax error: unexpected end of file"
  unrunnable          exit 127   -> unrunnable
So a real defect is still caught and a harness failure names itself.

VERIFIED under the WSL-first PATH, which is the condition that produced the 160:
test_workflow_shell_syntax 3 passed, "syntax-checked 160 shell blocks with
C:\Program Files\Git\bin\bash.exe; 0 failed, 0 unrunnable"; test_merge_gate_controls
unaffected at 23 passed; ruff check + format clean at the pinned 0.15.22; mypy clean
on the new module.

NOT DONE HERE: test_dependabot_automerge_guardrails and test_installed_coord_hooks
still call shutil.which directly. They are the remaining adopters and this does not
close #1272.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…its controls (BACKLOG #1272)

FIXING MY OWN PREVIOUS COMMIT, WHICH INTRODUCED THE DEFECT IT WAS FIXING. 368c1ce
created tests/_bash_support.py and adopted it in ONE module while leaving
test_merge_gate_controls holding its own private copies of the same six helpers. That
is two copies of a resolver whose entire item is "the fix existed and did not
propagate" -- non-propagation, committed by the propagation commit. Caught by the
Dispatcher on review, not by me.

test_merge_gate_controls now IMPORTS them by alias rather than having its call sites
rewritten, and that choice is load-bearing rather than lazy: THE PROVEN MODULE KEEPS
ITS BEHAVIOUR AND ITS TESTS KEEP GUARDING THE HELPER. A promotion that moved the
helpers away from the tests proving them would leave a helper that looks identical,
is now used in four places instead of one, and is proven in none.

THE NEGATIVE CONTROL TRAVELS WITH THEM, for the same reason. The sys.executable
stand-in -- a real, runnable program that is not a shell, which the namespace probe
MUST refuse -- is what makes _bash_sees more than a hopeful call. Left behind, the
promoted helper would be unguarded everywhere it is now used. It lives in
tests/test_bash_support.py beside the module it controls.

ONE CONTROL ADDED: the 126/127-versus-2 split is now asserted, not merely described.
If 2 ever entered CANNOT_RUN_CODES every real syntax error would be reclassified as a
harness fault and silently stop failing -- this split's own purpose, inverted. That is
a mutation nothing else in the suite would have caught.

VERIFIED under the WSL-first PATH, which is the condition that produced the original
19: test_merge_gate_controls + test_bash_support + test_workflow_shell_syntax = 27
passed. ruff check + format clean at the pinned 0.15.22. The orphaned imports the move
left behind (os, shutil, subprocess, and two now-unused aliases) are removed.

STILL NOT DONE, AND THIS STILL DOES NOT CLOSE #1272: test_dependabot_automerge_guardrails
and test_installed_coord_hooks have not adopted it. The second is not a copy of the
same fix -- it resolves `shutil.which("sh") or shutil.which("bash")`, trying sh FIRST,
so a helper that proves only BASH usable leaves it discovering sh the same unproven
way. Pin the shell, not the name; and now, prove the shell, not the name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ne exists (BACKLOG #1216)

CLOSES THE BUILD FOR #1216. #1272 was filed tonight for the same defect and is a
duplicate -- neither filer could see the other, because #1216 is on origin/main and
#1272 is on an unpushed branch. Builder 3 held #1216, released it, and ruled the
merge in this direction; disposition of #1272 routes to the Lander.

THE CONTRACT IS BUILDER 3's CORRECTION AND IT IS WHY THIS IS GENERAL. My first
helper found "a usable bash". That is not enough: test_installed_coord_hooks
resolves `shutil.which("sh") or shutil.which("bash")` -- sh FIRST. A bash-only
helper would have repaired that file BY ACCIDENT on this box, where which("sh")
finds nothing, and left it broken wherever an UNUSABLE sh sits on PATH, because sh
would win the `or` and never be probed. So require_shell takes the caller's own
preference order and PROBES EVERY NAME.

PINNED BY A TEST THAT CANNOT PASS VACUOUSLY: require_shell(tmp, "mf-not-a-real-shell",
"bash") must fall through to a working bash, AND the same call with only the
impossible name must FAIL. Without the second arm, a helper that returned something
unprobed would pass the first whenever the fallback happened to work anyway.

LOUD FAILURE, NOT SKIP -- and here the ITEM's text is the half being corrected, on
Builder 3's ruling. #1216 says "skip honestly". ci.yml sets defaults.run.shell: bash
on every OS, so a leg without a usable shell cannot run the gate at all: a skip
there is a green that proves nothing, and a red at least gets investigated. The cost
is near-zero because git-derived candidates are tried first, so a box with git
neither skips nor fails. The item text needs amending to match; that is not mine.

RELATIVE INVOCATION EVERYWHERE. Not converting a namespace at all is stronger than
converting one correctly, and an absolute Windows path is precisely what the WSL
launcher mangled -- backslashes eaten, exit 127.

MEASURED, single-variable, under the WSL-first PATH that produced the original
failures:
  before   19 failed, 28 passed, 7 skipped   (three modules)
  after    72 passed,  7 skipped, 0 failed   (all five, incl. the promoted helper)
Every one of the 19 now passes without changing which bash PATH offers. ruff check +
format clean at the pinned 0.15.22; mypy clean on the helper.

THE FINDING THAT OUTLIVES THE FIX. #1216's own text says "Three independent lanes
reached it separately." I am the fourth, and I re-derived it from scratch while a
fully-diagnosed item -- cause, control and fix -- sat on origin/main since
2026-08-11, and while test_merge_gate_controls carried the same mechanism in a
docstring dated 2026-08-10. Two independent records existed before tonight and four
lanes still paid. The remedy is a habit, not a patch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…upersession

Two files conflicted, ONE hunk each, and BOTH sides had changed both -- a real call under 8a, not a
mechanical resolution. Resolved by reading what each side is FOR:

scripts/asvs/apply.py -- TOOK THE LANE'S SIDE, and main's own comment is the argument for it.
main carries the ORIGINAL `k not in c` scoping under a long note that says, in its own words, "THE
INTENT ABOVE IS RIGHT AND THIS IMPLEMENTATION OF IT IS KNOWN-INCOMPLETE -- see the open BACKLOG
#1242". The lane's side IS #1242's fix: compare against the type the payload STATED, with _ORDERED
and _SUBTABLES excluded for stated reasons.

So keeping main's side would preserve a carefully-written warning ABOUT a defect this change
REMOVES -- an invalidated claim with no marker, which is the failure git cannot detect because only
one side changed the world and the other described it.

Nothing load-bearing is lost. main's "latent, not live" conditional-tense discipline survives in the
lane's version as EVIDENCE rather than caution: measured with the writer's dict branch disabled, a
payload omitting the key was refused while one carrying it exited 0 and wrote a Python repr into a
TOML string.

tests/test_asvs_apply.py -- APPEND COLLISION, main's side of the hunk is EMPTY. Kept both.
Verified by NAME rather than by count: all 22 of main's tests present, ZERO lost, 2 added --
test_the_TYPE_guard_sees_a_corruption_the_payload_ALSO_MENTIONS (must refuse) and
test_the_TYPE_guard_does_NOT_refuse_a_field_the_writer_COERCES_BY_DESIGN (must not). That is the
asymmetric pair, not a single arm.

Verified on the result: new predicate present exactly once, old `k not in c and type` form gone,
zero conflict markers in either file.

Built with plumbing; no working tree touched.
wshallwshall added a commit that referenced this pull request Aug 22, 2026
…no engine module

CI caught what my local run could not: test_tooling_partition asserts every test
either imports an engine module or is declared as tooling, and the new
test_coord_seat_session_key.py does neither -- it drives seat.ps1 through
subprocess, so it imports nothing from messagefoundry.

Its sibling test_coord_seat_prompt.py was already listed; this adds the new file
directly after it, keeping the manifest alphabetical.

WORTH RECORDING: this is the same gate flagged as the blocker on PR #487, where
it had been reached by SIMULATING the check in Python rather than running it.
Hitting it here for real, on a different file, confirms the mechanism that
simulation predicted. The simulation was right about the gate and could not tell
which file would trip it.

The local suite passes this gate now (9 passed), but note that passing locally is
what it did before too -- the failure only appears once the full partition runs.
… module

CI CONFIRMS THE BLOCKER THAT WAS ONLY SIMULATED. PR #487 was opened as a draft
specifically to settle this by running the check rather than modelling it in
Python, and the real result names the same file the simulation predicted:

  test_tooling_partition.py::test_every_non_engine_test_is_classified
  these tests import no engine module, so they are unclassified: ['test_bash_support.py']

It failed on all three test legs plus the harness leg.

WORTH RECORDING, because it is the reason the draft was worth opening: the
simulation was right about the GATE and could not have been trusted about the
FIX. It ran against this branch's older copy of test_tooling_partition.py, while
a merge takes main's newer rule file -- so a green simulation would have proved
nothing about the tree that actually runs. Only CI tests the tree that merges.

The identical failure appeared independently on PR #486 for a different file
(test_coord_seat_session_key.py), which is corroboration from an unrelated
branch that the gate behaves as described rather than reacting to this one.

This clears ONE of the two blockers on #487. The other stands and is unrelated:
merging this branch would restore a vault-derived count that main deliberately
dropped, with merge-tree reporting no conflict.
The figure is vault-derived and this file ships to PyPI. A coverage count over a
closed public requirement set discloses the uncovered set by subtraction, which is
why the scorecard is vaulted in the first place.

`main` already words this clause without a figure. Merging this branch as-is would
have brought the number BACK, and `git merge-tree` reports NO CONFLICT because the
two sides edited different line ranges of the same comment -- so nothing would have
objected. That was the second of two blockers recorded on PR #487.

ONLY THE COMMENT MOVES. The guard itself is this branch's fix and is deliberately
untouched: main still carries `k not in c`, which skips every key the payload
carries, and repairing that is what these commits are for.

RECORDED BECAUSE I NEARLY DID THE OPPOSITE. Comparing the two comment blocks, I
judged main's better-worded and figure-free version to be the later revision and
concluded the fix was to take main's whole block. It is the EARLIER one. Diffing
the CODE rather than the prose showed main still has the unrepaired guard, so
taking its comment would have shipped a comment describing an implementation that
no longer exists -- and would have read as reverting the fix. Prose quality is not
version order.

A note now sits inline saying the total is omitted on purpose, so a future merge
that reintroduces it has something to contradict.
wshallwshall added a commit that referenced this pull request Aug 22, 2026
…de it, plus BACKLOG #1306 (#486)

* fix(coord): a CLI seat declaration must attach to the session that made it

seat.ps1 keys one record per (worktree, session). Both hooks read session_id
off their stdin payload and pass it as -SessionId. The CLI path -- the exact
invocation the SessionStart banner prints and tells every seat to run -- has no
payload and no id a person could type, so Get-SessionKey fell through to the
literal string 'nosid'.

The result was two valid records in one box: the hook's, carrying the session
and no goal, and the declaration's, carrying the goal and no session. Nothing
reported it because neither record is wrong.

Measured across the live seats directory before the fix:

    21 declarations, 18 of them in nosid.json
    18 of 18 boxes holding a declaration ALSO held a session-keyed record
       reading seat=null, goal=null

fleet.ps1 rendered each of those boxes twice, once as a declared seat and once
as NOT-DECLARED. So the fleet could see a goal, could see a live session, and
could not join them -- which is the one question a declaration exists to
answer. That is the hollow-record failure CLAUDE.md section 5 describes,
arriving one layer further in: the schema was fed, and what fed it could not be
attributed.

Get-SessionKey now falls back to $env:CLAUDE_CODE_SESSION_ID. Precedence,
highest first: -SessionId, the payload's session_id, the env var, then 'nosid'.
Env is ranked below the payload so a hook holding the real thing always wins,
and the new rung is labelled sessionIdSource='env' so a reader can still tell
where the identity came from.

The variable is authoritative rather than a guess. On the session that found
this, the SessionStart hook had written its record with sessionIdSource='param'
from its payload, and $env:CLAUDE_CODE_SESSION_ID held the same value.
CLAUDE_CODE_HOST_SESSION_ID is deliberately NOT read: it is the `local_`-
prefixed session-management MCP namespace, and keying on it would re-split
every box.

Tests cover the env rung, the precedence, and a negative control proving the
rung does the work. Note what let this survive: every test in
test_coord_seat_prompt.py passes -SessionId explicitly, so the suite was green
and blind to the invocation the banner actually prints. The new helper builds
its environment from scratch rather than mutating os.environ, because the suite
itself runs inside a session that sets the variable.

* backlog: file #1306 -- the worktree gate denies a read-only hooksPath query, reproduced before filing

Reported by the Builder 1 seat, relayed by the Dispatcher, allocated and filed here
because allocation and the commit that files an item cannot be split across
worktrees.

The gate decides on command SHAPE rather than on whether a value is assigned, so a
bare read of core.hooksPath is refused with a message saying it would change the
shared configuration -- when it changes nothing. The explicit --get spelling is
unaffected, so a caller who needs the value has one, which is most of why this is
a small item.

A second instance turned up while filing it, and it is the worse half: writing the
entry through a shell heredoc was itself denied, because the heredoc QUOTES the
command strings. The rule matched the key inside a documentation payload that
assigns nothing and runs nothing. So the same predicate that blocks a read blocks
writing the item describing the read, and the obvious next move for whoever hits it
is to reword the evidence out of the record. The entry was written with the file
edit tool instead; no gate was routed around.

It fails closed and there is no bypass. This is a false positive, not a hole, and
the fix must not be read as licence to loosen shape-matching generally.

Two failed measurements are recorded in the item because either would have closed
it as unfounded. The first drove the gate with parameters it does not have and
returned the same failure on every case including the control. The second omitted
cwd from the payload, so the gate was OFF and allowed the write case it must deny.
A gate probe whose positive control does not deny is measuring nothing, and it
looks exactly like a clean result.

The leak gate then blocked the first attempt at this commit for carrying a worktree
slug. It was right: this ledger is public and the slug is internal coordination
detail the item does not need. Removed rather than allowlisted, which is what the
gate's own remediation text says to do.

* test(tooling): classify the new seat-session-key test, which imports no engine module

CI caught what my local run could not: test_tooling_partition asserts every test
either imports an engine module or is declared as tooling, and the new
test_coord_seat_session_key.py does neither -- it drives seat.ps1 through
subprocess, so it imports nothing from messagefoundry.

Its sibling test_coord_seat_prompt.py was already listed; this adds the new file
directly after it, keeping the manifest alphabetical.

WORTH RECORDING: this is the same gate flagged as the blocker on PR #487, where
it had been reached by SIMULATING the check in Python rather than running it.
Hitting it here for real, on a different file, confirms the mechanism that
simulation predicted. The simulation was right about the gate and could not tell
which file would trip it.

The local suite passes this gate now (9 passed), but note that passing locally is
what it did before too -- the failure only appears once the full partition runs.
…ranch moved it to

`test_the_bash_namespace_probe_rejects_an_interpreter_that_cannot_see_the_fixture`
is registered at `tests/test_merge_gate_controls.py`. This branch's #1272 work
split it into a new `tests/test_bash_support.py` and left the registry pointing
at the old home, so the node id dangles.

MEASURED, because "whose defect is this" was the whole question:

  origin/main   test_merge_gate_controls.py has the function   YES
                tests/test_bash_support.py exists              NO
  this branch   test_merge_gate_controls.py has the function   NO
                tests/test_bash_support.py exists              YES

The registry line is BYTE-IDENTICAL on both. So it resolves on main and dangles
here, and the branch is what moved it. Not a pre-existing main defect.

THE CHECK IS DOING EXACTLY ITS JOB. Its own docstring names the failure it
exists to catch -- "a registry of dangling node ids satisfies every count-based
assertion" -- which is why it resolves each node instead of counting them.

Verified with a planted control rather than a bare green: 7 passed, then
deliberately repointed at a nonexistent file to confirm 3 tests fail, then
restored to 7 passed. A green I have not seen fail is not evidence.

NOT FIXED HERE, and it is pre-existing on main rather than this branch's:
the entry sits in the `green` list of a control about the backlog-hygiene
gate's four benign shapes, and a bash-namespace probe has nothing to do with
those. Correcting the path makes the registry resolve; deciding which control
should own the entry is a separate question and not one to settle inside a
stranded branch.
@wshallwshall wshallwshall changed the title b1 lane: #1216/#1272 interpreter proof, #1242 ASVS payload types, #1245/#1233/#1256/#1020/#1022 auth+store, #1262 doc-lane gates (conflict-resolved rebuild of #432) b1 lane (PARTIAL, not a supersession of #432): interpreter proof, ASVS payload types, auth+store, doc-lane gates Aug 22, 2026
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

CORRECTION TO THIS PR'S OWN TITLE, measured rather than remembered.

The title called this a "conflict-resolved rebuild of #432". That is FALSE and it
is the dangerous direction of false: this branch carries a SUBSET, and anyone who
read the title would close #432 as superseded and silently drop most of it.

File sets against origin/main:

#432 36 files
#487 10 files
shared 8
only on #432 28 <-- would be LOST if #432 were closed as superseded
only on #487 2 (tests/negative_controls.toml, tests/tooling_manifest.txt)

The 28 include ADR 0164, messagefoundry/auth/service.py, messagefoundry/api/app.py
and auth_routes.py, all three store backends, transports/soap.py, the webconsole
account route, docs/SECURITY.md, docs/CONFIGURATION.md, .github/workflows/ci.yml
and scripts/docs/backlog_status_check.py.

#432 IS NOT SUPERSEDED BY THIS PR AND MUST NOT BE CLOSED ON ITS ACCOUNT. Whether
its remaining content is still needed is a separate question -- the branch is five
days old and some of it may have landed by content through other lanes -- and that
question has NOT been answered here. Ancestry cannot answer it either: main
squash-merges, so a commit count says nothing about content. It needs matched
probes on both sides, the way #497's delta was measured.

WHAT THIS PR ACTUALLY IS: the eight shared files plus two of its own, held in
draft on a real defect. Three rows of
test_release_age_passes_an_aged_release_and_holds_a_fresh_one fail here because
its curl stub is not interposing on windows-2025 and the step body reaches live
PyPI -- filed as BACKLOG #1312 (PR #494). The require_shell change is correct and
is what made those rows run at all; the failure is downstream of it, not an
objection to it.

wshallwshall added a commit that referenced this pull request Aug 22, 2026
…the live network (#494)

Found while triaging PR #487. Three rows of
test_release_age_passes_an_aged_release_and_holds_a_fresh_one now RUN on
windows-2025 (rc=0, so the step body completed) and FAIL, every one returning
age_ok='true' where 'false' is expected.

THE PASSING ROW IS THE FINDING. The test's own comment calls `aged 30 days` the
discriminating pass, "without this row the whole release-age suite would be
satisfied by a step that denies unconditionally." It is green because
requests==2.32.3 really is old -- a fact about PyPI, not about the guardrail.

The reading is forced by the workflow being fail-closed rather than inferred from
the failures. Every branch of the age step routes to age_ok=false: unwired
ecosystem, empty or ERR pairs, blank newVersion, name or version failing shape
validation, ERR or empty body, missing or null timestamp, unparseable date,
future date, under-age. true reaches the output only from a real aged upload
time, so true on a row whose fixture is {"urls":[]} is positive evidence the body
reached the network. The workflow is NOT the defect.

Not the cwd/script.name change: I replicated both invocation forms against Git
Bash with full_env built the same way and the same chmod(0o755) stub, and the
stub interposed under BOTH. The open question is which interpreter require_shell
resolves on that runner image, which cannot be measured off the runner.

The test discards the one output that would settle it. _run_step_body captures
the child and returns only what it parses out of GITHUB_OUTPUT, so the step's own
"was published Nh ago" notice and every ::warning:: never reach the CI log. The
failure reports a wrong boolean and withholds the sentence naming the age it
computed. That is step one of any fix, before theorising about PATH.

Downstream of #1216/#1272, not an objection to them: require_shell is what made
these rows run at all, and this was invisible underneath the WSL-launcher
failures it removed. Nobody holds either number -- the Builder 1 seat measured
zero claim rows and zero commits for both, with a positive control, and declined
the hand-off -- so this needs a dispatcher assignment rather than an owner by
default.
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Do not resolve this toward 487 without reading this first

Left open and unarmed deliberately. Recording a finding so it is not lost in a session transcript.

Provenance, stated plainly: the session that derived this was cut off mid-command when its Claude account was cancelled on 2026-08-22 at 16:56Z. The gh pr comment call was composed and then refused by a classifier ten seconds before the cut, so it never posted. This is that body, recovered from the transcript and re-verified against the repo before posting.

Verified here, independently: main is newer than this branch on the conflict area. Last commit touching tests/test_dependabot_automerge_guardrails.py:

side commit date via
origin/main e03e8e06 2026-08-22 #505, "the shared bash resolver"
this branch 9f9b3c4e 2026-08-20 merge of the #1216/#1272 lane

So main postdates this branch by two days on that file, and the landing change is exactly the bash-resolver work the conflict sits in. Main supersedes 487 there.

Relayed, NOT verified here: a peer lander session reports it ran a per-file adversarial pass (one verdict per file, each handed to a skeptic told to refute it) and concluded that some of this branch's assertions were written against the OLD selection order and are therefore now wrong rather than merely redundant. I have not checked that stronger claim, and it should be re-derived rather than taken on trust.

The distinction matters for whoever picks this up: resolving toward 487 on the guardrails file would not just re-add redundant assertions, it may re-add assertions that now assert the wrong thing -- and they would go green against the old order while the resolver has moved.

Unchanged and still true from the original body: this is a draft on purpose, and mergeStateStatus is DIRTY.

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