ADR 064 · Accepted

Central Privacy Model

Context

The extension is metadata-only by construction almost everywhere. Telemetry (adr-058) stores no prompts or responses — only an exception FQCN on failure — and already bounds its growth with a retention purge (nrllm:telemetry:purge). But two tables do persist per-request content, and until now they did so unconditionally:

  • tx_nrllm_eval_result.details — a JSON snapshot of the per-prompt graded model output (adr-060). This is real model output.

  • tx_nrllm_skill_audit.scan_result and detail — the prompt-injection scan output and a free-form detail string (adr-061).

Both tables also carry pure-metadata columns (identifiers, counts, checksums, trust level, timestamps) that are not sensitive.

ADR-058 named the gap explicitly:

The central privacy model (retention tiers none/metadata/redacted/full) is a later workstream; this middleware is metadata-only by construction.

So there was no single, operator-configurable answer to "how much per-request content does this extension keep, and for how long?", and no purge for the two content tables — only telemetry had one.

Decision

Introduce one central privacy model, read from the extension configuration and applied at every content sink before a write.

1. Four levels — a backed PrivacyLevel enum on a strict-to-loose scale:

  • none — drop content, keep metadata.

  • metadata — drop content, keep metadata (the default).

  • redacted — store a bounded, credential-scrubbed copy.

  • full — store content verbatim.

The enum exposes persistsContent() (true for redacted/full), requiresRedaction() (true for redacted) and a severity() ordering with strictest(a, b) so a global default and any future per-scope override combine with "strictest wins"none is strictest, full loosest.

2. Safe defaultPrivacyPolicy::level() returns PrivacyLevel::METADATA when the setting is unset or invalid. That is the behaviour the extension already had by construction, so an un-configured instance keeps content out of the database, not in it.

3. Two governed sinks. EvaluationResultRepository and SkillAuditRepository inject PrivacyPolicyInterface and pass their content columns through filterContent() before insert (details; scan_result and detail). null (dropped content) is stored as the empty string so the columns stay well-typed. Every metadata column is untouched.

4. Honest, bounded redaction. ContentRedactor (the redacted level) masks a small set of high-signal secrets — credential-bearing URL query parameters (reusing ErrorMessageSanitizerTrait, not a copied regex), obvious bearer / API tokens, and email addresses — and caps length at 2000 characters. Its doc block states plainly that it is a heuristic, not a guaranteed PII scrubber; operators who must not store content at all use none/metadata.

5. Centralised retention. Each content repository gains a purgeOlderThan(int $timestamp): int mirroring TelemetryRepository::purgeOlderThan() (run_date for eval results, crdate for the audit trail). A new command nrllm:privacy:purge (--days, defaulting to the configured privacy.retentionDays, floor 1) purges all three log tables — eval results, skill audit and telemetry — and reports the count per table. The retention window clamps a missing / zero / negative setting to 30 days: 0 must never mean "delete everything immediately". The existing nrllm:telemetry:purge stays for backward compatibility.

6. Configuration. Two settings under a new privacy category in ext_conf_template.txt: privacy.level (options[None|Metadata|Redacted|Full], default metadata) and privacy.retentionDays (int+, default 30).

Consequences

  • ●● One operator-facing answer to "what is stored". A single setting, read in one place and enforced at every content sink, replaces the previous implicit, per-table behaviour.

  • ●● Safe by default. An un-configured instance stays metadata-only — the historical behaviour — so this is not a silent behaviour change for existing installs. It generalises the principle telemetry already followed.

  • Bounded, purgeable growth for the content tables too. The two content logs get the retention story telemetry already had; nrllm:privacy:purge bounds all three from the scheduler.

  • Strictest-wins ordering is ready for per-scope overrides. The severity() / strictest() API expresses tightening today even though only the global default is wired, so a future per-configuration override composes without a rework.

  • Redaction is deliberately shallow. redacted removes known secret shapes and caps length; it does not guarantee PII removal. The honest doc block prevents a false sense of safety, and none/metadata remain the answer when content must not be stored.

  • Two purge commands now exist. nrllm:telemetry:purge is kept for backward compatibility alongside the broader nrllm:privacy:purge. Operators scheduling the new command can retire the old one.

  • Lossy at redacted/full boundaries. At redacted the stored details JSON may be truncated or masked and is no longer guaranteed to round-trip; consumers that need the verbatim snapshot must run at full.

Net Score: +6 (2×●● +4, 2×● +2, 1×◐ +0.5, 1×✕ −1.5, 1×◑ −1).

Alternatives considered

  • Per-table settings instead of one model. Rejected: it multiplies operator surface and drifts, and gives no single answer to the compliance question. One enum applied at every sink is the point.

  • A comprehensive PII scrubber. Rejected as dishonest for the scope: robust PII detection is a large, error-prone problem. A bounded heuristic with an explicit "not a guarantee" contract, plus none/metadata for store-nothing, is the truthful design.

  • Drop content unconditionally (no ``full``/``redacted``). Rejected: the eval snapshot and injection-scan output have legitimate debugging and provenance uses. Making retention a choice — with a metadata-only default — serves both the privacy-first and the diagnostics case.

  • Reuse ``nrllm:telemetry:purge`` for all tables. Rejected: overloading a command named for one table is surprising. A dedicated nrllm:privacy:purge owns cross-table retention; the old command stays for compatibility.

References

  • adr-058 — Telemetry Middleware (metadata-only by construction; named this workstream as the follow-up).

  • adr-060 — Quality Evaluation (owns tx_nrllm_eval_result).

  • adr-061 — Skill Trust, Egress and Audit (owns tx_nrllm_skill_audit).

  • ADR-017 — SafeCastTrait / ErrorMessageSanitizerTrait (the shared credential redaction reused here).

← All ADRs