refactor(redact): lift + generalize the ADR-0018 scrubbers into a shared engine/redact/ module (JEF-486)#258
Merged
thejefflarson merged 1 commit intoJul 23, 2026
Conversation
…red engine/redact/ module (JEF-486) The breach notifier's redaction primitives were private to notify.rs and keyed on BreachNotice. ADR-0031's read-only MCP server must reuse the exact same scrubbers so the two sanctioned egress paths can't drift in what they consider safe to emit off-cluster. Lift `sanitize`, `scrub_decision_names`, `scrub_cve_tokens`, and the counts-only ATT&CK-outcome reducer into a new `engine/src/engine/redact/` module directory, generalized off BreachNotice to take primitive inputs (`&str`, `&[&str]`, an AttackRef iterator). `sanitize` — previously the adjudication prompt's own copy in reason::adjudicate::guards — now has ONE home in redact/, re-exported by guards so the prompt path is unchanged. notify.rs re-points to the shared primitives; the notifier-specific name-collection stays local (`decision_names`). Behavior is byte-identical for the notifier: a new snapshot test pins the exact serialized redacted payload (default / verbose / armed) captured from the pre-refactor code. New redact unit tests cover the generalized inputs the MCP server will pass (a verdict text blob, a CVE line, a bare decision name) plus the fence/CVE/name edge cases. Every file stays under the 1,000-line cap. Closes JEF-486 Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
thejefflarson
deleted the
thejefflarson/jef-486-lift-generalize-the-redaction-primitives-out-of-notifyrs
branch
July 23, 2026 00:51
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.
What
Lifts the breach notifier's redaction primitives out of
notify.rsinto a new sharedengine/src/engine/redact/module and generalizes them offBreachNotice, so the (future) read-only MCP server from ADR-0031 reuses the exact same scrubbers — the two sanctioned egress paths can't drift in what they consider safe to emit off-cluster.Grounded in
docs/adr/0031-read-only-mcp-server-tiered-redaction.md§3, which calls forsanitize/scrub_decision_names/scrub_cve_tokensto be "lifted and generalized from their current private home ... into a shared redaction module."Generalized API (
engine/src/engine/redact/)sanitize(&str) -> String— strip prompt/wire STRUCTURE (fences, braces, backtick, CR/LF). Now the ONE home; previously the adjudication prompt kept its own copy inreason::adjudicate::guards, which now re-exports this so the prompt path is unchanged.scrub_decision_names(text: &str, names: &[&str]) -> String— replace each supplied name (longest-first, trimmed, deduped) with[redacted]. SEMANTIC name stripping only — no CVE coupling, so the MCPrawtier can relax it independently.scrub_cve_tokens(text: &str) -> String— case-insensitiveCVE-…token scrub; independent so the MCPforensictier can relax it.redacted_attack_outcome(refs: impl IntoIterator<Item = &AttackRef>, cap) -> Vec<Value>— the counts-only ATT&CK-outcome reducer (distinct, ordered, sanitized, capped techniques — never the per-objective targets).notify.rsre-points to these; the notifier-specific name-collection (entry + objective node-keys + bare suffixes) stays local asdecision_names. Each new file is well under the 1,000-line cap.Byte-identical proof (the load-bearing check)
Captured the exact serialized redacted payload from the pre-refactor code for a fixture exercising every scrubber (structure chars + a CVE token + a full node-key AND its bare secret name), across default / verbose / armed shapes, then pinned those exact strings in a new test
redacted_payload_is_byte_identical_to_pre_refactor. It passes against the refactored code, proving the notifier's output is unchanged to the byte. All pre-existing ADR-0018 notifier tests stay green.Tests
engine::redact::tests— 12 unit tests on the generalized inputs the MCP server will pass (verdict text blob, CVE line, bare decision name) plus fence/CVE/name edge cases.Checks
cargo fmt— cleancargo clippy --all-targets -- -D warnings— cleancargo test— 814 lib + all integration tests pass (incl. the file-size guard: no file >1000 lines)Scope note / decision recorded
The ticket's "counts-only helper" was ambiguous (parenthesized as "
redacted_payload/ whatever it's called"). I interpreted it as the reusable redaction rule — the ATT&CK-outcome reducer that drops per-objective targets and keeps only distinct techniques + a count — and lifted that (redacted_attack_outcome), rather than moving the notifier's wholeredacted_payloadwire-shape builder (which stays innotify.rs, since it assembles the notifier's specific JSON, not a shared primitive). This keepsredact/focused on shared redaction decisions and out of per-path wire shapes, matching ADR-0031's "one redaction implementation" goal without over-building.Closes JEF-486
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP