Any framework¶
guard_tool wraps any sync or async Python function that an agent can call. The call is checked before it runs and the
result is scanned after.
from guardlayer import GuardLayer
from guardlayer.integrations.tools import guard_tool
guard = GuardLayer()
@guard_tool(guard, session="user-42")
def run_shell(cmd: str) -> str:
return "ran: " + cmd # your real implementation
print(run_shell(cmd="ls -la")) # runs
print(run_shell(cmd="rm -rf ~")) # returns a refusal; the function never runs
| Option | Default | Meaning |
|---|---|---|
session |
none | a session id, a GuardSession, or a zero-argument callable returning one (e.g. the current user) |
approve |
none | approve(result) -> bool for review verdicts. With no approver, review is treated like block. |
on_block |
"message" |
return a refusal string the model can read, or "raise" to raise ToolBlocked |
withhold_at |
block |
results at or above this verdict are replaced by a notice |
on_injection |
"withhold" |
"strip" cuts the injected part out of a string result and keeps the rest, so the agent can still finish its task. Needs a session (the next side-effecting action then goes to review); falls back to withholding when the cut would be most of the text, a detection has no location (the classifier), or the remainder still looks hostile. It improves usability, it doesn't clean the content: on held-out LLMail-Inject attacks that were stripped rather than withheld, the attacker's target address was still in the remainder 117 times out of 269. Its safety comes from the session, which holds the agent's next side-effecting action for review; that's why it needs one. Also on guard_tools (LangGraph) and guardrails (OpenAI Agents SDK). |
name |
the function name | the tool name used for capabilities and rules |
@guard_tool(guard, session=lambda: request.user_id, approve=ask_on_slack)
def send_email(to: str, body: str) -> str: ...
The wrapper keeps the function's signature and docstring (functools.wraps), so framework decorators such as
LangChain's @tool or the Agents SDK's @function_tool still work on top of it.
Your own agent loop¶
If you run the loop yourself, call the scans directly:
for call in model_response.tool_calls:
verdict = guard.scan_tool_call(call.name, call.args, session=session_id)
if verdict.is_blocked or (verdict.needs_review and not approve(verdict)):
messages.append(tool_message(call, refusal_message(call.name, verdict)))
continue
output = run(call)
scanned = guard.scan_tool_result(call.name, output, session=session_id)
messages.append(tool_message(call, scanned.text if scanned.allowed else withheld_message(call.name, scanned)))
refusal_message and withheld_message live in guardlayer.integrations.tools.