From bb3d257df6602192e94b849f5be4167fedc0804a Mon Sep 17 00:00:00 2001 From: pxkundu Date: Thu, 16 Oct 2025 21:38:51 -0500 Subject: [PATCH] Fix input guardrails to block tool execution before agent starts This fixes issue #889 where input guardrails were running in parallel with agent execution, allowing tools to run even when guardrails should block them. Changes: - Modified non-streaming path to run guardrails sequentially before _run_single_turn - Modified streaming path to await guardrails completion before starting the streamed agent turn - Added immediate exception raising in _run_input_guardrails_with_queue when tripwire is triggered This ensures that when a guardrail triggers, no LLM calls or tool executions happen, preventing unnecessary token consumption and potential security issues. --- src/agents/run.py | 67 ++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/src/agents/run.py b/src/agents/run.py index e91279ad6..4df340592 100644 --- a/src/agents/run.py +++ b/src/agents/run.py @@ -599,26 +599,26 @@ async def run( ) if current_turn == 1: - input_guardrail_results, turn_result = await asyncio.gather( - self._run_input_guardrails( - starting_agent, - starting_agent.input_guardrails - + (run_config.input_guardrails or []), - _copy_str_or_list(prepared_input), - context_wrapper, - ), - self._run_single_turn( - agent=current_agent, - all_tools=all_tools, - original_input=original_input, - generated_items=generated_items, - hooks=hooks, - context_wrapper=context_wrapper, - run_config=run_config, - should_run_agent_start_hooks=should_run_agent_start_hooks, - tool_use_tracker=tool_use_tracker, - server_conversation_tracker=server_conversation_tracker, - ), + # Run input guardrails first, before starting the agent turn. + # This prevents tools from executing when guardrails should block the input. + input_guardrail_results = await self._run_input_guardrails( + starting_agent, + starting_agent.input_guardrails + (run_config.input_guardrails or []), + _copy_str_or_list(prepared_input), + context_wrapper, + ) + # Only proceed with the agent turn if guardrails passed + turn_result = await self._run_single_turn( + agent=current_agent, + all_tools=all_tools, + original_input=original_input, + generated_items=generated_items, + hooks=hooks, + context_wrapper=context_wrapper, + run_config=run_config, + should_run_agent_start_hooks=should_run_agent_start_hooks, + tool_use_tracker=tool_use_tracker, + server_conversation_tracker=server_conversation_tracker, ) else: turn_result = await self._run_single_turn( @@ -888,6 +888,9 @@ async def _run_input_guardrails_with_queue( for done in asyncio.as_completed(guardrail_tasks): result = await done if result.output.tripwire_triggered: + # Cancel remaining tasks + for t in guardrail_tasks: + t.cancel() _error_tracing.attach_error_to_span( parent_span, SpanError( @@ -898,6 +901,10 @@ async def _run_input_guardrails_with_queue( }, ), ) + queue.put_nowait(result) + streamed_result.input_guardrail_results = [result] + guardrail_results + # Raise the exception immediately to prevent the agent turn from starting + raise InputGuardrailTripwireTriggered(result) queue.put_nowait(result) guardrail_results.append(result) except Exception: @@ -991,16 +998,16 @@ async def _start_streaming( break if current_turn == 1: - # Run the input guardrails in the background and put the results on the queue - streamed_result._input_guardrails_task = asyncio.create_task( - cls._run_input_guardrails_with_queue( - starting_agent, - starting_agent.input_guardrails + (run_config.input_guardrails or []), - ItemHelpers.input_to_new_input_list(prepared_input), - context_wrapper, - streamed_result, - current_span, - ) + # Run input guardrails first, before starting the streamed agent turn. + # This prevents tools from executing when guardrails should block the input. + # We await this to ensure guardrails complete before the agent starts. + await cls._run_input_guardrails_with_queue( + starting_agent, + starting_agent.input_guardrails + (run_config.input_guardrails or []), + ItemHelpers.input_to_new_input_list(prepared_input), + context_wrapper, + streamed_result, + current_span, ) try: turn_result = await cls._run_single_turn_streamed(