Skip to content

Redaction (mutation pipeline)

The RedactionScanner (guardrails/redaction.py) is the mutation-producing stage. Unlike the content scanners it returns no verdict — it is a pure transformer that rewrites secret/PII material in otherwise-allowed payloads, replacing each match with a fixed [REDACTED:<TYPE>] placeholder. The rewritten payload is forwarded via the proto mutated oneof.

Redaction runs only when no scanner BLOCKed the exchange (a BLOCK always wins). It is the defence-in-depth layer for material scanners deliberately do not hard-deny (emails are ALLOW, credit cards are review-pass by default), plus a second chance to mask block-grade secrets when the regex scanner is disabled.

Structural replacement semantics

The engine applies redaction structurally (redact_value): it walks the params / result JSON tree and rewrites only string values, so the mutated payload is guaranteed valid JSON with the same shape as the original:

  • Mappings and sequences are rebuilt only when something changed; scalars pass through untouched.
  • Substitutions are counted and reported; the count lands in metrics (mcp.guardrails.redactions) and the decision metadata.

Known limitations (deliberate):

  • Mapping keys are not redacted — only values. Rewriting keys could collide or break downstream lookups.
  • Tuples become lists — sequences are rebuilt as list (correct for JSON-shaped payloads).
  • String-encoded params.arguments (some MCP clients encode arguments as a JSON string) are redacted as plain text; an unquoted numeric secret inside them can invalidate the inner JSON document. The outer payload stays valid. Such deployments should rely on the RegexScanner verdict.
  • The PEM-header fallback is best-effort — it masks a truncated private-key block; with the RegexScanner enabled, any BEGIN marker is already BLOCKed upstream, so this path only fires when the regex scanner is disabled.

Default redaction set

From default_redaction_patterns(), most-specific first:

Pattern Replaces Placeholder
private_key (block) Full PEM block (BEGIN…END, any of RSA/EC/OPENSSH/DSA/none) [REDACTED:PRIVATE_KEY]
private_key (header) Truncated PEM block (BEGIN marker only) [REDACTED:PRIVATE_KEY]
aws_access_key AKIA… [REDACTED:AWS_ACCESS_KEY]
aws_temp_key ASIA… [REDACTED:AWS_TEMP_KEY]
google_api_key AIza… [REDACTED:GOOGLE_API_KEY]
github_pat gh[pousr]_… [REDACTED:GITHUB_PAT]
gitlab_pat glpat-… [REDACTED:GITLAB_PAT]
slack_token xox[baprs]-… [REDACTED:SLACK_TOKEN]
llm_api_key sk-… (≥20 chars) [REDACTED:LLM_API_KEY]
email Email addresses [REDACTED:EMAIL]
credit_card 13–16 digit card-shaped numbers [REDACTED:CREDIT_CARD]

Patterns unsuitable for substitution are deliberately excluded and remain scanner verdicts: hidden_ascii (no meaningful replacement for a zero-width character) and the review-grade shapes (key_value_credential, high_entropy_blob, connection_string, jwt) which are too false-positive-prone to rewrite blindly on the wire path.

The egress set (egress_redaction_patterns(), used only for the AgentAlignment external LLM call) adds jwt, connection_string and key_value_credential on top of the default set — exactly the shapes whose review verdict feeds the second-stage gate.

Behaviour knobs

Env var Default Effect
ENABLE_REDACTION true Master switch.
REDACT_ON_REVIEW true HUMAN_REVIEW payloads are also redacted: the review verdict is kept (pass+warn or deny per HUMAN_REVIEW_MODE) and the mutated payload rides along, so review-grade PII is masked instead of passing verbatim. Under failOpen this means a scanner exception no longer forwards secrets in cleartext. false restores the legacy pass-unmutated behaviour.
REDACT_REQUEST_PARAMS false Request-side opt-in. Off by default: a secret in tool-call params is a signal to BLOCK (the RegexScanner already does), not to silently rewrite the caller's request. Enable only where rewritten arguments are deliberately tolerated.
REDACTION_MAX_BYTES 262144 (256KiB) Payload byte cap for the redaction sweep (~11 regexes over the untruncated payload). Over-cap payloads skip redaction, pass unchanged, and are flagged redaction_skipped=size in the audit span. Safe trade-off: block-grade secrets in over-cap payloads are still denied by the RegexScanner's head/mid/tail windows; only best-effort masking of ALLOW-grade PII is skipped.

The redaction sweep is offloaded to a worker thread (asyncio.to_thread) so it never stalls the event loop.

Upgrade note

With redaction enabled, the decisions counter and audit outcome field gain an outcome="mutated" value. Dashboards/alerts counting "successful" decisions as outcome="allow" should be widened to outcome=~"allow|mutated".