Skip to content

tls!: the engine always serves TLS, minting a self-signed certificate on first run (BACKLOG #1276 part A, ADR 0172 supersedes ADR 0143) - #534

Open
wshallwshall wants to merge 48 commits into
mainfrom
claude/builder-1-1245-1276a
Open

tls!: the engine always serves TLS, minting a self-signed certificate on first run (BACKLOG #1276 part A, ADR 0172 supersedes ADR 0143)#534
wshallwshall wants to merge 48 commits into
mainfrom
claude/builder-1-1245-1276a

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

A default-security-posture change, and the title says so on purpose. Split out of #458 by the lander so it would not land under an unrelated title.

Why this is its own PR

Both repos require zero approving reviews, so arming is merging unread and the PR title is the only durable record of what shipped. As originally stacked, this rode inside a PR titled "username identity is case-sensitive and must not depend on store collation" — a reader six months out would have had no way to learn that always-serve-TLS shipped in it.

The three rulings are one change

The Liaison's framing, and it is why the ADR is here rather than in a follow-up:

  1. always-TLS — the engine serves an operator certificate if present, mints one otherwise
  2. ADR 0143 superseded in the same PR — done, see below
  3. the re-mint is audited, not silent — a second owner fork on the same item, still to come

Landing any one alone leaves a document or a control that is trusted and wrong.

ADR 0172 supersedes ADR 0143's decision, and keeps its analysis

0143 did not merely assume no auto-TLS — it considered this exact change and declined it. Its own words, quoted in 0172: "A full fix — terminate TLS on the loopback bind so effective_https is true." Its Accepted decision rests on that decline, so leaving it untouched would put an ADR on main reading Accepted on a premise the code had deleted, with nothing reporting it.

0172 supersedes the decision rather than amending it, because 0143's client-migration sizing is shown measurably false against origin/main. It keeps a "What of ADR 0143 survives" section for the analysis that still holds. The docs/adr/README.md index row is in the same commit.

Verified before pushing

17c16128 fast-forwards 33b9451a; an ADR file is in the diff — the lander's stated merge-time gate — with the same check on the pre-graft tip returning zero as the negative control. 48 ahead of main, 1 behind.

Opened unarmed, behind #533.

wshallwshall and others added 30 commits August 20, 2026 07:36
…he caller's spelling (BACKLOG #1268)

`users.username` was the one identifier column in the SQL Server schema carrying no COLLATE
clause, so it inherited the DATABASE default -- case-INsensitive on a stock install -- while
every sibling identifier column in the same file pinned Latin1_General_100_BIN2 and both other
backends were case-SENSITIVE. `Admin` and `admin` were two accounts on two backends and one
account on the third, under a UNIQUE constraint that reads as if it had settled the question.

That portability defect became a security defect because a second site answered the same
question by a different rule. `_login_local` gated WP-3 bootstrap expiry/supersession
enforcement on a PYTHON comparison against the caller's input, while the lookup one line below
was resolved by the COLUMN'S collation. The two disagree in exactly one direction: `Admin`
fails the Python guard, so retirement never runs, then succeeds at the lookup and returns the
very row the skipped call would have disabled.

MEASURED before the fix, on a lapsed unclaimed bootstrap with the ASVS 6.4.1 credential expiry
disarmed so it could not mask the result:

    login("admin")  -> refused, account disabled      -- the control fired
    login("Admin")  -> ok=True, session issued        -- the control never ran

A lapsed first-run credential logging in because one letter was capitalised. SDS-3.7 exactly: a
compensating control resting on the false premise that the username the gate compared is the
username the store matched. No live exposure -- zero deployments (CLAUDE.md section 0).

Two limbs, and the second does not depend on the first:

  - the column now pins the collation its own file's convention already required;
  - the gate compares the value THE STORE RETURNED, then re-reads by id because retirement may
    have disabled the row. That stays correct under a collation the engine does not control --
    an operator-supplied database, a restored dump, a column altered downstream -- where the
    column fix alone leaves the gate one ALTER COLUMN from being wrong again with nothing
    reporting it.

Cost is one extra lookup on the bootstrap path only; an ordinary login does the single lookup
it always did plus a string compare, so the original guard's stated intent is preserved.

The test file keeps its own retraction: both gate tests PASSED against the unfixed code in
their first form, because they used the supersession arm and `create_local_user` retires the
bootstrap eagerly at service.py:2685 -- the account was already disabled before the login ran.
Supersession can never exercise this defect; only the expiry arm reaches the login path with
retirement still pending.
…entity decision may depend on store collation (BACKLOG #1268)

Records the decision behind the #1268 fix, because the diff shows WHAT and not WHY, and the
why is the half that decays. Two rules:

  1. Usernames are case-sensitive, on every backend.
  2. No identity decision may be delegated to store collation -- compare the value the store
     RETURNED, never the caller's input.

Rule 2 is the load-bearing one and is deliberately independent of rule 1, so the gate stays
correct on a database whose collation the engine does not control.

Rejected alternatives are recorded with their reasons, because each is re-proposable and each
is wrong non-obviously. Case-INsensitive normalisation is the one a reader will reach for
first: it requires a canonicalisation that is not locale-neutral (the Turkish dotless i), so a
wrong fold silently MERGES two accounts -- and a UNIQUE constraint would enforce that merge
rather than catch it. It would also have to hold across three backends and the audit trail,
every one a fresh place for the two rules to diverge again.

Flagged and deliberately NOT decided here: two accounts differing only in case are themselves
a confusability risk, and case-sensitivity preserves it. That is closeable additively by a
registration-time refusal, which is a different control and does not require reopening this
decision. Not built, and not filed as an item by this lane.

Also records what the change does NOT do: the DDL is creation-guarded, so an existing SQL
Server database keeps its original column collation and no re-type is attempted. Zero
deployments means nothing to migrate; it is written down so a later reader does not mistake
the schema-hash bump for a column alteration.
…ort it instead of raising (BACKLOG #1259)

The parse_items conflict refusal shipped and is on main. It protected programmatic readers and the
CI leg. NOTHING called it before a commit, so the item's own failure sentence was still true: a
conflicted docs/BACKLOG.md can be COMMITTED and every ledger gate passes over it.

MEASURED at dd655da against a docs/BACKLOG.md carrying a realistic PROSE-level conflict, with the
file restored byte-identical afterwards:

    ledger gate ....... Passed        backlog_status_check.py ... exit 1
    forbidden-content . Passed
    control characters  Passed
    gitleaks .......... Passed        overall rc 0 -- the commit lands
    bandit ............ Skipped

THE FIRST VERSION OF THAT MEASUREMENT WAS CONFOUNDED AND SAID THE OPPOSITE. Injecting a conflict
that ADDS headings made the ledger gate fail -- on OWNERSHIP, because both sides' numbers read as
unallocated, not on the conflict. That reads as "already covered". A real ledger conflict is
usually two sides editing PROSE, adds no heading, and is the case nothing sees. Testing the
heading case instead would have closed the item as a non-defect.

Two parts:

  1. main() catches the ValueError per source and reports it with the PATH, returning 1. It raised
     from inside scan(), so every caller rendered an uncaught traceback -- which reads as "the
     checker is broken" rather than "your ledger is conflicted", sending an author to the wrong
     file, and the exception carries a LINE number but no path while the checker scans the
     published ledger plus every archive by default. Also removes a real double-parse.
  2. A LOCAL pre-commit hook, scoped to the ledger files.

NOT pre-commit/pre-commit-hooks' merge-conflict check, and the reason is not dependency
squeamishness -- that repo is absent here, so it would be a new third-party source with its own
pinned rev, but the deciding argument is SINGLE SOURCE. parse_items DEFINES what a readable ledger
is, and CLAUDE.md section 11 requires this file be read through it and never a hand-rolled scan. A
generic textual matcher is exactly that second, silently different definition.

Scoped to the ledger files rather than always_run: the checker resolves its own sources, so running
it elsewhere buys nothing and spends the credibility a gate needs on the day it fires.
pass_filenames is false because feeding it a staged path list would narrow the corpus --min-items
exists to protect.

Verified both directions: the conflicted ledger now Fails with the checker's own message naming the
file, and the clean ledger still Passes -- a gate that fires on the healthy case is one everybody
learns to skip. Mutation-verified: repointing the hook entry elsewhere reds the wiring test.

The wiring test asserts on DIRECTIVES with comments stripped, not raw text. Its first draft failed
on this commit's own explanatory prose, which names the alternative it rejects -- a substring check
over a commented config cannot tell a wiring from a note explaining why that wiring was not chosen.

NOT DONE: the item's stale DO-NOT-BUILD banner still stands. A builder may not edit the ledger; it
routes to whoever writes banners.
…-authored banner)

ADR 0165 pairing. The fix and ADR 0169 are the builder lane's work; this banner is the
lander's, because a builder may not author ledger content and a PR that honestly cites its
item is red by construction until someone who may write docs/BACKLOG.md does.

Closes 1268 with both limbs recorded, and states the residual rather than only the fix: the
users DDL is creation-guarded, so an existing SQL Server database keeps its original column
collation. Nothing to migrate -- zero deployments, CLAUDE.md section 0 -- and limb 2 is correct
on such a database anyway, which is why it was built not to depend on limb 1.

parse_items before/after: 313/232/81 -> 313/231/82, the expected 0 items / -1 open / +1 closed
for one closure. Item 1268 declares exactly one status; the open-alphabet glyph was REMOVED
from the original filing line rather than left to coexist with the closing banner.

Built with plumbing (read-tree, update-index, commit-tree) so no working tree was touched --
the branch is checked out in a live sibling worktree. Plumbing bypasses pre-commit, so the
ledger, forbidden-content and backlog-status checks were run by hand against the result.
…2's fifth sighting against its own argument

Two body amendments to landed items. No item filed, no item closed, no banner
status changed: parse_items reads 313 items / 232 open / 81 closed before and
after, the expected 0/0/0 delta for an amendment.

#1259 -- the 2026-08-15 banner had INVERTED. It read "DO NOT BUILD -- ALREADY
BUILT AND UNLANDED ON PR #394" and named #394 landing as its own expiry
condition. #394 landed, so the banner then said do-not-build over work that was
only half done. That is the dangerous direction: it removes an item from the
queue so nobody looks again.

The first scope option shipped -- parse_items refuses a source carrying conflict
markers, defined :94, called :132, reached before any parsing. The second did
not, and the item's own failure sentence stayed true: no pre-commit hook called
parse_items, so a conflicted ledger could still be committed with every gate
passing.

Also folded in a measurement the item did not carry, and it is the difference
between a defect and a non-defect here: the SHAPE of the conflict decides
whether anything catches it. A conflict that ADDS HEADINGS is caught
incidentally, by the ownership guard, because both sides' numbers read as
unallocated -- which renders as "already covered". A real ledger conflict is two
sides editing PROSE, adds no heading, and the ownership guard has nothing to
fire on. Measured with a prose-only conflict at dd655da, file restored
byte-identical: every gate passes, overall rc 0, the conflicted ledger commits,
while backlog_status_check.py on the same file exits 1.

#1292 -- a fifth sighting on main's own tip, recorded because it WEAKENS the
argument the fourth entry made rather than confirming it. The ack series is now
13, 14, 24 against a fixed sent=36. The fourth entry's case was that the
shortfall reproduces to within one ack, which is what made "systematic"
persuasive; 24 is a spread, not a reproduction. The verdict is NOT flipped to
starvation -- two clustered points and one outlier is not a distribution. What
changed is that neither reading now has an argument from reproducibility, so the
store-side discriminator is the only thing that settles it.

The same entry records that this sighting is NOT what was redding main. That
run's failing assertion is tests/test_connscale_smoke.py:128, the FD probe.
no_loss passed, and it is computed as agg_sent minus engine_read so acked is not
in it at all. The sent/acked figures are real and incidental -- pytest prints the
record's full repr on any assertion failure, and three sessions attributed the
red to this item because the numbers pattern-matched it. When a sighting names
an item, quote the line with the caret, never the values in the dump.

Amendments to items already on origin/main add no heading, so the ledger gate's
ownership check is never consulted; docs-only, so the claim gate's code-touching
scope does not apply. Every added line verified cp1252-encodable.


Cherry-picked by the lander onto PR #458 under ADR 0165: the dispatcher seat authored these
dispositions and holds no push route, so routing them separately would have left #1259's INVERTED
do-not-build banner standing over work landing in this very PR -- the #60 failure the hygiene gate
exists to prevent. Applied with plumbing; merge-tree with a COMMIT-scoped base returned rc=0.

Correcting one clause of the handoff that offered this commit: it said the change 'touches no file
your branches touch'. It does -- all three commits touch docs/BACKLOG.md, the single-writer file.
The conclusion (it cannot conflict) is right and measured; the stated reason is not. The regions are
disjoint by ITEM, not by file.
…llision

PR #428 landed as 4c28bad adding ADR 0167's index row; this branch adds 0169's. Both are
appended as the LAST line of docs/adr/README.md, so git sees one line changed two ways. Every
other path content-merged cleanly, including docs/BACKLOG.md and auth/service.py, which are
changed on BOTH sides.

RESOLVED AGAINST A COMPUTED TARGET, NOT BY EYE, because an index is exactly where keep-both-sides
is a hope rather than a resolution:

    A = rows at merge-base   157
    B = rows at PR head      158   (adds 0169)
    C = rows at origin/main  158   (adds 0167)
    target = C + (B - A)     159

    result                   159 rows, 0167 present, 0169 present, ZERO duplicate numbers

Built with plumbing; no working tree touched.
# Conflicts:
#	docs/adr/README.md
…its EXIT CODE asks (BACKLOG #1235)

`is_live_shape` was extracted so the exit list and the test stop restating the rule. The branch
that NARRATES the verdict to a human was left behind, still testing `hit.number <= floor` alone
and never consulting `pr_shaped`.

MEASURED at 4c28bad, after the single-definition refactor landed: six hits printed BOTH

    [PR/issue/foreign-repo shaped -- very likely NOT a backlog citation]
    -> ABOVE THE FLOOR (1295) ... This is the live shape.

two contradictory annotations on the SAME hit, two lines apart. All six are genuinely foreign
(a code-server discussion, a Mirth discussion, a pyodbc issue). The exit code passes over them
correctly; only the prose called them live.

THE PART WORTH KEEPING: this survived the very refactor whose subject is single-definition
cleanup. The definitions a tool COMPUTES with got unified; the one it NARRATES with was missed,
because no assertion reads it. A third definition is not caught by the pass that removes the
second.

The test asserts the CONTRADICTION rather than either sentence alone -- in isolation each is
true, since the hit IS above the floor and IS pr-shaped. Only their co-occurrence on one hit is
the defect, so only that can pin it. Paired with a negative control: a genuinely live citation
must still be narrated as the live shape and must still fail the gate, or the fix trades a wrong
answer for no answer.

Mutation-verified: restoring the floor-only branch reds exactly one test, and the suite returns
to green on revert.
…t resolutions of one conflict

Both this seat and the lane resolved the SAME docs/adr/README.md append collision against 4c28bad
independently, neither knowing the other was on it. That is a coordination failure and it is mine:
I pushed to a branch a live session was holding, having told that session the freeze SHA one message
earlier.

The lane's branch carries one thing origin does not -- BACKLOG #1235's annotation fix, making the
citation gate's ANNOTATION ask the same predicate its EXIT CODE asks. Origin carries three the lane
does not: #1268's closing banner and the dispatcher seat's #1259 / #1292 amendments.

Neither side is discardable, so this merges rather than picks. merge-tree rc=0 -- no conflict.

Verified on the result rather than assumed:
  - docs/adr/README.md: 159 rows, 0167 AND 0169 both present, zero duplicate numbers
  - docs/BACKLOG.md blob differs from BOTH parents, which is what a correct three-way merge looks
    like and is the check that catches a silent revert
  - zero conflict markers in any file
…ind fail-opens (BACKLOG #1229 residual)

The gate blanks quoted spans before any rule reads the line. It already learned once that two
ordered regexes cannot express "whichever quote opened first owns the span" -- that was the
straddle #1229 fixed. Two more places were still deciding span boundaries without that rule.

ONE: the scan resolved OWNERSHIP correctly and ignored ESCAPING entirely. In sh a backslashed
quote is an ordinary literal and the command around it RUNS, but the scan read it as an opener,
paired two of them across a live command and deleted the middle. Nothing reached any rule.

    echo \" ; git -C <governed> checkout main ; echo \"     ALLOW
    echo \" ; git -C <governed> reset --hard ; echo \"      ALLOW   (destructive)

RULE-AGNOSTIC, and that is the reason it matters more than a missing case: it is a SCANNER
defect, so it disarms whatever rule sits behind it rather than one verb.

TWO: the quoted-program-path collapse was two ordered regexes run BEFORE the scan, double quotes
first -- the exact shape the scan replaced. It could pair a quote with a distant path token
ACROSS a gated command and rewrite the whole middle to a bare token, verb and arguments gone.

Both are now decided inside the single left-to-right pass, on spans it already owns. The
asymmetry is deliberate and is shell semantics, not an optimisation: a backslash escapes inside
a double-quoted span but NOT inside a single-quoted one, where sh gives it no special meaning.
Treating them alike would swallow the rest of a line from a trailing backslash, which is the
fail-open direction this function exists to avoid.

A THIRD GAP FOUND WHILE TESTING, WITH NO ESCAPE INVOLVED. PowerShell -replace is
case-insensitive by default, so the old collapse accepted every spelling by accident while the
rules downstream compare case-sensitively -- the token survived in a form they skip:

    "...\git.exe" -C <governed> reset --hard    DENY
    "...\GIT.EXE" -C <governed> reset --hard    ALLOW

Now canonicalised on emit, which is the fail-closed direction: it hands the rules a verb to
judge rather than a token they ignore.

ALL PRE-EXISTING, none a regression, and all three survived a dedicated pass over this exact
function. Nothing in the repository could see them: across the 13 existing gate suites, ZERO
carried a backslash-escaped-quote case (positive control: the token appears in four of them,
every hit unrelated).

The new suite carries both arms -- the escape must not hide a command, and a genuine quoted span
must still be blanked so a commit message cannot supply a verb. Without the second, "never blank
anything" would pass. Mutation-verified per arm with an applied-check on each mutant, because a
mutation that silently fails to apply reads exactly like a test that cannot fail: neutering the
escape arm reds the escape case, neutering the collapse reds the suite, dropping the
canonicalisation reds the two case-varied spellings, and the file restores byte-identical.

Verification scope: 581 passed / 13 skipped across every test_worktree_gate* module, ruff and
ruff format clean, mypy strict clean over 267 files. The x12 and xml extras are absent from this
venv, so this does not reproduce CI collection.

A case for `worktree add` was written and REMOVED rather than fixed around. A report said the
escape hid it; the discriminating probe -- same command with and without the escape -- allows
either way, so the escape hides nothing there and the rule-3b resolver says why. Keeping a
failing case built on a false premise would have pressured the next reader to widen a security
gate until it went green.
…ver it

The lane handed me tip b05b48f (7 ahead of main at 13dd27d, by its own
measure). Origin's head was 5627d4a, and b05b48f is NOT a descendant of it:
the two lines had diverged by 4 commits on origin against 5 on the lane.

A force-push of the handed-off tip would have discarded all four, which is why
this is a merge:

  5627d4a  merge of the lane's 22ff4bb into the PR head
  c6070a4  merge origin/main -- ADR index append collision, already resolved
  a663b89  backlog: retire 1259's inverted banner, record 1292's fifth sighting
  92d9002  backlog: close 1268, both limbs (lander-authored banner)

Verified before committing, because a clean merge is not the same claim as a
correct one: the merged tree carries fb1f53c's two files byte-identically to
the fix (scripts/hooks/worktree_gate.ps1 and the escaped-quote test), so
combining the sides did not restore what the fix removes.

Note for the lane: the ADR index did not collide on your merge because c6070a4
had already resolved it on origin, not because this wave stopped colliding.
…d on a clean run (BACKLOG #1235)

Two more ways this gate said "clean" without that meaning anything.

ONE: THE CLI FALSE-CLEANED ON AN EMPTY POPULATION. The default path list is CWD-relative
(`Path("docs").rglob(...)`), not repo-relative. Measured from a directory with no docs/:

    No unresolved backlog citation in 0 file(s).
    Resolved against 0 allocated item numbers (open and closed).
    exit 0

Both counts zero and nothing objecting -- a green that is a statement about the ENVIRONMENT
rather than the subject.

AND IT ARMS BY CLOSING THE ITEM, which is why it is not a hypothetical. The >200-file population
floor lives only in test_the_docs_scan_actually_covers_something, so it guards the pytest arm and
nothing else. Wiring this CLI into .github/ or .pre-commit-config.yaml is the documented way to
satisfy the item's first disjunct -- and doing so inherits a gate that reports clean when it
scanned nothing.

Now refuses non-zero on an empty path list OR an empty allocation, and prints what it scanned and
the directory it scanned from, because the cause is a relative default and the count alone does
not show that. Refusing on an empty allocation too is deliberate: an unreadable ledger resolves
every citation to "no filed item", which fails the other way and would bury a real run in false
positives. Both zeros mean the same thing -- the tool is not looking at this repository.

TWO: THE COVERAGE BOUND WAS ABSENT FROM EXACTLY THE RUN A READER ACTS ON. "Not scanned: the
private companion repository" printed only after the hits loop, so the early clean return skipped
it -- the bound missing at the moment someone concludes "clean", which is the whole population it
exists to qualify. It is in the module docstring, and a docstring is not what a CI log shows.

Now one named constant printed on every exit path. That is this item's own single-definition rule
applied to prose: two copies of a caveat drift, and the copy that goes stale is the one nobody
reads, because it only prints on the path they are not on. A test pins the single definition.

Both arms carry their control -- a populated scan must still reach a verdict rather than trip the
emptiness guard, or the fix is "always refuse"; and the clean path must be the clean path, or the
bound assertion proves nothing. Mutation-verified with an applied-check on each mutant, since a
mutation that silently fails to apply reads exactly like a test that cannot fail: disabling the
emptiness guard reds one test, removing the clean-run bound reds one test, and the file restores
byte-identical.

Verification scope: 45 passed in tests/test_dangling_citation_check.py, ruff and ruff format
clean, mypy strict clean over 267 files. The tooling tier was still running when this was
committed and is NOT claimed green here. x12 and xml extras are absent from this venv, so none of
this reproduces CI collection.
…ift guard demanded

CI red on 1a7a31c: test_every_non_engine_test_is_classified failed with

    assert not ['test_worktree_gate_escaped_quote.py']

The new test imports no engine module, so the partition guard requires it to be named
either in tests/tooling_manifest.txt or in _STAYS_WITHOUT_IMPORTING. That guard exists
precisely so a new worktree-gate test cannot quietly join the engine legs and grow the
tier back, which is what would have happened here.

The manifest is the right side and the choice is not a judgement call: all thirteen
sibling test_worktree_gate_*.py files are listed there and none appear in
_STAYS_WITHOUT_IMPORTING. Placed between _emitter and _git to match the block's order.

Verified before committing: the named file exists at this commit, so the companion check
that every manifest name resolves to a real file stays satisfied, and the name appears
exactly once so the duplicate check does too.

Lander-authored on the builder-1 lane's behalf -- the fix is a mechanical registration
fully determined by the guard's own message and by where its siblings already sit.
One genuinely new commit, 1972575. The lane's previous tip b05b48f is already
fully merged here (measured: zero commits outstanding on that side), so this
brings only the #1235 work and not a re-merge of what landed earlier.

Merged rather than taking the handed-off tip, for the same reason as last time:
origin carried six commits the lane's tip did not, including the escaped-quote
manifest registration that cleared this PR's CI red. Verified in the result --
the merged tree keeps tests/tooling_manifest.txt at 122 lines with the
escaped_quote entry present exactly once, and carries the lane's two files.

CARRIED FORWARD FROM THE LANE, UNVERIFIED AND LABELLED AS SUCH: the tooling tier
was still running when this was committed and its result is not in hand. That
tier is where the changed gate actually runs, so it is the relevant regression
check. What is verified is 45 passed in tests/test_dangling_citation_check.py,
both arms mutation-verified with a byte-identical restore, ruff and ruff format
clean, mypy strict clean over 267 files. That is not a green tier and is not
offered as one.

BEHAVIOUR CHANGE, deliberate: the CLI now exits non-zero where it previously
exited 0 on an empty population. Anything invoking it from a directory without
docs/ will now red where it passed silently. That is the fix, not a regression.
…LOG #1229 residual)

The comment above the parametrisation said the verb set covers more than one rule, because the
defect is rule-agnostic. IT DOES NOT. Measured, which is the only reason I know: both verbs come
back with the SAME rule's message -- "would change the working tree of the SHARED PRIMARY
checkout". Two verbs, one rule.

That matters because the property the suite claims to pin is precisely the one it cannot see. A
scanner defect disarms every rule equally, so coverage that would actually pin it has to exercise
DIFFERENT rules; exercising one rule twice tests the parametrisation, not the property.

And "denied" is a weaker claim than "denied by the rule we think". A suite asserting only that a
deny happened cannot distinguish a rule-agnostic scanner fix from a lucky overlap in one rule's
matching. The stronger form -- reading back the rule ids the gate RECORDS and asserting they are
distinct -- was built independently on another lane, cross-checked at 10 of 11 rows passing
against this gate unchanged, and that is the right home for the coverage. This file should not
grow a second, thinner version of it.

Corrected in place rather than deleted. A reader who saw only a fixed comment could not tell this
suite had ever asserted coverage it does not have, and the gap is real.

NOT fixing the coverage here, deliberately: widening this file would duplicate work that exists
and is better, and the decision about which suite lands is the Dispatcher's and the Lander's.
… not this one (BACKLOG #1235)

This file already opens by distinguishing itself from `backlog_citation_check.py` on SEMANTICS --
that gate asks whether a citation names the right ledger FILE, this one asks whether the number
names anything at all. The names differ by one word and the paragraph exists because of it.

They also differ on WIRING, and that is the half that misleads. The sibling is run by
backlog-hygiene.yml and its test is named in ci.yml's docs-lane list. THIS script is run by no
workflow and no pre-commit hook; it reaches CI only through its test, which is manifest-classified
and therefore runs on the tooling leg.

MEASURED WITH A POSITIVE CONTROL IN THE SAME PASS, because a zero without one proves nothing:
`dangling_citation_check` returns ZERO hits across .github/ and .pre-commit-config.yaml, while
`backlog_status_check`, `ledger_check` and `scan_forbidden` return 3, 2 and 3. The probe finds
wiring where wiring exists, so the zero is a real zero.

WHY IT IS WORTH WRITING DOWN RATHER THAN LEAVING TO BE LOOKED UP: a grep for `citation_check` HITS
A WORKFLOW, and the hit belongs to the sibling. The answer is confident, well-formed, and wrong in
the direction of believing a change here is CI-covered when it is not -- which is the direction
that lets a behaviour change ship unexercised.

I asked exactly this question about my own emptiness-guard change one commit ago and reported the
answer as ONE PROBE rather than a clearance, because I had grepped the NAME while the gate is
wired by manifest PATH. The gate-fix seat ran the path angle independently, with the control
above, and cleared it. The clearance is theirs; this paragraph is so the next person does not have
to spend the same round trip -- or, worse, gets the sibling's hit and stops.

Comment-only. No behaviour change.
TEST-ONLY. The fix this rests on is already on this branch; nothing here
changes the hook.

The escape-blind span scan blanks text BEFORE any rule is dispatched, so it
disarms whichever rule would have judged that text. The suite shipped with the
fix parametrises two verbs and both reach the SAME rule, the one guarding the
primary working tree. A rule-agnostic defect with single-rule coverage rebuilds
the condition that let this class live in the first place: fourteen green gate
suites, none able to see it. A later change repairing one rule's own matching
while leaving the scan blind would keep every existing row green.

This adds one arm per rule -- primary working tree, linked worktree hijack,
shared git configuration, worktree removal -- against a real repository,
because three of the four ask git itself what it is looking at and are
unreachable from a synthetic fixture.

IT ASSERTS THE RULE IDS THE GATE ITSELF RECORDED, not merely that a deny
happened. Those are different claims, and only the second catches a fix that
denies for an accidental reason; a suite checking outcomes alone passes when
the right answer arrives by the wrong route. The ids come from the gate's own
receipt log rather than from the deny prose, which is written for a human and
is rewritten whenever a remediation changes. That instrument carries its own
positive control, because a reader whose pattern has drifted returns a partial
set and can satisfy the assertion by accident.

Every offender row runs its bare, unescaped command first and requires it to be
gated. An arm that cannot fail is not evidence, and that is not hypothetical
here: a synthetic fixture reported an ungoverned verb as a fail-open when it
was simply unreachable.

Measured: five rows red against the pre-fix hook and green against the fix on
this branch, while the remaining rows stay green against both.

ONE ROW FROM THE ORIGINAL DRAFT WAS DROPPED RATHER THAN PORTED, said here so a
deletion does not read as coverage quietly reduced. It pinned a different
design for the same function -- refusing to let an escape extend a
double-quoted span -- and this branch deliberately honours the escape there,
which is what a shell does. That row asserted a deny no shell requires, so it
is a statement about the rejected design and not about the gate.

The file is named for what it adds rather than for the defect. The obvious name
sat one character from the suite already beside the fix, which is a trap in a
directory a reader scans by eye.

Classifies itself in tests/tooling_manifest.txt. Without an entry
test_every_non_engine_test_is_classified fails, and the classification is what
decides which CI leg a suite runs on -- an unclassified tooling suite can be
deselected on the engine legs and unreached by the tooling job, leaving it
present, green, and never actually run. That is the condition a gate test can
least afford, and it is the same shape as the defect this suite covers.

The sibling suite's own manifest entry is NOT added here: it is already
upstream, landed separately after CI went red on exactly this check. Recorded
because its absence from this diff is otherwise indistinguishable from not
having noticed -- the same reason the dropped row above carries a reason.

Deliberately not duplicated: the commit-message and unterminated-quote
boundaries are pinned in the quote-straddle suite, and the escape-inside-a-real-
span control and the program-path spellings in the suite beside the fix.

Not an arm, and recorded because it was reported as one: git worktree add
allows with AND without the escape, measured independently by two sessions on
separate trees. It is not evidence about escape handling, and whether it should
be governed at all is a separate question.
…ix re-opened #1229 on PowerShell (BACKLOG #1229 residual)

MY PREVIOUS COMMIT INTRODUCED A REGRESSION AND THIS IS IT. Honouring a backslash escape inside a
double-quoted span is correct POSIX. But line 999 scans BOTH tool names through ONE matcher --
`$tool -in @("Bash", "PowerShell")` -- and POWERSHELL HAS NO BACKSLASH ESCAPE; its escape is the
backtick. On a PowerShell payload the scan held a span open that PowerShell had already CLOSED,
straddled the live command between it and a later quote, and blanked it.

THAT IS #1229's OWN STRADDLE, RE-CREATED BY THE FIX FOR #1229, ON THE OTHER HOST.

    Write-Output "C:\Temp\" ; git -C <governed> reset --hard ; Write-Output "x"    ALLOW
    ... one fewer backslash (control)                                              DENY
    ... two backslashes (even count)                                               DENY

An ODD count before the closer was the trigger, and the verdict had MOVED against origin/main.

FOUND BY THE SEAT WHOSE DROPPED DESIGN ROW ASSERTED EXACTLY THIS PROPERTY -- a row I argued should
go, and which they dropped agreeing with me. Their commit message called it "a deny no shell
requires". True of sh, FALSE OF POWERSHELL. I made the argument; neither of us checked the second
half; the conservative design was conservative for a reason we did not articulate.

THE FIX: `$PosixEscapes`, threaded from the tool name through Get-ScannableSegments to the scanner,
gating BOTH escape arms. DEFAULT $false, and the direction is the point -- honouring the escape
makes spans LONGER, so it blanks MORE and can hide a command (fail OPEN); refusing it leaves more
text visible to the rules (fail CLOSED). An unrecognised host gets the conservative reading and
only a host known to use backslash escapes opts in.

EVERY TEST ROW IS PINNED TO WHETHER THE COMMAND ACTUALLY EXECUTES ON THAT HOST, measured with an
inert payload that COMPUTES (111*3 -> 333) rather than echoes, so an echo-back could not be
mistaken for a run:

    odd-BS       PowerShell   333 printed, middle RAN            -> DENY
    odd-BS       Bash         "unexpected EOF", nothing parses   -> ALLOW is harmless
    straddle     PowerShell   no 333, middle did NOT run         -> ALLOW is CORRECT
    straddle     Bash         333 printed, middle RAN            -> DENY

The two ALLOW rows are not concessions: denying them would be a FALSE DENY on a command that
cannot run. "Should this deny" is a question about the shell, not about the gate -- so the rows
record the observation rather than a previous verdict, and a gate that denied everything would
fail them.

Verification: 586 passed / 13 skipped across every test_worktree_gate* module, ruff clean. x12 and
xml extras absent from this venv, so this does not reproduce CI collection.

NOT CLAIMED: the same lens reports this one character also disarming rules 3c and 3d. I have not
reproduced those and they are not addressed here.
…suite

The lane based 4d46a2a on 0642792 and reported it as one ahead of 49f425b. It is one
ahead of 0642792: the gate-fix seat's four-rule coverage suite (49f425b) landed on this
branch in between, so the two lines had diverged one against one.

Taking the handed-off tip would have discarded that suite -- the only coverage this PR has
that exercises more than a single rule, and the coverage whose absence let the regression
this very commit fixes go unnoticed. So this is a merge.

Verified in the result rather than trusted for merging cleanly: the merged tree carries
4d46a2a's worktree_gate.ps1 byte-identically (the $PosixEscapes fix, defaulting to false
so an unrecognised host gets the fail-CLOSED reading), the coverage suite file, and the
manifest at 123 lines classifying both new test files.

458 REMAINS HELD AND AUTO-MERGE STAYS OFF. This commit clears the cause the hold was
placed for; it does not clear the hold. The gate-fix seat's refutation phase has not
finished, and two of its findings -- the same character reportedly disarming rule 3c
(shared core.hooksPath) and rule 3d (another session's worktree) -- are unreproduced and
unaddressed here, as this commit's own message states.
…ing about where a span ends (BACKLOG #1229 residual)

SECOND REGRESSION FROM MY OWN ESCAPE FIX, and the host flag could not close this one because the
failing host is BASH -- where the escape is real and honouring it is correct.

The interpreter-argument regex was `[^"]*`, which is escape-BLIND: it stops at the first quote,
including an escaped one. Once the span blanking became escape-AWARE, the two disagreed about
where the argument ends:

    bash -c "bash -c \"git -C <governed> reset --hard\""
    extraction got:   `bash -c \`     truncated at the escaped quote, no verb in it
    blanking removed: the whole span
    result:           nothing reached any rule -> ALLOW

MEASURED: origin/main DENY x3, the escape-aware fix ALLOW x3, control DENY on both. The inner
command really runs -- `bash -c "bash -c \"expr 111 \* 3\""` prints 333, so this is execution and
not a parse curiosity.

ON MAIN THE TWO AGREED BY ACCIDENT, both being escape-blind, which left the verb visible OUTSIDE
the span. My fix removed the accident on one side only. That is the whole defect: not a wrong
rule, a rule applied to one of two places that had to move together.

Fix: the double-quoted extraction becomes escape-aware under the same $PosixEscapes flag, so both
sides share one answer. The SINGLE-quoted arm stays escape-blind, matching Remove-QuotedSpans --
sh gives the backslash no special meaning inside a single-quoted word.

Found by the gate-fix seat, who separated introduced-from-inherited by running every shape against
BOTH gates rather than arguing about it, and who stood down mid-dispatch on the collision guard
rather than overwriting this file while I was editing it.

NOT CONFIRMED AND NOT CLAIMED: the same lens reports rules 3c and 3d disarmed by this character.
My reproduction of those does not reproduce -- my CONTROL allows on origin/main too, which means
my harness is not exercising rule 3c at all, so it is evidence about my construction and not about
the gate. Their measurement stands unrefuted; mine is simply not a test of it. Left open rather
than claimed either way.

Verification: 587 passed / 13 skipped across every test_worktree_gate* module, ruff clean. x12 and
xml extras absent from this venv, so this does not reproduce CI collection.
…ail both ways

TEST-ONLY, and it corrects a weakness in my own earlier commit rather than
anything in the gate.

An adversarial pass asked whether this suite could pass VACUOUSLY. Mutation-
tested standalone, with a per-mutant applied-check, it could not:

    gate mutated to deny every verb against a governed tree   7 passed, 0 failed

It could see a gate that had stopped denying and NOT one that had started
denying everything. Every row it carried agreed that a deny was correct, so a
fix that widened the gate until the offenders passed would have satisfied all of
them.

THE CAUSE WAS A RULE I APPLIED IN THE WRONG PLACE. The earlier draft dropped its
must-ALLOW rows on the grounds that equivalents live in the sibling suites, and
that a load-bearing fact should be stated once and linked. That is right for
PROSE and wrong for a TEST: a suite discriminates only through the assertions it
actually runs, and delegation is not coverage when the file is read, run, or
trusted on its own.

So each rule now has an ordinary use of the same subject that must be ALLOWED --
a commit message for the primary-tree rule, creating a NEW branch for the hijack
rule, a harmless key for the shared-config rule, a read-only listing for the
worktree-removal rule. Deliberately close to each offender: same subject,
differing only in being legitimate.

After the change, measured the same way:

    deny-every-verb mutant                    3 failed   (was 0)
    scanner replaced by the identity function 1 failed   (was not scored)

THE MUTATION HARNESS CARRIES AN APPLIED-CHECK AND IT EARNED IT IMMEDIATELY: it
hashes the file and refuses to score a mutant that did not change it. The
identity mutant did not match its anchor on the first run and was reported as
NOT SCORED rather than as survived -- and separately, an ad-hoc probe written
without that check silently measured an unmutated gate and read as a clean pass.
A mutant that fails to apply is indistinguishable from a test that cannot fail.

ONE MUTANT STILL SURVIVES AND IT IS NOT A GAP. Disabling the escape branch
OUTSIDE a span leaves the in-span rule, which makes the span unterminated, and
an unterminated span is emitted RAW -- so the command stays visible and the gate
still denies. Verified rather than reasoned: the mutated gate was driven against
the offender payload directly and returned DENY. There is nothing for the suite
to see, so passing is correct. A plausible cause that retires a finding without
being tested is how a real gap gets closed on paper.

The decisive check is against a REAL gate rather than a synthetic mutant: this
file still goes five rows red against origin/main's hook and green against the
fix on this branch.
…sed (BACKLOG #1229 residual)

TWO OF MY COMMIT MESSAGES CARRY A FALSE CLAIM AND A COMMIT MESSAGE CANNOT BE AMENDED ONCE PUSHED,
so the correction goes where the next reader will actually be: beside the tests.

4d46a2a and c308cc3 both say rules 3c and 3d are unaddressed and that I did not reproduce them.
THEY ARE CLOSED. Verified COLD by the seat that found them, on a rig predating my fix that I did
not write: all four PowerShell shapes now DENY where the previous fix ALLOWed, 3c (shared
core.hooksPath) and 3d (another session's worktree) included.

MY NON-REPRODUCTION WAS MY HARNESS. My control allowed on origin/main too, which means my shapes
never exercised rule 3c at all. I said so at the time and left it open rather than claiming either
way, which was right -- but the note as written sends the next reader hunting two live defects
that do not exist, and an honest hedge that misdirects is still a defect in the record.

Gating the escape AT THE SCANNER closed all three rules at once. That is what rule-agnostic cuts
both ways means: one character disarmed three rules and one flag re-armed them.

AND MEASURING AGAINST MAIN FOUND SOMETHING I DID NOT SET OUT TO FIX:

    one-level escaped interpreter arg    main ALLOW -> DENY     INHERITED, now closed
    two-level escaped interpreter arg    main DENY  -> DENY     my regression, repaired

The left column is the point. A fix judged only against the branch it repairs cannot tell
"restored" from "improved" -- I would have reported the weaker claim, and the inherited hole would
have stayed open with nobody counting it as anyone's.

LEFT OPEN AND NOT CLAIMED: twelve further shapes the same lens reports as inherited from main.
None introduced here, none fixed here, with the Dispatcher as a disclosure question.
… would neuter this test (BACKLOG #1229 residual)

460's escape-sequence gate flags three invalid escapes in this file. They are real: each becomes a
SyntaxError on a future Python and would fail at COLLECTION, so the module vanishes and a security
regression suite stops running while everything stays green. Exactly the class this file is about.

BUT THE GATE'S PRESCRIBED REMEDY -- "make the whole literal raw" -- IS WRONG HERE AND WOULD HAVE
SILENTLY NEUTERED THE REGRESSION ROW. `_ODD_BS` contains BOTH an invalid `\T` and a valid `\`.
Raw preserves the `\` as TWO backslashes, turning the line's ODD trailing count into an EVEN one
-- and per the measured table ODD is the ALLOW case under test while EVEN DENIES. The remedy
yields a GREEN test of the non-regressing case.

A remedy that turns a failing check into a passing one by changing what is checked is worse than
the warning it silences, and nothing would have reported it.

Caught by the Lander, who measured it by TRANSFORMING THE SOURCE LINE rather than retyping it --
and whose first attempt was itself corrupted by shell escaping, so they redid it programmatically.
I verified the same way: comparing ast.literal_eval before and after, never reading the line.

    whole literal raw       value preserved = FALSE
    escape only the invalid value preserved = TRUE

Applied to all three: `\T`, and two in a docstring (`` \` `` and `\*`) where the same trap
applies, since that docstring also carries valid `\\"` sequences. Rendered text proven identical
for each, so the documentation displays exactly as before.

Verified after: compiles clean under -W error::SyntaxWarning, 16 passed, and the rendered _ODD_BS
still carries an ODD trailing backslash count -- i.e. the row still exercises the ALLOW case it was
written for.

FOR THE 460 AUTHOR, and it is a real defect in the remedy text rather than a nit: "make the whole
literal raw" is unsafe for any literal that ALSO contains a valid backslash escape. Narrow it to
"escape the invalid sequence".
…G #1229 residual)

The verified-state note claims this change IMPROVES on main -- a one-level escaped interpreter
argument that main allows and this branch denies. It did not say what the construct is, which
makes the claim unverifiable: "main ALLOW -> DENY" asks a reader to take the author's word for
what was measured, and the entire value of that left column is that it can be re-run.

Named now under the owner's ruling that a construct is published only ALONGSIDE THE FIX THAT MAKES
IT INERT. This one is inert in this commit and travels in the same change, so it cannot be lifted
from here and used against this tree. That is the condition the ruling sets, and it is met here in
a way it would not be for the eleven still-live shapes, which stay vaulted and are not named.

Confirmed independently before naming it: the seat that found the class re-ran it cold against
c308cc3 and 7979ac6 on a rig I did not write, with four controls, and reports main ALLOW ->
DENY. The improvement claim therefore does not rest on its author's measurement.

Also records, for the vault inventory, that the inherited list is now ELEVEN rather than twelve --
this shape came off it.
… (BACKLOG #1229 residual)

Remove-QuotedSpans kept the git token of any closed span whose leaf was `git` or
`git.exe`, lowercased, so a quoted PROGRAM path stayed visible to the
case-sensitive rules. That was right about programs and wrong about everything
else: it read `cp -r "/c/backups/Git" restore` -- a backup directory followed by
an ordinary word that happens to be a git verb -- as a git command. At least 15
such shapes were measured DENY here and ALLOW on the blob before the emit landed.

THE FALSE DENY WAS NOT THE WORST OF IT. An argument-position emit makes its line
look like a git command carrying a gated verb, and rule 3 hands the FIRST such
line it finds to Test-WorktreeHijack. Measured from inside a linked worktree,
two lines -- copy a directory named Git, then a real `git switch` onto a free
branch -- the hijack came back ALLOW. So this closes a fail-OPEN as well as a
fail-CLOSED defect, with one predicate.

Test-GitProgramPosition asks whether a command BOUNDARY is reachable leftward
across tokens transparent to command dispatch: pairs this scan already blanked,
assignment prefixes, an allowlist of wrapper words, their options and operands,
completed redirections and shell keywords. A bare word that is none of those ends
the chain, which is exactly what makes cp, mv, ls, echo, python and docker stop
denying while sudo, env, FOO=1, timeout 5 and find -exec keep denying.

BOTH ENDS, per the owner ruling of 2026-08-20 and recorded at the site:
  GAINED -- the argument-position family allows in every case spelling, including
    `cp -r "/c/backups/git" restore`, which DENIED on origin/main as well. That
    is a deliberate DENY-to-ALLOW move against main, not only a repair.
  PAID -- an UNLISTED wrapper word fails open: `myrunner "/usr/bin/git" -C <gov>
    reset --hard` allows where main denied. Pinned as a tripwire that reds when
    somebody closes it.

The `.exe` spelling is governed by the same predicate rather than short-circuited.
A short-circuit costs nothing against the measured false-deny corpus, but it was
measured to leave the shadow fail-open ALLOWing through a file named `Git.exe`,
so the uniform rule is the one that actually closes the hole.

MEASURED over the real hook, 93 rows against origin/main and HEAD: zero controls
moved against HEAD, all 38 program-position shapes deny, the argument family and
the rule 3c/3d argument-position false denies allow, and all three shadow shapes
deny. Pytest with this change: 535 passed 1 skipped over the ten other
test_worktree_gate modules plus test_gate_rule_scan_agreement and
test_gate_installed_parity, 57 in test_worktree_gate_escaped_quote, 28 in
test_worktree_gate_hijack. The one skip is the installed-parity test, which skips
while the gate source is uncommitted and will assert once this lands -- an owner
reinstall from a current checkout is the remedy; install-gate.ps1 refuses under
CLAUDECODE and CI has no installed gate to compare, so green CI is not evidence.
…name (BACKLOG #1229 residual)

The interpreter recursion asked one question -- did the flag SHAPE match -- and
then applied the OUTER tool name's escape convention to whatever it extracted.
Both halves were wrong, in opposite directions, and each alone was measured
insufficient.

ONE: a flag match is not evidence that the program executes its argument.
`$shFlag` is `-[a-z]*c` under (?i), so it matches -C, -ic, -rc, -static, -sync
and -exec, and `$cmdExeFlag` walks an ordinary POSIX path one component at a
time. Over a hand-built axis, 28 of 33 NON-interpreter invocations matched and 18
of 36 real interpreter invocations did not. So `grep -c 'git reset --hard'
/c/logs/history.log` -- an ordinary search of a shell history -- DENIED, and so
did rg, ag, curl, sort, uniq, wc, cut, head, tail, ls, tar, gzip, md5sum, cmp,
diff, rsync, gcc -static and make -C. Driven on the real binaries with a payload
that COMPUTES (expr 111 \* 3 -> 333), every one of those left no marker while
bash -c and sh -c both printed 333. Get-FlagOwner now answers the question, and a
program that does not execute its argument gets no recursion.

TWO: the escape convention belongs to the interpreter that RECEIVES the payload.
A Bash tool call invoking pwsh applied POSIX backslash rules to a PowerShell
payload, so the scan held open a span PowerShell had already closed and blanked
the gated command between it and a later quote. Measured on this box:

    pwsh -NoProfile -Command '$d = "C:\Temp\" ; 111*3 ; Write-Output "x"'  -> 333: it RAN
    bash -c '$d = "C:\Temp\" ; expr 111 \* 3 ; echo "x"'  -> unexpected EOF: INERT

Same characters, opposite correct answers. One flag per line cannot express that,
which is why each extracted payload now carries its own convention taken from
Get-FlagOwner, while the outer line keeps the outer host's rules -- the outer
line really is the outer host's. Three shapes go from a fail-open ALLOW back to
DENY: pwsh -Command, pwsh -c and powershell -Command carrying that straddle.

THE LEFTWARD SCAN IS WHAT MAKES IDENTITY WORKABLE, and it overturns a claim this
file made. The $cmdExeFlag note said the ls /usr/src/c false deny was "a
program-identity problem, and that is why it is not fixed here", resting on five
candidate mutants each defeated by something that executes. Four of the five --
echo hi;cmd /k, (cmd /mnt/c, cmd /d /Q/C, cmd /usr/src/c -- break ADJACENCY, not
identity: every candidate asked whether the token IMMEDIATELY LEFT of the flag
was a cmd spelling. A scan that walks left past options and switch components,
bounded by the last command separator, survives all four, measured. The fifth, a
renamed or aliased cmd.exe, is not closed and is not claimed.

So tests/test_worktree_gate_interpreter_sigils.py's pinned false-deny tripwire
fired, exactly as its own docstring said it would. It is rewritten to assert the
closure with the four adjacency counterexamples as its other arm, and the note it
pointed at is corrected in place rather than deleted.

DISCLOSED COST: `myrunner -c '<gated>'` -- an unknown program with a -c flag --
goes DENY to ALLOW. If such a program is a real interpreter that is a fail-open.
It is the same class as the 18 interpreter spellings the flag matcher already
misses (perl -e, node -e, eval, ssh host CMD), and the previous catch was an
accident of the flag shape rather than a decision, but it is a deliberate move
against origin/main and is pinned as a tripwire.

NOT CLOSED and not claimed: PowerShell's own escape is the BACKTICK and that
convention is modelled nowhere. A -Command payload that closes its inner strings
with a backtick-escaped quote straddles the same way, and allows on origin/main,
on HEAD and here -- measured to really run. Remove-QuotedSpans has a two-valued
convention where three are needed. Named by subject; no number cited.

MEASURED over the real hook, 86 rows against origin/main and HEAD: all three
fail-opens deny, all 26 non-interpreter false denies allow, every one of 30
interpreter controls still denies including the four adjacency shapes, and the
six must-allow controls are unmoved. Zero rows changed by this layer across the
ten pre-existing rig specs.
…G #1229 residual)

test_an_UNLISTED_wrapper_word_is_a_known_open_residual carried two rows under one
heading, and the heading was true of only one of them. `myrunner` really is
absent from Test-GitProgramPosition's vocabulary. `setarch` is PRESENT -- what
ends the chain there is its OPERAND, `x86_64`, a bare word and therefore not
transparent.

That matters because the two have different remedies, and a reader acting on the
wrong one would add `setarch` to a list that already contains it and watch
nothing change. So the allowlist has two ways to lose, and both are now named.

The obvious widening -- let a wrapper be followed by one bare operand -- was
measured rather than reasoned about before being declined: it would close the
setarch row and cost `watch ls "/c/backups/Git" clean`, which is main=ALLOW,
this build=ALLOW today and would become DENY. A real shape traded for a narrow
one, so the row is disclosed instead of closed.

Found by re-reading the committed docstring against the vocabulary it describes,
which is the check that would have caught it before it landed.
… residual)

Get-FlagOwner's docstring said the five shapes that defeated five earlier
program-token candidates were "all five measured to survive" the bounded leftward
scan. That is true of four of them and false of the fifth, and the fifth is the
one a reader needs to know about, because it is the cost this change takes.

The five are `echo hi;cmd /k`, `(cmd /mnt/c`, `cmd /d /Q/C`, an alias, and a
renamed copy of cmd.exe. The first three break ADJACENCY -- every earlier
candidate asked whether the token IMMEDIATELY LEFT of the switch run was a cmd
spelling -- and all three deny under a scan that walks left past options and
switch components. The last two break IDENTITY, and an allowlist cannot close
them: an unknown name gets no recursion.

The $cmdExeFlag note beside it already said this correctly. The function's own
docstring did not, and a completeness claim that is one member too wide is the
liability CLAUDE.md section 11 names.
…LOG #1229 residual)

Remove-QuotedSpans keeps the git token of a closed quoted span so a quoted PROGRAM path
stays visible to the rules. Two changes were layered on top of that and both are withdrawn
here:

  (a) a case-INSENSITIVE emit canonicalised to lowercase, so `GIT.EXE` -- a real Windows
      spelling the case-SENSITIVE rules downstream skip -- would still present a verb;
  (b) Test-GitProgramPosition, added because (a) then read `cp -r "/c/backups/Git" restore`
      as a git command, keeping the token only where the span is dispatched as a program.

WHY, and it is not the false denies. (b) bought those back at the price of two fail-OPENS on
shapes origin/main DENIES, measured over the real hook:

    cmd /c "<...>\Git\bin\git.exe" -C <governed> reset --hard  main=DENY  experiment=ALLOW
    . "<...>\Git\bin\git.exe" -C <governed> reset --hard       main=DENY  experiment=ALLOW
        (the second a PowerShell dot-source, on a PowerShell tool call)

Spending a security gate's DENY to buy a tidier false-deny profile is the wrong direction.
Re-measured after the revert, both rows are DENY again. Get-FlagOwner -- the interpreter
axis, 548cf50 -- is KEPT and untouched; its wrapper set is a different mechanism from (b)'s
and keeps its own coverage in tests/test_worktree_gate_interpreter_flags.py.

The emit is now case-SENSITIVE (-cmatch, no ToLowerInvariant). Not a regression: origin/main
ALLOWs `GIT.EXE` in program position too, by another route -- its collapse regex is
case-insensitive but substitutes $1, preserving a case the rules then fail to recognise.

TESTS RETIRED, each because the behaviour it asserted exists nowhere any more:

  - test_a_wrapper_prefix_does_not_move_a_quoted_git_program_out_of_program_position
    (24 rows) pinned Test-GitProgramPosition's wrapper vocabulary. The function is gone, and
    every row used the GIT.EXE spelling, which measures ALLOW on origin/main as well.

  - test_an_UNLISTED_wrapper_word_is_a_known_open_residual asserted ALLOW for `myrunner` and
    `setarch x86_64` ahead of a quoted lowercase git path. Both measure DENY on origin/main
    and on this tree, so the residual it disclosed does not exist. Its own note says to
    delete the row rather than restore the ALLOW.

  - test_the_argument_position_ALLOW_is_a_RECORDED_WEAKENING_for_the_lowercase_spelling
    asserted ALLOW for `cp -r "/c/backups/git" restore` from primary.parent. It PASSED and
    could not have failed: from that cwd no governed target resolves, so the verdict comes
    from the target resolver and not from the emit. Run from INSIDE the governed repo the
    same line measures DENY on origin/main and on this tree -- the opposite of its claim.

  - test_the_program_path_token_survives_every_spelling lost its `GIT.EXE` and `Git` rows and
    keeps `git` and `git.exe`. The two dropped spellings measure ALLOW on origin/main.

WHAT REPLACED THEM, so neither end of the trade lives only in prose:

  - the argument-position rows now run from INSIDE the governed repo, where the emit is the
    only variable, and each carries a control varying THE LEAF CASE ALONE. Measured 14 of 14,
    both blobs: Title-cased leaf ALLOW, lowercase leaf DENY.
  - the uppercase quoted PROGRAM spelling is pinned as a known-open residual (asserts ALLOW).
  - the lowercase argument-position leaf is pinned as a known-open FALSE DENY (asserts DENY).

Verification: 33-row and 28-row probe matrices driven over the real hook against origin/main
and this tree returned IDENTICAL verdicts on every row -- no weakening and no tightening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wshallwshall and others added 18 commits August 21, 2026 06:09
…redicate (BACKLOG #1229 residual)

Prose only. Every assertion in tests/test_worktree_gate_hijack.py is unchanged and every row
passed before and after: `cp -r "/c/backups/Git" switch` on line one must not eat a real
`git switch <free branch>` on line two.

What the prose said was that these rows guard "the position predicate" and
Test-GitProgramPosition. That function was reverted, so the comment pointed at nothing and
the next reader would have scored the rows as dead coverage.

What they actually guard is the CASE-SENSITIVITY of the emit in Remove-QuotedSpans, which is
why the leaf axis is `Git` / `Git.exe`. A case-insensitive emit hands line one a git token,
rule 3 records that segment, and rule 3b returns ALLOW on the hijack behind it. So relaxing
the case rule is a fail-OPEN on 3b and not only a false-deny question -- which is precisely
why this coverage lives here rather than beside the false-deny rows in
tests/test_worktree_gate_escaped_quote.py.

Also corrected: "the unconditional emit" now reads "a case-INSENSITIVE emit". The emit IS
unconditional in position after the revert, so the old wording named current behaviour while
meaning the withdrawn experiment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t a comment (BACKLOG #1229 residual)

The program-position experiment was reverted because it moved two shapes from DENY
to ALLOW while origin/main denies both:

    cmd /c "<...>\Git\bin\git.exe" -C <governed> reset --hard    main=DENY  experiment=ALLOW
    . "<...>\Git\bin\git.exe" -C <governed> reset --hard         main=DENY  experiment=ALLOW
        (the second is a PowerShell dot-source, on a PowerShell tool call)

That reason lived only in prose -- the narrative block at the top of the file and two
assertion-failure strings -- so nothing executable would have reported it if the
predicate came back. Measured: restoring the experiment blob reds 16 tests, and every
one of the 16 is a FALSE-DENY row whose own text instructs the reader to delete it.
Followed literally, the suite's stated remedy lands both fail-opens green.

The nearest existing coverage is not this shape: test_worktree_gate_interpreter_flags
asserts DENY for `cmd /c '<whole gated payload>'`, where git sits INSIDE the quoted
span. Here git.exe IS the quoted span and the verb is outside it, which is what the
position predicate changed.

Measured on the real hook, cwd = the governed repo:
    both rows                       main=DENY  fb93c9c=ALLOW  HEAD=DENY
    control, UNGOVERNED target      ALLOW on every blob
The ungoverned control is what stops the row degenerating into "this gate denies any
cmd /c"; the fb93c9c column is the anti-vacuity check -- the test genuinely reds if
the predicate returns, rather than passing against anything.

Docstring says DO NOT delete this row on red, inverting the two neighbouring tripwires,
because a red here is a re-opened hole rather than a closed one.

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

#1305 -- the worktree gate matches git by SPELLING, so a case variant bypasses every rule.
Measured against origin/main driving the real hook as a subprocess: a capitalised spelling of
the program name ALLOWS where the lowercase spelling DENIES, with a benign negative control
allowing on both. Not confined to one rule -- checkout allows too. No quoting, wrapper or
escape involved. Windows resolves the spellings to the same executable so all of them RUN; the
gate's comparison is case-sensitive so only one MATCHES. It is matching a spelling and calling
it a program. Reproduces identically on origin/main and on the unlanded lane branch, so
#1229's residual work neither opened nor closed it.

Filed in the ledger rather than vaulted: this guard is coordination tooling, is not shipped in
the wheel, and is not graded by any ASVS cell. The exhaustive spelling-by-rule matrix is kept
in the episode note rather than here.

#1301 -- a banner citing a sha must cite a commit naming its own item. The #1022/#1259
transposition passed every gate: valid Markdown, unchanged item count, untouched status glyph,
and the misplaced paragraph carried no glyph of its own so parse_items had no second banner to
object to. The signal present throughout: the overwritten banner cited two shas whose subjects
both ended in that item's number; the paragraph replacing it cited a sha naming a different
item. Filed as a PARTIAL control and described as one -- it fires only on banners citing shas.

The forbidden-content gate blocked the first attempt at this commit over a branch slug in
#1305's prose. Removed rather than allowlisted, per the gate's own instruction.

Both appended at the file tail, the lowest-conflict edit against the re-scored main.
parse_items: 314/230/84 before, 316/232/84 after -- the expected +2 open, no other movement.
Committed from the worktree that OWNS the two allocations, which is what the
ledger gate is for rather than something to route around. BACKLOG #1301 and
#1305 are allocated to this worktree (claude/builder-1-160cec); a merge made
from any other worktree trips ledger_check.owns(), and alloc.ps1 has no
transfer verb. Nothing was renamed, copied or --no-verify'd.

Also brings in origin's three commits, which this branch's local tip lacked.
That side and this one had DIVERGED, so a force-push would have discarded the
four-rule coverage suite -- the third time this lane has met that trap.

Conflicts, each resolved on what the hunk actually is:

  * #1022 -- TOOK MAIN. Both sides retire the same 2026-08-15 do-not-build
    banner; main's pass is later (08-21 vs 08-20) and names the two commits
    PR #394 carried, so this lane's copy is superseded, not lost.

  * #1268 -- TOOK THIS LANE. Main still scores it open because on main both
    limbs do survive; this branch is what ships them. Taking main's re-score
    would have landed the fix under a banner saying it was not built, and
    dropping that banner is what the one-item-one-status rule requires.

  * the new-item block -- TOOK BOTH. A pure insertion collision: main appended
    #1299-#1317, this lane appended #1305 and #1301, no number claimed twice.
    Either side alone silently deletes real filings.

  * docs/adr/README.md -- TOOK BOTH, ordered 0169, 0170, 0171.

backlog_status_check: 568 items, each declaring exactly one status.
worktree_gate.ps1 verified content-identical to this lane's fixed copy after
both merges, so neither merge quietly reverted the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves the one conflicted file, docs/BACKLOG.md, in two hunks. Files nothing
and fixes nothing -- no item's disposition moves here.

HUNK 1, under #1317: took main whole. The scoping clause this branch carried
("That cell's own ruling is a separate question about allowlist width; this item
is the build") is ALREADY the last line of main's side, its sentence completing
past the conflict at "which both readings of the cell now require". The lander's
approved plan said re-attach it; re-attaching would have DUPLICATED it. Asserted
in the resolution script rather than trusted, so the merge fails rather than
silently duplicating if that ever stops holding.

HUNK 2: kept both sides. A pure tail append -- main adds #1318-#1323, #1327,
#1328; this branch adds #1305 and #1301. Neither number is on the other side.

VERIFIED with parse_items, not a hand-rolled scan: 340 items resolved against 338
on main and 332 on HEAD, ZERO lost from either side, added-beyond-main is exactly
[1301, 1305], no duplicate numbers. Negative control main-minus-HEAD returns
[1318..1323], so the set comparison can distinguish and the zeros are not vacuous.
…BACKLOG #1314)

Both SMTP connectors branch three ways on TLS posture. The `not use_tls` and
`not tls_verify` arms each carry an escape gate AND an absolute credential gate.
The verified arm carried a body guard and no credential gate at all -- while
`tls_check_hostname` is read right below it and fed to the context builder. So
`use_tls=true` + `tls_verify=true` + `tls_check_hostname=false` + a username
constructed silently, and `smtp.login()` then ran against any peer whose cert
chained to the anchor, whatever name it was issued to.

Adds a third credential gate to each, matching the wording and the ABSOLUTE
placement of the two beside it. Keyed on no escape, deliberately: in both existing
arms the escape governs the BODY posture and never the credential, and a hop must
not be able to attest its way to a credentialed unverified-name session.

DIRECT HAD NO `else` ARM AT ALL -- the chain fell straight through to the context
build -- so that branch is new rather than extended. The gate is inlined in both
files rather than extracted, which is this cell's stated convention: the verify-off
arms say a shared helper was refused because "a third spelling is how the next bug
gets written".

VERIFIED, and the controls are the point:
- RED FIRST on email: "DID NOT RAISE ValueError" before the gate existed.
- Direct's tests were written AFTER its gate, so they had never been red. Disarmed
  the gate to `if False and ...`: the refusal test failed and a control still
  passed. Restoring it revealed a second thing worth recording -- `git checkout --`
  reverts to HEAD, which never held the fix, so it destroyed the gate instead of
  the mutation. Caught by asserting the restore rather than assuming it.
- NEGATIVE CONTROL both files: `tls_check_hostname=false` with NO username still
  constructs, so the gate is keyed on the credential and does not widen the arms.
- POSITIVE CONTROL both files: a fully verified hop WITH credentials still
  constructs, so the refusal tests are not green by refusing everything.
- 66 passed across both modules; ruff format, ruff check and mypy strict clean.

Not measured: no live handshake was run here. The item already carries one.
Severity is conditional -- zero deployments, so this is what a deploying site
would hit, not an exposure today.
…5 scope 2)

Band B1 item 2 of four. Adds `harden_crl_check(ctx, crl_file)` beside
`harden_verify_flags`, whose docstring says in as many words that it does strict
RFC 5280 path validation and NOT revocation, plus `read_crl_facts` in pki.py as
the sibling of `read_cert_facts` that the item names.

RE-MEASURED THE TRAP TABLE FIRST, because the item says to and forbids inheriting
it. CPython 3.14.6 / OpenSSL 3.5.7, TLS 1.2 pinned, throwaway synthetic PKI:

  arm                              crls  good client   revoked client
  CA only, no flag (SHIPPED)          0  ACCEPTED      ACCEPTED       <- the gap
  cafile= CA + FRESH crl, flag        1  ACCEPTED      REFUSED revoked
  cadata= CA + FRESH crl, flag        0  REFUSED       REFUSED        <- trap 1
  cafile= CA + STALE crl, flag        1  REFUSED       REFUSED        <- trap 2

All four inherited rows reproduce. THE FIFTH ARM IS NOT MEASURED AND I AM NOT
RECORDING IT AS RULED OUT: `capath=` needs a hashed directory and `openssl rehash`
was unavailable here, so what that arm tested was an UNHASHED directory failing,
which is expected and answers nothing. The item's capath question stays open.

Both traps are ASSERTIONS in the helper, not comments:
- loads through `cafile=` only, then requires `cert_store_stats()["crl"] >= 1`.
  `cadata=` sets the flag with zero CRLs loaded and then refuses EVERY client with
  "unable to get certificate CRL", silently. The count is the only thing that
  separates "loaded" from "silently ignored".
- refuses an already-expired CRL at construction, because past nextUpdate OpenSSL
  refuses every client rather than just revoked ones -- a PKI housekeeping lapse
  becomes a total outage whose first symptom is every partner dropping at once.
- refuses a missing file rather than degrading to no revocation checking.

tls_policy.py stays STDLIB-PURE at import time: it declares itself "pure stdlib ssl
helpers" and touches cryptography only in two prose comments, so the parsing lives
in pki.py and both imports here are function-level.

VERIFIED: red-first (ImportError, symbol absent), then 91 passed in the module;
ruff format, ruff check and mypy strict clean.

NOT YET DONE, so this is not the item: scope 1 (the tls_crl_file setting on three
factories + ApiSettings), scope 3 (the posture-keyed fail-closed refusal), scope 4
(the pre-expiry alarm on CertExpiryRunner), and the real-handshake rig. This commit
asserts the FLAG IS SET; it does not yet assert a revoked client is refused, and
those are different claims.
…ed (BACKLOG #1005)

The previous commit set a flag and said so. "The flag is set" and "a revoked
client is refused" are different claims, and only the second is worth having.
The item makes the same point: on the two builders it touches most there is no
real client-cert handshake anywhere, and test_mllp_tls.py asserts
`ctx.verify_mode == ssl.CERT_REQUIRED` and stops.

Three tests, driving an actual mTLS handshake on 127.0.0.1 against a context
built by harden_crl_check. TLS 1.2 is pinned so client auth happens IN the
handshake -- under 1.3 the client certificate arrives after the server has
finished and the refusal surfaces on a later read instead, which is how a
revocation test can look green while proving nothing.

1. A revoked client IS refused, and the assertion reads OpenSSL's reason rather
   than merely that the handshake failed.
2. A good client IS ACCEPTED by the same context. This is the control that
   separates working revocation from a context refusing everyone -- which is
   exactly what trap 1 produces, and what a flag assertion cannot tell apart.
3. WITHOUT the CRL, the revoked client GETS IN. The shipped gap the item is
   filed against, pinned deliberately as a negative control: if this ever starts
   failing, revocation arrived by some other route and the item's premise needs
   re-deriving rather than the test relaxing.

Synthetic throwaway PKI in a module-scoped tmp fixture. No PHI, no network beyond
loopback, nothing persisted.

94 passed in the module; ruff format, ruff check clean.
…G #1005 scope 1)

Band B1 item 1, wired in the SAME commit as the setting on purpose. A setting the
factories accept and nothing reads is a silent no-op, which is the exact shape this
whole item is about -- so it never exists in that state.

SURFACE: `tls_crl_file` on MLLP(), Http() and DICOM(), plus ApiSettings.
tls_client_crl_file beside tls_client_ca_file. Each declaration matches its OWN
local shape -- MLLP's union spans two lines, Http's is one, DICOM's takes EnvRef
across three -- rather than one imposed spelling. There is no separate TOML key
list to edit: connections_file.py:290 states "the factory IS the schema", and
_INBOUND_KEYS allow-lists top-level entry keys, not settings keys.

THE ITEM'S LINE NUMBERS WERE STALE, so every coordinate was re-derived from the
symbol. tls_ca_file resolves in FIVE factories; Email() and Direct() also carry it
and are outbound SMTP, outside this item.

PLACEMENT IS THE LOAD-BEARING PART AND IT DIFFERS FROM ITS SIBLING'S. harden_crl_check
loads into the trust store, so it must follow the CA load, inside the mTLS branch. In
mllp.py and dicom.py harden_verify_flags already sits after the CA. IN api/tls.py IT
SITS BEFORE IT -- so placing the CRL call beside its sibling there would load a CRL
into an empty store, producing a context with the check flag set and zero CRLs, which
refuses EVERY client. That is trap 1, reached by copying a neighbour.

One wiring covers two listeners: http_listener.py calls _mllp_ssl_context, so the
inbound HTTP listener inherits it.

THREE ASSERTIONS ON THE MLLP BUILDER, the module the item names as stopping at
`ctx.verify_mode == ssl.CERT_REQUIRED`:
- configured -> cert_store_stats()["crl"] >= 1 AND the flag set. The count is what
  separates "loaded" from "silently ignored".
- NEGATIVE CONTROL, pinning the shipped gap: mTLS on, no CRL -> no revocation flag.
  If this ever fails, revocation arrived by another route and the premise needs
  re-deriving. Do not relax it.
- PLACEMENT GUARD: a CRL with no mTLS is not loaded -- nothing to revoke when no
  client certificate is requested.

VERIFIED: mutation test, disarmed the wiring to `if False and ...` -> exactly the
configured-case test failed, both controls survived; restore byte-identical by sha256
(not `git checkout --`, which reverts to HEAD and would have deleted the wiring).
296 passed, 3 skipped across the six affected modules; ruff format, ruff check and
mypy strict clean.

STILL OPEN on the item: scope 3 (posture-keyed fail-closed refusal) and scope 4
(pre-expiry alarm on CertExpiryRunner). And `capath=` remains UNMEASURED, not ruled
out -- no rehash tool on this box.
…use it (BACKLOG #1005)

I recorded capath= as "cannot be tested here" because my probe reported no rehash
tool. THAT WAS FALSE and a peer caught it. Git for Windows ships both openssl and
c_rehash. Measured now, with controls:

  arm                              crls  good        revoked
  capath= hashed dir                  0  ACCEPTED    REFUSED: certificate revoked
  cafile= bundle [POSITIVE CONTROL]   1  ACCEPTED    REFUSED: certificate revoked
  cafile= CA only, no flag [BASELINE] 0  ACCEPTED    ACCEPTED

capath= ENFORCES REVOCATION IDENTICALLY. But cert_store_stats()["crl"] reports ZERO
for it, because a hashed directory is read LAZILY at verification rather than at load
time. So the >= 1 assertion in harden_crl_check -- exactly right for cafile=, and the
whole defence against trap 1 -- IS NOT A VALID LIVENESS CHECK FOR capath= and would
REJECT A WORKING CONFIGURATION. Recorded in the docstring where the next person will
hit it, rather than left as an open question they would re-derive.

WHY MY FIRST ANSWER WAS WRONG, three faults in one probe, each of which alone
produces a confident false negative:
1. subprocess could not CreateProcess c_rehash -- it is a SHELL SCRIPT, not an exe --
   and I swallowed the FileNotFoundError with a bare except and no diagnostic.
2. `openssl rehash` on this build PRINTS "Not available; use c_rehash script" AND
   EXITS 0. Had my subprocess resolved it, my returncode==0 check would have recorded
   a hashed directory that was never hashed. An exit code reading success from a tool
   that just said it cannot do the job.
3. Passing a Windows path into `bash -lc` hashed nothing and still returned 0.

The guard that saved it was asserting the .r0 file EXISTS before reporting any arm,
rather than trusting the rehash step. Without it, three separate mechanisms were each
ready to hand me "capath does not work" from a directory OpenSSL was never given.

94 passed; ruff format, ruff check, mypy strict clean.
… instance (BACKLOG #1005 scope 3)

Band B1 item 3. The revocation sibling of the cleartext exposed-gate, and it fires
on the OPPOSITE condition: those refuse a listener with NO TLS, this refuses one
whose TLS is correct but whose client certificates are never checked for revocation.
Measured on this tree, a revoked-but-chain-valid client is ACCEPTED.

THE ITEM DID NOT NAME THE ACTUAL BLOCKER, which was a missing model field. Source
carried tls_hop_attested and tls_hop_attested_reason; Destination carried those PLUS
tls_revocation_attested. So there was nothing on the inbound side to key an
attestation on. Adding it first was the real first step -- every sibling refusal here
pairs with a per-connection attestation, and a revocation refusal with no escape
would be the only control without one, refusing a site whose PKI covers revocation
outside the engine.

THE GATE keeps the sibling's three rungs in the sibling's order: attested permits;
an UNSTAMPED posture permits, because that call ran outside the enforced gate and
must never acquire a new refusal; otherwise refuse only when the instance is BOTH
enforcing AND PHI.

NO BLUNT PROCESS-WIDE ESCAPE, deliberately. MEFOR_ALLOW_INSECURE_TLS governs weakened
TLS, and a listener that verifies its peers correctly but skips revocation is not a
weakened-TLS hop. Reusing that env var would let one variable silence a control it
was never scoped to.

SCOPED to the three connectors that can hold a client certificate -- MLLP (which also
serves the inbound HTTP listener), HTTP and DIMSE. NOT raw TCP/X12: they have no TLS
option, so there is no certificate to revoke. Note the name mismatch that would bite
a copy-paste: the FACTORY is DICOM() and the CONNECTOR TYPE is DIMSE.

NINE TESTS, one control and seven ladder rungs around it:
- the refusal itself, and the same refusal on DIMSE and HTTP
- POSITIVE CONTROL: a configured tls_crl_file passes, so the refusal can be
  SATISFIED and not merely avoided -- without it the refusal test would pass equally
  against a gate that refuses every mTLS listener
- the per-connection attestation passes
- non-PHI warns, non-enforcing warns, unstamped posture never refuses
- PLACEMENT GUARDS: TLS without mTLS is not a revocation gap, and a connector with no
  mTLS surface is ignored

VERIFIED by mutation: disarming the refusal to `if True or ...` failed EXACTLY the two
refusal tests and left all seven ladder and guard tests passing. Restore byte-identical
by sha256, from a saved copy rather than `git checkout --`, which reverts to HEAD.
257 passed, 3 skipped across seven modules; ruff format, ruff check, mypy strict clean.

REMAINING on the item: scope 4, the CRL pre-expiry alarm on CertExpiryRunner.
…wn (BACKLOG #1005 scope 4)

Band B1 item 4, and the last of the four. The freshness PREFLIGHT already landed
with harden_crl_check (a7afcff) -- it refuses an already-expired CRL at
construction. This is the other half: warn BEFORE that happens.

WHY A CRL BELONGS IN THE CERT MONITOR AT ALL: the operator question is identical --
"is a file I depend on about to expire". CertExpiryRunner already reads notAfter off
every served cert and alerts inside a warn window. A CRL's nextUpdate is the same
shape, so MonitoredCert gains a `kind` discriminator (defaulted, so every existing
construction is unchanged) and _inspect dispatches to read_crl_facts.

BUT IT ALERTS DOWN A SEPARATE SINK METHOD, AND THAT SEPARATION IS THE POINT. An
expiring CERTIFICATE degrades one identity and is fixed by reissuing it. An EXPIRED
CRL makes OpenSSL refuse EVERY client presenting a certificate under that issuer --
not merely revoked ones -- so it is a total interface outage fixed by a PKI refresh.
Emitting both down cert_expiry would hand an operator ONE STRING FOR TWO CAUSES WITH
OPPOSITE REMEDIES, which is the defect this fleet spent tonight finding in an
unrelated banner. The logging sink escalates an expired CRL to ERROR rather than
WARNING for the same reason: the listener is already down, not approaching a
deadline.

INBOUND ONLY, deliberately: a CRL verifies peers we REQUIRE certificates from, and
only an inbound listener does that.

NOT DONE, AND NAMED RATHER THAN OMITTED: the API listener's own
ApiSettings.tls_client_crl_file (added in 5790321) is NOT yet watched. Threading it
needs a new Engine constructor parameter from its caller, which is a wider change
than this item's core. The three connection listeners are covered; the API one is not.

FIVE TESTS:
- a CRL inside the window alerts crl_expiry and NOT cert_expiry -- asserting
  crl_calls alone would pass if the runner fired BOTH, so both are asserted
- an expired CRL reports negative days
- POSITIVE CONTROL: a fresh CRL alerts NOTHING, so the two above are not green
  against a monitor that alerts on every CRL handed to it
- a cert and a CRL on one connection route to their own methods, which is the
  realistic configuration and where the separation earns its keep
- an inbound tls_crl_file is collected from a real Registry, built with the real
  InboundConnection and ConnectionSpec types rather than a fake shape

VERIFIED by mutation: disarming the crl dispatch failed EXACTLY the three routing
tests and left the fresh-CRL control and the collection test passing. Restore
byte-identical by sha256 from a saved copy. 297 passed across seven modules; ruff
format, ruff check clean; mypy strict clean on all 267 source files.
… diagnostic limb)

#1245's AVAILABILITY defect is already closed -- _unclaimed_bootstrap is now the
single predicate both readers call, and the confirming experiment exists. What the
item keeps open is the DIAGNOSTIC limb, and it fences it explicitly: "DO NOT CLOSE
THIS ON A GREEN RE-RUN. A green re-run is consistent with an intermittent, not
evidence against one."

THE RECORDED FAILURE IS `must_change_password` FALSE AFTER admin_reset_password --
NOT the retirement assertion below it. One investigation pass was already spent on
the wrong assertion; the item says so in as many words, so the comment says it here
where the next reader is standing.

A bare assert cannot distinguish three causes, and all three produce an identical red:
  (a) the write never landed
  (b) it landed and something reset it
  (c) the read path is wrong
A PAIRED READ AT THE SAME INSTANT -- the store record AND the raw row -- splits them.
Uses the same `store._db` seam four other tests in this file already use, rather than
introducing a second idiom for reading a row.

TWO LINES, AND IT DOES NOT REQUIRE REPRODUCING THE FAILURE FIRST. That is what makes
it worth adding before anyone tries to, and it is the item's own argument.

VERIFIED BY FORCING IT, because a diagnostic whose failure path never runs is
decoration. Injected the (a/b) condition -- cleared the column before the paired read
-- and the assertion produced:

  #1245 intermittent: store record must_change_password=False, raw row
  must_change_password=0. DISAGREE -> the read path is wrong (c). BOTH falsey -> the
  write never landed or was reset (a/b), and the raw value tells you which by whether
  the row exists at all.

Both values render and a reader lands on (a/b) without re-deriving anything. Injection
reverted, file byte-identical by sha256.

139 passed across three auth modules; ruff format and check clean.

NOT CLOSING THE ITEM. The item forbids closing on a green re-run, and this commit adds
instrumentation rather than finding the mechanism. Six candidates are already refuted
by reading in the item body; a seventh is deliberately not offered as a story.
…G #1276 part A)

Owner ruling 2026-08-22, option 3, quoted by the liaison: "engine always serves
TLS, supersede 0143 in the same PR". Before this, tls_enabled was literally
bool(tls_cert_file) with both keys defaulting None, so an unconfigured engine
opened a CLEARTEXT socket. An operator certificate still always wins -- this is a
fallback BENEATH [api].tls_cert_file, never a replacement.

UNCONDITIONAL ON PURPOSE. A conditional scheme is what let the tray, the harness
and the DAST target each decide the scheme their own way; that divergence is the
defect the item exists to remove, and you cannot disagree about a scheme that has
no conditional.

THE EXISTING TEST CAUGHT A REAL BREAK AND I FIXED THE CODE, NOT THE TEST.
`tls_terminated_upstream` (+ trusted_proxies) declares a reverse proxy terminating
TLS in FRONT of the engine and speaking plaintext to it. My first cut minted there
too, which does not harden that deployment -- it BREAKS the proxy's own hop. So
"always serves TLS" means the engine never leaves a hop unprotected, NOT that it
terminates TLS in every topology. The helper returns None there and the serve path
skips the wiring, exactly as before.

ONE TEST IS DELIBERATELY INVERTED, and it is renamed so the diff cannot be misread:
test_serve_loopback_without_tls_passes_no_ssl_factory asserted "plaintext loopback:
no TLS wiring", pinning the behaviour the ruling removes. It now asserts the factory
IS wired and the pair IS on disk. That case -- loopback -- is precisely ADR 0143's
premise, which is why the ruling pairs this with superseding it.

DECISIONS I MADE AND THE ALTERNATIVES REJECTED, both delegated to me explicitly:
- PATH: beside the store database. That directory is already the engine's own
  writable state, already operator-controlled via --db / [store].path, and is NOT
  operator-authored config -- which keeps dead option 1 (engine edits operator TOML)
  dead. REJECTED: a new [api].tls_generated_dir knob, a setting for a question with
  one sensible answer.
- LIFETIME: 365 days, inheriting the `cert self-signed` CLI default rather than
  inventing a second lifetime for the same primitive.

MINT-ONCE COMES FREE: _write_private_key already uses O_EXCL + 0o600 + the Windows
DACL tighten and REFUSES to overwrite, so a second start loads rather than rotating.

FIVE TESTS: operator cert wins and nothing is minted (the control -- it also asserts
the state dir stays EMPTY); a first run mints a loadable pair; the CN names the bind
host, or every verifying client fails against the cert minted for it; a second start
reuses byte-identical material; and the pair survives build_api_ssl_context, which
is what serve actually calls.

327 passed across six modules; ruff format, ruff check clean; mypy strict clean on
all 267 source files.

REMAINING ON #1276: part B (the five DEV ONLY prohibitions + pki.py's "every shipped
first-party client defaults to http" prose), part C (eight http:// client defaults,
one a shipped surface), the ADR 0143 supersession, and the ROTATION shape -- which is
an open decision with the dispatcher: nothing re-mints an expired pair today, so on
day 366 this serves a certificate every client rejects.
…erves TLS (BACKLOG #1276)

The lander caught that the 1276a branch changed 41 files and touched ADR 0143 in
ZERO of them. They were right, and the reason is stronger than "the ruling said to".

ADR 0143 DID NOT MERELY ASSUME NO AUTO-TLS. It CONSIDERED terminating TLS on the
loopback bind, named it "a full fix", and DECLINED it -- its Accepted decision rests
on that decline. #1276 part A does precisely the declined thing. Landing it with 0143
untouched would put an ADR on main that reads Accepted on a premise the code has
deleted: trusted, wrong, and nothing anywhere reports it.

WHY SUPERSEDE RATHER THAN AMEND, and it is a measurement not a preference. 0143
declined on sizing: "an XL: it means moving the whole API to https by default and
migrating every client (harness, apiclient, tray, IDE) in lockstep". Measured on
origin/main, that is false:
  tray       INFERS -- service_toml_uses_tls, ONE caller
  apiclient  DOES NOT -- zero tls_cert_file references; it is GIVEN a URL
  IDE        DOES NOT -- its hits are MLLP CONNECTOR schema, same name, other setting
  harness    DOES NOT INFER, it ASSUMES -- hardcoded http://127.0.0.1:8765
One inference site plus one-line default flips. The XL that justified the decline
does not exist, so the decision falls with its premise.

0143's ANALYSIS SURVIVES AND IS WHY THIS IS BETTER: its diagnosis that a secure cookie
over cleartext http is dropped by Chrome and Safari and BREAKS LOGIN is correct. Its
app.state.loopback mechanism goes vestigial where the engine terminates TLS -- but NOT
in the tls_terminated_upstream topology, which still reaches the engine over plaintext.
Recorded so nobody deletes it as dead.

CARRIES BOTH OWNER RULINGS, since three halves belong to one change: always-TLS
(22:19Z), 0143 superseded in the same PR, and the re-mint AUDITED not silent (22:50Z).

NUMBER ALLOCATED FROM THIS WORKTREE and the index row is in this same commit, per the
ledger gate's ownership rule.

NOTE ON THE GATES, CORRECTED BY MEASUREMENT AFTER THE FIRST WRITING OF THIS MESSAGE.
The original text claimed committing from a fresh worktree "would have failed the
ledger gate, which pins 0172 to THIS worktree". THAT IS FALSE and I tested it rather
than keep asserting it: this commit WAS made from a second worktree, and the ledger
gate PASSES -- ledger_check.py exits 0 there, and re-running the hooks shows "ledger
gate (ADR/BACKLOG number reuse) ... Passed".

So ADR ownership does NOT require the committing worktree to be the allocating one;
the registry lives beside the SHARED object store and every worktree of the repo sees
the same allocations. Only ONE gate was ever in play here, not two.

I ALSO NEARLY MISREAD THAT TEST. The cherry-pick showed no hook output, and I started
to conclude the hooks had not run -- but I had piped it through `tail -12`, which cut
the hook lines off the top. My instrument truncated the evidence, not the gate. The
same shape cost a peer a false "backup missing" an hour ago with `tail -3`.
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