Pydantic AI
Integrate Intutic governance with Pydantic AI (pydantic-ai/pydantic-ai-slim; the [pydantic-ai] extra installs pydantic-ai-slim 2.53 or later) — Pydantic's typed agent framework.
Pydantic AI is governed on two independent surfaces:
- LLM egress — point the model client's
base_urlat the Intutic proxy (or launch underintutic exec). - Local tool execution — Pydantic AI tools run inside toolsets attached to your
Agent. No config or hook file can gate them, so the blocking gate ships SDK-side inintutic-clawde, using a veto point the SDK itself documents for exactly this purpose.
How it works
The pydantic-ai adapter is detected when pyproject.toml, requirements.txt, or uv.lock declares pydantic-ai or pydantic-ai-slim (the substring match covers both package names). Like LangGraph, it writes a .env.intutic file with proxy base-URL env vars plus a comment block pointing at the SDK gate.
Pydantic AI documents WrapperToolset — an AbstractToolset subclass whose whole job is delegating to a wrapped toolset, built for exactly this "intercept before delegating" shape. IntuticWrapperToolset subclasses it and overrides call_tool to gate before delegating to super().call_tool(...).
Setup
1. Initialize Intutic
intutic init2. Route LLM traffic through the proxy
Source .env.intutic, launch under intutic exec, or set base_url explicitly on your model client — same as LangGraph.
3. Gate local tool execution (SDK)
pip install intutic-clawde[pydantic-ai]The simplest path wraps every toolset already attached to an Agent in one call:
from pydantic_ai import Agent
from intutic_clawde.gate import Gate, GateConfig, install
from intutic_clawde.gate.adapters.pydantic_ai import guard_agent
install(Gate(GateConfig()))
agent = Agent(model, tools=[...], toolsets=[my_toolset])
guard_agent(agent) # mutates in place; also returns agent. Covers toolsets=, not tools= (see below)Or wrap a single toolset explicitly:
from intutic_clawde.gate.adapters.pydantic_ai import IntuticWrapperToolset
agent = Agent(model, toolsets=[IntuticWrapperToolset(wrapped=my_toolset)])On a deny, call_tool raises ModelRetry(str(exc)) — confirmed by reading pydantic_ai/tool_manager.py (pydantic-ai-slim 2.31.1 and 2.53.0): ToolManager._raw_execute calls toolset.call_tool(...) directly, catches any ModelRetry it raises, and converts it into a RetryPromptPart. The model sees the refusal message in place of a tool result, and super().call_tool — the tool body — never runs.
Why ModelRetry and not ApprovalRequired: Pydantic AI also documents AbstractToolset.approval_required(...)/raising pydantic_ai.exceptions.ApprovalRequired as a veto-adjacent mechanism. This adapter does not use it — ApprovalRequired is Pydantic AI's human-in-the-loop primitive; raising it defers the call (surfaced via DeferredToolRequests, expecting a human or an external system to resume the run with a decision) rather than denying it. An unattended governed run has nobody to answer that deferral, so treating a gate block as "approval required" would either hang the run or, depending on the caller's deferred-tool handling, let the call through by default. ModelRetry is an immediate, non-deferred refusal — the correct match for Gate.guard()'s contract.
4. Trace attribution
from openai import AsyncOpenAI
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from intutic_clawde.gate import intutic_headers
client = AsyncOpenAI(
base_url="http://localhost:4000/v1",
default_headers=intutic_headers(session_id=run_id, harness="pydantic-ai"),
)
model = OpenAIChatModel("gpt-5-mini", provider=OpenAIProvider(openai_client=client))What gets written
Same shape as LangGraph's .env.intutic — proxy URLs plus a pointer at intutic_clawde.gate.adapters.pydantic_ai.guard_agent.
To undo what intutic gates install writes here, run intutic disconnect --harness pydantic-ai: each file goes back to what it held before Intutic first wrote it, or is deleted if Intutic created it, and edits you made since are kept. .env.intutic stays while another harness that writes it is still connected. See intutic disconnect.
What the adapter does NOT do
Same structural gaps as every SDK-gated framework — see LangGraph's "What the adapter does NOT do" — plus Pydantic-AI-specific limits:
guard_agentdoes not cover@agent.tool-registered functions. It wrapsagent._user_toolsets(explicitAgent(toolsets=[...])entries) andagent._dynamic_toolsets, but deliberately leavesagent._function_toolset— the toolset backing@agent.tool/Agent(tools=[...])— untouched. Those are plain functions under the hood; wrap them with@guard(fromintutic_clawde.gate.framework) before registering, or build an explicitFunctionToolset(tools=guard_tools([...]))and pass it viaAgent(toolsets=[...])soguard_agentcovers it uniformly with everything else. Reaching into_function_toolsetdirectly would break@agent.tool's own registration bookkeeping if a caller adds more tools after callingguard_agent.guard_agentreaches into private attributes._user_toolsets/_dynamic_toolsetsare private (confirmed present atpydantic-ai-slim2.31.1 and 2.53.0 by readingpydantic_ai/agent/__init__.py);Agent.toolsetsis a read-only computed property rebuilt from those two lists on every access, so there is no public replacement point on an already-constructedAgent. This is an accepted, documented fragility: a future pydantic-ai release could rename these attributes and silently turnguard_agentinto a no-op — it would not raise, which is the whole risk of a private-attribute reach-in.- Already-wrapped toolsets are left alone. Calling
guard_agenttwice does not double-wrap an already-IntuticWrapperToolset-wrapped entry.
Config details
| Property | Value |
|---|---|
| Harness type | pydantic-ai |
| Config file | .env.intutic |
| Detection | pydantic-ai (matches pydantic-ai and pydantic-ai-slim) in pyproject.toml, requirements.txt, or uv.lock |
| Format | Shell environment variables |
| Write strategy | Atomic (write to .intutic-tmp, then rename) |
| Tool gate | SDK-side (intutic_clawde.gate.adapters.pydantic_ai.IntuticWrapperToolset/guard_agent, a WrapperToolset.call_tool override raising ModelRetry) — covers Agent(toolsets=[...]) entries only, not @agent.tool-registered functions; no sync-daemon hook file |