backlog: close #339's stale OPEN item, correct #197's banner, file #346 - #148
Merged
Conversation
Three ledger corrections trailing PR #136 (55bbafb, the ADR 0087 sandbox codec fix). 1. #339 OPEN item 2 said "The edit was NOT made" about docs/adr/README.md:121. 08d898b MADE it, in that same PR, once claude/adr-asvs-scorecard-as-data merged (ADR 0156, PR #120) and the worktree guard released. The item is CORRECTED rather than deleted: it read "was NOT made" for the life of the branch and at least four sessions saw it in that state, so the record should show what happened, not just end up right. 2. #197's banner still claimed "Closes the WP-L3-17 (ASVS 15.2.5) residual". That was the LAST copy of the claim in this file - README.md:121 was fixed by 08d898b and the private ASVS-L3-REMEDIATION-PLAN / THREAT-MODEL rows in the vault. Two reasons it is wrong, both verified against the code rather than inferred from the ADR: (a) DEFAULT_FORBIDDEN_MODULES blocks socket/ssl/asyncio/multiprocessing and the secret-bearing packages but NOT os/subprocess, so a sandboxed Handler still reaches host command execution - confinement is address-space only; (b) until #339 the pipe pickled the child's return and the ENGINE PARENT deserialized it, so the boundary was bypassable outright. Banner also gains _sandbox_codec.py, which it did not name. 3. #346 filed - the sandbox import boundary is enforced only at runtime, under an off-by-default flag. A type the child must construct or receive cannot live under a DEFAULT_FORBIDDEN_MODULES prefix; CapturedResponse violated that and made mode=subprocess + ADR 0013 loopback re-ingress DOA until #339 relocated it. The guard runs in the child, at dispatch time, only under mode=subprocess - so a re-violation gives a green suite, a byte-identical mode=off, and breakage ONLY for installs that enabled the sandbox for security reasons. It fails selectively against the population least able to report it. Number allocated via scripts/coord/alloc.ps1, never grepped. Measured for the item rather than asserted: `git grep -l FORBIDDEN_MODULES -- tests/` is EMPTY, and _sandbox_codec.py's imports are today all under config/ and parsing/, so the invariant currently HOLDS - this is about keeping it, not repairing it. Verified by falsification, not by a green run: probing #346's banner to a non-banner line failed the checker at exactly BACKLOG.md:8429 naming item #346, so the gate demonstrably sees this item. Reverted; 269 items, each declaring exactly one status, exit 0.
wshallwshall
enabled auto-merge (squash)
August 2, 2026 15:18
#141 (BACKLOG #345) landed while #148 was in CI. Both PRs append a new item at EOF after #343, so this conflicted on exactly one hunk - the predicted collision, not a surprise. gh pr update-branch CANNOT resolve it; it fails and leaves the PR DIRTY. Resolution: TAKE BOTH. #346 and #345 are independent OPEN items appended at the same point, so their relative order carries no meaning (this file has not been numerically ordered since #340 - the tail reads 340, 344, 339, 341, 342, 343). Restored the `---` separator the conflict markers had displaced. Verified by measurement rather than by eyeballing whether the markers were gone: - conflict markers remaining: 0 - backlog_status_check.py: 270 items, each with exactly one status banner (269 before + #345 - the count I predicted BEFORE resolving, which is what makes it a check rather than a description) - `git diff origin/main -- docs/BACKLOG.md` = exactly 3 hunks, all mine: #197's banner, #339's OPEN item 2, and #346 appended - #345's block diffed byte-identical against origin/main - nothing of #141's lost - CRLF preserved, 0 bare LF Coordination: the resolving edit was blocked by the collision gate while the #347 session held this file dirty mid-rebase. Waited for it to commit rather than overriding the gate or rewriting the file with a shell command - routing around a guard through a tool it does not hook is overriding it with extra steps.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…re it is trusted The item told an implementer HOW to fix the assertion but not how to know the fix works. Shipping the replacement on an unfalsified green would reproduce the defect inside the remedy -- a green taken as evidence for a property it cannot see is the whole item. So the fix direction now closes by requiring the deliberate break: hand the store an IdentityCipher or plant a plaintext body, watch the rewritten test go RED, then restore. With the trap that makes it more than a formality, from the session that settled #344's instance 2 today: proving the INSTRUMENT can fire is only half -- the WORKLOAD must also be able to produce the failure class. Their 800-iteration repro loop returned 800/800 green against a live SQL Server while hunting a lock- contention bug, because running the two tests in isolation was the one configuration that could not generate contention. They had falsified the probe and not the rig, which felt like all of it. A rig that excludes the condition it hunts reports silence, and silence reads like evidence. Merged origin/main first (PR #148 / BACKLOG #346 landed): clean auto-merge, no conflict this time, verified by CONTENT and not only by count -- 271 items, #345, #346 and #347 all present, and all five of #347's late revisions still resolving in the merged file.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…ason (#147) * backlog(#347): PHI-at-rest tests assert short-substring absence against base64 ciphertext `tests/test_store_encryption.py:95` asserts `"DOE" not in raw` against a value encrypted under a fresh random key, so the base64 body is fresh random text every run. Measured p ~ 5e-4 per run per leg; it fired on PR #142's windows-2022 py3.14 leg with encryption working correctly. Filed rather than fixed: the fix direction is the maintainer's call (decoded-bytes assertion vs. non-recoverability vs. full-plaintext absence), and simply widening or deleting the substring check would drop the PHI-at-rest property it reaches for. Includes the sibling audit: the identical 3-char shape survives at :303, a 4-char instance at test_content_search.py:123, and ~13 >=6-char instances whose rate is immaterial but whose shape is the same. `test_off_by_default_stores_plaintext` does NOT share the shape (deterministic equality) and needs no change. * backlog(#347): lead with the instrument, and correct the rate to the per-CI-run figure Reframed after the session that owns PR #142 reproduced the diagnosis independently. Three substantive corrections, none of them cosmetic: 1. FRAMING. The banner led with the flake; it now leads with what is actually wrong. The substring check fails when encryption worked AND would pass on a weak encoding that happened to avoid those three characters. The second half is the PHI defect; the flake is only what made someone look. 2. RATE. 5.5e-4 is per assertion per leg. There are two such assertions and three OS legs (ubuntu + windows-2022 + windows-2025, verified against ci.yml), so the figure an operator experiences is 1 in 303 full CI runs, not 1 in 1,820. Both over-estimate caveats kept: L is from one measured value, and the hex-fingerprint prefix is not base64. 3. SCOPE. Reversed my own recommendation to sweep the >=6-char sites for shape. Rewriting a dozen correct assertions costs review attention for no risk reduction; the pattern-propagation concern is answered by writing the ">=6 characters" rule into the :49-58 convention comment instead. Fix list is now :95, :303, and test_content_search.py:123 (4 chars, unsafe under that same rule -- an addition to the reviewing session's list, in its own framing). Also records the confirmation-by-prediction: #142's re-run came back 25 passed with the prediction written beforehand, so a green re-run confirms a chance collision rather than resetting the question. * backlog(#347): exact rates, a verified count, and the float64 zero that hid in the analysis Three sessions reviewed this; each correction below is theirs, verified here rather than adopted. RATES. Recomputed exactly: 1 in 1,821 per assertion per leg, 1 in 911 per leg, 1 in 304 per full CI run (I had floored two of them). JANE at test_content_search.py:123 is 8.58e-6 = 1 in 116,509. THE ZERO. The 14-char row was written as "p=0 (unreachable)". It is 7.44e-24. Cause reproduced: 1-(1-64**-14)**144 UNDERFLOWS to exactly 0.0 in float64, silently, with no warning, in a column of plausible values -- inside an analysis arguing that token length is the discriminator, at the one row where length breaks the arithmetic. The item now carries the trap, the exact value, and the idiom that does not underflow (-expm1(N*log1p(-x))). Every figure recomputed both ways and agreeing. THE COUNT. Three different numbers were quoted before anyone checked (7, then 5, then ~16 for a different denominator). Re-derived from the tree: 7 lines / 8 clauses against ciphertext in test_store_encryption.py, 2 of them unsafe; ~16 repo-wide. The item now states the basis AND the exclusions (:905-908 and :927-928 are caplog assertions against log text, not ciphertext; :56/:532/:625 are full-plaintext; :512 is deterministic twice over) so the count is not re-litigated a fourth time. #344 CITATION. Kept -- its owner confirmed the framing and supplied the discriminator that stops a reader folding the two: this one would fire at exactly the same rate on an infinitely fast machine. Related line now says what the citation is NOT. #346 / ADR 0158. Added as the closer sibling: an assertion that passes for a reason unrelated to the property it tests. ADR 0158 cited WITHOUT a link -- I guessed its filename, checked, and was wrong; the file is on PR #145's branch, not on main. VERIFICATION. backlog_status_check.py falsified against this item: a deliberately doubled banner makes it fail at BACKLOG.md:8429 naming #347. The first probe attempt silently no-op'd on a cp1252 decode and "passed" -- the same shape this item is about. * backlog(#347): two safety grounds, and a correction to the one I had just adopted The #142-owning session pointed out that :512 is safe for a reason unlike the others -- ':' is not in the base64 alphabet -- and suggested it as a third safety category. It is a real category and it is now ground (1), stated more usefully than either of us first had it: a token containing a character the value cannot contain is a PROOF at any length, and it is the same principle that makes the recommended fix (assert ADT not in raw) deterministic. Rule and remedy are now one idea. But the argument as given does not survive, and I checked before adopting it. The haystack is <marker>:<base64>, NOT base64 alone, and the marker carries colons -- so ':' IS representable and ground (1) does not cover :512. It is deterministic for a different reason: fixed marker layout with the version field reading v1, plus a body with no ':' for the run to straddle. Structure, not alphabet. The item now says so explicitly, because a rule that is right about the conclusion and wrong about the mechanism is the thing this whole item is about. Verified mechanically rather than argued: over the haystack's actual character set, the ADT fixture carries \r & . \ ^ | (unrepresentable -> ground 1 genuinely holds), while DOE, JANE, SECRET, WESTWING, SECRETSTATEMRN and ':v2:' are ALL fully representable -- so ground (1) applies to none of them and length is their only defense. That is what the table already claimed; now it is checked. Also records why the float64 trap survives review, from #344's owner: the naive expression is correct everywhere you would sanity-check it and silently wrong only in the tail. * backlog(#347): cite ADR 0158 by its rule and its class, not by its number The session landing ADR 0158 (PR #145) confirmed #347 is a genuine instance and supplied the precise anchor instead of a general pointer. Verified against the ADR on its branch before citing -- all three lines read verbatim as quoted: :246 "An equality check satisfiable by coincidence is not an equality check." :60 Class 2 -- a control that cannot OBSERVE or ACT ON its own failure :61 Test: "if this control were broken, what would tell me?" :56 / :439 the taxonomy explicitly disclaims completeness Citing the RULE is what makes the reference survive renumbering, and the Class 2 test is the sharper statement of this defect than anything I had written: if the encryption were replaced tomorrow with a weak encoding, "DOE" not in raw would still go green. The answer to "what would tell me" is the control itself. Still deliberately unlinked -- 0158 is on #145's branch, absent from main, so a relative link renders broken. The follow-up (file this against 0158 once it is on main) is recorded as NOT done here, with the reason: that session declined to add instances its author did not choose, and padding a rescued document at merge time is its own defect. Their call, recorded so it does not read as an oversight. * backlog(#347): itemise the provenance, and source the one number that was not mine Three fixes, all of the same defect the item is about -- an observational claim carrying more confidence than its sourcing supports. 1. THE 200k SIMULATION IS NOT MINE. It arrived with the originating defect report and I never ran it; the sentence read as though this filing corroborated the rate that way. Every word was accurate, which is the shape: an unsourced observational claim inside a sentence whose whole job is telling the reader how much to trust the number beside it. Now attributed, with this filing's actual contribution (exact Fraction derivation, cross-checked against expm1/log1p) stated separately. 2. "PRODUCED INDEPENDENTLY BY THREE SESSIONS" was an aggregate confidence claim. Two sessions derived rates; the third contributed process discipline. Replaced with itemised attribution -- who supplied the framing, the >=6 rule, the discriminator, the demand to falsify the gate -- and an explicit statement that no claim rests on a count of who agreed. A session count is not evidence. 3. "#344 IS a fixed bound meeting variable latency" -> "#344's THESIS is". That item's instance 2 has since been re-diagnosed as a swallowed lock-timeout (SET LOCK_TIMEOUT 0 -> native 1222 caught and returned as a normal empty, with the dispatcher then parking in a terminal IDLE) -- not a bound at all. The wholesale characterisation was over-broad, and the item now says not to lean on "#344 = timeouts" as a premise. The chain that prompted this went two sessions -> one -> none -> mechanism-only -> mechanism-only-labelled-as-deduction, on a separate claim, every step a good-faith correction, and the conclusion correct throughout. Only the stated mechanism was hollow, and the stated mechanism is what the next reader carries. * backlog(#347): require the replacement assertion to be falsified before it is trusted The item told an implementer HOW to fix the assertion but not how to know the fix works. Shipping the replacement on an unfalsified green would reproduce the defect inside the remedy -- a green taken as evidence for a property it cannot see is the whole item. So the fix direction now closes by requiring the deliberate break: hand the store an IdentityCipher or plant a plaintext body, watch the rewritten test go RED, then restore. With the trap that makes it more than a formality, from the session that settled #344's instance 2 today: proving the INSTRUMENT can fire is only half -- the WORKLOAD must also be able to produce the failure class. Their 800-iteration repro loop returned 800/800 green against a live SQL Server while hunting a lock- contention bug, because running the two tests in isolation was the one configuration that could not generate contention. They had falsified the probe and not the rig, which felt like all of it. A rig that excludes the condition it hunts reports silence, and silence reads like evidence. Merged origin/main first (PR #148 / BACKLOG #346 landed): clean auto-merge, no conflict this time, verified by CONTENT and not only by count -- 271 items, #345, #346 and #347 all present, and all five of #347's late revisions still resolving in the merged file.
wshallwshall
added a commit
that referenced
this pull request
Aug 2, 2026
…lane in terminal IDLE (BACKLOG #344 instance 2) (#153) * fix(tests): a swallowed SQL Server lock-timeout stranded an ADR 0070 lane in terminal IDLE (BACKLOG #344 instance 2) The intermittent sqlserver-only `assert False` in test_stage_dispatcher.py was never the 8.0s bound. Proven, not argued. MECHANISM. claim_fifo_heads runs under SET LOCK_TIMEOUT 0, so a momentarily contended head raises native error 1222 instantly -- and the store CATCHES 1222 and returns a normal EMPTY result (store/sqlserver.py, the _is_lock_timeout branch; deliberate, the "never-block" yield, logged only at DEBUG). The dispatcher's EMPTY branch then takes T12: phase=IDLE with NO timer armed. Production's periodic sweep re-readies exactly such a lane; the ADR 0070 tests set sweep_interval=3600 with an empty lane_provider ON PURPOSE, so IDLE is TERMINAL there and the lane can never be re-claimed. No budget would have helped: the poll cannot succeed at any size. PROOF. Holding a conflicting row lock across the post-unpark re-claim on a live SQL Server reproduces it deterministically -- empty_claims (0,0,0) -> (1,0,1), lane IDLE, park_until=None, no armed timer, head still `pending` and DUE -- and releasing the lock does NOT recover it. The same stranding then appeared UNFORCED on a local full-file run (test_adr0070_1_retry_forever_never_stops_alerts_stuck[sqlserver]). So this is a test-rig gap, not an engine defect. SQLite's immunity is structural (in-process, no lock-timeout translation); Postgres's 119 observations are too few to exonerate it, so read "SQL Server-only" as "server-backed store". FIX, two halves. 1. _wait_until now RAISES on expiry (BACKLOG #344 proposal 6) with the lane's phase, park deadline, streak, task liveness/exception, the store row's status and next_attempt_at, the clock, and empty_claims -- plus a one-line VERDICT naming which hypothesis the reading proves, so no future occurrence is re-diagnosed from scratch. All 60 bare-assert call sites convert for free (the two that legitimately tolerate a timeout pass required=False; the one that carried a message passes note=). 2. The ADR 0070 phase waits go through _wait_lane, which stands in for the sweep those tests disable -- re-readying a lane found in TERMINAL IDLE, but COUNTED and capped at one, and warned about, so a genuine contention regression still fails loudly rather than being absorbed. The stand-in is disabled when IDLE is itself the target phase. FALSIFIED. With the stand-in budget forced to 0, the same injected EMPTY reproduces the original failure -- now carrying the full diagnostic instead of a bare `assert False`. A guard that passes against unpatched code would have measured nothing. Verified: ruff format + check, mypy strict, and the full file green on all three backends (sqlite + a live SQL Server 2022 container + a live Postgres 16), 76 passed per server leg. NOTE for the merge: PR #138 (claude/ci-margin-correction) also edits #344 instance 2 -- it ADDS a "RE-DIAGNOSED" block recording the mechanism as unresolved. That block is superseded by this one and the two will need a one-time reconciliation. * fix(tests): three corrections from the adversarial review of the #344 fix Adversarial verification of the mechanism (30 agents, 23 candidates, 20 refuted) converged on the same diagnosis, but surfaced three things the first pass had wrong or incomplete. All three verified against the source before being written down. 1. empty_claims is NOT a complete discriminator on its own. `> 0` is biconditional with the T12 stranding only because the other EMPTY consumer (T11, EMPTY-with-dirty) is unreachable under this topology; and `== 0` covers at least three distinct stuck states -- CLAIMING (stalled claim), PROCESSING (hung serializer) and READY (claimer not picking it up) -- which only the PHASE separates. _verdict now reads phase alongside empty_claims and names what each combination means, including the previously unhandled "EMPTY recorded but the lane is not IDLE" case. 2. There is a SECOND, quieter route to the same spurious EMPTY on SQL Server. The batch claim's lock probe runs WITH (UPDLOCK, ROWLOCK, READPAST), so a contended head is SKIPPED, and the head-pinned contiguity step then drops the whole lane ("rn=1 missing drops the whole lane => EMPTY") -- with NO log line at all, not even the DEBUG one. Same observable, strictly harder to see. The fix already covers it (identical T12 stranding); the ledger and the verdict now name it. 3. Postgres is NOT structurally immune, and saying "too few observations" understated it. Its claim uses FOR UPDATE SKIP LOCKED (store/postgres.py) -- the same head-of-line skip. Only SQLite's 0/1,105 is structural. The ledger now says so instead of leaning on the sample size. Also fixes a real shadowing bug introduced by the first commit: the store-row loop used `for lane in ...`, rebinding the `lane` PARAMETER, so with two live dispatchers the second one's verdict would have named a stale lane. Measured while here: 12 consecutive full-file runs against a live SQL Server produced ZERO sweep stand-ins (the one failing run was a concurrently-running experiment of mine stealing DB time -- 56s vs the usual 27s), so the cap of 1 has comfortable margin. Re-verified: ruff format + check, mypy strict, the full file green on sqlite + live SQL Server + live Postgres, and both falsification arms still hold. * backlog(#344): the banner is a claim about the body, so it lands with the body #138's session declined my merged-banner proposal, and the argument is better than mine was. A banner asserting "instance 2 resolved" while main still carries "what is NOT settled is the mechanism" is a green signal ahead of its evidence -- the exact failure this item is about, and backlog_status_check.py cannot catch it because it counts banners rather than reading them. Their banner is accurate against the document that will be on main after #138; mine is accurate against the document that is on main after THIS branch. So the banner belongs in the same landing as the RESOLVED body, and that is here. I did NOT take their offered wording. They supplied instance 3's text so I could "name all three" -- but instance 3 and proposal 5 exist only on their branch, so naming them would recreate the mirror of what they just declined: a banner describing a body that is not there if this PR lands first. So the count is gone entirely, per CLAUDE.md §11 / Secure_Development_Standards §3 -- a completeness claim is a liability, and with concurrent PRs adding and closing instances an enumeration goes stale on whichever lands first. The banner now states only what this branch delivers (instance 2 resolved, with the mechanism named) and characterises the rest without counting, so it is true in either merge order. The reason is recorded in the banner itself so the next editor does not helpfully restore a tally. Also merges origin/main (dc0da09, #148's #345/#346 work). Both directions verified by CONTENT, not by the item count: my #344 text (RESOLVED, READPAST, "FOR UPDATE SKIP LOCKED", "not structurally immune") and main's new ##345/##346 all resolve in the merged file, and the merge touched no test files. Gate green at 270. KNOWN, and deliberately left: this still conflicts with #138 on exactly this one line (git merge-tree --write-tree reports a single hunk at :8328, banner only). That is the agreed division -- when #138 lands, take this banner and keep their two corrected sentences in the RE-DIAGNOSED paragraph. * docs(handoff): stopping note for #344 instance 2 — this work exists in exactly one place Paused on the owner's instruction via the coordinator session. Nothing is half-done; the four preceding commits are coherent and independently verified. Why this file exists: every other session's work is pushed to a remote branch. This is LOCAL ONLY -- no PR, no remote branch at all (`git rev-parse --verify origin/<branch>` fails outright). A resumer checking GitHub finds nothing and would reasonably conclude #344 instance 2 was never fixed. Records what is NOT recoverable from the diff: - The mechanism, which was expensive to prove: SET LOCK_TIMEOUT 0 -> native 1222 -> the store CATCHES it and returns a normal EMPTY (deliberate, DEBUG-logged only) -> T12 parks the lane in a TERMINAL IDLE because these tests disable the sweep on purpose. Plus the second, silent READPAST route, and why Postgres is not immune. - The deterministic reproduction recipe. The scratch harnesses were deleted and this is the part nobody should re-derive: hold a conflicting row lock across the post-unpark re-claim, watch empty_claims go (0,0,0) -> (1,0,1) with the head still pending AND due. Includes the signature to match and the [3] clause -- the lane does NOT recover when the lock is released -- which is what proves TERMINAL rather than slow. - That an ISOLATED repro loop cannot reproduce this (800/800 green): running the two tests alone is the one configuration that cannot generate contention. Force it. - Verification already done, including the falsification arm, so nobody redoes it blind. - How to rebuild the DB rigs, with the traps: the native MSSQLSERVER service owning 1434, the single shared MEFOR_STORE_* config that forbids both backends in one pytest invocation, and the silent skip that makes a green local run meaningless. - The #344 banner agreement with PR #138, which until now lived only in two sessions' heads and both are pausing -- including why the agreed order is preferable but NOT load-bearing, so a resumer neither panics about ordering nor restores an instance tally (instance 3 / proposal 5 exist only on #138's branch). - That the ledger claim on 344 is HELD ON PURPOSE and scoped to tests/ only. NOTE on what is deliberately absent: the branch name and worktree path. The leak guard blocked the first version of this file, correctly -- it matches worktree/branch slugs BY SHAPE, on the reasoning that a slug is whatever the task happened to be called and can therefore name a customer engagement, and this repo is public. Not a false positive and NOT allowlisted. Those two strings live in .git/mefor-coord/HANDOFF-344-instance-2.md, which is never published; the committed note points at it and is fully usable without it, since a reader is already on the branch. Docs-only: no code, no test, no ledger banner touched. * docs(handoff): a THIRD source of the 1222, and name the fix commit not the tip Two additions from the #348 / ADR 0159 session, which reached the same 1222 from the opposite end and sent it over rather than sitting on it. Still paused -- this finishes the stopping note, it does not resume work. 1. A CANCELLED store call is a third route to the same EMPTY, and it refines how the fix commit words the cause. CancelledError derives from BaseException, so the store's `except Exception: await conn.rollback()` idiom never runs its rollback on cancellation, and aioodbc's Pool.release() returns the connection to the free deque with no rollback or reset. THEIR measurement (live container): cancelling release_claimed left 7 KEY X locks on `queue`, vs 0 for a claim_fifo_heads control, after which a real claim returned EMPTY-all and a raw writer got 1222. VERIFIED HERE before recording it, rather than relayed: store/sqlserver.py has 90 rollback sites guarded by `except Exception` and ZERO using `except BaseException` or a `finally`. So the gap is real in our source, not just in principle. Why it matters: 58f8f13's message reasons from MOMENTARY contention -- a live producer holding a head lock in flight. A connection poisoned this way holds its locks as long as it sits unclaimed in the pool, so the 1222 can repeat across successive sweep ticks instead of clearing on the next. The chain into T12 and the terminal IDLE is identical; the duration and origin of the contention are not. It does NOT change this branch's conclusion -- production still recovers via the sweep, so "test-rig gap, not an engine defect" stands, and that session explicitly declined to escalate severity on it. Recorded as: if you see a 1222 you cannot attribute to a live producer, look at #348 first. 2. Name the fix commit, not the tip. HEAD was circulated between sessions as "the fix"; it is a banner edit plus this note and touches no code. THE FIX IS 58f8f13. The handoff now says so in a callout above the commit table, because the mis-relay already happened once and a tip SHA moves every time this note is amended. Cross-referenced by LEDGER NUMBER (#348 / ADR 0159), never by SHA -- that work is also unpushed and may be rebased. Docs-only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ledger tail for PR #136 (
55bbafb1, the ADR 0087 sandbox codec fix). Docs only — one file, +22/-2.What this corrects
1. #339's OPEN item 2 asserted something false about its own PR. It read "The edit was NOT made" about
docs/adr/README.md:121. That edit was made —08d898bc, in PR #136 itself, onceclaude/adr-asvs-scorecard-as-datamerged (ADR 0156, PR #120) and the worktree guard released.Corrected rather than deleted: the sentence stood for the life of the branch and at least four sessions read it in that state. The record should show what happened, not merely end up right.
2. #197's banner carried the last surviving copy of a false closure claim — "Closes the WP-L3-17 (ASVS 15.2.5) residual". Two independent reasons it is wrong, both checked against code rather than inferred from the ADR:
DEFAULT_FORBIDDEN_MODULESblockssocket/ssl/asyncio/multiprocessingand the secret-bearing packages but notos/subprocess— a sandboxed Handler still reaches host command execution. Confinement is address-space only.__reduce__.The other copies were fixed by
08d898bc(ADR README) and in the private vault (THREAT-MODEL.md15.1.5,ASVS-L3-REMEDIATION-PLAN.mdWP-L3-17, the risk register 1f). The banner also gains_sandbox_codec.py, which it did not name.3. #346 filed — the sandbox import boundary is enforced only at runtime, under an off-by-default flag.
A type the child must construct or receive cannot live under a
DEFAULT_FORBIDDEN_MODULESprefix.CapturedResponseviolated that and mademode=subprocess+ ADR 0013 loopback re-ingress DOA until #339 relocated it toconfig/response.py. The only guard runs in the child, at dispatch time, and only undermode=subprocess.So a re-violation gives a green suite, a byte-identical
mode=off, and breakage only for installs that enabled the sandbox for security reasons — it fails selectively against the population least able to report it.Number allocated via
scripts/coord/alloc.ps1, never grepped.Verification
Measured for the item rather than asserted:
git grep -l FORBIDDEN_MODULES -- tests/→ empty. Nothing enforces the rule today._sandbox_codec.py's imports are all underconfig/andparsing/, so the invariant currently holds — this item is about keeping it, not repairing it.Gate checked by falsification rather than a green run: probing #346's banner to a non-banner line failed
backlog_status_check.pyat exactlyBACKLOG.md:8429, naming item #346. Reverted; 269 items, each declaring exactly one status, exit 0.Coordination
docs/BACKLOG.mdis shared with several in-flight branches. My hunks are #197's banner, #339's item 2, and #346 appended at EOF — disjoint from #344 (ci-margin-correction), #323/#139 (repo-security-review), and #340 (#143/#144). Verified each target string was uniquely addressable before editing.🤖 Generated with Claude Code