Pydantic AI capability¶
VshCapability gives a Pydantic AI agent a capability-rooted filesystem without
giving the model a host shell, a host filesystem mount, subprocess access, or a
second simulation engine. Eleven agent tools enter the same Rust-owned VSH runtime
used by the Python and Rust SDKs.
Use this page as the integration reference. For a guided build, continue with either deterministic review or the evidence-first LLM judge.
Choose the control layer¶
| Requirement | Configuration | Who may authorize commit? |
|---|---|---|
| Native policy is enough | VshCapability(workspace, policy=...) |
Native VSH policy |
| Exact application rules | hook_handler=my_handler |
Your deterministic Python code |
| Semantic review of complete evidence | hook_handler=judge.hook_handler |
Explicitly configured CommitJudge |
| Human or external workflow | Return HookDecision.review(...) |
Your later authenticated approval path |
A hook does not replace native policy. Hard-denied operations never become approvable, and every commit still passes capability, transaction, single-use, dependency, and stale-state checks.
Install and attach¶
Install the optional integration. The published distribution is named vsh-python;
imports remain under vsh.
The extra pins pydantic-ai-slim==2.40.0. Install the provider package required by
your model separately and configure its credentials through that provider. Importing
base vsh does not load Pydantic AI; only vsh.pydantic_ai requires the extra.
This complete application gives one agent a balanced-policy workspace:
import asyncio
import os
from pydantic_ai import Agent
from vsh.pydantic_ai import VshCapability
async def main() -> None:
filesystem = VshCapability(
"/srv/my-project",
policy="balanced",
)
agent = Agent(
os.environ["PYDANTIC_AI_MODEL"],
instructions=(
"Use only the supplied VSH tools for workspace files. Make the smallest "
"change that satisfies the request. If VSH returns pending_approval, report "
"its transaction and feedback instead of claiming success."
),
capabilities=[filesystem],
)
result = await agent.run("Set timeout_seconds to 30 in config/service.toml.")
print(result.output)
asyncio.run(main())
VshCapability(workspace, ...) constructs and owns its runtime. There is deliberately
no VshCapability.open(...). Runtime.open(...) remains the native SDK entry point
when an application is not using the Pydantic AI capability.
Execution model¶
Pydantic AI agent
│ tool call
▼
VshCapability ── one RunRequest ──► native VSH runtime
│
├─ immutable base snapshot
├─ Monty + virtual filesystem
├─ canonical diff + policy
├─ optional commit hook
└─ revalidate + commit, or keep pending
▲
└──────────── VshToolResult ────────────────────────────────
Each ordinary tool is one transaction. VSH starts from a fresh host snapshot, runs the operation against the virtual filesystem, freezes the resulting evidence, evaluates policy, and commits only when authorized. Tool registration is sequential, preventing parallel calls from the same model turn from creating avoidable same-workspace races. Separate agents or capabilities can still race; native stale checks protect commit.
Constructor reference¶
VshCapability(
workspace,
*,
data_directory=None,
policy="balanced",
worker_path=None,
hook_handler=None,
hook_scope=HookScope.REVIEW_REQUIRED,
hook_id="vsh.pydantic-ai",
review_content_bytes=0,
id="vsh",
defer_loading=False,
)
| Parameter | Meaning |
|---|---|
workspace |
Existing host directory exposed inside Monty as /workspace |
data_directory |
Trusted VSH state location; it must not overlap the workspace |
policy |
Native transaction profile, normally balanced or strict |
worker_path |
Explicit matching Monty worker, mainly useful from a source checkout |
hook_handler |
Sync or async application-owned RequestEvent -> HookDecision handler |
hook_scope |
Review only policy-pending work, or every non-denied request |
hook_id |
Stable identity bound into hook evidence and approvals |
review_content_bytes |
Native byte budget for immutable before/after/read content |
id |
Pydantic AI capability and toolset identifier |
defer_loading |
Defer toolset loading through Pydantic AI's capability mechanism |
Use a new hook_id when the meaning of your review rules changes. This prevents audit
records from making two materially different review policies look identical.
Tool reference¶
All paths are capability-rooted Monty paths. Prefer /workspace/... in prompts and
tool calls; never put a host absolute path into model instructions.
| Tool | Signature | Use it for |
|---|---|---|
vsh_read |
(path) |
One UTF-8 file |
vsh_write |
(path, data, append=False) |
Create, replace, or append text |
vsh_list |
(path="/workspace") |
One directory listing |
vsh_mkdir |
(path, parents=True, exist_ok=True) |
Directory creation |
vsh_remove |
(path, recursive=False, missing_ok=False) |
File or bounded tree removal |
vsh_move |
(source, destination) |
One virtual rename/move |
vsh_copy |
(source, destination, recursive=False, overwrite=False) |
File or bounded tree copy |
vsh_glob |
(pattern, path="/workspace", max_results=1000) |
Bounded path matching |
vsh_search |
(query, path="/workspace", case_sensitive=True, max_results=100) |
Bounded literal text search |
vsh_patch |
(path, old, new, count=1) |
Exact text replacement |
vsh_run |
(code, intent) |
Dependent work in one atomic transaction |
One tool or one compound transaction?¶
Use ordinary tools when operations are independent. This is clear and gives the agent small structured results:
await filesystem.vsh_read("/workspace/config/service.toml")
await filesystem.vsh_patch(
"/workspace/config/service.toml",
"timeout_seconds = 10",
"timeout_seconds = 30",
)
Use vsh_run when a later step depends on an earlier virtual result, or when several
changes must be reviewed and committed together:
result = await filesystem.vsh_run(
"""
config = vsh_read('/workspace/config/service.toml')
if 'require_auth = true' not in config:
raise ValueError('authentication invariant is missing')
vsh_patch(
'/workspace/config/service.toml',
'timeout_seconds = 10',
'timeout_seconds = 30',
)
vsh_write('/workspace/config/change-note.txt', 'timeout: 10 -> 30\n')
""",
intent="Raise the service timeout while preserving authentication.",
)
Code passed to vsh_run is Python syntax executed by Monty, but it can call only the
ten listed vsh_* filesystem functions. read_file and write_file do not exist.
Pass normal Python arguments—do not pass one JSON object as a positional argument.
The intent is useful context for policy and review, never authority or proof.
Agent-visible results¶
Every tool returns a VshToolResult:
transaction: str
state: str
result: JSON-compatible value
changed_paths: int
hook_verdict: str | None
feedback: str | None
requires_review: bool
| State | Host effect | result |
What the agent should do |
|---|---|---|---|
committed |
Exact transaction committed and verified | Available | Report completion |
pending_approval |
No proposed mutation committed | None |
Report transaction and feedback; revise or hand off |
rejected |
No proposed mutation committed | None |
Report rejection; do not retry unchanged work |
denied |
Native policy/capability refused execution | None |
Explain the denial; change the request or configuration |
result.requires_review is true only for pending_approval. Monty paths become
strings, tuples become arrays, mappings are normalized recursively, and bytes use
{"encoding": "base64", "data": "..."}.
Guest results are withheld until commit when a hook is attached. Review feedback can therefore describe a simulated read or write without releasing its guest return value. This does not erase reads already performed inside the isolated virtual execution.
Give the main agent an explicit response rule:
agent = Agent(
model,
instructions=(
"When a VSH result is pending_approval, do not say the task is complete. "
"Explain the feedback, preserve the transaction ID, and propose the smallest "
"corrected transaction."
),
capabilities=[filesystem],
)
Deterministic hooks¶
Application code can decide from immutable native evidence:
from vsh import HookDecision, HookScope, RequestEvent
from vsh.pydantic_ai import VshCapability
def review(event: RequestEvent) -> HookDecision:
if not event.evidence_complete or event.evidence_truncated:
return HookDecision.review("Complete evidence is required.")
if event.deleted_paths:
return HookDecision.review("Deletion requires a second reviewer.")
if event.touched_paths > 4:
return HookDecision.review("Split this into at most four changed paths.")
return HookDecision.follow_policy()
filesystem = VshCapability(
"/srv/my-project",
policy="balanced",
hook_handler=review,
hook_scope=HookScope.ALL_REQUESTS,
hook_id="bounded-change-review-v1",
)
follow_policy() preserves the native result: auto-approved work commits, while
policy-pending work stays pending. approve(...) authorizes the exact complete
transaction; review(...) keeps it pending with feedback; reject(...) makes it
terminal. See the deterministic tutorial
for an exact-content allowlist.
LLM judge hooks¶
An LLM judge is opt-in. It is an explicit hook object, not an implicit property of the capability:
from vsh.pydantic_ai import CommitJudge, VshCapability
judge = CommitJudge(
model=judge_model,
review_instructions=(
"Authentication must remain enabled. Approve a timeout-only edit only when "
"the complete after-content proves require_auth = true."
),
content_filter=lambda path: path == "config/service.toml",
)
filesystem = VshCapability(
"/srv/my-project",
policy="strict",
hook_handler=judge.hook_handler,
hook_id="service-config-judge-v1",
review_content_bytes=64 * 1024,
)
review_instructions extends VSH's fixed evidence-first instruction set; it does not
replace it. content_filter is separate from native capture: both the native byte
budget and explicit host permission must cover every required path before a model call
is made. Continue with the judge tutorial and
judge reference.
Runtime, concurrency, and cleanup¶
- Keep one capability per workspace/configuration instead of constructing one per tool call; it owns runtime stores and a supervised worker pool.
- Tool calls contributed by one capability are sequential. Bound concurrent agent runs at the application level as well.
- Independent capabilities targeting the same workspace can preview concurrently, but commits revalidate host identity and dependencies and may raise a stale conflict.
- The runtime's trusted data directory must remain outside the untrusted workspace.
- From a source checkout, build the matching worker with
cargo build --release --locked -p vsh-monty-workerand passworker_pathif package discovery is unavailable.
Troubleshooting map¶
| Symptom | Likely cause | Action |
|---|---|---|
Tool says pending_approval |
Policy or hook withheld approval | Surface feedback; do not claim completion |
| Judge never runs | Default scope ignores auto-approved work | Use strict policy or deliberately select ALL_REQUESTS |
| Judge reports incomplete evidence | Content capture/filter/budget is insufficient | Check review_content_bytes, content_filter, file type, and size |
vsh_run reports an unknown function |
Model invented a non-VSH helper | Use the exact vsh_* names and Python call syntax above |
| Commit becomes stale | Host changed after preview | Produce and review a new transaction; never reuse the old approval |
| Worker cannot start | Matching packaged/source worker is unavailable | Build it and set worker_path explicitly |
For lower-level lifecycle details, see commit hooks, policies and budgets, and the security model.