LangGraph, CrewAI and the OpenAI Agents SDK are Python frameworks. The Python SDK (pip install theauth) has guards for all three and a generic decorator in theauth.integrations. Any other process that can make an HTTP call can ask theAuth before running a tool with the REST endpoint below. For Node orchestrators such as LangGraph.js there is a TypeScript guard, createHttpGuard.
The endpoint
Mount the theAuth handler (for example at /api/auth) and call it with the agent bearer token.
data.code is the decision code, for example no_permission, rate_limited or approval_required. Treat any non 200 answer, and any network error, as a denial.
Python guards
The Python SDK ships the guards in theauth.integrations. They match the TypeScript helpers: authorize runs before the tool, and a refusal comes back as a structured denial with the decision code and approvalRequired. Nothing is imported from LangGraph, CrewAI or the OpenAI Agents SDK when you import theauth, so the frameworks stay optional.
Every guard is built on one Gate. Build it once from a client and the agent bearer token. The gate fails closed: a network error, a timeout, a malformed answer or an exception in your own callback becomes a denial with code guard_error, and the tool does not run.
Use TheAuthClient for an async app. The same gate then works in async tools, because it checks with await when the tool is a coroutine function. To check by agent id instead of token, pass client.authorize and the id: Gate(client.authorize, "agent_123").
Python (LangGraph)
Wrap the tool function. The signature and docstring are kept, so LangChain still builds the argument schema from them.
A refused call returns the denial dict as the tool message content, so the graph continues and the model can tell the user:
To guard every tool in one place, wrap the node that executes tool calls. guard_node checks each pending call of the last AI message before the node runs. If one is refused, the node does not run: refused calls get the denial as a tool message and the others get a skipped notice, so every tool_call_id is answered.
For an async graph use aguard_node with TheAuthClient and Gate.from_token(async_client, token). Prefer guard_tool when a step can hold several calls and you want the allowed ones to run.
Python (CrewAI)
CrewAI tools return text to the model, so give the guard a text formatter. Put it under @tool:
A refusal reads Refused (approval_required): <reason>. Ask the user to approve it. For a tool class that subclasses BaseTool, or a tool you already built, call guard_crewai_tool(tool, gate). It wraps _run or func in place.
Python (OpenAI Agents SDK)
guard_function_tool takes the object @function_tool returns and gives back a copy whose on_invoke_tool checks the gate first. The input JSON is parsed for the argument check, and input that is not valid JSON is refused.
Pass the result in Agent(tools=[...]) as usual. The denial is returned as JSON text. Use guard_function_tools([...], gate) for a list.
Python (any function)
@guard(gate, "name") works on any sync or async function:
Parameters named self, cls, ctx, context, config and runtime are treated as run context, not tool input, and are not sent as arguments. Other values are converted to JSON safe form first (Pydantic models and dataclasses become dicts, unknown objects become strings).
Python approvals
When the decision is approval_required, the gate can open a request, wait for a human and retry once with the approved id. Pass request_approval. It receives an ApprovalContext (tool name, agent id, action, resource, arguments, audit id) and returns the approved request id, or None to refuse.
wait_for_approval polls client.approvals.get(id) until the request is approved. It returns None when the request is denied, expired or missing, or when timeout_s passes, and the gate then refuses the call. The read route needs an admin user token. The request itself is opened by your backend with theauth.approval.request(...), because POST /approvals on the admin API only opens admin: actions. /api/open-approval below is your own route:
In an async app use await await_approval(admin_client.approvals, id, timeout_s=120). A sync gate cannot await, so give it a sync request_approval and a sync client. The approved id is spent on the single retry, and the approval is bound to the same action, resource and arguments. See Approval.
TypeScript guard for Node orchestrators
Use guard.check(toolName, args) when you want the denial without wrapping a function: it returns null when allowed. The guard never throws. Network errors, timeouts and server errors resolve to a denial with code guard_error.
Both authorize routes accept an optional approvalId in the body, which spends an approved request once when the action, resource and arguments match. createHttpGuard does not send one itself. For a retry after approval, call the route yourself with the approved id, or run the check in a Node service that holds theauth.authorize and use the requestApproval option of the Vercel AI SDK helper. Last modified on October 19, 2026