c56a4b42f8 Add host-backed OS and filesystem access to CodeMode sandbox (#262)
* feat(code_mode): expose host-backed OS access to the sandbox

Sandboxed `run_code` had no way to reach the filesystem, environment, or
wall clock: Monty supports it through an OS callback / `AbstractOS` and
directory mounts, but `CodeMode` never threaded `os`/`mount` into
`feed_start` or the snapshot resume loop, so callers couldn't enable it.

Add `os` and `mount` options on `CodeMode`/`CodeModeToolset`, thread them
through `feed_start` and every `resume` site (OS auto-dispatch stops the
moment a resume omits them), and make the `run_code` description reflect
whether host-backed access is configured.

* test(code_mode): harden OS-access tests around the threading invariants

Add edge cases that pin the behaviours most likely to regress: OS access
surviving across REPL-persisted `run_code` calls, a raising `os` callback
degrading to `ModelRetry` instead of crashing the loop, and `mount`
accepting a `list[MountDir]`. Hoist the never-invoked callback used by the
description/forwarding assertions into one shared helper.

* docs(code_mode): tighten and verify the filesystem/OS access section

Trim the host-access docs to the essentials and make the example
self-contained (drop the undefined helper). The snippet and the documented
`mount`/callback constructions are run end-to-end to confirm they work.

* docs(code_mode): correct per-request scoping wording

`os`/`mount` are static capability fields (no per-run resolver), so the
"stateful AbstractOS rooted at a per-user directory" guidance over-claimed.
Reword to: build CodeMode per request to scope access. Every other doc line
was re-checked empirically against pydantic-monty 0.0.17.

* fix(code_mode): don't advertise env/clock for mount-only sandboxes

A `mount` only exposes filesystem paths; `os.getenv`/`os.environ` and
`datetime.now()`/`date.today()` still require an `os` handler. The
description used one host-access note for both, so mount-only agents were
told env/clock were routed to the host and would emit calls that fail and
burn run_code retries (verified against pydantic-monty 0.0.17).

Split the description into three states (none / mount-only filesystem / os),
and correct the README and docstrings that conflated the two.

* docs(code_mode): correct two run_code description claims verified against monty

Audited every statement in the run_code description, docstrings, and README
against pydantic-monty 0.0.17. Two were imprecise:
- "imported at the top of your snippet" -- mid-snippet imports work, so the
  rule is just "before use".
- OS-enabled note said calls route "to the host environment", but an
  in-memory AbstractOS (e.g. OSAccess) handles them too -- it's the
  configured OS handler, not necessarily the host.

* docs(code_mode): clarify overlay-mode write persistence and fix wording

The mount docs implied writes reach the host, but MountDir defaults to
copy-on-write overlay mode, so writes stay in the sandbox unless mode is
'read-write'. Also tighten two awkward/redundant doc lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(code_mode): rename public OS/mount surface to be backend-neutral

The public type aliases leaked the Monty backend name into a surface we
can't rename later. Rename them to match the existing CodeMode/CodeModeToolset
convention, and rename the os= parameter to os_access= so it stops shadowing
the stdlib os module that sandboxed code itself uses.

- MontyOS -> CodeModeOS, MontyOSCallback -> CodeModeOSCallback, MontyMount -> CodeModeMount
- CodeMode/CodeModeToolset param os= -> os_access= (mount unchanged)
- internal resume()/feed_start() forwarding keeps Monty's literal os= kwarg

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(code_mode): stop shadowing the os module in the execution loop

The OS/mount threading named its parameter `os`, shadowing the stdlib
module inside the execution-loop helpers. Rename the variable to
`os_access` (matching the public field) while keeping Monty's required
`os=` keyword only at the resume/feed_start call sites. Also inline the
single-use restriction-line helper into `_base_description`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(code_mode): make CodeMode config fields keyword-only

The option list keeps growing; pin tools/max_retries as the only
positional args and force os_access/mount (and future config) to be
passed by name via a KW_ONLY sentinel, so adding options can't silently
shift positional meaning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(code_mode): make os_access/mount docs clear on first read

Public docs should let a reader grasp the host-access surface without
reverse-engineering it. Reframe the docstrings and README around when to
reach for each primitive instead of what is switched off, drop the
type-restating prose the annotations already carry, and lead with concrete
tasks (share a dataset; inject just the secrets the agent needs).

Tighten the os-access test sweep so each test asserts exactly its invariant:
drop redundant negative description asserts (one note is interpolated, so the
positive phrase alone proves selection), drop an assertion already owned by
another test, and type the tmp_path fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(code_mode): clarify os_access callback return semantics

The raw-callback example claimed non-allow-listed keys "stay hidden" by
returning NOT_HANDLED. Verified against Monty: NOT_HANDLED *refuses* the
call (raises in the sandbox -> model retry), it does not return None. A
model probing for an optional secret would crash and burn retries.

Distinguish the two return modes explicitly so users don't pick the
wrong one: return a value (incl. None) to answer/hide, NOT_HANDLED to
refuse a capability outright.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(code_mode): lock in os_access value-vs-NOT_HANDLED semantics

Returning a value (including None) from an os_access callback answers
the call -- a None reads back like an unset env var, so the sandbox
keeps running. Returning NOT_HANDLED refuses the call, raising in the
sandbox and surfacing as ModelRetry. These two paths are easy to
confuse and silently regress, so pin both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* code_mode: warn default run_code prompt about unavailable fs/env; drop Any from CodeModeOSCallback

The PR's dynamic restriction note covered the mount and os-enabled states but not
the default state, which still listed os/pathlib as importable without saying
their I/O fails -- the exact wasted-retry case this change reduces. Extend the
default note to state filesystem, env, and clock calls are unavailable without a
mount or OS handler.

Also type the public CodeModeOSCallback alias with object instead of Any: the
harness only forwards the callback to Monty and never calls it, so object
typechecks cleanly (pyright strict, 0 errors) and keeps Any out of the public API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: David SF <david.sanchez@pydantic.dev>
2026-06-15 11:32:21 -05:00
2026-06-12 16:55:22 +05:30
2026-03-21 00:30:54 -05:00
2026-04-13 18:33:40 +05:30
2026-03-20 15:33:12 -05:00
2026-06-12 16:55:22 +05:30
2026-06-12 16:55:22 +05:30

Pydantic AI Harness

CI PyPI versions license

The batteries for your Pydantic AI agent.


Pydantic AI's capabilities and hooks API is how you give an agent its harness -- bundles of tools, lifecycle hooks, instructions, and model settings that extend what the agent can do without any framework changes.

Pydantic AI Harness is the official capability library for Pydantic AI, maintained by the Pydantic AI team. Pydantic AI core ships capabilities that require model or framework support, and capabilities fundamental to every agent -- web search, tool search, thinking. Everything else lives here: standalone building blocks you pick and choose to turn your agent into a coding agent, a research assistant, or anything else. This is also where new capabilities start -- as they stabilize and prove themselves broadly essential, they can graduate into core.

The capability matrix tracks where we are. Tell us what to prioritize.

Contents: Installation · Quick start · Capability matrix · An ecosystem agent · Help us prioritize · Build your own · Contributing · Version policy · Pydantic AI references · License

Installation

uv add pydantic-ai-harness

Extras for specific capabilities:

uv add "pydantic-ai-harness[codemode]"   # CodeMode (adds the Monty sandbox)
uv add "pydantic-ai-harness[logfire]"     # ManagedPrompt (Logfire-managed prompts)

The code-mode extra is also supported as an alias.

Requires Python 3.10+ and pydantic-ai-slim>=1.95.1.

Quick start

uv add "pydantic-ai-slim[anthropic,mcp,duckduckgo,logfire]" "pydantic-ai-harness[code-mode]"
import logfire
from pydantic_ai import Agent
from pydantic_ai.capabilities import MCP, WebSearch
from pydantic_ai_harness import CodeMode

# See https://ai.pydantic.dev/logfire/ for setup details.
logfire.configure()
logfire.instrument_pydantic_ai()

agent = Agent(
    'anthropic:claude-opus-4-7',
    capabilities=[
        # Wraps every tool into a single run_code tool, sandboxed by Monty
        # (https://github.com/pydantic/monty -- pulled in by the [code-mode] extra).
        # The model writes Python that calls multiple tools with loops, conditionals,
        # asyncio.gather, and local filtering -- one model round-trip for N tool calls.
        CodeMode(),
        # Connect to any MCP server -- here, the open-source Hacker News server
        # (https://github.com/cyanheads/hn-mcp-server). native=False forces the
        # local MCP toolset so CodeMode can wrap the tools; without it,
        # providers that natively support MCP server connectors execute the tools
        # server-side and bypass the sandbox.
        MCP('https://hn.caseyjhand.com/mcp', native=False),
        # Provider-adaptive web search; native=False routes through the local
        # DuckDuckGo fallback (the [duckduckgo] extra above) so CodeMode can batch
        # web searches alongside the HN calls in a single run_code.
        WebSearch(native=False),
    ],
)

result = agent.run_sync(
    "Across the top, best, and 'show HN' Hacker News feeds, find the most-discussed "
    "story with at least 100 points. Pull its comment thread, its submitter's profile, "
    "and any web coverage. Summarize what you find in one paragraph."
)
print(result.output)
"""
The most-discussed HN story across top/best/show clearing 100 points is "Vibe coding
and agentic engineering are getting closer than I'd like" by Simon Willison (748 points,
853 comments, on the Best feed), submitted by long-time HNer e12e. The piece argues
that the two modes Willison once kept mentally separate -- throwaway "vibe coding" and
disciplined "agentic engineering" -- are blurring, since agents like Claude Code now
reliably handle non-trivial tasks like "build a JSON API endpoint that runs a SQL query"
with tests and docs on the first pass. The HN thread is unusually substantive, with
commenters debating whether LLMs created or merely *exposed* sloppy engineering
practices and warning of a "normalization of deviance" as engineers stop reviewing diffs.
"""

Logfire trace from the Quick start run

See this run as a public Logfire trace → Each run_code span fans out into the tool calls the model issued from inside the sandbox -- it's the easiest way to understand what code mode actually did.

Capability matrix

We studied leading coding agents, agent frameworks, and Claw-style assistants to map every capability area that matters for production agents. Each one is tracked as an issue in this repo.

Vote on whatever is linked in the Status column -- PRs if we're actively building it, issues if it's planned -- to help us decide what to work on next.

Category Capability Description Status Community alternatives
Tools & execution Code mode Sandboxed Python execution via Monty -- one run_code call replaces N tool calls Docs
Tool search Progressive tool discovery for large tool sets Pydantic AI
File system Read, write, edit, search files with path traversal prevention Docs pydantic-ai-backend (vstormco)
Shell Execute commands with allowlists, denylists, and timeouts Docs pydantic-ai-backend (vstormco)
Repo context injection Auto-load CLAUDE.md/AGENTS.md and repo structure 🚧 PR #175 pydantic-deep (vstormco)
Verification loop Run tests after edits, auto-fix failures 🚧 PR #169
Context management Sliding window Trim conversation history to stay within token limits 🚧 PR #191 summarization-pydantic-ai (vstormco)
Context compaction LLM-powered summarization of older messages 🚧 PR #191 summarization-pydantic-ai (vstormco)
Limit warnings Warn agent before hitting context/iteration limits 🚧 PR #191 summarization-pydantic-ai (vstormco)
Tool output management Truncate, summarize, or spill large tool outputs 🚧 PR #185
System reminders Inject periodic reminders to counteract instruction drift 🚧 PR #181
Memory & persistence Memory Persistent key-value memory across sessions 🚧 PR #179 pydantic-deep (vstormco)
Session persistence Save and restore full conversation state 🚧 PR #176
Checkpointing Save, rewind, and fork conversation state 📝 #196 pydantic-deep (vstormco)
Agent orchestration Sub-agents Delegate subtasks to specialized child agents 🚧 PR #178 subagents-pydantic-ai (vstormco)
Skills Progressive tool loading -- search, activate, deactivate 🚧 PR #183 pydantic-ai-skills (DougTrajano), pydantic-deep (vstormco)
Planning Break complex tasks into structured plans before execution 🚧 PR #180
Task tracking Track tasks, subtasks, and dependencies 📝 #65 pydantic-ai-todo (vstormco)
Teams Multi-agent teams with shared state and message bus 📝 #195 pydantic-deep (vstormco)
Safety & guardrails Input guardrails Validate user input before the agent run starts 🚧 PR #182 pydantic-ai-shields (vstormco)
Output guardrails Validate model output after the run completes 🚧 PR #182 pydantic-ai-shields (vstormco)
Cost/token budgets Enforce token and cost limits per run 🚧 PR #182 pydantic-ai-shields (vstormco)
Tool access control Block tools or require approval before execution 🚧 PR #182 pydantic-ai-shields (vstormco)
Async guardrails Run validation concurrently with model requests 🚧 PR #182 pydantic-ai-shields (vstormco)
Secret masking Detect and redact secrets in agent I/O 🚧 PR #172 pydantic-ai-shields (vstormco)
Approval workflows Require human approval for sensitive operations 🚧 PR #173 Pydantic AI (builtin)
Tool budget Limit total tool calls or cost per run 🚧 PR #168
Reliability Stuck loop detection Detect and break out of repetitive agent loops 🚧 PR #186
Tool error recovery Retry failed tool calls with backoff and budget 🚧 PR #171
Tool orphan repair Fix orphaned tool calls in conversation history 🚧 PR #184
Reasoning Adaptive reasoning Adjust thinking effort based on task complexity 🚧 PR #174
Current time Inject current date/time into system prompt 🚧 PR #170

Packages by vstorm-co are endorsed by the Pydantic AI team. We're working with them to upstream some of their implementations into this repo.

An ecosystem agent

The Quick start above is deliberately small. Here's the other end of the spectrum -- an agent wired up with capabilities drawn from across the Pydantic AI ecosystem: this repo, core pydantic-ai, and the community packages we vouch for in the matrix above.

import logfire
from pydantic_ai import Agent
from pydantic_ai.capabilities import MCP, Thinking, ToolSearch, WebSearch
from pydantic_ai_harness import CodeMode

# Community packages, alphabetical:
from pydantic_ai_backends import ConsoleCapability
from pydantic_ai_shields import CostTracking, InputGuard, SecretRedaction, ToolGuard
from pydantic_ai_skills import SkillsCapability
from pydantic_ai_summarization import ContextManagerCapability
from pydantic_ai_todo import TodoCapability
from pydantic_deep import MemoryCapability, StuckLoopDetection
from subagents_pydantic_ai import SubAgentCapability, SubAgentConfig

# See https://ai.pydantic.dev/logfire/ for setup details.
logfire.configure()
logfire.instrument_pydantic_ai()

agent = Agent(
    'anthropic:claude-opus-4-7',
    capabilities=[
        # --- Tool execution & discovery ---
        # Wraps every tool into a single run_code, sandboxed by Monty.
        CodeMode(),

        # Progressive tool discovery for large tool sets; discovered tools fold into run_code.
        ToolSearch(),

        # --- Reasoning ---
        # Provider-adaptive thinking; uses native extended thinking on supporting models.
        Thinking(effort='xhigh'),

        # --- Context management ---
        # Sliding window + LLM compaction. By @vstorm-co:
        # https://github.com/vstorm-co/summarization-pydantic-ai
        # Pydantic AI also ships `AnthropicCompaction` and `OpenAICompaction` for
        # provider-native compaction.
        ContextManagerCapability(max_tokens=180_000),

        # --- Tools ---
        # Connect to any MCP server -- here, the open-source Hacker News server
        # (https://github.com/cyanheads/hn-mcp-server).
        MCP('https://hn.caseyjhand.com/mcp'),

        # Provider-adaptive web search; falls back to a local DuckDuckGo implementation.
        WebSearch(),

        # Filesystem + shell. By @vstorm-co: https://github.com/vstorm-co/pydantic-ai-backend
        ConsoleCapability(),

        # --- Memory & persistence ---
        # Persistent ./MEMORY.md per agent name. By @vstorm-co:
        # https://github.com/vstorm-co/pydantic-deepagents
        MemoryCapability(agent_name='harness-example'),

        # --- Orchestration ---
        # Agent skills (Anthropic's spec) by @DougTrajano:
        # https://github.com/DougTrajano/pydantic-ai-skills
        # @vstorm-co's pydantic-deep also offers skills loading; the two have different
        # spec footprints (Doug's is closer to programmatic skills).
        SkillsCapability(directories=['./skills']),

        # Spawn sub-agents with their own toolsets and instructions. By @vstorm-co:
        # https://github.com/vstorm-co/subagents-pydantic-ai
        SubAgentCapability(subagents=[
            SubAgentConfig(
                name='researcher',
                description='Deep research on a topic',
                instructions='You are a thorough research assistant.',
            ),
        ]),

        # Track tasks and subtasks; in-memory by default, AsyncPostgresStorage available.
        # By @vstorm-co: https://github.com/vstorm-co/pydantic-ai-todo
        TodoCapability(enable_subtasks=True),

        # --- Safety & reliability ---
        # The next four are by @vstorm-co: https://github.com/vstorm-co/pydantic-ai-shields
        # Per-run cost cap with a callback hook.
        CostTracking(budget_usd=5.0),

        # Reject prompts that look like prompt-injection attempts.
        InputGuard(guard=lambda p: 'ignore previous instructions' not in p.lower()),

        # Block or require approval per tool name.
        ToolGuard(blocked=['rm'], require_approval=['write_file']),

        # Detect API keys/tokens in tool I/O and redact before they reach the model.
        SecretRedaction(),

        # Bail out if the agent gets stuck calling the same tools in a loop.
        # By @vstorm-co: https://github.com/vstorm-co/pydantic-deepagents
        StuckLoopDetection(),
    ],
)

This snippet is illustrative, not literally copy-pasteable: a few capabilities have setup requirements (a ./skills directory, a Postgres database for TodoCapability's persistent storage), and the community packages move independently of this one. The capability matrix tracks each one's status. As the harness ships first-party versions, the imports above will collapse onto fewer packages -- but the example will keep working, since the API surface is the same.

Help us prioritize

Vote on whatever is linked in the Status column above. If there's a PR, vote on the PR -- it means we're actively building it. If there's only an issue, vote on the issue.

Want something that's not on the list? Open a capability request.

Build your own

Capabilities are the primary extension point for Pydantic AI. Any of the existing capabilities in this repo can serve as a reference for building your own.

Publishing as a standalone package? Use the pydantic-ai-<name> naming convention. See Publishing capability packages.

Contributing

We welcome capability contributions. Here's how:

  1. Start with an issue. Open a capability request describing the behavior you want. This lets us discuss the approach and priority before code is written -- we can close an approach without closing the problem.
  2. Then open a PR. Once the issue exists, you're welcome to open a PR with an implementation. Link the issue in your PR. We review based on community interest -- upvotes on both the issue and PR count.
  3. Don't chase green CI. Get the approach working, then let us know. We'll take it from there -- we may push to your branch, rewrite, or open a follow-up PR. You'll be credited as the original author. (See the Pydantic AI contributing guide.)

Note

: PRs that modify pyproject.toml or uv.lock from non-team members are auto-closed by CI to prevent supply chain risk. If you need a new dependency, open an issue.

Development

make install   # install dependencies
make format    # ruff format
make lint      # ruff check
make typecheck # pyright strict
make test      # pytest
make testcov   # pytest with 100% branch coverage

Version policy

Pydantic AI Harness uses 0.x versioning to signal that APIs are still stabilizing. During 0.x:

  • Minor releases (0.1 → 0.2) may include breaking changes -- renamed parameters, changed defaults, restructured APIs. As the library grows, especially as capabilities gain provider-native support (starting as a local implementation, then auto-switching to the provider's built-in API when available), we may need to reshape APIs we couldn't fully anticipate in the initial design.
  • Patch releases (0.1.0 → 0.1.1) will not intentionally break existing behavior.
  • All breaking changes are documented in release notes with migration guidance.
  • Where practical, we'll keep the previous behavior available under a deprecated name or configuration option before removing it.

This is why Pydantic AI Harness is a separate package from Pydantic AI, which has a stricter version policy. As the core capabilities stabilize, we'll move toward 1.0 with stability guarantees to match.

Pydantic AI references

  • Capabilities -- what capabilities are, built-in capabilities, building your own
  • Hooks -- lifecycle hooks reference, ordering, error handling
  • Extensibility -- publishing packages, third-party ecosystem
  • Toolsets -- building tools for capabilities
  • API reference -- full API docs

License

MIT -- see LICENSE.

S
Description
No description provided
Readme MIT
8 MiB
Languages
Python 99.9%