auth,store: username identity is case-sensitive and must not depend on store collation (BACKLOG #1268), plus the ledger parse hook - #458
Conversation
…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
… does not guarantee `test_worker_kill_reaps_the_whole_process_tree` asserted the LATER of two events at the instant the EARLIER one landed, and it is currently reding a REQUIRED context: PR #458's ubuntu leg failed at tests/test_sandbox.py:998, "the grandchild survived the worker kill", and the same test failed on `main` in run 32206563674. THE DOCSTRING WAS THE DEFECT, NOT ONLY THE TIMING. It claimed pipe-EOF is equivalent to "grandchild reaped". It is not, and the two observables are not the same event: - pipe EOF fires when the last holder of the write end releases its fds -- at process EXIT; - `os.kill(pid, 0)` raises ESRCH only once the pid is REAPED; - the grandchild is ORPHANED, because the sandbox worker (its parent) is killed and `proc.wait()`ed first, so pytest cannot `waitpid` it and reaping falls to PID 1 or the nearest subreaper, asynchronously. So t_reap is STRICTLY AFTER t_exit == t_EOF, always, and the test asserted the later one with no wait, poll or deadline between them. It passed only when the reap won a race it was never entitled to win. TIMING-DEPENDENT AND WRONG ARE NOT ALTERNATIVES HERE -- the ordering is guaranteed by the mechanism. A sleep or a retry would have made it green while leaving the false equivalence in place for the next reader to rely on again, which is why the docstring is part of the change rather than a footnote to it. The two senses of "reap" that collide here are now stated: `_reap_process_tree` reaps in this codebase's sense -- TERMINATE every process in the tree, which is what the engine guarantees and what the pipe-EOF assertion genuinely proves -- while POSIX `waitpid` reaping is a different act on a different schedule, and the test now waits for TERMINATION only. It asks for nothing the engine does not promise. VERIFIED: tests/test_sandbox.py -> 25 passed; the false-equivalence sentence is gone (grep returns 0); ruff 0.15.22 clean; no cp1252-unsafe character introduced. ITS ADVERSARIAL REVIEW HAD NOT REPORTED WHEN THIS WAS COMMITTED -- committed to protect the work across a usage-window boundary, with any finding to be fixed forward. Nothing is pushed. The platform split matters for whoever reads this next: the ordering argument is POSIX-shaped, and this box is Windows, so the CI red is the behavioural evidence rather than a local reproduction.
…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.
…ound both of them require (#464) * feat(probe): a connscale gap now records WHICH path degraded it, and the smoke test asserts only what it can The FD probe returned the same all-None sample from THREE different paths and the record could not say which one fired. That is why `main`'s tip red was mis-attributed by four sessions today: the failing assertion was `tests/test_connscale_smoke.py:128`, `assert r.fd_count_peak is not None`, and the record's repr carried `sent=36 acked=24` -- so the attribution followed the NUMBERS in the dump to the item about sent/acked, rather than the ASSERTION, which was about the probe. The artifact did not carry the information needed to attribute it. WHAT CHANGED. `ProbeDegraded` names each cause distinctly -- the subtree walk timing out, erroring fast, returning zero rows, or returning a snapshot with no root; and the per-PID read timing out, erroring or parsing nothing. `ProcSample.degraded` carries it, `_gap(cause)` replaces the single shared `_EMPTY_PROC`, and `_resolve_errored` becomes a property over the recorded cause so a boolean can no longer be set without saying why. `is_budget_exhausted()` is the discriminator, stated once: a path that SPENT its budget measures the runner; one that failed without spending it is a broken probe. Those get opposite readings and they were previously indistinguishable. THE SMOKE ASSERTION IS NARROWED, NOT RELAXED, AND THE DIFFERENCE IS THE WHOLE POINT. It used to demand `fd_count_peak is not None` of EVERY record, with no tolerance for a probe that honestly could not measure -- so a degraded probe red a REQUIRED context as though the ENGINE were at fault. It now asserts `> 0` only where a value is present AND still requires that AT LEAST ONE record measured something. A run where the probe never measured at all still fails. Buying a green by deleting the requirement was the available wrong answer here and it is not what this does. Twelve cases in a new tests/test_connscale_probe_degradation.py pin each cause to its path, including the two that must record NO cause on success, and one asserting a resolution that fails without naming a cause is still given one -- so the enum cannot be bypassed by a future path that forgets to classify. Does NOT fix the underlying walk timeout, and does not claim to. It makes the next occurrence attributable, which is what four sessions lacked today. VERIFIED: tests/test_connscale_probe_degradation.py + tests/test_connscale_smoke.py -> 32 passed; tests/test_sandbox.py unaffected; ruff 0.15.22 clean on all five files; no cp1252-unsafe character introduced (census identical against HEAD). harness/ is not in CI's mypy scope (`mypy messagefoundry messagefoundry_webconsole --exclude 'messagefoundry/tray/'`), and the 3-file mypy invocation reports the same single package-discovery error at HEAD as with this change, so it is pre-existing and not introduced here. ITS ADVERSARIAL REVIEW HAD NOT REPORTED WHEN THIS WAS COMMITTED. Committed anyway to protect the work across a usage-window boundary, on the explicit understanding that a review finding is fixed FORWARD -- which is exactly what happened to the sibling #1290 commit earlier on this branch, where the review found a blocking defect and e2ce84ad corrected it before anything left this tree. Nothing here is pushed. * fix(test): the orphan-reap assertion asked for an ordering the engine does not guarantee `test_worker_kill_reaps_the_whole_process_tree` asserted the LATER of two events at the instant the EARLIER one landed, and it is currently reding a REQUIRED context: PR #458's ubuntu leg failed at tests/test_sandbox.py:998, "the grandchild survived the worker kill", and the same test failed on `main` in run 32206563674. THE DOCSTRING WAS THE DEFECT, NOT ONLY THE TIMING. It claimed pipe-EOF is equivalent to "grandchild reaped". It is not, and the two observables are not the same event: - pipe EOF fires when the last holder of the write end releases its fds -- at process EXIT; - `os.kill(pid, 0)` raises ESRCH only once the pid is REAPED; - the grandchild is ORPHANED, because the sandbox worker (its parent) is killed and `proc.wait()`ed first, so pytest cannot `waitpid` it and reaping falls to PID 1 or the nearest subreaper, asynchronously. So t_reap is STRICTLY AFTER t_exit == t_EOF, always, and the test asserted the later one with no wait, poll or deadline between them. It passed only when the reap won a race it was never entitled to win. TIMING-DEPENDENT AND WRONG ARE NOT ALTERNATIVES HERE -- the ordering is guaranteed by the mechanism. A sleep or a retry would have made it green while leaving the false equivalence in place for the next reader to rely on again, which is why the docstring is part of the change rather than a footnote to it. The two senses of "reap" that collide here are now stated: `_reap_process_tree` reaps in this codebase's sense -- TERMINATE every process in the tree, which is what the engine guarantees and what the pipe-EOF assertion genuinely proves -- while POSIX `waitpid` reaping is a different act on a different schedule, and the test now waits for TERMINATION only. It asks for nothing the engine does not promise. VERIFIED: tests/test_sandbox.py -> 25 passed; the false-equivalence sentence is gone (grep returns 0); ruff 0.15.22 clean; no cp1252-unsafe character introduced. ITS ADVERSARIAL REVIEW HAD NOT REPORTED WHEN THIS WAS COMMITTED -- committed to protect the work across a usage-window boundary, with any finding to be fixed forward. Nothing is pushed. The platform split matters for whoever reads this next: the ordering argument is POSIX-shaped, and this box is Windows, so the CI red is the behavioural evidence rather than a local reproduction. * fix(test): the run-level FD bound let a real collapse pass, and the POSIX probe called a success blind Two defects, both found by the adversarial reviews of 937803a9 and 1a514e14, both proven end-to-end by the reviewers rather than argued. Fixed forward; neither commit had left this tree. FINDING 1 (CRITICAL) -- THE GREEN MEANT LESS, IN EXACTLY THE WAY THE BRIEF PREDICTED. The one-line assertion 937803a9 replaced was doing DOUBLE DUTY: besides checking each reading, it was the only thing guaranteeing `fd_count_monotonic` had two readings in a group to COMPARE. `_monotonic_slo` SKIPS None readings by design, so with a run-wide `any(measured)` bound it can return ok=True observed='monotonic' having compared NOTHING. Measured on the real smoke: forcing every step after the first to WALK_TIMEOUT left one group holding a single reading, PAIRS ACTUALLY COMPARED=0, and the SLO still reported monotonic -- and a genuine 1000-handle FD collapse passed the same way, while the unforced control compared 2 pairs and a real 1000->100 drop correctly failed. So the bound is now at the SLO's GRANULARITY, not the run's: some (sweep_mode, claim_mode) group must hold two measured readings, which is the same key `_monotonic_slo` groups on (BACKLOG #1101). Grouping on sweep_mode alone would reintroduce the bug one level up, since two readings split across claim modes are never compared with each other. AND THE JUSTIFICATION I SHIPPED FOR THE WEAK BOUND WAS ITSELF WRONG. It claimed the run-wide form was "the same bound assertion (5) already places on the reload probe". Same SHAPE, wrong ANALOGY: `reload_seconds` feeds no monotonicity SLO, so one reading there costs nothing, while `fd_count_peak` feeds one. Borrowing a bound from the one probe where the analogy is safe is how this got through. FINDING 2 -- THE POSIX PATH CALLED A SUCCESS BLINDNESS. `_pid_running` did `os.kill(pid, 0)` and then read /proc, two syscalls with a gap. If the reaper retired the pid inside that gap the /proc read raised FileNotFoundError, `_proc_state` returned None, and the test's blindness guard turned a SUCCESSFUL reap into a red on Linux -- a new false failure introduced by 1a514e14 on the platform where the original red actually fires. A missing /proc/<pid> on a kernel that HAS /proc is a definite answer, "gone", not an inability to answer. Only a platform with no /proc at all (macOS) genuinely cannot separate a zombie from a runner, and None is now reserved for that. The check reads the DIRECTORY rather than `sys.platform`, because the question is whether this kernel exposes the interface, which is what the code actually depends on. THREE TESTS MOVED WITH THE SEMANTICS, and one is renamed rather than merely edited: `test_a_spent_budget_gap_is_tolerated_when_another_step_measured` becomes `..._is_tolerated_beside_a_comparable_pair`, because "another step measured" was never the condition -- two readings split so that no group holds two are never compared. A test whose NAME asserts the wrong rule is a second source of truth for it. ADDED test_one_measured_reading_in_a_group_is_not_a_comparison as the regression test for finding 1, carrying the measurement that proves it: a green resting on an empty comparison is what this file exists to prevent, and it had just shipped one. VERIFIED: 31 passed in test_connscale_probe_degradation.py; 27 passed across test_connscale_smoke.py and test_sandbox.py; the corrected bound rejects the reviewer's failing shape (per-group={...: 1}) and accepts the control ({...: 2}). ruff 0.15.22 clean. Windows host: the POSIX branch of finding 2 is argued from the code and the reviewer's WSL2 measurement, not reproduced locally, and I am not claiming otherwise.
… 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".
… 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>
…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.
|
STILL CARRIES GENUINELY UNLANDED WORK -- measured, so this is not stale-by-assumption. Method: for each changed file, the lines this branch ADDED relative to its merge base 13dd27d, searched against main's copy of the SAME file at aed856b. Dot-prefixed paths read as ABSENT FROM MAIN ENTIRELY (4 files) ADDED LINES PRESENT ON MAIN: 7 to 21 percent across every remaining file Those low percentages are incidental common lines, not partial landings. Contrast #432, closed an hour ago, where 13 of 15 differing files matched at 100 percent and the two outstanding lines both resolved -- that is what landed-by-content looks like, and this is not it. WHAT BLOCKS IT, and it is not the conflict. ADR 0169 is allocated to the worktree repo-security-review-afbae3, whose session has been INTERRUPTED for roughly 13 hours. ledger_check requires owns("adr", "0169") for an added ADR file, and that key is the WORKTREE PATH, not a live session -- the directory still exists on disk, so the allocation is intact and resolvable, but not from any other seat's worktree. NOTHING IS AT RISK: the branch is on origin and this PR is open, so the work is stalled rather than endangered. It needs an assignment, not a rescue. |
|
THE BRANCH BEHIND THIS PR IS DIVERGED, NOT AHEAD, AND THIS PR SHOWS THE SMALLER SIDE. Measured after the Liaison identified this worktree as the predecessor builder-1 seat. This PR's head is ON ORIGIN AND IN THIS PR, ABSENT LOCALLY (3): LOCAL AND NOT IN THIS PR (12) — eleven #1229 residuals plus a filing: CORRECTING MY OWN EARLIER COMMENT ON THIS PR. I measured the four-absent-files and 7-to-21-percent figures against this PR's head, and they are right about that. They understate the branch: the twelve above are outside what I probed, and one of them files two allocated-but-unwritten backlog numbers, which is the same allocated-and-unfiled exposure that justified rescuing #1307/#1308 earlier tonight. NOTHING IS AT RISK. WHOEVER TAKES THIS MUST NOT FORCE-PUSH EITHER WAY. The two sides both contain unique work; the resolution is a merge, and it has to come from the worktree that owns ADR 0169 and the #1305/#1301 allocations, which is the same interrupted seat. |
…aude/builder-1-160cec
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.
…assified (BACKLOG #1005) CI caught this on four legs and the lander correctly refused to treat it as a flake: one deterministic assertion naming the remedy. The connection-scoped completeness floor censuses all 15 connection factories, and #1005's scope 1 added tls_crl_file to DICOM, Http and MLLP -- a new TLS-shaped parameter that was neither reported nor exempt. The guard did exactly what it exists for, on my change. EXEMPT, AND THE REASON HAS TWO HALVES BECAUSE STATING ONLY ONE WOULD BE THE WEAKER: - it is a material PATH, like the tls_ca_file entry it sits beside, not a posture switch; and - its ABSENCE is GATED rather than reported. check_inbound_revocation (#1005 scope 3) refuses an mTLS listener with no CRL on an enforcing PHI instance, the same shape as the ADR 0092 hop cell gating tls/tls_verify further down the list. A connection-scoped reader that merely reported "no CRL configured" would be strictly WEAKER than the refusal that already exists, which is why reporting is the wrong side of this fork rather than the more thorough one. VERIFIED THE EXEMPTION IS LOAD-BEARING: removing it fails exactly that assertion and leaves the other 30 passing, so the pass is the classification and not a coincidence. Restore byte-identical by sha256. 31 passed; ruff format and check clean. WORTH RECORDING: this is option B earning itself. Had 458 merged at its older armed head, the six older claims would have released against a main that did not carry #1005 -- and these eight commits would have hit this guard in a follow-up PR instead. The bigger diff surfaced the gap BEFORE the merge.
Builder 1's lane. Two fixes to the worktree gate's own correctness, plus the ledger and
classification work each one turned out to need.
What is in it
The gate's escape-blind fail-opens (BACKLOG #1229 residual). Span ownership is now decided
once, closing two paths where the gate returned ALLOW on input it should have denied. Two verbs
under one rule, not three -- see the retraction below.
An empty population read as a clean run (BACKLOG #1235). The CLI's default path list is
CWD-relative, so invoked from a directory with no
docs/it reported zero files and zeroallocated item numbers and exited 0. Nothing objected, because both counts agreeing at zero
is what a clean run looks like. It refuses a non-zero exit on an empty population now, and
prints what it scanned and where from.
The sharp edge is how it would have armed: the population floor lived only in a pytest, so
anyone wiring the CLI into CI -- the documented way to satisfy the item -- inherited a gate that
reported clean having scanned nothing. Closing the item is what would have armed it.
The coverage bound was missing from exactly the run a reader acts on. "Not scanned: the
private companion repository" printed only after the hits loop, so an early clean return
skipped it. One named constant on every exit path now, with a test pinning the single definition.
Verification, with its scope stated
tests/test_dangling_citation_check.py45 passed. Across thetest_worktree_gate*modules,581 passed / 13 skipped. Both arms of each fix mutation-verified with an applied-check per
mutant and a byte-identical restore.
ruffandruff formatclean;mypy --strictclean over267 files.
This is not a green full suite and is not offered as one. The
x12andxmlextras areabsent from the authoring environment, so that run does not reproduce CI collection. The
toolingtier -- which is where the changed gate actually runs, and therefore the relevantregression check -- was still running at authoring time and its result is not in hand here.
CI on this PR is the first place both are answered.
A deliberate behaviour change
The emptiness guard makes the CLI exit non-zero where it previously exited 0. Anything
invoking it from a directory without
docs/will now fail where it passed silently. That isthe fix rather than a regression.
Callers are cleared, and the clearance is a second author's. The original check grepped the
script name, while the gate is wired by manifest path -- so it was one probe, not a
clearance. Both angles have since been run: no workflow or pre-commit hook invokes this script,
no path-shaped route reaches it either, and the changed exit code is pinned by
tests/test_dangling_citation_check.pyat lines 373 and 388 -- a suite that ISmanifest-classified and therefore runs on the tooling leg rather than being deselected.
A near-name trap sits here and is documented in the file itself.
scripts/docs/backlog_citation_check.pyis CI-wired;scripts/docs/dangling_citation_check.pyis not. A grep for
citation_checkhits a workflow and the hit belongs to the sibling --confident, well-formed, and wrong in the direction of believing a change is CI-covered when it is
not, which is the direction that lets a behaviour change ship unexercised. Verified with a
positive control in the same pass: the dangling script returns zero wiring hits while
backlog_citation_check,backlog_status_check,ledger_checkandscan_forbiddenreturn 2, 4,2 and 3 -- so the probe finds wiring where wiring exists and the zero is real.
The test contains an attack construct on purpose
tests/test_worktree_gate_escaped_quote.pycontains input of the class the gate exists torefuse. That is authorised, not an oversight -- reviewed and ruled on, and the reason the
test is meaningful at all: a gate asserted only against benign input cannot demonstrate it
denies hostile input. The construct is named here by class rather than narrated as a
procedure, and the analysis behind it is held privately rather than in this repository.
Four retractions, carried because they are already in flight
worktree addis NOT affected by the escape. The original report named three verbs; thethird is wrong -- that path ALLOWs with or without the escape, because rule 3b governs
checkout/switch only. It was removed rather than widening the gate to satisfy it. Left
uncorrected, this sends a reader to loosen a gate that is behaving correctly. Two seats
measured this independently in separate trees.
A third defect is fixed here that no item mentions.
-replaceis case-insensitive bydefault while the rules compare case-sensitively, so a differently-cased executable token
ALLOWed where the lowercase form DENIED. Found while testing, no escape involved. Fixed by
canonicalising the token on emit. Named here because a reader diffing this branch against
the item will not find it there.
An authoring note recorded the gate fix as "not yet built" for roughly twenty minutes
after it was, and another session acted on that line. Corrected at the source.
The escaped-quote suite does not test the property it claimed. Its author wrote that the
verb set covers more than one rule; measured, both verbs return the same rule message.
So the suite exercises its own parametrisation rather than the rule-agnostic behaviour it
advertised. Corrected in place rather than deleted, and deliberately not widened here --
a companion suite covering four distinct rules, asserting the rule ids the gate itself
records, is arriving separately, and duplicating it is the redundancy this lane already
avoided once.
This is worth stating plainly because it is the shape that let the original defect survive:
fourteen gate suites were green and none could see the class. A green suite over one rule
is not evidence about a rule-agnostic fix, and landing one alongside such a fix rebuilds that
condition a layer up.
Rule-agnostic coverage, from a second author
tests/test_worktree_gate_rule_agnostic_coverage.pycovers four distinct rules against a realgit repository -- primary working tree, linked-worktree hijack, shared git config, worktree removal
-- and asserts the rule ids the gate itself recorded in its receipt log, with a per-row
unescaped positive control. Discrimination was demonstrated, not assumed: 5 rows red against the
pre-fix hook and green against the fix.
Asserting the rule id rather than the outcome is the part worth keeping past this PR. "Denied"
and "denied by the rule we think" are different claims, and only the second catches a fix that
denies for an accidental reason.
One row from the original draft was dropped rather than ported, with its reason recorded: it
pinned a design this branch's fix deliberately rejects. A deleted test row with no stated reason
reads as coverage quietly reduced.
The fix does not protect this machine until someone installs it
tests/test_gate_installed_parity.pyfails on a developer box here, and it is correct to fail.Measured independently: the installed hook at
~/.claude/hooks/worktree_gate.ps1isbyte-identical to
origin/main-- the running gate is the unfixed one. The fix lives in thecheckout, and the gate executes from an installed copy.
That leg goes green in CI, and the green means nothing here. The test skips when
~/.claude/hooksis absent, which is every hosted runner. So CI does not guard installed-vs-sourceparity, and its pass must not be read as "the gate is fixed everywhere".
This requires a human after this PR lands.
scripts/worktree/install-gate.ps1refuses when$env:CLAUDECODEis set -- by design, because a session that can install the gate can also removeit -- so it must be run from a plain terminal. Until then the fix protects nothing on any box whose
installed copy predates it.
Lander notes
The lane's tips were merged, never force-pushed. Twice the handed-off tip was not a descendant
of the PR head, and taking it would have discarded commits present only on origin -- including
the ADR-index collision resolution and ledger banners. Each merge was verified in the result
rather than trusted for being conflict-free.
tests/tooling_manifest.txtgained the escaped-quote test's registration (lander-authored).CI required it: the partition drift guard refuses any
tests/test_*.pyimporting no enginemodule that is named in neither the manifest nor
_STAYS_WITHOUT_IMPORTING. That guard existsso a new gate test cannot quietly join the engine legs and grow the tier back. All thirteen
sibling
test_worktree_gate_*.pyfiles sit in the manifest, so the placement was determinedrather than chosen.
BACKLOG #1229 is not closed by this. This is the residual. Its own ledger closure was never
written, which is a separate gap and is not papered over with a residual banner here.