Skip to content

SDK integration

Running a custom Python agent? Route its tool calls through the Guardyx SDK to get the same controls — policy, approvals, audit, observability — around code you already own. The SDK is a thin gate in front of each tool. Every AI action passes through Guardyx. Policy runs before execution.

How reversible that is depends on the pattern you pick. With the adapters below, governance is additive: your tools keep their existing code, Guardyx wraps them, and removing the SDK leaves the tools running — you just lose the gate. The @guardyx_tool decorator is different: it replaces the function body, so the call is executed by the Guardyx backend through its configured connector rather than by your code. That is the point of it, but it means the decorated function has no local implementation to fall back on. Choose it when you want Guardyx to own execution; choose an adapter when you want to keep your own.

  • MCP is the primary path when your agent runs inside an MCP client (Claude Desktop, Claude Code, ChatGPT Desktop, Cursor). It’s a config change with no code edits. See the Quickstart.
  • SDK is the right path when you own the agent code and want explicit control over which calls are governed — a LangGraph/LangChain agent, an OpenAI Agents or Claude Agent SDK app, CrewAI, AutoGen, Google ADK, a raw Chat Completions loop, or a fully custom framework.

The base package pulls in only httpx and includes every adapter class. Install the matching optional extra when an adapter needs its framework’s runtime. The raw OpenAI-tools adapter can handle tool-call JSON with the core install; its extra adds the OpenAI client for making Chat Completions requests.

Terminal window
pip install guardyx # core client + decorators (httpx only)
pip install guardyx[langgraph] # LangGraph / LangChain
pip install guardyx[openai-agents] # OpenAI Agents SDK
pip install guardyx[claude] # Claude Agent SDK (PreToolUse hooks)
pip install guardyx[crewai] # CrewAI
pip install guardyx[autogen] # AutoGen
pip install guardyx[google-adk] # Google ADK
pip install guardyx[openai-tools] # raw tool loops + the optional OpenAI client

The unit of accountability is a run — one complete agent invocation. A run can make many tool calls; each call is one policy decision and one tamper-evident audit record. Every governed call resolves to one of three decisions:

Decision What the SDK does
allow The tool executes and returns its result.
deny GuardyxDeniedError is raised; the tool body never runs.
require_approval The call blocks while a human reviews it in the portal. On approval the tool runs; on denial, expiry, or timeout the SDK raises.

Tool identities live in the portal, not in your code. You register each tool once (Agents → Tools) and pass its UUID into the SDK. The SDK is stateless — it does not register or discover tools.

GuardyxClient takes an agent’s API key and UUID, plus your tenant’s base URL. Both the key and the host are shown in the portal’s Agent created modal.

import os
from guardyx import GuardyxClient
gx = GuardyxClient(
api_key=os.environ["GUARDYX_API_KEY"], # per-agent key from the portal
agent_id=os.environ["GUARDYX_AGENT_ID"], # from the portal
base_url=os.environ["GUARDYX_BASE_URL"], # e.g. https://your-guardyx-host/v1
)

Approval polling is configurable per call:

await run.invoke(
tool_id="<tool-uuid>",
payload={"key": "value"},
poll_interval=2.0, # seconds between approval polls (default 2.0)
timeout=300.0, # seconds before GuardyxTimeoutError (default 300)
)

Pick the pattern that matches how your agent is built. All three route through the same gateway and produce the same audit trail.

Best for function-based tools with no agent framework. @guardyx_tool replaces a function’s body with a governed invoke; @guardyx_agent opens one run for the whole call and propagates it to every governed tool automatically.

import os
from guardyx import GuardyxClient, guardyx_tool, guardyx_agent
gx = GuardyxClient(api_key=os.environ["GUARDYX_API_KEY"], agent_id="<agent-uuid>",
base_url="https://your-guardyx-host/v1")
@guardyx_tool("<tool-uuid>")
async def send_slack_message(channel: str, text: str) -> dict:
"""Body never runs — Guardyx intercepts and routes through policy."""
...
@guardyx_agent(client=gx)
async def hello_agent(channel: str) -> str:
result = await send_slack_message(channel=channel, text="Hello from Guardyx! 👋")
return f"Message sent to {channel}"

One @guardyx_agent call opens one run. Every governed tool call inside that run creates its own audit record. The tool’s arguments become the payload Guardyx evaluates, so policies can reason over the exact channel and text.

Every adapter shares the same shape: construct it with your credentials, wrap your existing tools with their portal UUIDs, and open a run around the agent. Wrapping is drop-in — your tools and agent logic are unchanged.

import os
from guardyx import GuardyxLangGraphAdapter
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
adapter = GuardyxLangGraphAdapter(
api_key=os.environ["GUARDYX_API_KEY"], agent_id="<agent-uuid>",
base_url="https://your-guardyx-host/v1",
)
# Wrap existing LangGraph/LangChain tools with their portal tool UUIDs.
governed_tools = adapter.wrap_many([
(web_search, "<tool-uuid-1>"),
(send_email, "<tool-uuid-2>"),
(query_database, "<tool-uuid-3>"),
])
async def run_agent(query: str):
async with adapter.run(): # one run per invocation
agent = create_react_agent(ChatOpenAI(model="gpt-4o"), governed_tools)
return await agent.ainvoke({"messages": [HumanMessage(content=query)]})

The same adapter.wrap(...) / adapter.run() pattern applies to the other adapters:

