A Pydantic AI agent runs inside your own Python process with the authority of whatever tools, dependencies and credentials your code hands it, so its security is decided almost entirely by application code. Pydantic AI is an open-source, MIT-licensed agent framework from the team behind Pydantic. The documentation is explicit that there is no Pydantic-hosted agent runtime: the agent, its tool functions and its provider API keys live in your service, and the optional hosted services (Pydantic Logfire for tracing and the Pydantic AI Gateway for model access) plug in only if you choose them. This guide covers the security-relevant parts of the framework: the trust boundary the project documents, tool authorisation and approval, dependency injection of per-caller identity, MCP connections, model credentials, untrusted message history, telemetry and cost limits. It is for engineers and security reviewers taking a Pydantic AI agent to production.
This is an independent guide from NHI Mgmt Group, based on Pydantic's public documentation as of November 2026. It is not sponsored or reviewed by Pydantic. Product features change, so confirm settings against Pydantic's current documentation.
Key takeaways
- The application owns authorisation: the project's threat model places authentication, tenant identity, tool permissions, provider credentials and stored state with the server application, and says schema validation does not establish a caller's permission.
- Scope tools per caller, per run: build or filter the toolset against the authenticated user carried in the run's dependencies, so the model can only ever see tools that user is entitled to.
- Use deferred tools for sensitive actions:
requires_approval=True,ApprovalRequiredandApprovalRequiredToolsetend the run with aDeferredToolRequestsoutput, and the docs describe approval as a guard against the model acting alone, so re-check the caller's authority inside the tool. - Treat client-supplied history as untrusted:
message_historyis trusted server-side state, so pass anything from a browser throughsanitize_messagesfirst, or keep the history on the server. - Telemetry includes content by default: traces carry prompts, completions and tool arguments unless you set
include_content=False, so decide what may leave the process before turning instrumentation on.
The trust boundary Pydantic documents
The repository's THREAT_MODEL.md, published as a working draft for maintainer review, is the best starting point for a security review. It lists the deployment profiles and who owns each boundary.
| Deployment profile | Who owns the boundary (per the threat model) | What to review |
|---|---|---|
Server application using Agent | The application: authentication, tenant identity, tool permissions, provider credentials, stored state | Route authentication, per-caller toolsets, secret storage |
| Server application with a UI adapter (AG-UI, Vercel AI) | Adapter sanitises client protocol messages; the host application owns HTTP, session, CORS and CSRF security | Sanitisation settings, server-held history, conversation ownership |
| Local CLI or Harness tools | Tools run with the launching process's authority unless an execution backend isolates them | Which commands and paths the agent can touch |
| Sandboxed or remote workspace | The chosen backend's credentials, mounts, network settings and lifetime | Sandbox egress, mounted secrets |
| Durable agent (DBOS, Temporal, Restate, Prefect and others) | Application and engine own persisted workflow state, queues, credentials and replay | State store access, what workflow state contains |
Two statements in the threat model shape everything else. First, model-generated tool arguments are untrusted input to the tool implementation: Pydantic validation checks the argument schema, but a valid argument can still name an account, file or URL the caller may not touch. Second, the project treats a client forging an approval or rewriting history as a documented design boundary rather than a framework flaw, because possession of the endpoint is the authorisation boundary. Apply vendor security updates promptly, and read the threat model before triaging a suspected framework issue, since it states which checks the library performs and which belong to your application.
Tools and least privilege
Tools are plain Python functions registered with @agent.tool (with a RunContext) or @agent.tool_plain, or grouped in toolsets. Toolsets are where least privilege is applied.
| Control | API | Use it for |
|---|---|---|
| Filter tools | toolset.filtered(lambda ctx, tool_def: ...) | Hide tools the current user or tenant may not use |
| Build tools per run | Dynamically built toolset from the run context | Construct only the tools the authenticated caller needs |
| Prepare definitions | prepare_tools hook, toolset prepare | Adjust or drop tool definitions per step |
| Prefix or rename | prefixed(), renamed() | Avoid name collisions between MCP servers and local tools |
| Require approval | approval_required(...) / ApprovalRequiredToolset | Approval for some calls, decided from context, tool definition and arguments |
| Bound execution | tool_timeout, Tool(max_retries=N) | Stop hung or looping tools |
- Authorise inside the tool: look up the caller from
ctx.depsand check the requested resource against your own policy before acting, rather than trusting that the model picked an allowed argument. - Raise
ModelRetryfor refusals the model can fix: a refused argument becomes a tool result the model can correct; keep the message free of internal detail. - Make side-effecting tools idempotent: the production docs note that under durable execution a step in flight when the process died runs again from its start, so pass an idempotency key to payment and write APIs.
- Isolate model-written code: run untrusted shell commands and generated code in a VM or microVM-backed sandbox from the Pydantic AI Harness execution environments. The docs describe the Bubblewrapped Workspace and
FileSystempath patterns as guards against accidental writes by a trusted agent, so use a VM-backed sandbox where isolation is the goal. See the AI Agent Sandboxing and Code Execution Guide.
Dependencies: carrying caller identity into the run
Pydantic AI's dependency injection passes a typed object, declared with deps_type and supplied as deps= on each run, to tools, instructions and output validators through RunContext. That makes it the natural place to carry the authenticated user, tenant, scoped database handle and per-user tokens.
- Populate deps from verified identity: build the deps object in your authenticated route handler from the session or token claims, never from fields in the prompt or history.
- Hand tools scoped clients, not root credentials: put a tenant-scoped database connection or a user-delegated token in deps, so even a misused tool reaches only that caller's data.
- Keep secrets out of instructions: deps are passed to code, not shown to the model, unless an instructions function writes them into the prompt; review dynamic instructions for leaked values.
- Bind state to the user: the threat model requires conversations, memory namespaces, deferred results and workspace reattachment to be bound to the authorised user; a client-supplied
conversation_idis not proof of ownership.
Human approval with deferred tools
When a tool marked requires_approval=True is called, the run ends with a DeferredToolRequests output listing the pending calls, and nothing stays in memory while you wait. You store the run's Conversation, ask a person, and continue with deferred_tool_results built from the decision. For approval that depends on arguments or context, raise ApprovalRequired from the tool; RunContext.tool_call_approved is True once approved. See the Human-in-the-Loop for AI Agents Guide.
- Persist paused runs server-side: the docs recommend resuming with your own
deferred_tool_resultsrather than the client's when stakes are high. - Authorise the approver: where a
HandleDeferredToolCallshandler issues a resume token, the threat model says to treat it as a bearer credential and to check who is calling in the code that resolves it;answered_byand decision metadata are not proof of who approved. - Default to deny on timeout: the production example treats an unanswered approval as a denial after a set period.
- Know what a model reviewer is:
ToolCallJudgeandApprovalReviewerin the Harness are documented as model-based filters, not security boundaries, and they send reviewed arguments to the reviewing model's provider.
MCP servers
MCPToolset connects an agent to MCP servers over Streamable HTTP, SSE or stdio. See the MCP Security Guide for the protocol view.
- Per-user authentication: a shared
MCPToolsetconnects as a single identity. Pass a function asauth(orheaders) that reads the user's token fromctx.deps; each run then opens its own MCP session. The docs warn against sharing a fixed-authtoolset across concurrent runs for different users, because one session would serve several users. - Configuration files are code: an MCP config file names executables to spawn and expands
${VAR}against the full process environment, so the docs say to load only files you control. - Server instructions stay off unless trusted: on
MCPToolset, including a server's instructions is off by default because the model receives them with your own instructions' authority; theMCPcapability turns it on, so passinclude_instructions=Falsefor servers you do not trust. - Sampling is opt-in: a server can only make model calls through your client once you set a
sampling_model, and those calls spend your model credentials. - Transport hardening: pass your own
http_clientfor an internal CA or mTLS; its cookie jar is shared by every run, so keep it off toolsets acting for different users. - Approval for MCP tools: wrap the toolset with
approval_required()for write operations.
Model credentials and cost
Each provider reads its key from an environment variable such as OPENAI_API_KEY or ANTHROPIC_API_KEY, or takes api_key= on its provider class, so keys can be loaded from a secret manager like any other credential. The optional Gateway uses one PYDANTIC_AI_GATEWAY_API_KEY for all providers, and each Gateway key can carry daily, weekly or monthly spending caps at project, user and key level. See How to find LLM API keys in code and CI and the LLMjacking Guide.
| Limit | Setting | Note from the docs |
|---|---|---|
| Per-run caps | UsageLimits(request_limit, tool_calls_limit, cost_limit) | Raises UsageLimitExceeded; cost is best-effort, so keep request_limit alongside cost_limit |
| Longer budgets | SpendLimits (Harness) | A brake, not a reservation; spans processes only with a shared store; set on_unpriced='raise' |
| Gateway caps | Per project, user and key | Enforced before a request reaches the provider |
| Time | tool_timeout, run timeouts, cancellation | Bound each step and the whole run |
| Concurrency | max_concurrency, ConcurrencyLimitedModel | Concurrency, not requests per minute |
The threat model adds a rule worth enforcing in review: a credential must reach only the service it was issued for, and a provider's base_url is part of that service, so a configurable base_url from an untrusted source can redirect your key. See How to set spend limits on LLM API keys.
Message history, UI adapters and fetching
- Sanitise client history:
sanitize_messagesstrips client-supplied system prompts and instructions, drops non-HTTP file URL schemes and uploaded file references, resets workspace references and model IDs and removes unresolved tool calls. The UI adapters apply the same defaults. - Prefer server-held history: with
manage_message_history='server', only the client's new turn crosses the adapter boundary; you still authorise the conversation ID you load. - Authenticate at the transport: the docs say to run the agent inside your own authenticated route and treat every caller who gets through as able to submit any history.
- Keep the SSRF defaults: local downloads use
safe_download, which blocks private addresses by default and keeps metadata addresses blocked when local URLs are enabled.
For the web layer around the agent, see the FastAPI Security Guide.
Telemetry and data handling
Pydantic AI emits OpenTelemetry traces following the GenAI semantic conventions to Logfire or any OTel backend. Every run carries a run_id and conversation_id, and run metadata such as a tenant or user ID lands on the run span.
- Decide on content: set
include_content=FalseonInstrumentationSettingsto keep prompts, completions and tool arguments out of spans, andinclude_binary_content=Falsefor files. Neither touches run metadata, your own spans or captured HTTP bodies. - Choose where traces go: Logfire runs in a US or EU region, as a dedicated deployment, or self-hosted on Kubernetes; the enterprise docs note the Logfire SDK scrubs likely-sensitive values such as tokens and passwords, but not prompt and response content.
- Review every export channel: the threat model notes that suppressing one content field does not guarantee the content is absent from every event, exception or integration.
See Agent Observability vs Audit Logging.
Where identity fits
Every production agent carries at least two kinds of non-human identity: the provider API key (or Gateway key) that pays for model calls, and the credentials its tools use against databases, SaaS APIs and MCP servers. Issue a separate Gateway or provider key per service and environment with its own cap, give each agent its own scoped identity for downstream calls instead of a shared service account, and prefer per-user delegated tokens passed through deps for MCP and API calls made on a user's behalf. See AI Agent Identity vs Service Account.
Detection and measurement
- Approval coverage: share of write-capable tools behind
requires_approvalorApprovalRequiredToolset(target: all of them). - Usage limit hits:
UsageLimitExceededrate per agent; a spike suggests loops or abuse. - Refused tool calls: authorisation refusals inside tools per tenant, alerting on bursts.
- Credential hygiene: number of provider keys shared across services or environments (target: zero) and age of each key.
- Content exposure: share of instrumented agents with an explicit
include_contentdecision recorded. - Cost per tenant: token and cost metrics by provider and model, compared with caps.
Common failure patterns
- One global toolset for every user: the model sees tools the caller should not, and refusal relies on prompt wording.
- Trusting validated arguments: a schema-valid account ID is still someone else's account; authorise in the tool.
- Passing browser history straight to
message_history: lets a client inject system prompts and fake tool results. - Sharing a fixed-auth MCP toolset across users: one MCP session serves several users.
- Instrumenting with default content capture into a shared backend: prompts and tool arguments with personal data or secrets end up in traces.
- Relying on
cost_limitalone: unpriced models are not counted; keeprequest_limittoo.
Framework mapping
| Risk | OWASP LLM Top 10 2025 | MITRE ATLAS | NIST SP 800-53 Rev 5 | Pydantic AI control |
|---|---|---|---|---|
| Injected content steering tool use | LLM01:2025 Prompt Injection | AML.T0051 | AC-6 | Per-caller toolsets, authorisation in tools, sanitised history |
| Agent taking unapproved actions | LLM06:2025 Excessive Agency | AML.T0053 | AC-6, CM-7 | Deferred tools, approval-required toolsets |
| Secrets and personal data in traces | LLM02:2025 Sensitive Information Disclosure | AU-2 | include_content=False, backend choice | |
| Runaway cost or loops | LLM10:2025 Unbounded Consumption | SC-7 | UsageLimits, SpendLimits, Gateway caps | |
| Leaked or shared provider keys | IA-5 | Secret manager, per-service keys |
In NIST CSF 2.0 terms, per-caller tool scoping sits under PR.AA-05 and trace monitoring under DE.CM-09.
Practitioner checklist
- Read
THREAT_MODEL.mdand map your deployment profile. - Build deps from verified identity in an authenticated route; never from prompt content.
- Filter or build toolsets per caller and authorise every resource inside each tool.
- Put every write or payment tool behind approval, persist paused runs server-side and authorise approvers.
- Use per-user
authfunctions for MCP and load MCP config only from files you control. - Load provider keys from a secret manager, one key per service and environment, with caps.
- Set
UsageLimitswith both request and cost limits, plus timeouts. - Sanitise or server-hold message history and decide
include_contentbefore enabling traces. - Run model-written code in a VM-backed sandbox.
Standards and references
- Pydantic: Going to Production
- Pydantic: Pydantic AI threat model
- Pydantic: Deferred tools and human-in-the-loop approval
- Pydantic: Toolsets
- Pydantic: MCP client
- Pydantic: Messages and chat history
- Pydantic: Pydantic AI for Enterprise
- OWASP: Top 10 for LLM Applications 2025
Related NHI Mgmt Group resources: FastAPI Security Guide, AI Agent Tool and Plugin Security Guide, MCP Security Guide.