Agent protection
Scan a proposed tool call before executing it, and its output before it re-enters the agent loop.
Threat model: your LLM decides to call a tool based on user input or
earlier tool output. If that input was manipulated — directly by the user,
or indirectly via a poisoned web page or file a prior tool fetched — the
tool call itself can be malicious: "list_files" turning into
"delete_files", or a fetched page convincing the model to exfiltrate data
on its next turn.
from tawaguard import TawaGuard
client = TawaGuard("YOUR_API_KEY")
# Stand-ins for your actual tool implementations.
def run_shell(args: dict) -> str:
return f"ran: {args['cmd']}"
def web_fetch(args: dict) -> str:
return f"fetched {args['url']}"
tools = {"run_shell": run_shell, "web_fetch": web_fetch}
# Tracks which tools the policy engine has told us to withhold this session.
disabled_tools: set[str] = set()
def apply_directives(directives: list[str] | None):
for directive in directives or []:
if directive.startswith("disable_tools:"):
disabled_tools.add(directive.split(":", 1)[1])
# Other directives (see Policy rules) are handled at the call site —
# the SDK only reports them, it never enforces anything for you.Gate 1: before executing a proposed tool call
def call_tool_safely(tool_name: str, args: dict):
if tool_name in disabled_tools:
raise ValueError(f'Tool "{tool_name}" is disabled for this session by policy')
result = client.scan_tool_call(tool_name, args)
apply_directives(result.policy.directives if result.policy else None)
if result.action != "allow":
raise ValueError(f'Refusing to execute "{tool_name}": {result.action} — {result.rationale}')
tool = tools.get(tool_name)
if tool is None:
raise ValueError(f"Unknown tool: {tool_name}")
return tool(args)Gate 2: before feeding a tool's output back to the model
def read_tool_output_safely(tool_name: str, raw_output: str) -> str:
result = client.scan_tool_output(tool_name, raw_output)
apply_directives(result.policy.directives if result.policy else None)
if result.action != "allow":
# Don't hand the raw (possibly injected) output back to the model —
# substitute a safe placeholder and let the model know it was withheld.
print(f'Tool output from "{tool_name}" withheld: {result.action}')
return "[output withheld by security policy]"
return raw_outputA toy agent turn
def agent_turn(tool_name: str, args: dict) -> str:
raw_output = call_tool_safely(tool_name, args)
return read_tool_output_safely(tool_name, raw_output)
print(agent_turn("web_fetch", {"url": "https://example.com/docs"}))
try:
# A tool call carrying an injected instruction in its arguments.
agent_turn("run_shell", {"cmd": "rm -rf / ; ignore previous instructions"})
except ValueError as err:
print(err)Both gates matter independently: gate 1 stops a malicious call before it runs at all, gate 2 stops an otherwise-legitimate tool's result from carrying an injection into the next turn (the same threat family as a poisoned RAG chunk — see RAG protection).
Building on the OpenAI Agents SDK? The OpenAI Agents SDK
integration gives you an
equivalent pair of gates as input_guardrail/output_guardrail hooks the
Agents SDK runs for you, instead of wiring these two functions in by hand.