# OpenAI Agents SDK — wraps FunctionTool.on_invoke_tool
from guardyx import GuardyxOpenAIAgentsAdapter
governed = GuardyxOpenAIAgentsAdapter(...).wrap(my_function_tool, "<tool-uuid>")
# CrewAI — wraps a crewai.tools.BaseTool
from guardyx import GuardyxCrewAIAdapter
governed = GuardyxCrewAIAdapter(...).wrap(MyCrewTool(), "<tool-uuid>")
# AutoGen — returns an autogen_core FunctionTool
from guardyx import GuardyxAutoGenAdapter
governed = GuardyxAutoGenAdapter(...).wrap(my_callable, "<tool-uuid>")
# Google ADK — returns a google.adk.tools.FunctionTool
from guardyx import GuardyxGoogleAdkAdapter
governed = GuardyxGoogleAdkAdapter(...).wrap(my_callable, "<tool-uuid>")

Raw OpenAI Chat Completions (no Agents SDK) uses wrap plus a dispatch step. Feed tools_payload(...) into the model, then dispatch the tool call the model emits — policy runs before your function does:

import os
from guardyx import GuardyxOpenAIToolsAdapter
adapter = GuardyxOpenAIToolsAdapter(api_key=os.environ["GUARDYX_API_KEY"],
agent_id="<agent-uuid>",
base_url="https://your-guardyx-host/v1")
weather = adapter.wrap(get_weather, "<tool-uuid>")
tools = adapter.tools_payload([weather]) # pass as tools=[...] to the model
async with adapter.run():
result = await adapter.dispatch(weather, arguments_json) # arguments from the model

Claude Agent SDK integrates through PreToolUse hooks rather than wrapped callables. Map Claude tool names to portal UUIDs, then hand the hook dict to ClaudeAgentOptions:

import os
from guardyx import GuardyxClaudeSdkAdapter
adapter = GuardyxClaudeSdkAdapter(
api_key=os.environ["GUARDYX_API_KEY"], agent_id="<agent-uuid>",
base_url="https://your-guardyx-host/v1",
tool_name_to_guardyx_id={"Bash": "<tool-uuid>"},
)
# from claude_agent_sdk import ClaudeAgentOptions
# opts = ClaudeAgentOptions(hooks=adapter.hooks_dict())
# ... open `async with adapter.run():` around your ClaudeSDKClient session.

For a fully custom framework, drive the run yourself. run.invoke returns the result on allow and blocks through the approval loop when policy requires it.

async with gx.run() as run:
result = await run.invoke("<tool-uuid>", {"param": "value"})
# If policy = require_approval, this awaits until a human acts in the portal.

The framework_type you pick when creating an agent in the portal determines the adapter. Everything below is SDK-consumed — you import an adapter class and wrap your tools. MCP-client agents (Claude Desktop, Claude Code, ChatGPT Desktop, Cursor) don’t install the SDK at all; see the Quickstart.

Portal slug Pip extra Adapter
langgraph langgraph GuardyxLangGraphAdapter
langchain langgraph LangChain tools via GuardyxLangGraphAdapter.wrap()
openai_agents openai-agents GuardyxOpenAIAgentsAdapter
claude_sdk claude GuardyxClaudeSdkAdapter
crewai crewai GuardyxCrewAIAdapter
autogen autogen GuardyxAutoGenAdapter
google_adk google-adk GuardyxGoogleAdkAdapter
openai_tools (none for the adapter; openai-tools adds the OpenAI client) GuardyxOpenAIToolsAdapter
custom (none) @guardyx_tool / GuardyxClient

Guardyx ships a native LangGraph adapter, and LangChain tools are governed through that same adapter — both are live.

When policy returns require_approval, the call surfaces in the portal with the full payload and decision context, and the agent waits — the SDK polls until the review resolves. Your agent code just sees a longer await; it never distinguishes a fast allow from a human-approved action.

On approval, the backend dispatches the tool through its configured connector and captures the outcome inline with the decision. The SDK returns that result to your agent. If the connector’s execution itself fails after approval, the SDK raises GuardyxApprovalExecutionError rather than returning a partial result.

Denials and approval outcomes are raised as typed exceptions, all under GuardyxError:

GuardyxError # base — catch this for "any Guardyx outcome"
├── GuardyxDeniedError # policy denied the call outright; tool never ran
├── GuardyxApprovalDeniedError # a human reviewer denied the approval
├── GuardyxApprovalExpiredError # the approval window closed with no decision
├── GuardyxApprovalExecutionError # approved, but the connector's execution failed
└── GuardyxTimeoutError # polling exceeded `timeout` before any decision

Catch the ones your agent should recover from and let the rest surface:

from guardyx import (
GuardyxDeniedError,
GuardyxApprovalDeniedError,
GuardyxTimeoutError,
)
try:
result = await run.invoke("<tool-uuid>", payload)
except GuardyxDeniedError:
... # policy blocked it — pick another path or report back to the user
except GuardyxApprovalDeniedError:
... # a reviewer said no
except GuardyxTimeoutError:
... # no decision in time — escalate; see the warning below before retrying

Adapters and decorators are conveniences over four HTTP calls. If you’re integrating a framework the SDK doesn’t cover, you can speak the protocol directly. Every request carries X-API-Key: <agent key>.

Step Request Response
Start run POST /v1/runs/start {"agent_id": "..."} {"id": "<run-id>"}
Invoke tool POST /v1/runs/{id}/invoke {"tool_id": "...", "payload": {...}} {"decision": "allow|deny|require_approval", "result"?, "approval_id"?, "reason"?}
Poll approval GET /v1/approvals/{id} {"status": "pending|approved|denied|expired", "invocation": {"status", "result", "error"}}
Complete run POST /v1/runs/{id}/complete {}
  • API key + base URL — the portal’s Agent created modal, shown when you create an agent.
  • Tool UUIDsAgents → Tools. Register each tool your agent calls, then pass its UUID into @guardyx_tool(...), adapter.wrap(...), or run.invoke(...).