feat(guardrails): redact the participant name on a message

`name` on a user or assistant turn identifies a person and was going to the
provider intact. The proxy this integrates with already redacts it, so the
integration was the weaker of the two.

On a tool or function turn the same field carries the function's name, which
has to arrive unchanged or the call stops routing. That case is skipped, and
a test asserts the value is never even sent to the shield.
This commit is contained in:
Ninad Phalak 2026-09-03 21:22:30 -05:00
parent a0abb9a499
commit 6536d61adf
No known key found for this signature in database
GPG key ID: 59119ED515433744
2 changed files with 41 additions and 0 deletions

View file

@ -122,6 +122,18 @@ def _collect_content(container: MutableRequest, slots: _SlotSink) -> None:
_collect(part, "text", slots)
def _collect_participant_name(message: MutableRequest, slots: _SlotSink) -> None:
"""Redacts `name` where it identifies a person, never where it names a function.
On a user or assistant turn `name` is the participant, which is personal data.
On a tool or function turn the same field carries the function's name and has
to reach the provider unchanged, or the call no longer routes.
"""
if message.get("role") in ("tool", "function"):
return
_collect(message, "name", slots)
def _collect_tool_arguments(message: MutableRequest, slots: _SlotSink) -> None:
"""Tool arguments carry the values a user asked the model to act on."""
for tool_call in message.get("tool_calls") or ():
@ -311,6 +323,7 @@ class LLMShieldProxyGuardrail(CustomGuardrail):
for message in data.get("messages") or ():
if isinstance(message, dict):
_collect_content(message, slots)
_collect_participant_name(message, slots)
_collect_tool_arguments(message, slots)
_collect_responses_fields(data, slots)
_collect_prompt(data, slots)

View file

@ -330,6 +330,34 @@ class TestRequestCoverage:
assert data["input"] == ["[EMAIL_1]", "[PHONE_1]"]
@pytest.mark.asyncio
async def test_participant_name_is_redacted(self):
"""`name` on a user turn identifies a person."""
guardrail = _guardrail()
mock = _mock_post(guardrail, {"texts": ["hi", "[PERSON_1]"]})
data = {"messages": [{"role": "user", "name": "Jane Doe", "content": "hi"}]}
await guardrail.async_pre_call_hook(user_api_key_dict=None, cache=None, data=data, call_type="completion")
assert mock.call_args_list[0].kwargs["json"]["texts"] == ["hi", "Jane Doe"]
assert data["messages"][0]["name"] == "[PERSON_1]"
@pytest.mark.asyncio
async def test_tool_function_name_is_left_alone(self):
"""On a tool turn the same field is the function name.
Redacting it would stop the call routing, so this asserts it is never sent
to the shield at all.
"""
guardrail = _guardrail()
mock = _mock_post(guardrail, {"texts": ["result"]})
data = {"messages": [{"role": "tool", "name": "get_weather", "content": "result"}]}
await guardrail.async_pre_call_hook(user_api_key_dict=None, cache=None, data=data, call_type="completion")
assert data["messages"][0]["name"] == "get_weather"
assert mock.call_args_list[0].kwargs["json"]["texts"] == ["result"]
@pytest.mark.asyncio
async def test_every_shape_in_one_request_is_redacted(self):
guardrail = _guardrail()