Skip to content

Commit 4a95b7f

Browse files
committed
fix: never allow redacting tool blocks
1 parent 0ea2ce7 commit 4a95b7f

File tree

2 files changed

+67
-3
lines changed

2 files changed

+67
-3
lines changed

src/strands/event_loop/streaming.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,10 @@ def handle_redact_content(event: RedactContentEvent, state: dict[str, Any]) -> N
265265
state: The current state of message processing.
266266
"""
267267
if event.get("redactAssistantContentMessage") is not None:
268-
state["message"]["content"] = [{"text": event["redactAssistantContentMessage"]}]
268+
# Always keep toolUse or toolResult blocks to avoid destroying the history
269+
content = [block for block in state["message"]["content"] if "toolResult" in block or "toolUse" in block]
270+
content.append({"text": event["redactAssistantContentMessage"]})
271+
state["message"]["content"] = content
269272

270273

271274
def extract_usage_metrics(event: MetadataEvent, time_to_first_byte_ms: int | None = None) -> tuple[Usage, Metrics]:

tests_integ/test_bedrock_guardrails.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import boto3
66
import pytest
77

8-
from strands import Agent
8+
from strands import Agent, tool
99
from strands.models.bedrock import BedrockModel
1010
from strands.session.file_session_manager import FileSessionManager
1111

@@ -189,7 +189,7 @@ def test_guardrail_output_intervention_redact_output(bedrock_guardrail, processi
189189
In async streaming: The buffering is non-blocking.
190190
Tokens are streamed while Guardrails processes the buffered content in the background.
191191
This means the response may be returned before Guardrails has finished processing.
192-
As a result, we cannot guarantee that the REDACT_MESSAGE is in the response
192+
As a result, we cannot guarantee that the REDACT_MESSAGE is in the response.
193193
"""
194194
if processing_mode == "sync":
195195
assert REDACT_MESSAGE in str(response1)
@@ -209,6 +209,67 @@ def test_guardrail_output_intervention_redact_output(bedrock_guardrail, processi
209209
)
210210

211211

212+
@pytest.mark.parametrize("processing_mode", ["sync", "async"])
213+
def test_guardrail_output_intervention_does_not_redact_tool_result(bedrock_guardrail, processing_mode):
214+
REDACT_MESSAGE = "Redacted."
215+
bedrock_model = BedrockModel(
216+
guardrail_id=bedrock_guardrail,
217+
guardrail_version="DRAFT",
218+
guardrail_stream_processing_mode=processing_mode,
219+
guardrail_redact_output=True,
220+
guardrail_redact_output_message=REDACT_MESSAGE,
221+
region_name="us-east-1",
222+
)
223+
224+
@tool
225+
def list_users() -> str:
226+
"List my users"
227+
return """[{"name": "Jerry Merry", "email": "[email protected]"},
228+
{"name": "CACTUS", "email": "[email protected]"}"""
229+
230+
agent = Agent(
231+
model=bedrock_model,
232+
system_prompt="You are a helpful assistant.",
233+
callback_handler=None,
234+
load_tools_from_directory=False,
235+
tools=[list_users],
236+
)
237+
238+
response1 = agent("List my users.")
239+
response2 = agent("Hello!")
240+
241+
assert response1.stop_reason == "guardrail_intervened"
242+
243+
"""
244+
In async streaming: The buffering is non-blocking.
245+
Tokens are streamed while Guardrails processes the buffered content in the background.
246+
This means the response may be returned before Guardrails has finished processing.
247+
As a result, we cannot guarantee that the REDACT_MESSAGE is in the response
248+
However, response2 should not be blocked anyway.
249+
"""
250+
if processing_mode == "sync":
251+
assert REDACT_MESSAGE in str(response1)
252+
assert response2.stop_reason != "guardrail_intervened"
253+
assert REDACT_MESSAGE not in str(response2)
254+
# Input not redacted being an output intervention
255+
assert agent.messages[0]["content"][0]["text"] != REDACT_MESSAGE
256+
# Tool blocks not redacted
257+
assert any("toolUse" in block for block in agent.messages[1]["content"])
258+
assert "toolResult" in agent.messages[2]["content"][0]
259+
# Output correctly redacted
260+
assert agent.messages[3]["content"][0]["text"] == REDACT_MESSAGE
261+
else:
262+
cactus_returned_in_response1_blocked_by_input_guardrail = BLOCKED_INPUT in str(response2)
263+
cactus_blocked_in_response1_allows_next_response = (
264+
REDACT_MESSAGE not in str(response2) and response2.stop_reason != "guardrail_intervened"
265+
)
266+
assert (
267+
cactus_returned_in_response1_blocked_by_input_guardrail or cactus_blocked_in_response1_allows_next_response
268+
)
269+
# Output correctly redacted
270+
assert agent.messages[3]["content"][0]["text"] == REDACT_MESSAGE
271+
272+
212273
def test_guardrail_input_intervention_properly_redacts_in_session(boto_session, bedrock_guardrail, temp_dir):
213274
bedrock_model = BedrockModel(
214275
guardrail_id=bedrock_guardrail,

0 commit comments

Comments
 (0)