Guard Tool Calls¶
The model can now request either document tool. It can also request private
file .env. A system-prompt sentence is guidance, not an authorization check.
The policy must run after Axio validates arguments and before the handler reads data.
Outcome¶
You will attach a DocumentAccessGuard to read_document. It accepts allowed
paths, rejects disallowed paths, and prevents the handler from running on
denial.
Fast Track¶
Subclass
PermissionGuardand implementcheck.Return keyword arguments to allow a call.
Raise
GuardErrorto deny a call.Attach the guard through
Tool(..., guards=(guard,)).Verify direct denial and the Agent’s error event.
Hands-on delta¶
1. Put access data in executable policy¶
Add this guard beside the handlers in harness.py:
@dataclass(frozen=True, slots=True)
class DocumentAccessGuard(PermissionGuard):
allowed_paths: frozenset[str]
async def check(
self,
tool: Tool[Any],
**kwargs: Any,
) -> dict[str, Any]:
path = str(kwargs["path"])
if path not in self.allowed_paths:
raise GuardError(f"{tool.name} denied access to {path}")
return {**kwargs, "path": path}
document_access = DocumentAccessGuard(
allowed_paths=frozenset({"README.md", "docs/architecture.md"}),
)
read_document_tool = Tool(
name="read_document",
handler=read_document,
description="Read one document by exact path; do not use for discovery.",
guards=(document_access,),
)
check receives validated keyword arguments. Returning a dictionary allows the
call and can replace arguments. Here, the guard checks the exact path before
the handler reads the document.
Raising GuardError denies the call. Axio does not invoke the handler after a
guard denial.
Guards run in tuple order. Each guard receives the keyword arguments returned by the previous guard. Read Tool System for the full execution order.
2. Keep search from becoming a side door¶
The previous lesson’s search_documents returns only records whose visibility
is public. Keep that filter.
A guard protects one tool invocation. It does not replace authorization in the document service or database. The backing service must still apply the current user’s access policy for every data path.
Use Writing Guards for reusable guards, audit guards, and concurrency guidance.
Try It¶
First call the Tool directly. Then send the same denied input through the
Agent to verify its stream behavior.
Run uv run python examples/tutorial/guard_tool_calls.py from the repository
root.
async def try_guard() -> None:
allowed = await read_document_tool(path="README.md")
assert allowed == "README.md: Project overview"
assert handler_calls == ["README.md"]
try:
await read_document_tool(path=".env")
except GuardError as error:
assert ".env" in str(error)
else:
raise AssertionError(".env must be denied")
transport = StubTransport(
[
make_tool_use_response(
tool_name="read_document",
tool_input={"path": ".env"},
),
make_text_response("I cannot access .env."),
]
)
agent = Agent(
system="Use document tools for facts about project documents.",
transport=transport,
tools=[read_document_tool, search_documents_tool],
)
events = [
event
async for event in agent.run_stream(
"Show .env",
MemoryContextStore(),
)
]
denied_results = [
event for event in events if isinstance(event, ToolResult) and event.is_error
]
assert len(denied_results) == 1
assert handler_calls == ["README.md"]
public_results = await search_documents_tool(query="")
assert "README.md" in public_results
assert "docs/architecture.md" in public_results
assert ".env" not in public_results
print(denied_results[0].content)
async def main() -> None:
logging.getLogger("axio.agent").setLevel(logging.CRITICAL)
await try_guard()
if __name__ == "__main__":
asyncio.run(main())
A direct Tool call raises GuardError. During an Agent run, Axio converts the
denial into an error tool result. The transport can then produce a safe final
response or request a different action.
This distinction keeps policy errors inside the agent loop without hiding them from your application event stream.
Done when¶
[ ]
DocumentAccessGuardreturns arguments for an allowed exact path.[ ] It raises
GuardErrorfor a disallowed path.[ ] The guarded handler does not run after denial.
[ ]
read_document_toolreceives the guard as a one-item tuple.[ ] An Agent run emits one error
ToolResultfor the denied call.[ ] The backing search path still limits results independently.
Next failure¶
Agent.run returns the final text, but the application cannot render the tool
request, denial, retry, or token usage as they happen. The next lesson switches
the harness boundary to run_stream and handles every action explicitly.
Continue with Stream Every Action.