Usage Guide¶
Requirements¶
To use the container-backed Sandbox (third-party packages, full Python features), you need either Podman or Docker installed and running. The sandbox auto-detects which runtime is available. Monty-only usage (limited stdlib modules plus external functions) comes built-in.
How the sandbox works¶
Backyard provides two backends that both maintain persistent state across code executions:
| Backend | Execution | Isolation | Speed | Supports |
|---|---|---|---|---|
| Monty | Rust-based Python interpreter | Resource limits (memory, timeout, recursion), opt-in filesystem mounting | ~5ms per call | limited set of stdlib modules (asyncio, dataclasses, datetime, json, math, os, pathlib, re, sys, typing), basic Python syntax (variables, functions, loops, etc.), any arbitrary Python function via external_functions |
| Container | Out-of-process (Docker/Podman) | Resource limits (CPU, memory, timeout), opt-in filesystem mounting | ~500ms per call | Any Python package, any feature |
The Sandbox class starts with Monty by default and transparently converts to a container when the code requires features Monty cannot provide, automatically restoring state by replaying code-execution history.
Configuration¶
The sandbox configuration can be set at initialization and updated later if needed (e.g., increasing memory limits on a container sandbox to accommodate data analysis libraries; allowing write access; adding files to the workspace).
from backyard import Sandbox
sandbox = Sandbox(
max_duration_sec=30.0, # Max execution time per call
max_memory_mb=256, # Max memory (both backends respect this)
max_cpus=2.0, # Max CPUs (container only)
enable_network=False, # Network access (container only)
enable_package_cache=True, # Cache PyPI downloads (container only)
tmpfs_size_mb=16, # Size of in-memory tmpfs for /tmp (container only, 0 to disable)
workspace=["/path/to/dir"], # Directories and/or files to mount
sandbox_type="auto", # "auto" (default), "container", or "monty"
readonly=False, # If True, start in read-only mode.
)
Integrating as a tool for an AI agent¶
Option A: Python API (recommended for agent harnesses)¶
from backyard import Sandbox
class CodeExecutionTool:
def __init__(self):
self.sandbox = Sandbox(
max_duration_sec=30.0,
readonly=True, # Agent must request write access
track_file_changes=True,
)
def run_code(self, code: str) -> dict:
result = self.sandbox.run(code)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"success": result.success,
"result": result.result,
"file_changes": result.file_changes,
"duration_sec": result.duration_sec,
}
def run_with_packages(self, code: str, packages: list[str]) -> dict:
result = self.sandbox.run(code, dependencies=packages)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"success": result.success,
}
def reset(self):
self.sandbox.reset_session()
def close(self):
self.sandbox.close()
Option B: Provide external functions¶
Inject Python functions into the sandbox namespace:
from backyard import Sandbox
def fetch_data(url: str) -> str:
import urllib.request
with urllib.request.urlopen(url) as response:
return response.read().decode()
# This works in the Monty sandbox, too! enable_network is not strictly required,
# since the external_functions execute in the host's environment, not the sandbox.
with Sandbox(enable_network=True) as sb:
result = sb.run(
"data = fetch_data('https://example.com/data.json'); len(data)",
external_functions={"fetch_data": fetch_data},
)
print(result.result)
Option C: Provide input variables¶
Input variables can be passed to the sandbox as keyword arguments.
Setting up as an MCP server¶
You can expose Backyard as an MCP (Model Context Protocol) server for use with AI agents that support MCP tools. Create a simple wrapper:
# mcp_sandbox_server.py
import json
import sys
from backyard import Sandbox
sandbox = Sandbox(readonly=True)
def handle_request(request: dict) -> dict:
action = request.get("action")
if action == "execute":
result = sandbox.run(
request["code"],
dependencies=request.get("dependencies"),
)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"success": result.success,
"result": result.result,
}
elif action == "reset":
sandbox.reset_session()
return {"success": True}
elif action == "grant_write":
sandbox.grant_write_access()
return {"success": True}
elif action == "revoke_write":
sandbox.revoke_write_access()
return {"success": True}
elif action == "ping":
return {"success": sandbox.ping()}
return {"success": False, "error": f"Unknown action: {action}"}
if __name__ == "__main__":
for line in sys.stdin:
line = line.strip()
if not line:
continue
request = json.loads(line)
response = handle_request(request)
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
Then configure your MCP client to use this server with stdio transport.
Workspace and file mounting¶
Mount directories and files into the sandbox:
Files and directories are mounted at /sandbox/<name> inside the sandbox.
File change tracking¶
File changes are automatically tracked when workspace directories are configured:
sandbox = Sandbox(workspace=[Path("/home/user/project")])
result = sandbox.run("""
with open('new_file.txt', 'w') as f:
f.write('created by sandbox')
""")
if result.file_changes and result.file_changes.has_changes:
print(f"Added: {result.file_changes.added}")
print(f"Modified: {result.file_changes.modified}")
print(f"Deleted: {result.file_changes.deleted}")
# Timestamps for each change:
for change in result.file_changes.all_changes:
print(f" {change.change_type}: {change.path} at {change.timestamp}")
Ignored directories¶
The following directories are always hidden from the sandbox:
.git.venvnode_modules__pycache__.pytest_cache.mypy_cache.ruff_cache
Additionally, patterns from .gitignore files in mounted workspace directories are applied.
Read-only mode¶
Read-only mode prevents the sandbox from modifying files:
# Start in read-only mode
sandbox = Sandbox(readonly=True)
# Grant write access mid-session
sandbox.grant_write_access()
# Revoke write access
sandbox.revoke_write_access()
In container mode, toggling read-only restarts the container and replays execution history.
Per-call resource overrides¶
Override the sandbox's default limits for a single execution:
result = sandbox.run(
"compute_intensive_task()",
max_duration_sec=60.0, # Extend timeout for this call
max_memory_mb=512, # Increase memory for this call
)
Session management¶
# Reset state (clears namespaces, preserves container)
sandbox.reset_session()
# Full reset (closes container, clears everything)
sandbox.clear()
# Health check
if sandbox.ping():
print("Sandbox is responsive")
Converting between backends¶
The automatic conversion from Monty to Container replays all previous successful code executions to restore state:
sandbox.run("x = 42")
sandbox.run("import numpy") # Triggers conversion to container (in auto mode)
sandbox.run("x + 1") # x=42 is preserved from Monty session
You can also control conversion behavior:
# Use Monty only (raises SandboxNotSupportedError on incompatible code)
sandbox = Sandbox(sandbox_type="monty")
# Use container from the start
sandbox = Sandbox(sandbox_type="container")
# Convert an existing session
sandbox.convert(SandboxType.CONTAINER)
sandbox.convert(SandboxType.MONTY, force=True) # Clears incompatible history
Note: When sandbox_type="auto", the sandbox converts to a container if it encounters unsupported code, but only if no file changes have been made. If file changes exist, it raises a SandboxNotSupportedError suggesting a fresh session.
Tracking conversion progress¶
When converting to a container with long replay histories, you can monitor progress via progress_callback. The callback receives (current_step, total_steps, execution_result, phase):
from backyard import Sandbox
from backyard.sandbox import ReplayPhase
def on_replay_progress(
current: int,
total: int,
execution: object | None,
phase: ReplayPhase,
) -> None:
if phase == ReplayPhase.BEFORE:
print(f"Replaying step {current}/{total}...")
elif phase == ReplayPhase.AFTER:
status = "✓" if execution and getattr(execution, "success", False) else "✗"
print(f" {status} step {current} completed")
elif phase == ReplayPhase.COMPLETE:
print(f"Replay complete ({total} steps)")
sandbox = Sandbox(progress_callback=on_replay_progress)
Error handling¶
from backyard.errors import (
SandboxError,
SandboxTimeoutError,
SandboxMemoryError,
SandboxNotSupportedError,
SandboxPermissionError,
)
try:
result = sandbox.run(code)
if result.failed:
print(f"Execution failed:")
print(f" stdout: {result.stdout}")
print(f" stderr: {result.stderr}")
print(f" timed_out: {result.timed_out}")
print(f" memory_limit_hit: {result.memory_limit_hit}")
except SandboxNotSupportedError as e:
print(f"Code incompatible with Monty: {e}")
print("Either modify the code or use sandbox.convert(SandboxType.CONTAINER)")
except SandboxPermissionError as e:
print(f"Write denied: {e}")
print("Call sandbox.grant_write_access() first")