Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ Ordered by value descending, then difficulty ascending (cheapest first at equal
| 47 | **#132** | Fixed 'now' test-time override (frozen clock for reproducible transform tests) | 5 | 3 | _fill-in_ | DEMAND-GATE | Value 5 stands (a wall-clock-free transform or a tolerant diff gets regression comparison today — "parity/breadth with a clean workaround"), and the seam claim is verified: `route_message` takes `ingest_time` at dryrun.py:517 and the two internal call sites hardwire `time.time()` at :679 (`_dry_run_raw`) and :753 (`dry_run`). But "a --now flag threaded through two entry points" undercounts the surfaces, and the ones it misses are the ones the item is ABOUT. `checks.py:1058,1126` calls `dry_run(reg, raw, inbound=..., snapshot_on_send=...)` with no ingest_time — and checks.py is the `.expect` fixture comparator, i.e. the repo's actual deterministic-regression gate. `trace_dry_run` is a separate module (`dryrun_trace`, invoked from __main__.py:2926-2931). And the item's own Trigger names the Test Bench: ide/src/testBench.ts shells `dryrun` at five sites (:240, :325, :354, :440) and would need the flag plus an affordance. Engine + CLI + fixture gate + a TypeScript extension is D3 work, not D2's "small additive change on an existing seam". Quadrant stays fill-in; tier stays DEMAND-GATE. |
| 48 | **#172** | Gzip/zip compression codec + file-connector option | 5 | 3 | _fill-in_ | DEMAND-GATE | File-feed parity breadth with a clean code-first workaround: the reusable codec shipped including `zip_compress`/`zip_decompress` (`messagefoundry/parsing/compression.py:40-48`), so a zip-delivering partner is served by a Handler call today. What remains is connector-level — widening `_SUPPORTED_COMPRESSION` (`messagefoundry/transports/file.py:88`), which forces an archive-member-to-message decision, plus REMOTEFILE, which has zero compression to extend. |
| 49 | **#1014** | connscale smoke test's fixed 24-port block is not parallel-safe across worktrees; the flaky marker hides the collision | 5 | 3 | _fill-in_ | P3 | `test_connscale_smoke_end_to_end` hard-codes `base_port = 41000` and needs 24 CONTIGUOUS inbound ports, so two checkouts running the suite at once contend for the same block — which is the normal topology here (24 worktrees were live on 2026-08-04). It self-heals via `@pytest.mark.flaky(reruns=2)` commented "CI runners are noisy", so a determinate resource collision wears a noise label and the retry does work the port allocation should be doing. Value 5: it costs retries and misdiagnosis rather than correctness. Difficulty 3: allocate the block dynamically and assert contiguity, rather than widening the retry. |
| 50 | **#1020** | The first-run bootstrap Administrator is created with no email address, and the PHI notification gate cannot see it | 5 | 3 | _fill-in_ | P2 | `_ensure_bootstrap_admin` calls `create_user` with no `email=` (`auth/service.py:527-533`), so the account holding `frozenset(Permission)` has a NULL email and `SecurityEventNotifier.notify` returns before enqueueing (`pipeline/security_notify.py:130`), making all ten notice types no-op for it — including LOGIN_AFTER_FAILURES, the compromise signal. The part with teeth is gate blindness: the PHI startup gate refuses to serve without a notification channel but computes readiness from `notify_security_events` plus `email_smtp_host` plus `email_from` alone (`__main__.py:2260`), so it would report a healthy channel while no notice about the all-permission account could be delivered. The allocated title's second half is REFUTED and the body says so: lockout is time-bounded (15 minutes by default, `settings.py:1769`), an admin reset clears it (`store/store.py:7693`), the documented break-glass is a sealed file (`api/app.py:5104`), and no email-driven recovery flow exists anywhere in the code. Value 5 because every event is still an audit row surfaced by `GET /me/security-events`. Difficulty 3 with no schema change; the only judgment is which of three candidate fixes the owner wants. |
| 50 | **#1020** | The first-run bootstrap Administrator is created with no email address, and the PHI notification gate cannot see it | 5 | 3 | _fill-in_ | P2 | `_ensure_bootstrap_admin` calls `create_user` with no `email=` (`auth/service.py:527-533`), so the account holding `frozenset(Permission)` has a NULL email and `SecurityEventNotifier.notify` returns before enqueueing (`pipeline/security_notify.py:130`), making all ten notice types no-op for it — including LOGIN_AFTER_FAILURES, the compromise signal. The part with teeth is gate blindness: the PHI startup gate refuses to serve without a notification channel but computes readiness from `notify_security_events` plus `email_smtp_host` plus `email_from` alone (`__main__.py:2260`), so it would report a healthy channel while no notice about the all-permission account could be delivered. The allocated title's second half is REFUTED and the body says so: lockout is time-bounded (15 minutes by default, `settings.py:1769`), an admin reset clears it (`store/store.py:7693`), ~~the documented break-glass is a sealed file (`api/app.py:5104`)~~ **[STRUCK 2026-08-13 -- NOT AN UNLOCK PATH. `_emit_bootstrap_admin` writes that file only for a NEWLY CREATED account on the bootstrap path, gated on an empty users table, so it cannot recover an EXISTING locked admin. And for a SOLE administrator the 'admin reset clears it' limb fails too -- that needs a SECOND admin, and self-reset is refused. Only the 15-minute expiry remains, which is the lock lapsing rather than a recovery path. An amendment written to correct a premise introduced a false one; see #1236]**, and no email-driven recovery flow exists anywhere in the code. Value 5 because every event is still an audit row surfaced by `GET /me/security-events`. Difficulty 3 with no schema change; the only judgment is which of three candidate fixes the owner wants. |
| 51 | **#1019** | install-selfheal.ps1 has no installed-vs-source payload-parity instrument, and it wires the most privileged hook in the estate | 5 | 3 | _fill-in_ | P3 | The installer copies `worktree-selfheal.ps1` to `~/.claude-hooks/` and wires it as a user-scope SessionStart hook with no divergence detection: no `-Status` (the surface is `-ConfigDir` plus `-HookPath`), no version stamp, a bare `Copy-Item -Force` (`:57`), and no test that reads the installed copy — every selfheal test binds `ROOT` under a synthetic home, and the repo's only installed-vs-source parity test names the gate copy alone (`tests/test_gate_installed_parity.py:44`). The title's "no parity instrument at all" is narrowed in the body: source-level guard parity DOES exist (`test_both_installers_carry_the_same_refusal`), as do the CLAUDECODE refusal, the backup-validate-rollback, and an unconditional payload refresh — what is absent is payload parity. The claimed comparator is also wrong, and PR #191 has since sharpened this: it landed payload parity on `install-git-hooks.ps1` (SHA256, IN SYNC/STALE, plus a pytest-side assertion), so `install-selfheal.ps1` is now the ONLY installer in the estate without one, with two worked examples to copy. Privilege is substantiated: user scope in every config dir, and a hook that runs `git checkout` on the shared primary unattended (`worktree-selfheal.ps1:106`). Measured 2026-08-04, installed and source agree (`c41c70ecf885`), so detection is absent rather than divergence present. Difficulty 3 with four constraints, chiefly folding CRLF exactly as the existing instruments do. |
| 52 | **#1027** | The documented `pytest` command silently excludes the webconsole package, so a local green is not evidence about ~344 tests | 5 | 3 | _fill-in_ | P3 | `pyproject.toml` sets `testpaths = ["tests"]`, so the command CLAUDE.md:333 documents as the verification gate (`QT_QPA_PLATFORM=offscreen pytest -q`) never collects `packaging/messagefoundry-webconsole/tests` — while CLAUDE.md section 5 states a task is not done until it passes. Not hypothetical: on 2026-08-04 `test_webui.py::test_webauthn_rp_fail_closed_legible` was FAILING on main all day and no lane saw it, surfacing only when one lane named both paths explicitly for a webconsole-touching change (`1 failed, 10681 passed, 851 skipped`). ⚠️ NOT a CI gap, verified rather than assumed: `ci.yml:250` installs `-e ".[dev,harness,fhir,dicom,x12,xml,webauthn]" -e packaging/messagefoundry-webconsole` and runs `Web console tests (pytest)` as a separate required step, so PRs merged on real coverage — the gap is local only, which is exactly why nothing red ever reached anyone. Difficulty 3 because the naive fix reds every local run: adding the path to `testpaths` makes that same `[webauthn]` failure the default local experience, since worktree venvs bootstrap a narrower extra set than CI. ⛔ Whichever option is chosen, a skip must announce itself — trading a silent exclusion for a silent skip is not a fix. |
| 53 | **#236** | Test-this-step and test-up-to-step with pinned upstream values | 5 | 4 | _fill-in_ | P2 | Real debug breadth — whole-handler traced values already fold onto rows (`mergeLiveValues`, ide/src/stepsModel.ts:544) so partial runs are a convenience, but pinning an expensive `db_lookup`/`fhir_lookup` has no equivalent at all; largely a stop condition plus state dump on ADR 0072's shipped trace, with the lookup mock and keeping `buildLensTraceArgs` (:674) incapable of emitting `--show-phi` the real work. |
Expand Down Expand Up @@ -6326,10 +6326,16 @@ filing.
>
> ⚠️ **AMENDED 2026-08-13 -- THE ITEM STAYS OPEN AND ITS QUESTION IS RE-SCOPED. Three corrections, and the third is the one worth reading.**
>
> **1. THE REMEDY IS STRONGER THAN THIS ITEM CREDITS, AND THE DIFFERENCE IS LOAD-BEARING.** `api/auth_routes.py:753` routes `POST /users/{user_id}/reset-password`, and `:757` takes `Depends(require_step_up(Permission.USERS_MANAGE))` -- **the admin RE-AUTHENTICATES; it is not a plain permission check.** This item's own research question says *"anything that clears a lock is itself a control an attacker wants, so an unlock route is a candidate with its own abuse surface, not an answer"* -- and the step-up is a **partial answer to exactly that abuse surface**, which nothing in the record credits. Coverage is uniform, so this is not a partial-surface finding: all three backends clear the lock in `set_password` (`store.py`, `postgres.py`, `sqlserver.py` -- `failed_attempts=0`, `locked_until=NULL`, two sites each).
> **1. THE REMEDY IS STRONGER THAN THIS ITEM CREDITS, AND THE DIFFERENCE IS LOAD-BEARING.** `api/auth_routes.py:753` routes `POST /users/{user_id}/reset-password`, and `:757` takes `Depends(require_step_up(Permission.USERS_MANAGE))` -- **the admin RE-AUTHENTICATES; it is not a plain permission check.** ~~This item's own research question says an unlock route is *"a candidate with its own abuse surface, not an answer"*, and the step-up is a partial answer to exactly that abuse surface, which nothing in the record credits.~~
>
> ⚠️ **THAT SENTENCE WAS WRONG IN BOTH HALVES AND IS CORRECTED HERE 2026-08-13. It was mine, and I landed it.** **(a) The record is NOT silent** -- cell **6.3.8** already describes *"the only reset is administrator-initiated behind a step-up gate"*. So **6.1.1 OMITS it; the record does not LACK it** -- a narrower and different defect. **(b) THE SIGN IS INVERTED. It is not a credit on 6.1.1's axis, it CUTS THE OTHER WAY.** 6.1.1 is about **malicious lockout**, and a stronger gate on the *sole* administrative remedy makes **RECOVERY HARDER**, not easier. **Record the step-up as a CONSTRAINT on the remedy, not as a mitigating control** -- it is part of why the sole-administrator case is a dead end, now filed as **#1236**.
>
> **How the error happened, because it is the reusable part:** the step-up's EXISTENCE was verified at `auth_routes.py:757` and is real. Its INTERPRETATION -- that it mitigates 6.1.1 -- was inherited from the report and never checked against the cell's axis. **Measuring the fact and adopting the framing are two acts, and only the first was performed.** Coverage is uniform, so this is not a partial-surface finding: all three backends clear the lock in `set_password` (`store.py`, `postgres.py`, `sqlserver.py` -- `failed_attempts=0`, `locked_until=NULL`, two sites each).
>
> **2. THE `docs/SECURITY.md:1474` CITATION IS STALE and will cost the next reader time.** At engine `9d98f339` that line is an unrelated **AD** row in the comparative-properties table. The disputed parenthetical is now at **`:1544`**, in the three-limiter anti-automation table. **Cite the greppable phrase, not the line number.**
>
> **AND THE DISCLOSURE GAP IS NOT ONE PARENTHETICAL -- it is a CONTRADICTION a reader hits FIRST.** `docs/SECURITY.md:59` reads *"never auto-disabled, so **a single-admin deployment can't be locked out**"*. In context that sentence is about auto-retirement, but **its words flatly deny the thing this item is about**, and it sits **1,485 lines BEFORE** the known parenthetical at `:1544` -- it is the one a reader greps for and stops at. Two further collisions on the same word: `:741` *"AD operators are never locked out"* (true, and for a reason worth stating) and `:782` *"admin is never locked out"* (scoped to the enroll/confirm routes). **A documentation fix that edits only `:1544` leaves the contradiction that is read first.**
>
> **3. WHY THIS ITEM GOT IT WRONG -- RECORDED AS METHOD, NOT MERELY AS A CORRECTION.** The filed absence claim searched `def admin_unlock|unlock_account|clear_lockout`. Measured on `main`: that regex returns **0 hits** in `auth/service.py`, a file containing exactly **1** `def admin_reset_password`. **The regex CANNOT STRUCTURALLY MATCH THE THING THAT SHIPS.** So the zero was a **true answer to a question nobody asked**, and this item's conclusion -- *"no dedicated administrative unlock exists"* -- is **literally true and materially misleading**: an unlock ships, as a side effect of a forced password rotation.
>
> > **THE PRESCRIPTION: an absence claim over a CAPABILITY must be written from the STATE, not from plausible function names.** *"Find every write to `locked_until`"* would have found it; *"find every function called something-unlock"* never could.
Expand Down Expand Up @@ -8253,3 +8259,28 @@ every worker session's handoff, which is the sentence the next session bases its
> **What makes this filable rather than a style note:** there is **no gate on either side**. The allocator answers *"is this number free"*; nothing asks *"is anything already pointing at it."* And a citation in another repository is invisible to every check this one runs. The two halves are each individually correct and the gap between them is the defect.

**Cluster:** Process / ledger integrity. **Priority:** P3. **Verdict:** build (the rule). **Severity:** minor and self-inflicted -- it corrupts cross-references between maintainer documents, touches no shipped code, and no deployment can observe it.

## 1236. a sole-administrator deployment has no recovery from account lockout: every exit is closed by design

> 🔢 **Filed 2026-08-13 -- an AVAILABILITY defect in the shipped control, NOT a documentation gap, and split out of #1131 for exactly that reason.** Value **6/10** -- Difficulty **4/10**. Each exit is individually deliberate and defensible; the defect is that **they close simultaneously for a deployment with one administrator**, and nothing in the code or the docs notices the conjunction.

> **THE FIVE EXITS, each measured on `origin/main`:**
> ```
> bootstrap account is literally "admin" auth/service.py:71 BOOTSTRAP_USERNAME = "admin"
> -> the username an attacker guesses first is the one that cannot recover
> created with no email -> SecurityEventNotifier returns early
> pipeline/security_notify.py:127-131
> -> the ACCOUNT_LOCKED notice never leaves the process
> self-reset refused api/auth_routes.py:762 (own account)
> admin reset needs ANOTHER admin -- there is not one
> re-bootstrap only on an EMPTY users table auth/service.py:529
> no argparse subcommand manages users -- no offline path either
> ```

> **WHY IT IS NOT A DOCUMENTATION ITEM, and why it must not ride #1131.** #1131 asks what the anti-automation documentation must SAY. This item is that the shipped system has no recovery to describe. **If it rode #1131, a documentation edit would close the item while the defect went untracked** -- the reader would see "lockout disclosure fixed" and reasonably conclude recovery exists.

> **The step-up gate on the sole remedy is a CONSTRAINT here, not a mitigation** (`auth_routes.py:757`, `require_step_up(Permission.USERS_MANAGE)`). It is correct on its own axis and it makes this case harder, which is precisely the shape #1131's amendment first got backwards.

> **How to prove a fix:** lock the sole administrator, then recover **without** editing the database by hand and **without** an already-authenticated second admin -- and assert the recovery path is itself gated, since an unlock affordance is a control an attacker wants. A fix that only lengthens the docs, or that opens an ungated reset, fails on opposite sides.

**Cluster:** Security / availability. **Priority:** P2. **Verdict:** build. **Severity:** conditional per CLAUDE.md section 0 -- on a first deployment with one administrator, a lockout would be unrecoverable without direct database access; **zero deployments, so nobody is locked out today.**
Loading