diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 448a38ba..7919610a 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -1729,6 +1729,13 @@ lane; demand-gated on a first enterprise Windows/AD deployment. > 🔢 **Re-scored 2026-08-03 → DEMAND-GATE.** Value **2/10** · Difficulty **6/10** · _money pit_. Value 2 stands — stdout + NSSM rotation, the RFC 5425 TLS syslog forwarder (`_TlsSysLogHandler`, logging_setup.py:281) and #50's disk metering already carry log durability and visibility, so this is marginal and substantially covered. But difficulty 5 prices the wrong shape of work. D5 is "a new connector/codec behind the transport registry" — this is not a connector. logging_setup.py's module docstring (lines 3-13) records that the engine "deliberately do[es] not add file handlers here" because NSSM owns rotation, and `grep FileHandler _(was 2/10 · 5/10.)_ > **On-trigger / demand-gate.** Numbered for tracking only — build when the trigger below fires (“demand-gate, don’t schedule”). +> ⚠️ **AMENDED 2026-08-11 — THE OWNER RULED THIS ITEM IN, and that ruling POST-DATES the DEMAND-GATE and the 2/10 above.** The owner's words: *"we never want to process stuff if the processing cannot be logged."* That coupling **is** the count-and-log invariant CLAUDE.md marks do-not-break — every received message is persisted before the ACK and nothing is accepted-and-dropped — so the gate is discharged and the trigger is moot. + +> **The 2/10 was arrived at by conflating VISIBILITY with ENFORCEMENT.** stdout, NSSM rotation, the TLS syslog forwarder and #50's disk metering make the log *visible*; none of them makes processing *stop* when the log cannot be written. The item is the enforcement, and that is what the owner ruled in. **A guard that logs a warning and lets processing continue does not satisfy this item** — that is the specific defect to check for at review. + +> **Work exists and is UNVERIFIED.** `w3-log-write-failure` (`d26d66a6`, pushed and anchored) carries five commits whose subjects claim: the halt let the backlog keep routing so the internal stages are stopped too; a reload-recovery claim that *"was a guess"* was measured and three docs corrected; two PHI-inventory anti-rot gates found red on the branch; and a console-contract seam bump. **The lane died mid-flight on a usage limit, so none of it is verified.** It carries **ADR 0162**, whose number was independently confirmed to come from `alloc.ps1` — a real allocation record exists and `main`'s highest is 0161 — so the number is sound, but the index row should be re-checked before landing since a rebase can drop it. + +> **The claim most worth verifying by execution is the partial halt:** a halt that stops intake while routed and outbound rows keep draining would still violate the invariant this item exists to protect. **Cluster:** Logging & Audit. **Priority:** P3. **Verdict:** demand-gate. **Severity (vs Corepoint):** minor. @@ -7581,6 +7588,17 @@ gate is the wrong shape, validation of the walk is the right one. > 5. **It weakens a forcing function, and that trade should be made deliberately.** `allow_unbounded_phi` (#186a) currently makes a PHI instance REFUSE TO START in prod unless both windows are bounded -- which forces every operator to choose a retention window consciously. With a bounded default that gate rarely fires: **more secure by default, less explicit by default.** > **Framing note:** 60 days is an OPERATIONAL default, not a compliance number. The compliance retention here is the audit chain, which stays keep-forever. Nothing in this item should imply 60 days satisfies a regulatory requirement. +> ⚠️ **AMENDED 2026-08-11 — BUILT, REFUTED, AND REVERTED. Do not read this item as merely unbuilt; the obvious implementation is now known to be WRONG.** The change was made (default `messages_days` / `dead_letter_days` `0` -> `60`), reviewed by another session, verified by execution, and **reverted at `ed8a09d7`** on `a-1212-retention`. Forward-only, so the two reverted commits stay readable as the record. + +> **THE DEFECT, and it is the inversion of this item's intent.** `serve`'s posture gate is wrapped in `if not settings.retention.allow_unbounded_phi:` — so when the **audited, deliberate keep-forever opt-out is TRUE, the auto-bound block is SKIPPED ENTIRELY and the field keeps its model default.** Measured: `RetentionSettings(allow_unbounded_phi=True).messages_days` returns `60` under the change. The `RetentionRunner` then starts and **purges PHI bodies after 60 days on exactly the instances whose operator consciously chose to keep them forever.** Silent PHI deletion against an explicit audited decision, landing *only* on the opt-in path. That is worse than the unbounded default this item was filed to fix. + +> **AND IT WAS A NO-OP WHERE IT WAS AIMED.** The auto-bound keys on `model_fields_set` — whether the operator *wrote* the key, not what its value is — so an operator who says nothing still resolves to **30** and always did. Changing the model default never survives the gate on any instance the gate runs on. + +> **A PREMISE IN THIS ITEM'S OWN TRAP 5 IS STALE.** Trap 5 warns the change *"weakens a forcing function"* because a PHI instance would otherwise refuse to start unbounded. **PR #71 already inverted that gate:** a production PHI instance under `enforce` with an unset window **no longer refuses — it auto-bounds at 30 and starts**, and the repo's own test flipped with it (`tests/test_cli.py:1619` is now `test_serve_auto_bounds_an_unset_body_window_in_prod`, asserting `rc == 0`). So the "more secure by default, less explicit by default" trade **was already made** and is not this item's to weigh. Note `docs/PHI.md` §8 still described the old refusal, which is how that stale premise survived into this filing — the doc-vs-code gap recorded under ASVS 14.2.4 ground (5), misleading a second engineer in practice. + +> **IF THE 60 IS STILL WANTED, the value to change is `auto_bound_days` in `messagefoundry/config/retention_classification.py`** — what the gate actually applies — **not the field default. And moving it `30` -> `60` LOOSENS the shipped PHI bound rather than tightening it**, which sharpens the owner's question rather than answering it. That module carries **nine** classified windows behind `MIN_PHI_RETENTION_WINDOWS = 9` with a **two-way** drift test against `docs/PHI.md` §2, so any real window change must move the tuple **and** the doc together or `tests/test_retention_classification_drift.py` reds. + +> **`audit_days` staying `0` was the one part of this item that held up** — the audit log is a tamper-evident chain with a ~6-year HIPAA expectation, and a sweep that "fixed all the zero defaults" would convert a compliance property into a defect while looking finished. **Cluster:** Store / Retention. **Priority:** P2. **Verdict:** build. **Severity:** conditional -- on a first deployment an operator who omits the setting would retain full raw PHI bodies indefinitely; there are zero deployments today.