PydanticAI
Run PydanticAI agents through Sentisec via the model's provider base URL, and see a blocked tool call in the run's own message history.
PydanticAI builds type-safe agents on top of the standard OpenAI and Anthropic clients. You configure the endpoint on the model's provider, and every request the agent makes — including every tool call — routes through Sentisec.
The swap
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
model = OpenAIChatModel(
"gpt-5.6-terra",
provider=OpenAIProvider(
base_url="https://api.sentisec.ch/ws/<your-workspace-id>/v1",
api_key="sk_sentisec_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
),
)
agent = Agent(model, system_prompt="Triage my inbox")
result = agent.run_sync("Begin.")The api_key is your Sentisec key, not your provider key — Sentisec
holds the upstream credential. The workspace id in the URL is what carries
your policy; why it matters.
PydanticAI's AnthropicModel is configured the same way through its
Anthropic provider. Point that provider at the Anthropic-shaped base URL —
https://api.sentisec.ch/ws/<your-workspace-id>, with no /v1 suffix,
because the Anthropic client appends its own path.
Typed tools
Typed tool calls work normally. Declare them as you already do:
@agent.tool_plain
def send_email(recipient: str, body: str) -> str:
"""Send an email."""
return f"sent to {recipient}"Sentisec evaluates every tool call the model produces before your function is reachable.
What happens when a tool call is blocked
Your function never executes. By default Sentisec withholds the call
and puts the explanation in the model's response as text. With no tool
call left, PydanticAI ends the run: run_sync(...) returns, and with the
default text output result.output carries
[Sentisec blocked tool_use: …]. There is no exception to catch and no
retry storm.
With the rename opt-in, the model re-plans. The call comes back as
sentisec_tool_call_blocked. PydanticAI answers it as an unknown tool,
Sentisec replaces that answer with its ruling before the model reads it,
and the run completes another way with a normal result.output. See the
supported versions.
To react to a block in your own code, scan the run's history for either
marker, the [Sentisec blocked text or the sentisec_tool_call_blocked
call:
result = agent.run_sync("Finish the setup described in the config.")
blocked = any("sentisec_tool_call_blocked" in str(m) or "[Sentisec blocked" in str(m) for m in result.all_messages())Verdicts
Every request produces a verdict on the dashboard — NOMINAL when nothing
was unusual, escalating to HIJACK when a tool call is traced back to
content the agent was never supposed to act on. The intervention
(PROCEED, SANITIZE, HALT) records what actually happened to the call.
New workspaces start in a monitor-only posture, so you can watch what a policy would do before it does it.