Repository navigation
fix(guardrails): mask PII inside a tool_result's nested content - #12930
Merged
diegosouzapw merged 2 commits intoSep 10, 2026
Merged
diegosouzapw merged 2 commits into
diegosouzapw merged 2 commits into
Conversation
applyToContentValue() descended into a content block's `content` only when
it was a string. A tool_result carries its payload as an array of parts --
the shape every agentic client sends back after running a tool -- so the
string test walked past it and the tool output reached the provider intact.
{"type":"text","text":"visible [SSN_REDACTED]"}
{"type":"tool_result","content":[{"type":"text","text":"ssn 123-45-6789"}]}
The failure is quiet in the worst way: the sibling text block is redacted,
so the payload comes back modified with a redaction in it and the
guardrail reports a detection. Nothing downstream can tell that a second
copy of the same value went out untouched.
sanitizeMessageLikeList() already calls applyToContentValue for `content`
one level up. Making applyToContentValue do the same for a nested block
makes the two agree on how deep masking goes, and covers the string case
it replaces, since applyToContentValue sanitizes a string and recurses
into an array. The payload is a JSON round-trip, so it is acyclic and the
recursion is bounded by its nesting.
CredentialMaskerGuardrail, in the same directory, already walks the whole
payload. Only the PII masker stopped at depth one.
Verified: 6 tests. Three mutations, all killed -- reverting to the
string-only test leaves exactly the two regression guards green, which is
what should happen. eslint clean, typecheck-core reports 0 errors.
diegosouzapw
merged commit Sep 10, 2026
403a1a6
into
cheaper-inference:release/v3.8.51
9 of 16 checks passed
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…per-inference#12930) Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings. Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
3 tasks
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.
applyToContentValue()descends into a content block'scontentonly when it is a string:A
tool_resultcarries its payload as an array of parts — the shape every agentic client sends back after running a tool — so the string test walks past it, and the tool output reaches the provider unmasked.Driving
PIIMaskerGuardrail.preCall()withPII_REDACTION_ENABLED=true:{"type":"text","text":"visible [SSN_REDACTED]"} {"type":"tool_result","tool_use_id":"toolu_1", "content":[{"type":"text","text":"tool output 123-45-6789"}]}Why this is worse than it looks
The sibling block is redacted. So the guardrail returns
modified: true, reports{detections: 1, redacted: true}, and the payload visibly contains a[SSN_REDACTED]. Every signal downstream says the masker did its job, while a second copy of the same value went out untouched. A guardrail that fails loudly is a bug; one that fails while reporting success is the reason I wrote a test for the counted-detection case too.CredentialMaskerGuardrail, in the same directory, walks the entire payload viawalkValue. Only the PII masker stopped at depth one.The change
sanitizeMessageLikeList()already callsapplyToContentValuefor a message'scontent. MakingapplyToContentValuedo the same for a nested block is the same call, one level down, so the two agree on how deep masking goes:This subsumes the string branch it replaces —
applyToContentValuesanitizes a string and recurses into an array — so no case is lost. The payload is aJSON.parse(JSON.stringify(...))round-trip before this runs, so it is acyclic and the recursion is bounded by its nesting; noseenset is needed.maskResponsesOutput()on the response path has the same depth-one shape. I have left it alone rather than widen this PR, and I am happy to send it separately.Verification
tests/unit/pii-nested-tool-result.test.ts, 6 tests. Checked by breaking it:contentcontentat allmodifiedflagUnder the first mutation the two guards — a string content on a block is still masked and a payload with nothing to mask is passed through unchanged — stay green, which is what should happen: that mutation restores the old behaviour for exactly the cases the old code handled.
eslintclean on both files;tsc -p tsconfig.typecheck-core.jsonreports 0 errors.