Skip to content

feat: privacy shield operator deny-lists, miss queue, idnum, eval gate (#760) - #766

Merged
Weegy merged 3 commits into
mainfrom
feat/760-privacy-deny-lists
Aug 20, 2026
Merged

feat: privacy shield operator deny-lists, miss queue, idnum, eval gate (#760)#766
Weegy merged 3 commits into
mainfrom
feat/760-privacy-deny-lists

Conversation

@Weegy

@Weegy Weegy commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes #760.

What

Four gaps in the prompt-masking layer's operator story, found during an external audit-readiness review: no operator-defined terms, no path for a human to report a missed value, idnum explicitly ungated ("informationally only"), and detection quality with zero CI regression protection.

How

Operator deny-list (custom_terms / custom_patterns)

  • Two new privacy-plugin setup fields. Literal terms match case-insensitively on unicode word boundaries (longest-first alternation); regex patterns are vetted at config change, not per turn: syntax check, then an escalating pathological-probe corpus ('a'*n + 'b' for growing n) under a 50 ms budget — a catastrophic-backtracking pattern is caught at small n before it could hang anything. Rejected patterns are reported loudly (customPatternRejected), never silently dropped — an operator who typed a protection must learn it is not active.
  • Spans report type custom / detector custom-terms in the receipt and ride the existing fail-closed surrogate machinery unchanged — findIdentityLeaks covers custom values automatically. Detector construction is fingerprint-cached; no custom config ⇒ byte-identical baseline-only behaviour (test-asserted).

Miss-report catch basin

  • Fail-closed guards execution failures, not non-detection. New intake on the PrivacyReceiptCard ("report a value the shield missed") → privacy_miss_reports (migration 0040; 0039 is Persist per-turn audit receipts (receipt store) #757/PR feat: persist per-turn privacy receipts with operator API (#757) #763) → review queue at /operator/privacy-reports (copy term → paste into custom_terms → resolve). Kernel routes auth-gated under /api/v1/operator/privacy/miss-reports, session actor server-side, zod at the boundary.
  • Privacy posture, stated honestly: the reported term is stored as the reporter typed it — reporting is a deliberate operator act on an auth-gated surface, and the literal value is exactly what the reviewer needs to build the rule. The intake UI says so.

idnum promoted from informational to gated

  • C0 patterns for DE Steuer-ID (grouped + bare 11 digits) and USt-IdNr., ES NIE/DNI, IT Codice Fiscale, UK NINO ([A-Z]{2} on purpose — over-match beats a leak), FR n° de sécurité sociale. All eight committed fixture shapes across the six locales detect (verified by test and by the eval).
  • Deliberately unpatterned: NL BSN — 9 bare digits with no distinguishing shape; a global 9-digit pattern would mask half the numeric universe. Recorded as a known miss inside nl's aggregate, not an ungated type (validation/README.md).

Detection quality as a CI gate

  • promptDetectorEval.ts --check: deterministic C0-only run compared against committed per-locale floors (validation/ci-baseline.json). de/en/es/fr/it sit at the official gates (0.97/0.85); nl's floor (0.88) records its pre-existing C0 gaps (de-centric address pattern + BSN) so a regression below today fails without permanently reddening CI. Floors are exact — the eval is pure regex over committed fixtures, so any drop is a real regression, not noise.
  • The check fails on an empty evaluation (zero locales, missing baseline locale, item count below floor) — the feat(ci): golden-set regression eval for LLM verifier behaviour (#129) #640 permanently-green-no-op failure mode is designed out. New CI step in the middleware job.

Tests

  • test/privacyCustomTermsAndIdnum.test.ts (19): detector units (word boundaries incl. umlauts, both rejection reasons — syntax and too_slow via (a+)+$), all 8 idnum shapes + negative, service wiring (term masked, receipt span type custom, patterns fail-closed, no-config byte-identical), manifest↔code key wiring (the Wave-L declared-but-never-read lesson), --check smoke via spawnSync incl. the non-zero-locale-count assertion.
  • test/privacyMissReports.test.ts (5): create with session actor, 400s before the pool on bad input, status filter threading, resolve-once + 409, malformed id.
  • Mutation check (rebuilt dist/): removing the custom-detector wiring from maskUserPrompt turns 2 tests red.
  • Full middleware + web-ui suites green; typecheck, typecheck:test, i18n parity + validator green.

Pre-PR review round (self-review findings, all addressed)

  • H1 (blocker) — letter-only probe corpus: (\d+)+$ passed vetting and a user prompt with a digit run could wedge a turn. Fixed twice over: the probe escalation now runs across letters, digits, mixed-alphanumeric, unicode and punctuation (test: (\d+)+$ is rejected too_slow), AND a runtime backstop bounds every operator pattern per turn — over budget throws CustomPatternRuntimeError, which the service's tier-2 catch converts into a BLOCKED turn. Deliberately no auto-disable: skipping the pattern on later turns would be fail-open for exactly the values it protects; the operator gets a greppable customPatternRuntimeExceeded log instead. Residual risk stated in the CHANGELOG (a polynomial pattern's first over-budget turn is slow before the block lands).
  • M2 — nl floor now sits below today's value with a deliberate margin (0.84 vs 88.2%) so adding honest nl fixtures the current patterns miss never reddens CI; the baseline comment no longer claims "exact current values".
  • M1 — each locale gained a near-miss negative (8-digit order number) so the precision proxy actually exercises the idnum alternation, plus a second idnum positive for de/en/es/fr (USt-IdNr., compact NINO, compact NIE, FR sécu with a Corsica 2A department). Documented over-match surface (bare 11-digit runs, NINO-shaped tokens) stays a design choice per the fail-closed philosophy.
  • M3turnId is now actually threaded chat page → receipt card → miss-report form (the declared-but-never-threaded defect shape).
  • M4 — the detector fingerprint cache moved from module scope into the service closure.
  • Plugin version bumped 0.4.0 → 0.5.0 in both package.json and manifest.yaml (the hub reads the manifest); docs/security-architecture.md gained the deny-list/miss-queue posture paragraph.

Deliberately out (named in CHANGELOG)


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Weegy added 3 commits August 20, 2026 13:31
…ivacy-shield

# Conflicts:
#	docs/CHANGELOG.md
#	docs/middleware-agent-handoff.md
#	middleware/src/index.ts
#	web-ui/app/_components/Nav.tsx
#	web-ui/messages/de.json
#	web-ui/messages/en.json
…ivacy-shield

# Conflicts:
#	docs/CHANGELOG.md
#	docs/middleware-agent-handoff.md
@Weegy
Weegy merged commit c57ba73 into main Aug 20, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Privacy Shield: operator deny-lists, miss-report review queue, idnum coverage, eval CI gate

1 participant