From 32c842286977fe68179f9035d110296a82dde46d Mon Sep 17 00:00:00 2001 From: EfeDurmaz16 Date: Fri, 15 May 2026 22:16:16 +0300 Subject: [PATCH] docs(guardrails): add pre-execution policy example --- docs/examples.md | 1 + docs/guardrails.md | 67 +++++++++++++++++++++ examples/basic/tool_policy_guardrail.py | 80 +++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 examples/basic/tool_policy_guardrail.py diff --git a/docs/examples.md b/docs/examples.md index 9fda81c382..8228bd2024 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -31,6 +31,7 @@ Check out a variety of sample implementations of the SDK in the examples section - Dynamic system prompts - Basic tool usage (`examples/basic/tools.py`) - Tool input/output guardrails (`examples/basic/tool_guardrails.py`) + - Tool input guardrails for pre-execution policy checks (`examples/basic/tool_policy_guardrail.py`) - Image tool output (`examples/basic/image_tool_output.py`) - Streaming outputs (text, items, function call args) - Responses websocket transport with a shared session helper across turns (`examples/basic/stream_ws.py`) diff --git a/docs/guardrails.md b/docs/guardrails.md index 0965a417f1..4ed41d1236 100644 --- a/docs/guardrails.md +++ b/docs/guardrails.md @@ -227,3 +227,70 @@ agent = Agent(name="Classifier", tools=[classify_text]) result = Runner.run_sync(agent, "hello world") print(result.final_output) ``` + +## Using tool input guardrails for pre-execution policy checks + +Tool input guardrails run immediately before a local function tool is invoked. This makes +them a good place to enforce deterministic checks on the exact tool payload that the model +requested, such as: + +- tool allowlists +- argument constraints +- caller or agent context +- destination or recipient allowlists +- budget or amount limits +- freshness, nonce, or replay checks that your application stores in context + +For example, a payment tool can reject a charge before execution if the amount or merchant +is outside the current policy: + +```python +import json +from dataclasses import dataclass + +from agents import ( + ToolGuardrailFunctionOutput, + ToolInputGuardrailData, + tool_input_guardrail, +) + + +@dataclass +class PaymentPolicy: + max_amount_usd: float + allowed_merchants: set[str] + + +@tool_input_guardrail +def enforce_payment_policy(data: ToolInputGuardrailData) -> ToolGuardrailFunctionOutput: + policy = data.context.context + args = json.loads(data.context.tool_arguments or "{}") + + if data.context.tool_name != "charge_card": + return ToolGuardrailFunctionOutput.reject_content("This tool is not allowed.") + + merchant = args.get("merchant") + amount = float(args.get("amount_usd", 0)) + + if merchant not in policy.allowed_merchants: + return ToolGuardrailFunctionOutput.reject_content( + f"Payment blocked: merchant {merchant!r} is not approved." + ) + + if amount > policy.max_amount_usd: + return ToolGuardrailFunctionOutput.reject_content( + f"Payment blocked: ${amount:.2f} exceeds the ${policy.max_amount_usd:.2f} limit." + ) + + return ToolGuardrailFunctionOutput.allow( + {"merchant": merchant, "amount_usd": amount, "agent": data.agent.name} + ) +``` + +Attach the guardrail to the function tool with `tool_input_guardrails=[...]`. If the +guardrail returns `reject_content`, the function tool is not called and the rejection message +is returned to the model so it can choose a different action or ask for approval. + +Tool input guardrails apply to local function tools executed by the SDK process. Hosted tools +are executed server-side by OpenAI, so Python-side tool input guardrails cannot intercept them +before execution. diff --git a/examples/basic/tool_policy_guardrail.py b/examples/basic/tool_policy_guardrail.py new file mode 100644 index 0000000000..8717bb0e91 --- /dev/null +++ b/examples/basic/tool_policy_guardrail.py @@ -0,0 +1,80 @@ +import asyncio +import json +from dataclasses import dataclass + +from agents import ( + Agent, + Runner, + ToolGuardrailFunctionOutput, + ToolInputGuardrailData, + function_tool, + tool_input_guardrail, +) + + +@dataclass +class PaymentPolicy: + max_amount_usd: float + allowed_merchants: set[str] + + +@tool_input_guardrail +def enforce_payment_policy(data: ToolInputGuardrailData) -> ToolGuardrailFunctionOutput: + """Check the exact payment payload before the tool is invoked.""" + policy = data.context.context + args = json.loads(data.context.tool_arguments or "{}") + + if data.context.tool_name != "charge_card": + return ToolGuardrailFunctionOutput.reject_content("This tool is not allowed.") + + merchant = args.get("merchant") + amount = float(args.get("amount_usd", 0)) + + if merchant not in policy.allowed_merchants: + return ToolGuardrailFunctionOutput.reject_content( + f"Payment blocked: merchant {merchant!r} is not approved." + ) + + if amount > policy.max_amount_usd: + return ToolGuardrailFunctionOutput.reject_content( + f"Payment blocked: ${amount:.2f} exceeds the ${policy.max_amount_usd:.2f} limit." + ) + + return ToolGuardrailFunctionOutput.allow( + {"merchant": merchant, "amount_usd": amount, "agent": data.agent.name} + ) + + +@function_tool(tool_input_guardrails=[enforce_payment_policy]) +def charge_card(customer_id: str, merchant: str, amount_usd: float) -> str: + """Charge a customer's saved card.""" + return f"Charged ${amount_usd:.2f} to {customer_id} for {merchant}." + + +agent = Agent( + name="Payment assistant", + instructions="Help with payments, but respect the payment policy.", + tools=[charge_card], +) + + +async def main() -> None: + policy = PaymentPolicy(max_amount_usd=100.0, allowed_merchants={"example.com"}) + + result = await Runner.run( + agent, + "Charge customer cus_123 $25 for example.com.", + context=policy, + ) + print(result.final_output) + + blocked = await Runner.run( + agent, + "Charge customer cus_123 $250 for unknown.example.", + context=policy, + ) + print(blocked.final_output) + + +if __name__ == "__main__": + asyncio.run(main())