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.
Development checkout
The run_only, response-budget, snapshot-limit and pre-commit JSON validation
additions below require this checkout; they have not been released in 0.6.0.
Set VshCapability(workspace, run_only=True) for a program-first, single-agent-tool
surface. The ten filesystem functions remain available inside Monty programs; only
their separate model-facing schemas are omitted. The default remains eleven tools.
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¶
The Bash options on this page are included in VSH 0.6.0. Follow the Bash setup to enable them on Linux/macOS.
Optional Unix Bash uses VshCapability(workspace, bash=BashConfig(...)). The same
vsh_run(code, intent, language="bash") tool enters the shared native transaction
pipeline; existing filesystem tools remain Monty-based. The language enum advertises
only host-enabled frontends. Pending/rejected calls return feedback but withhold
their result and both streams from the main agent. See bounded Bash.
With this opt-in, Monty programs can also call vsh_bash(code) between filesystem
operations. They share one snapshot, one canonical diff and one review/commit boundary.
The capability validates agent_json result compatibility in Rust before commit.
Unsupported sets, non-string dictionary keys and non-finite floats fail without host
mutation. Convert intentionally inside your program, for example sorted(my_set).
Ordinary Python SDK callers retain their richer native result types.
Agent responses default to a 16 KiB compact ASCII-JSON budget; set the host-only
max_response_bytes constructor option to adjust it (minimum 4096). A response with
response_truncated=true omits oversized result/stream display data but preserves the
actual transaction state. It does not mean a committed operation failed. Return
summaries and selected slices rather than whole files. Bash streams occur once as
base64 objects inside result, not again in top-level text fields. snapshot_limits
can separately constrain host capture; presentation limits never discard judge evidence.
| 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.52.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 or enabled Bash + 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,
bash=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 |
bash |
Optional BashConfig; explicitly enables the bounded Unix Bash frontend |
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, language="monty") |
Dependent work in one atomic transaction; Bash only when host-enabled |
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.",
)
By default (language="monty"), vsh_run accepts Python syntax executed by Monty;
its filesystem functions are the ten listed vsh_* calls. read_file and write_file
do not exist. Pass normal Python arguments, not one JSON positional object.
When the host opts into Bash, language="bash" accepts bounded shell source instead;
see the Bash profile and guided workflow. Both languages use the same commit gate.
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
language: str
stdout: str
stderr: str
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 and both streams are withheld for every noncommitted result, whether or not 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.
For committed Bash work, result contains exit_code, profile, and base64-wrapped
stdout/stderr bytes. The top-level string streams are convenient display text,
not authoritative binary evidence; use the encoded result streams for exact bytes.
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.