Reproduction steps:
- Scrub short PHI-bearing free text in STRICT mode (set by the operator's
phi_mode toggle, or auto-set by payload_is_clinical):
phi_guard.scrub("call John Doe re HIV results", strict=True) # 30 chars
phi_guard.scrub("Maria Garcia tested positive for HIV", strict=True) # 36 chars
phi_guard.scrub("admitted 2026-01-05, Jane Doe, sepsis", strict=True) # 37, has a date
- Compare with the same content over 40 chars:
phi_guard.scrub("x"*45 + " Jane Doe sepsis", strict=True)
Expected:
On a PHI-bearing flow, unrecognised free text is redacted wholesale — the
strict-mode contract, whose rationale is "personal names in narrative cannot be
detected by pattern." A name is as undetectable at 30 chars as at 300.
Actual:
"call John Doe re HIV results" -> unchanged (LEAK)
"Maria Garcia tested positive for HIV" -> unchanged (LEAK)
"admitted 2026-01-05, Jane Doe, sepsis" -> "admitted [date], Jane Doe, sepsis"
(date masked; name + diagnosis LEAK)
"xxxx... Jane Doe sepsis" (>40 chars) -> "[redacted — free text on a PHI-bearing flow]"
scrub() applies wholesale redaction only when len(value) > FREE_TEXT_CHARS (40);
at or below 40 it falls back to identifier masking, which cannot catch a name or
a free-text diagnosis. phi_guard.scrub feeds the approval-airlock card and the
signed command receipt (command_registry.redact_output, approval_airlock.redact),
so the name/diagnosis lands in both — under the mode the operator enabled to
prevent exactly that. This reintroduces the community #1fde22 leak below the
length threshold.
Suggested fix:
Drop the length floor in strict mode; redact any residual free text, but keep
precise masking for a value that is ONLY a recognised identifier:
if isinstance(value, str):
if strict:
stripped = value
for _name, _rx in _DETECTORS:
stripped = _rx.sub("", stripped)
if stripped.strip(): # free text beyond identifiers
return REDACTED_FREE_TEXT
return _scrub_text(value) # value was only identifiers -> mask them
hits = detect(value)
if hits:
return _scrub_text(value)
return value
In strict mode over-redaction is the safe failure, so the 40-char floor (meant to
spare short non-PHI values) is the wrong default there. Non-strict behaviour is
unchanged.
Station version (railcall version): station-v0.99
Module slug + version: N/A (platform — workbench/phi_guard.py)