diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 6a4f3baf..645aed2f 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -4100,6 +4100,8 @@ Retiring the tree costs the engine nothing operationally: **`tests/test_ech_egre > **This was a KNOWING scope choice, not an oversight**, and the branch's own test says so (`tests/test_admin_create_cli.py:207-212`). That is why the banner must carry it rather than read as closed: a reader who sees the bootstrap fix land and the item close would reasonably conclude the readiness gate was fixed too. **The gate is the half that would matter on a first deployment.** +> **THE REFUSAL PATH IS UNVERIFIED UNDER THE RUNNER THAT SHIPS, AND A HANG WOULD BE WORSE THAN THE DEFECT THIS ITEM FIXES.** Reported 2026-08-14 by the lane that built the gate, **against its own work**, and recorded here because it changes what "built" means for this item. The gate refuses by **raising during ASGI lifespan startup**, and in-harness that **hangs** rather than exiting: measured with a control -- the sibling non-raising lifespan test passes in 1.13s, raising from the lifespan **body** exits cleanly, and raising during **startup** hangs with zero output. It sits after `engine.start()` and before the task handles teardown expects, so the condition is **pre-existing**; the gate is merely the first thing to raise in that window. **What was NOT measured is what `uvicorn` does there**, which is the runner that actually ships. So a startup refusal that hangs a service is a live possibility and is strictly worse than the mis-report this item exists to correct: an operator can see a wrong readiness answer, but cannot see a process that never finishes starting. **Do not close this item on the gate landing until the refusal is demonstrated to TERMINATE under `uvicorn`, not merely under the test harness.** + **Cluster:** Security / authentication. **Priority:** P2. **Verdict:** build (small). **Severity:** on first deployment all ten out-of-band notices about the most privileged account would silently no-op, including lockout and success-after-failures. No present-tense exposure is claimed — this is wrong in the shipped code, with zero running instances. ⚠️ **The allocated title's second half is REFUTED and the body must say so, or a fixer will hunt for a missing unlock guard that is not missing.** The title read "and there is no administrative unlock path". Three independent paths exist, none involving email: lockout is **time-bounded** (`locked_until = now + lockout_minutes * 60`, `auth/service.py:755-759`, default 15 at `config/settings.py:1769`, enforced only while `now < locked_until` at `:659`); `POST /users/{id}/reset-password` clears `failed_attempts`/`locked_until` through `set_password` (`store/store.py:7693`); and the documented break-glass is the sealed `bootstrap-admin.txt` file (`api/app.py:5104-5146`, `docs/SECURITY.md:1750-1751`). Decisively: **there is no email-driven reset, unlock or recovery flow anywhere in the shipped code** — the notice body carries no link or token and ends "contact your MessageFoundry administrator" (`pipeline/security_notify.py:72-91`) — so the absent address removed no unlock path. @@ -8244,7 +8246,11 @@ effect. It costs the guardrail's reliability on exactly the shape a developer re > **What this does NOT close, stated so the fix is not over-read:** the SQL Server and Postgres store legs also skip locally, and that is a **different mechanism** -- path-gated CI jobs, not missing extras. Making extras loud will not make those loud. That gap cost real time on 2026-08-12, when a store-touching PR's SQL Server failure was invisible to every local run. -> 🔢 **Filed 2026-08-12 -- measured while verifying the `--prove-absences` port (PR #304), after a main-only baseline run built to attribute a pass-count delta turned out to be comparing two ENVIRONMENTS rather than two trees.** Value **6/10** -- Difficulty **2/10** -- _quick win_. `scripts/worktree/new.ps1:232` provisions a worktree venv with `$extras = if ($Sqlserver) { "dev,harness,sqlserver" } else { "dev,harness" }`. `.github/workflows/ci.yml:272` provisions the CI test leg with `-e ".[dev,harness,fhir,dicom,x12,xml,webauthn]"`. The five-extra difference is not theoretical: `pydicom`, `pynetdicom`, `lxml`, `xmlschema`, `signxml`, `webauthn`, `fhirpathpy` and `pyx12` are each absent from a worktree venv, confirmed by import. +> ✅ **CLOSED 2026-08-14 -- the owner-ruled LOUD-OMISSION half is BUILT and WIRED, landed in `558d2ed2` (PR #385).** `tests/_extras_probe.py` supplies `missing_extras()` / `report_header_lines()` / `write_incomplete_run_summary()`, and `tests/conftest.py` wires them into **pytest's own hooks** -- `pytest_report_header()` at `:366` and `pytest_terminal_summary()` at `:370`. **Verified by running, not by reading**, in both directions on two real interpreters: on an extras-complete venv the banner is **absent**; on the venv `scripts/worktree/new.ps1` actually builds it **fires in both surfaces**, naming the five extras, stating that the modules removed themselves at collection time, saying the result *"does NOT establish that the full suite is green"*, and printing the remedy command -- including on a bare full-suite run collecting 13,187 tests. **Scope is exactly the ruled half and no more:** `new.ps1:232` is unchanged, so options (a)/(b) (installing the five extras) were correctly NOT taken. +> +> ⚠️ **CLOSED WITH A NAMED RESIDUAL: THE WIRING ITSELF IS NOT REGRESSION-COVERED, and it is this item's own failure shape one layer up.** Measured by mutation: **deleting both hook functions from `tests/conftest.py` leaves `tests/test_incomplete_run_banner.py` reporting 7 passed**, while a real run goes from banner-present to banner-gone. Those 7 tests drive the probe against a fake reporter; **nothing asserts that pytest invokes them.** Repo-wide, `pytest_report_header` and `pytest_terminal_summary` appear **only** at `tests/conftest.py:366` and `:370` -- no test, lint or gate guards either. So a future `conftest` refactor can silently delete the loud-omission mechanism **with every check green**, which is precisely the silence this item was built to end. The item closes because the owner-ruled half shipped and works; **this residual is real and is recorded rather than folded into the closure.** + +> **Original filing, retained.** **Filed 2026-08-12 -- measured while verifying the `--prove-absences` port (PR #304), after a main-only baseline run built to attribute a pass-count delta turned out to be comparing two ENVIRONMENTS rather than two trees.** Value **6/10** -- Difficulty **2/10** -- _quick win_. `scripts/worktree/new.ps1:232` provisions a worktree venv with `$extras = if ($Sqlserver) { "dev,harness,sqlserver" } else { "dev,harness" }`. `.github/workflows/ci.yml:272` provisions the CI test leg with `-e ".[dev,harness,fhir,dicom,x12,xml,webauthn]"`. The five-extra difference is not theoretical: `pydicom`, `pynetdicom`, `lxml`, `xmlschema`, `signxml`, `webauthn`, `fhirpathpy` and `pyx12` are each absent from a worktree venv, confirmed by import. > **What it costs, measured by diffing COLLECTED NODE ID SETS rather than counts** -- `pytest --collect-only -q` in each venv, ids sorted and compared, because a count answers "how many" and never "which". **120 tests across 11 files never collect in a worktree venv:** @@ -8323,7 +8329,7 @@ every worker session's handoff, which is the sentence the next session bases its ## 1235. a citation to an unallocated backlog number is a trap that arms itself the day the number is issued -> 🔢 **Filed 2026-08-12 -- found by scanning, and one instance was live and unnoticed at the moment of filing.** Value **6/10** -- Difficulty **2/10** -- _quick win_. Documents in this project's PRIVATE companion repository cite public `BACKLOG #N` numbers that **were never allocated**. While the number is unissued the citation resolves to **nothing**, which is honest and harmless. The day someone legitimately allocates it, that citation begins resolving -- **to unrelated work, plausibly, and with nothing anywhere reporting a problem.** +> 🚧 **Filed 2026-08-12; PARTIAL as of 2026-08-14 -- the DETECTOR shipped, the RULE did not, and the detector is wired into nothing. NOT CLOSED.** Value **6/10** -- Difficulty **2/10** -- _quick win_. Documents in this project's PRIVATE companion repository cite public `BACKLOG #N` numbers that **were never allocated**. While the number is unissued the citation resolves to **nothing**, which is honest and harmless. The day someone legitimately allocates it, that citation begins resolving -- **to unrelated work, plausibly, and with nothing anywhere reporting a problem.** > **THE LEDGER'S OWN ERRATUM ALREADY NAMES THIS AS THE WORSE OUTCOME.** `scripts/hooks/ledger_check.py`'s header, explaining why the overlapping number space was recorded rather than renumbered, states that renumbering *"would only make stale citations resolve uniquely and WRONGLY, which is worse than resolving ambiguously."* A dangling reference advertises its own brokenness. A wrongly-resolving one advertises nothing, and reads as a working cross-reference forever. @@ -8333,6 +8339,12 @@ every worker session's handoff, which is the sentence the next session bases its > **Scope:** a rule, not a sweep. **Either allocate the number before citing it, or write the reference so it CANNOT resolve** -- naming the subject instead of a number (*"the retention runbook step, unallocated"*) is enough, and costs nothing. The sweep of existing instances is a separate, owner-present task in that repository, deliberately not folded in here. +> **WHAT LANDED 2026-08-14 IN PR #385, AND WHY IT DOES NOT CLOSE THIS ITEM.** `scripts/docs/dangling_citation_check.py` shipped with 23 unit tests. That is a real, working detector and it is the useful half. **It is not the deliverable this item asked for**, which its own Scope names as *"a rule, not a sweep"*. Three measurements at `origin/main`, each independently sufficient: **(1) the rule is unwritten** -- *"allocate the number before citing it, or write the reference so it CANNOT resolve"* appears nowhere at `origin/main` except this item's own prose (zero hits for `unallocated` across `CLAUDE.md`, `docs/LEDGER-GATE.md`, `CONTRIBUTING`, `scripts/`, `.github/`, `.claude/`). **(2) The detector is wired into nothing** -- repo-wide it is referenced by exactly **two** lines, both inside its own unit test; no workflow, no `.pre-commit-config.yaml` entry, no `.mefor-hooks/pre-commit`, no `pyproject.toml`. **(3) Even when invoked it exits 0**, because `main()` ends `return 1 if args.fail else 0` and `--fail` is opt-in and passed by nothing; a planted live-shape citation was reported correctly *and the process still exited 0*. No test runs `unresolved_citations` over the real `docs/` tree, so committing a new dangling citation turns nothing red. + +> **THE TWO RESIDUAL LIMBS, for whoever picks this up.** **(i)** Write the rule where authors actually read it -- `CLAUDE.md` or `docs/LEDGER-GATE.md`. **(ii)** Wire the detector **with `--fail`**, or add a test that runs it over the real `docs/` tree; the sibling gate already does exactly this (`.github/workflows/backlog-hygiene.yml` runs its checker against the real file on every PR). **And a coverage bound that survives both:** by its own docstring the detector *"does NOT see the private companion repository"* -- which is precisely where this item's filed instances live -- so even a fully wired detector only partially covers the stated population. + +> **Closing on the detector's existence would have been a compensating control resting on a false premise (SDS-3.7): a rule recorded as enforced where nothing enforces it.** The check that would have justified closure -- *do the files exist?* -- passes identically whether or not the rule landed and whether or not anything invokes the detector. It measures **presence, not enforcement**. The builder itself declined to close it, recording that the ledger half was the dispatcher's to author. + > **AMENDED 2026-08-14 -- THE TWO NAMED INSTANCES ARE NOT LIVE, AND THE ITEM'S MECHANISM WAS WRONG ABOUT WHY.** Both `#1203` and `#1231` hold **permanent allocation records** (`.git/mefor-coord/alloc/backlog/`, claimed 2026-08-09 and 2026-08-12). The claim above that `#1231` was *"allocated and released without being filed"* is incorrect: **`alloc.ps1` has no release.** `:41` states allocation is *"a one-way door -- claims are never released"*, and `:24` that *"numbers are never reclaimed ... holes are free, collisions are not"*. So neither number can be re-issued, neither citation can ever begin resolving to unrelated work, and both resolve to nothing **permanently** -- the state this item itself calls honest and harmless. **They are defused by construction, not "by an accident of timing".** > **WHY THEY ARE UNREACHABLE, stated as the conjunction it actually is.** Two independent facts are both required, and an earlier version of this amendment asserted only the first and over-claimed from it. **(1)** `alloc.ps1` never searches downward: `$start = $observed + 1` (`:392`, `:389` under the public-floor clamp) and the issuing loop only increments (`:394`), so no run fills a hole. **(2)** The floor is computed from the **committed ledger headings** -- measured non-destructively with `alloc.ps1 -ShowFloor`, which reports `floor: 1254` swept from `docs/BACKLOG.md` and `docs/archive/backlog/BACKLOG-CLOSED.md`. Those headings are **tracked content on refs**, so they survive a fresh clone, and both `#1203` and `#1231` sit far below them. That is what makes those two permanently unreachable, and it holds whether or not their allocation records survive. @@ -8906,3 +8918,20 @@ _FHIR_ID_RE.fullmatch("abc\n") -> False the fix **How to prove a fix.** It must be demonstrated against the FAILURE MODE, not in isolation -- run **both** testpaths together, then restore the bare import and confirm the same command fails again. Isolation is exactly the condition under which this defect reports success. **Provenance.** Diagnosed by the lane whose own commit tripped it: seven tests in one file failed in a full run and **passed in isolation, twice**. It reported the negative control alongside the fix -- restoring the bare import reproduced exactly those seven failures -- which is what makes the green meaningful. Recorded here rather than left in session mail because the collision outlives the commit that revealed it. It is the same shape as the rest of this cluster: **the name resolved to the neighbouring module, and nothing said so.** + +## 1256. the federated binding guards account-continuity but never subject-exclusivity, so two accounts can bind one identity + +> 🔢 **Filed 2026-08-14 - not started. SEPARATED FROM #1143 DELIBERATELY: that item asks which binding CEREMONY is defensible; this one is that no ceremony can help, because the guard asks the wrong direction of the question.** Value **7/10** · Difficulty **5/10**. The federated-login path enforces **account continuity** -- *"is THIS ACCOUNT now presenting a different subject?"* -- and never enforces **subject exclusivity** -- *"is this SUBJECT already bound to a DIFFERENT account?"* + +> **MEASURED at `origin/main`.** `auth/service.py:1109-1114` compares `user.oidc_subject` against the presented `federated_subject` and refuses on mismatch, then binds at `:1119-1120`. The comparison is keyed on the **user**, so it is structurally incapable of seeing a second account carrying the same `(issuer, subject)`. And nothing below it closes the gap: a scan for a `UNIQUE` constraint mentioning the federated columns returns **0** on **all three** backends (`store/store.py`, `store/postgres.py`, `store/sqlserver.py`). There is no database-level exclusivity either. + +**Cluster:** Security / authentication. **Priority:** P2. **Verdict:** build. +**Severity:** Conditional -- there are zero deployments (§0). On a first deployment with federation enabled, two local accounts could each be bound to the same external identity; the second binding neither refuses nor warns. Reaching it requires an operator or an admin action that binds the same subject twice, which bounds it without removing it. + +**Why the existing controls do not cover this, stated so the item is not re-closed as a duplicate.** Three shipped controls bound the ceremony and are correctly credited: the hybrid-only refusal (`auth/service.py:1100-1104`) means a federated principal with no on-prem directory object is refused, so the subject must already resolve to an existing username; the subject-continuity guard (`:1113-1123`, #1015 / ADR 0142); and the UPN suffix allow-list (`:926`). **All three constrain which subject may bind to a GIVEN account. None constrains how many accounts a given SUBJECT may bind to.** + +**Scope.** Decide and enforce exclusivity, then add the constraint that makes it structural rather than procedural. **Cost is asymmetric across backends and that is the real difficulty:** SQL Server types the federated columns `NVARCHAR(MAX)` (`store/sqlserver.py`), and a `MAX` column **cannot be an index key**, so a unique index there requires a **re-type**, not merely a new constraint. SQLite and Postgres do not share that cost. Pricing the SQL Server migration is part of the item, not a footnote to it. + +**How to prove a fix.** Bind `(issuer, subject)` to account A, then attempt the same binding on account B, and assert it is **refused** -- on every backend, because the enforcement point may differ per backend and a test passing on SQLite alone would certify nothing. The store suites for the two server backends **skip in a local run**, so this must be demonstrated in CI. + +**Provenance.** The exclusivity framing is the research conclusion of **#1143**, handed over as content by the lane that concluded it, with the number allocated here rather than by that lane. The uniqueness measurement and the `NVARCHAR(MAX)` index-key consequence were measured independently during earlier #1143 work and re-verified at `origin/main` before filing. #1143's own question -- whether TOFU is the defensible ceremony -- is **separable and unresolved**, and this item does not answer it.