I was looking at how Guard.scan_output() handles redaction and noticed that redacted_text still contains flagged URLs unchanged.
Repro on dev at 7a8be76:
fromunplug.guardimportGuardresult=Guard().scan_output('Link: http://198.51.100.7/collect')
# result.findings has: urls/ip_literal_url, replacement='[BLOCKED:url]'# result.redacted_text is: 'Link: http://198.51.100.7/collect'# URL is still there, untouchedThe URL scanner flags the IP-literal URL and gives it a [BLOCKED:url] replacement, but that replacement never makes it into redacted_text.
I think the issue is in OutputPipeline._redact (output.py:101-102). When a sanitizer exists (and it always does because Guard.__init__ always passes SecretsSanitizer), it just does:
returnself._sanitizer.sanitize(text).clean_text
That sanitizer only knows about registered secrets and generic secret patterns. It never sees the scanner findings at all. So the BasePipeline._redact fallback that would use apply_span_redactions(text, findings, policy) is effectively dead code for the output path.
Registered secrets DO get redacted in the same path, which makes this a bit subtle. I registered a test secret and ran both:
redacted_text='Key: [REDACTED:k] and URL: http://198.51.100.7/x'
Secret gone, URL still there. So the problem is specifically that the sanitizer branch only handles secret detection and ignores everything else (URLs, and probably PII/leakage findings too, though I only tested URLs).
This matters for anything using redacted_text as safe-to-forward output. If a host treats that field as sanitized and sends it downstream, flagged URLs slip through.
Environment: Python 3.14.2, Windows 11, unplug-ai==0.6.0, installed via uv sync --dev from the dev branch.
Relevant code:
sdk/src/unplug/pipelines/output.py line 101-102 (_redact sanitizer branch)sdk/src/unplug/core/privacy/secrets.py lines 179-191 (sanitize method)sdk/src/unplug/pipelines/base.py lines 157-167 (_redact fallback that uses findings, currently dead code)
I was looking at how
Guard.scan_output()handles redaction and noticed thatredacted_textstill contains flagged URLs unchanged.Repro on
devat7a8be76:The URL scanner flags the IP-literal URL and gives it a
[BLOCKED:url]replacement, but that replacement never makes it intoredacted_text.I think the issue is in
OutputPipeline._redact(output.py:101-102). When a sanitizer exists (and it always does becauseGuard.__init__always passesSecretsSanitizer), it just does:That sanitizer only knows about registered secrets and generic secret patterns. It never sees the scanner findings at all. So the
BasePipeline._redactfallback that would useapply_span_redactions(text, findings, policy)is effectively dead code for the output path.Registered secrets DO get redacted in the same path, which makes this a bit subtle. I registered a test secret and ran both:
Secret gone, URL still there. So the problem is specifically that the sanitizer branch only handles secret detection and ignores everything else (URLs, and probably PII/leakage findings too, though I only tested URLs).
This matters for anything using
redacted_textas safe-to-forward output. If a host treats that field as sanitized and sends it downstream, flagged URLs slip through.Environment: Python 3.14.2, Windows 11,
unplug-ai==0.6.0, installed viauv sync --devfrom the dev branch.Relevant code:
sdk/src/unplug/pipelines/output.pyline 101-102 (_redactsanitizer branch)sdk/src/unplug/core/privacy/secrets.pylines 179-191 (sanitizemethod)sdk/src/unplug/pipelines/base.pylines 157-167 (_redactfallback that uses findings, currently dead code)