observability: implement redaction filter (default strict)#107
Merged
Conversation
Add geno_lewm/_redaction.py as the single chokepoint between callers
and the JSONL sink (RFC-0013 §3.5, docs/spec/05-observability.md,
docs/spec/06-security.md). Four rules:
- Per-event allowlist: keys not in EventSpec.allowed_keys are
soft-dropped and counted (registry drift, not a bypass — does NOT
raise even in strict mode).
- Type allowlist: only None/bool/int/float/str/list-of-scalars/
shallow-dict-of-scalars allowed. bytes/tensors/sets/deep nesting
drop (and raise in strict mode).
- DNA pattern: ^[ACGTNacgtn]{20,}$ matched at any depth drops
(raise in strict mode).
- Deny-list: vcf_content / genotype / sample_id / user_email /
email / phone / address / dob / birthdate — at any depth — drop
(raise in strict mode).
Strict mode is on by default; GENO_LEWM_REDACTION_STRICT=0 disables.
Extend EventSpec with allowed_keys (frozenset[str]). Populate sensible
defaults for every v0.1 event. Tightening a set is MAJOR; adding a key
is MINOR.
Wire the filter into GenoLeWMLogger._log so every record passes through
the filter before JSON serialization. Unknown events fall back to an
empty allowlist (everything in data soft-drops, no leak possible).
RedactionStats tracks dropped_keys / dropped_denied / dropped_dna /
dropped_type. The metric geno_lewm.observability.redacted_keys
(RFC-0013 §4) will be exported by #25 — today the counter is
observable via redaction_stats().
Tests:
- tests/unit/test_redaction.py (16 cases): strict-mode raises on each
rule violation including nested DNA & deny-listed keys; permissive
mode drops + counts; type allowlist (scalars / lists / shallow
dicts); defensive copy; per-event allowlist round-trips for every
registered event.
- tests/property/test_redaction.py (2 tests, 12k payloads total):
permissive run over 10k random payloads — zero leaks; strict-mode
run over 2k — either raises or zero leaks.
Existing observability test updated: training.run.start payload now
uses config_path (in the allowlist) instead of the bare ``cfg`` key.
Closes #24
4 tasks
This was referenced May 20, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
RFC-0013 §3.5 and
docs/spec/06-security.mdrequire a single redaction chokepoint between callers and the log sink: DNA strings ≥ 20 bp, deny-listed field names, and out-of-allowlist payloads must never leak. INV-OBS-4 ("DNA strings ≥ 20 bp never appear in any log record") is non-negotiable.Solution
geno_lewm/_redaction.py— four-rule filter:EventSpec.allowed_keys): keys not listed are soft-dropped (registry drift, not a bypass — does NOT raise even in strict mode).None | bool | int | float | strand shallow containers thereof.bytes/ sets / deep nesting / tensors all drop; raise in strict mode.^[ACGTNacgtn]{20,}$matched at any depth → drop; raise in strict mode.vcf_content,genotype,sample_id,user_email,email,phone,address,dob,birthdate) — at any depth → drop; raise in strict mode.Strict mode is on by default (
GENO_LEWM_REDACTION_STRICT=0disables). All raises go throughgeno_lewm.errors.InvariantViolation— the linter from errors: AST linterraise_geno_lewm_errorandregistered_error_code#22 confirms.geno_lewm/observability.py— extendsEventSpecwithallowed_keys: frozenset[str]. Populated for every v0.1 event with realistic key sets. Tightening is MAJOR; adding is MINOR.The filter is wired into
GenoLeWMLogger._logso every record passes through the chokepoint before JSON serialization. Unknown events fall back to an empty allowlist — every payload key is soft-dropped, no leak possible.RedactionStats(counter object):dropped_keys/dropped_denied/dropped_dna/dropped_type. The metricgeno_lewm.observability.redacted_keyswill be exported by observability: implement metrics registry + Prometheus textfile exporter #25 — today the counter is observable viaredaction_stats().Validation
Tests:
tests/unit/test_redaction.py(16 cases): strict-mode raises on every rule including nested DNA / deny-listed keys; permissive drops + counts; type allowlist (scalars / lists / shallow dicts); defensive copy; per-event allowlist round-trip for every registered event.tests/property/test_redaction.py(2 tests, 12k payloads total): permissive run over 10k random payloads — zero leaks (the acceptance criterion); strict-mode run over 2k — either raises cleanly or zero leaks.cfg→config_path).Caveats / out of scope
geno_lewm.observability.redacted_keysas a Prometheus-exported metric depends on the metrics registry — that's observability: implement metrics registry + Prometheus textfile exporter #25. Counter is live today, exporter wiring lands with observability: implement metrics registry + Prometheus textfile exporter #25.clinvar_idre-personalization) intentionally not in the deny list — the open question stays open.random.Randomnot Hypothesis to keep the bootstrap suite zero-dependency. Acceptable per the issue ("10k random payloads — zero leaks").Closes #24