pydantic-ai-apcore
pydantic-ai integration for apcore.
Works in both directions:
ApcoreToolset— expose governed apcore modules to a pydantic-ai agentregister_toolset()— register the tools an agent already has as apcore modules, so they gain schema validation, ACL, approval gates, and audit, and can then be served over MCP, CLI, or A2A without being rewritten
Features
- AbstractToolset implementation — drop-in toolset for pydantic-ai agents
- Schema passthrough — apcore
input_schemamapped directly toToolDefinition.parameters_json_schema - Annotation mapping —
destructive,requires_approval,readonly,idempotent,open_world,streamingannotations surfaced as tool metadata and description warnings - Include/exclude filtering — glob-style patterns to select which apcore modules to expose
- Approval gating — modules with
requires_approval=Trueemitkind="unapproved"for pydantic-ai's human-in-the-loop flow - Per-call caller identity — resolve the calling principal from the agent run context so apcore ACL rules can tell agents apart
- Provider-safe tool names — dotted module IDs are translated to names the major model providers accept
- Reverse registration — existing pydantic-ai tools become governed apcore modules, ready for MCP/CLI/A2A
Requirements
- Python >= 3.10
- apcore >= 0.26.0
- apcore-toolkit >= 0.10.0
- pydantic-ai-slim >= 1.32.0
Both lower bounds are the earliest versions the test suite actually passes on, not the earliest version that happens to expose the APIs used here:
| Verified | Fails on | Why | |
|---|---|---|---|
| apcore | 0.26.0 – 0.27.0 | 0.12.0 | the code itself works from 0.13.0, where cacheable / paginated arrive; the effective floor is raised to 0.26.0 by apcore-toolkit |
| apcore-toolkit | 0.6.0 – 0.10.1 | — | floor kept at 0.10.0 to match the other *-apcore integrations, so installing several together cannot conflict |
| pydantic-ai-slim | 1.32.0 – 2.34.0 | 1.31.0 | earlier releases pin an OpenTelemetry API that no longer resolves |
ToolDefinition.sequential needs 1.0.10 and kind="unapproved" needs 1.0.0, so
the pydantic-ai floor is set by installability rather than by API surface.
Both major lines are verified: the test suite passes on 1.32.0, 1.66.0,
2.0.0, and 2.34.0.
Installation
pip install pydantic-ai-apcore
# Optionally project registered modules onto other surfaces
pip install "pydantic-ai-apcore[mcp]" # serve them as an MCP server
pip install "pydantic-ai-apcore[cli]" # expose them on the command line
pip install "pydantic-ai-apcore[all]"
Quick Start
from apcore import Registry, Executor, module
from pydantic_ai import Agent
from pydantic_ai_apcore import ApcoreToolset
# 1. Set up apcore registry and modules
registry = Registry()
executor = Executor(registry=registry)
@module(id="math.add", registry=registry)
def add(a: int, b: int) -> dict:
"""Add two numbers."""
return {"result": a + b}
# 2. Create agent with ApcoreToolset
agent = Agent(
"openai:gpt-4o",
toolsets=[ApcoreToolset(registry, executor)],
)
# 3. Run
result = agent.run_sync("What is 3 + 5?")
print(result.output)
API
ApcoreToolset
ApcoreToolset(
registry: Registry,
executor: Executor,
*,
identity: Identity | None = None,
identity_resolver: Callable[[RunContext[AgentDepsT]], Identity | None] | None = None,
include: list[str] | None = None,
exclude: list[str] | None = None,
toolset_id: str | None = None,
max_retries: int = 1,
sanitize_tool_names: bool = True,
)
| Parameter | Description |
|---|---|
registry |
apcore Registry containing discovered modules |
executor |
apcore Executor for running modules |
identity |
Fixed Identity used for every call — suitable when the whole agent runs as one principal |
identity_resolver |
Derives the Identity per call from the run context; takes precedence over identity |
include |
Glob-style patterns to include (e.g. ["api.*"]) |
exclude |
Glob-style patterns to exclude (e.g. ["internal.*"]) |
toolset_id |
Optional unique ID for this toolset instance |
max_retries |
Retries pydantic-ai allows per tool call |
sanitize_tool_names |
Translate dotted module IDs into provider-safe tool names |
Caller identity and ACL
apcore matches ACL callers patterns against Context.call_chain[-1], and
conditions.roles against the Context identity. This toolset writes the
resolved identity to both, so a rule like the following matches as written:
ACLRule(
callers=["agent.*"],
targets=["crm.delete"],
effect="allow",
conditions={"roles": ["data_admin"]},
)
Use identity_resolver when one toolset serves several principals — typically
reading whatever the surrounding application already knows from ctx.deps:
@dataclass
class AgentIdentity:
caller_id: str
roles: tuple[str, ...]
toolset = ApcoreToolset(
registry, executor,
identity_resolver=lambda ctx: Identity(
id=ctx.deps.caller_id, type="ai", roles=ctx.deps.roles
),
)
With neither identity nor identity_resolver, calls reach apcore without a
caller, and a default-deny ACL will reject them.
Tool naming
apcore module IDs are dotted by convention (executor.crm.read), but the major
model providers accept only [a-zA-Z0-9_-] in a tool name. Names are therefore
translated (executor_crm_read) before being offered to a model, and the
original module ID travels in ToolDefinition.metadata["apcore_module_id"] so
execution still targets the right module. Two IDs that translate to the same
name get a numeric suffix. Pass sanitize_tool_names=False to offer IDs
verbatim.
Filtering
# Only expose api.* modules, but not api.internal
toolset = ApcoreToolset(
registry, executor,
include=["api.*"],
exclude=["api.internal"],
)
Exclude takes precedence over include.
Annotation Handling
apcore ModuleAnnotations are mapped to pydantic-ai as follows:
| Annotation | Effect |
|---|---|
destructive=True |
Adds WARNING: DESTRUCTIVE to the description, and sets sequential=True so the call does not overlap with other tool calls |
requires_approval=True |
Sets kind="unapproved" + adds REQUIRES APPROVAL to description |
readonly, idempotent, open_world, streaming, cacheable, paginated |
Surfaced in ToolDefinition.metadata |
discoverable=False |
Excluded — Registry.list() omits these modules |
Other descriptor fields also reach the model: documentation and up to three
examples are appended to the description, and a sunset_date becomes a
DEPRECATED notice. version and tags travel in metadata.
Registering existing tools
The reverse direction: take the tools an agent already has and put them behind the apcore execution boundary.
from apcore import Registry
from pydantic_ai.toolsets.function import FunctionToolset
from pydantic_ai_apcore import register_toolset
toolset = FunctionToolset()
def send_invoice(customer_id: str, amount: float) -> dict:
"""Send an invoice to a customer.
Args:
customer_id: Who to bill.
amount: How much, in USD.
"""
return {"customer_id": customer_id, "sent": True}
toolset.add_function(send_invoice, takes_ctx=False)
registry = Registry()
result = register_toolset(toolset, registry, prefix="billing.")
# result.registered -> ["billing.send_invoice"]
add_function(takes_ctx=...)behaves identically on pydantic-ai 1.x and 2.x. The.tooldecorator does not: 2.0 changed it to require aRunContextfirst parameter and added.tool_plainfor functions that take none. Either decorator works if you only target one major version;add_functionworks on both.
Schemas are derived by apcore from each function's signature and docstring, the
same way a hand-written module would be, so the results are not second-class. A
tool's requires_approval flag carries over to the apcore annotation of the
same name — approval semantics survive the trip in both directions.
Tools that take a RunContext are skipped, and reported in
result.skipped with the reason. apcore invokes a module without a pydantic-ai
run context, and passing None would hand the function a broken context rather
than fail honestly.
Metadata gaps get flagged
pydantic-ai does not require parameter descriptions
(require_parameter_descriptions defaults to False), so a tool can reach a
model with nothing but parameter names. Scanning produces
apcore_toolkit.ScannedModule values whose warnings say where that happened:
for m in result.modules:
for w in m.warnings:
print(m.module_id, w)
# undocumented has no description for: x, y. A model sees only the parameter names.
Pass enhancers= to fill the gaps before registration — for instance
apcore_toolkit.AIEnhancer, which uses a local SLM and is off unless
APCORE_AI_ENABLED is set:
from apcore_toolkit import AIEnhancer
register_toolset(toolset, registry, enhancers=[AIEnhancer()])
Closures register on equal footing
Registration deliberately does not go through apcore_toolkit.RegistryWriter.
That writer resolves a callable from ScannedModule.target by import path,
which cannot reach a tool defined inside a function — a common shape in
pydantic-ai, where tools close over dependencies. The callables are held from
the scan instead, so such tools register and execute normally; their
target is still recorded, with a warning that it is not importable.
Once registered, they are ordinary apcore modules — anything that serves a registry can serve them:
from pydantic_ai_apcore import serve_mcp # needs the [mcp] extra
serve_mcp(registry)
serve_mcp / serve_cli are thin forwards to apcore-mcp and apcore-cli,
kept behind extras so the base install stays minimal. Calling one without its
extra installed raises an ImportError naming the command to fix it.
Two approval checkpoints
requires_approval produces a tool the agent run suspends on, returning
DeferredToolRequests. That is pydantic-ai asking whether the model may make
the call at all. The apcore approval gate is separate and runs later, once the
sanctioned call reaches the execution pipeline — the two are in series, not
duplicated. See examples/governance_comparison.py,
which demonstrates the ordering and runs offline without a model provider.
Examples
Each demo is a self-contained directory with its own README.md and acl.yaml,
following the same layout as the other *-apcore integrations. All of them run
offline — FunctionModel drives the tool calls, so no model provider or API key
is involved.
| Demo | Shows |
|---|---|
acl_demo |
ACL rules loaded from YAML via APCORE_ACL_PATH, and how identity_resolver gets the caller into the fields those rules match against. |
approval_demo |
The two approval checkpoints — pydantic-ai's and apcore's — and the order they fire in. |
scanner_demo |
The reverse direction: existing agent tools registered as governed apcore modules, then denied by ACL and gated on approval. |
python examples/acl_demo/app.py
python examples/approval_demo/app.py
python examples/scanner_demo/app.py
Each demo's ACL lives in its own acl.yaml, so the policy can be changed and
re-run without touching code:
default_effect: deny
rules:
- description: Only data-admin agents may delete
callers: ["agent.*"] # matched against Context.call_chain[-1]
targets: ["crm.delete"] # module-id glob patterns
effect: allow
conditions:
roles: ["data_admin"]
Development
git clone https://github.com/aiperceivable/pydantic-ai-apcore.git
cd pydantic-ai-apcore
pip install -e ".[dev]"
pytest
License
Apache-2.0
Metadata
Release files for pydantic-ai-apcore 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pydantic_ai_apcore-0.1.0.tar.gz | 28.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pydantic_ai_apcore-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.2 kB
Release files / pydantic_ai_apcore-0.1.0.tar.gz
| Download URL | pydantic_ai_apcore-0.1.0.tar.gz |
|---|---|
| Size | 28.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0a0a4a1c7d8134978e7036508cb5cf24863fd7c0f5a3b011722c91c9b27de65b
|
|
BLAKE2b-256 checksum How to use checksums |
f559699ef0df6493c46b8a570cd37d77eff698468cf1d5b4d72269a59ab16afa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / pydantic_ai_apcore-0.1.0-py3-none-any.whl
| Download URL | pydantic_ai_apcore-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2cddfcfc21a0a71630bf14d2b2c0b114ca6bacb038c315954533c43bfb86e694
|
|
BLAKE2b-256 checksum How to use checksums |
3d6efc14294211e0044e8013824197f4660355e17d8153ba1a46588047fcf647
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Security for pydantic-ai-apcore 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.