Repository navigation
feat(proxy): redact a finding instead of refusing the request - #104
Merged
Merged
Conversation
A refusal stops the leak and the work with it. An engineer whose agent is summarising a support ticket does not want the ticket blocked — they want the ticket without the customer number in it. Every comparable tool ships redaction for that reason; pasu could only say no. Blocking stays the default, and an explicit block beats an explicit redact. Stopping is the safe answer for a rule nobody has decided about, and where an operator has said both things about one rule they have contradicted themselves — the safe reading of a contradiction in a security policy is the stricter one. A blocking finding also wins over a redactable one in the same request: sending a body with one rule removed while another said stop answers the wrong question. One-way, deliberately. No original is kept, so nothing can restore it and this proxy never becomes a store of exactly the data it exists to stop from moving, reachable over the network. Reversible substitution is a different security posture and belongs to its own decision rather than being inherited from whichever was easier to build. The placeholder tells nothing. It preserves neither length nor shape; a mask that kept those leaks the value it hid, and a digit-shaped mask over a national ID leaks most of it. Overlapping findings merge and name every rule that contributed, sorted, so the body does not depend on which inspector ran first. Extraction and rewriting now share one traversal. Two functions deciding separately which fields hold prose would drift, and the day they did, a scanner would be reading a field the redactor no longer edits. The body is rebuilt from the parsed value rather than string-replaced, so what leaves is still valid JSON in the provider shape. Findings that cannot be replaced safely — a span off a character boundary, a body that will not rebuild — refuse rather than forward. Failing open there would send the thing that was found. Tested against what the provider actually received, not against what the proxy returned: a recording mock upstream asserts the value is absent, the placeholder is present, the rest of the prompt survived, and the body still parses. A blocked rule never reaches the mock at all. Mutation-checked — discarding the rewritten body fails two of them. The startup lines claimed a match was refused while redaction was on. They now say what is actually configured, including which rules are exceptions. Closes #102 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Closes #102.
A refusal stops the leak and the work. An engineer whose agent is summarising a support ticket does not want the ticket blocked — they want the ticket without the customer's number in it. Every comparable tool ships redaction for that reason; pasu could only say no.
Blocking stays the default
Stopping is the safe answer for a rule nobody has decided about. Three precedence rules, all in the same direction:
--block-rulebeats an explicit--redact-rule— where an operator has said both about one rule they have contradicted themselves, and the safe reading of a contradiction in a security policy is the stricter oneOne-way, deliberately
No original is kept. Nothing can restore it, and this proxy never becomes a store of exactly the data it exists to stop from moving, reachable over the network. Reversible substitution is a genuinely different security posture and deserves its own decision rather than being inherited from whichever was easier to build.
The placeholder preserves neither length nor shape. A mask that kept those leaks the value it hid, and a digit-shaped mask over a national ID leaks most of it. Overlapping findings merge and name every contributing rule, sorted, so the body does not depend on which inspector ran first.
Extraction and rewriting share one traversal
Two functions deciding separately which fields hold prose would drift, and the day they did, a scanner would be reading a field the redactor no longer edits. The body is rebuilt from the parsed value rather than string-replaced, so what leaves is still valid JSON in the provider's shape.
Tested against what the provider received
Not against what the proxy returned — that would prove only that it did not refuse. A recording mock upstream asserts the value is absent, the placeholder is present, the rest of the prompt survived (otherwise this is just a block), and the body still parses as JSON. A blocked rule never reaches the mock at all. Every occurrence is replaced, not the first.
Mutation-checked: discarding the rewritten body fails two of them.
Also
The startup lines claimed a match was refused while redaction was on. They now say what is actually configured:
🤖 Generated with Claude Code