LocalSandbox
A Python SDK for sandboxed filesystem operations, built on just-bash, AgentFS, and Pyodide. Provides AI agents with a persistent, isolated environment backed by SQLite.
⚠️ Warning: This project is in beta. While it provides isolation through WebAssembly and a simulated bash environment, it has not been security audited and should not be relied upon as a fully secure sandbox for running untrusted code. Use at your own risk.
Features
- Sandboxed Execution: Run bash commands in an isolated environment
- Python Execution: Run Python via Pyodide (WebAssembly) on the same virtual filesystem
- Persistent Filesystem: All file operations persist across commands in SQLite
- Key-Value Store: Separate KV API for agent state management
- Command History: Track all executed commands with timestamps and results
- Snapshot & Resume: Export/restore complete sandbox state
- Execution Limits: Configurable DOS protection (loop iterations, command counts)
- Async Support: Full async API via
asyncio.to_thread - Context Manager: Clean resource management with
withstatement
Installation
pip install localsandbox
# or
uv add localsandbox
Prerequisites
The package requires Deno to run the TypeScript shim. Install Deno
(brew install deno) and ensure deno is on your PATH.
Quick Start
from localsandbox import LocalSandbox
# Basic usage with context manager (recommended)
with LocalSandbox() as sandbox:
result = sandbox.bash('echo "Hello, World!"')
print(result.stdout) # Hello, World!
# Without context manager
sandbox = LocalSandbox()
try:
result = sandbox.bash('echo "Hello!"')
print(result.stdout)
finally:
sandbox.destroy()
# Seed initial files (all paths use /data prefix)
with LocalSandbox(files={"/data/app/main.py": 'print("hello")'}) as sandbox:
result = sandbox.execute_python('exec(open("main.py").read())', cwd="/data/app")
print(result.stdout) # hello
# Use file helpers (all paths use /data prefix)
with LocalSandbox() as sandbox:
sandbox.write_file("/data/config.json", '{"key": "value"}')
content = sandbox.read_file("/data/config.json")
exists = sandbox.exists("/data/config.json")
files = sandbox.list_files("/data")
# Key-value store
with LocalSandbox() as sandbox:
sandbox.kv.set("user_id", "12345")
user_id = sandbox.kv.get("user_id")
all_keys = sandbox.kv.keys()
Examples
More runnable scripts are in examples/.
Design Notes
- Python execution architecture:
docs/design/python-execution.md - Tool bridge design:
docs/design/pyodide-tool-bridge.md
API Reference
LocalSandbox
LocalSandbox(
files: dict[str, str | Path | bytes] | None = None,
snapshot: bytes | None = None,
cwd: str = "/data",
preset: ExecutionPreset = ExecutionPreset.NORMAL,
)
Parameters:
files: Initial filesystem contents. Supports string content,Pathobjects (read at creation), orbytesfor binary files. All paths should use the/dataprefix.snapshot: Restore from a previously exported snapshot (mutually exclusive withfiles).cwd: Initial working directory (default:/data).preset: Execution limits preset (STRICT,NORMAL, orPERMISSIVE).
Methods
Bash Execution
sandbox.bash(command: str) -> BashResult
Execute a bash command. Returns BashResult with stdout, stderr,
exit_code, and duration_ms.
Raises:
CommandError: Non-zero exit codeFileNotFoundError: File/directory not found (with.pathattribute)PermissionError: Permission denied (with.pathattribute)ExecutionLimitError: Execution limits exceededSubprocessCrashed: Shim subprocess failure
Python Execution
sandbox.execute_python(
code: str,
cwd: str | None = None,
preload_packages: list[str] | None = None,
toolset: PythonToolset | Sequence[Callable[..., object]] | None = None,
) -> PythonResult
Execute Python via Pyodide. The sandbox filesystem is mounted at /data in both
bash and Python environments. All paths should use the /data prefix for
consistency across all operations (bash, Python, and file helpers).
If preload_packages is provided, those Pyodide packages are loaded before
execution. No network access is granted unless preloading is requested.
If toolset is provided, the sandbox code can call host-side tools via from host_tools import call and search the declared toolset via host_tools.search().
The simplest form is to pass a list of typed Python callables. LocalSandbox infers the tool name, description, input schema, and output schema from the function signature, type hints, and docstring:
from localsandbox import LocalSandbox
def web_search(query: str) -> dict[str, list[str]]:
"""Search the web for information."""
return {"results": ["result1", "result2"]}
with LocalSandbox() as sandbox:
result = sandbox.execute_python(
"""
from host_tools import call, search
print(search("web"))
response = call("web_search", {"query": "hello"})
print(response["results"])
""",
toolset=[web_search],
)
Alternatively you can pass an explicit PythonToolset if you need full control over schemas, names, or timeouts.
execute_python() reuses a warmed Python runner when the tool manifest and preload_packages are unchanged. Files under /data always persist across calls. Python interpreter state may also persist across compatible calls, but sholudn't be depended on. The requested working directory is reapplied for each execution. Create a new LocalSandbox if you need a fresh interpreter.
File Operations
sandbox.read_file(path: str) -> str
sandbox.write_file(path: str, content: str) -> None
sandbox.list_files(path: str) -> list[str]
sandbox.exists(path: str) -> bool
sandbox.delete_file(path: str) -> None
Key-Value Store
sandbox.kv.get(key: str) -> str | None
sandbox.kv.set(key: str, value: str) -> None
sandbox.kv.delete(key: str) -> None
sandbox.kv.keys(prefix: str = "") -> list[str]
Command History
sandbox.history(limit: int = 100) -> list[HistoryEntry]
Get the history of tool calls executed on this sandbox. Returns a list of
HistoryEntry objects with:
id: Unique identifiername: Tool name (e.g., "bash" or "python")started_at: Unix timestamp when command startedcompleted_at: Unix timestamp when command finishedparameters: Dict withcommand/cwd(bash) orcodeLength/cwd(python)result: Dict withexitCode
from localsandbox import LocalSandbox
with LocalSandbox() as sandbox:
sandbox.bash('echo "hello"')
sandbox.bash('ls -la')
history = sandbox.history()
for entry in history:
print(f"Command: {entry.parameters['command']}, Exit: {entry.result['exitCode']}")
Snapshot & Resume
# Export current state
snapshot = sandbox.export_snapshot()
# Resume from snapshot
new_sandbox = LocalSandbox(snapshot=snapshot)
Lifecycle
sandbox.destroy() # Clean up resources (called automatically by context manager)
Async API
All methods have async versions prefixed with a:
import asyncio
from localsandbox import LocalSandbox
async def main():
sandbox = LocalSandbox()
try:
result = await sandbox.abash('echo "async!"')
await sandbox.awrite_file("/data/tmp/test.txt", "content")
content = await sandbox.aread_file("/data/tmp/test.txt")
await sandbox.kv.aset("key", "value")
value = await sandbox.kv.aget("key")
finally:
await sandbox.adestroy()
asyncio.run(main())
Execution Presets
| Preset | Max Loop Iterations | Max Commands |
|---|---|---|
| STRICT | 100 | 500 |
| NORMAL | 1,000 | 5,000 |
| PERMISSIVE | 10,000 | 50,000 |
from localsandbox import LocalSandbox, ExecutionPreset
# For untrusted input
sandbox = LocalSandbox(preset=ExecutionPreset.STRICT)
# For complex operations
sandbox = LocalSandbox(preset=ExecutionPreset.PERMISSIVE)
Architecture
LocalSandbox uses a TypeScript shim (running on Deno) that bridges Python to:
- just-bash: A bash interpreter/simulator written in TypeScript
- AgentFS: SQLite-based virtual filesystem
- Pyodide: Python interpreter compiled to WebAssembly for sandboxed Python execution
Each bash call spawns a Deno subprocess that runs just-bash against the AgentFS database. Python execution uses a persistent Deno/Pyodide subprocess across compatible calls, communicating via line-delimited JSON over stdio. When a toolset is provided, tool calls are relayed between the sandbox and host handlers through the same protocol.
Both bash and Python share the same virtual filesystem backed by SQLite.
Development
# Install dependencies
uv sync
# Run tests
uv run pytest
# Type checking
uv run pyright
# Lint and format
uv run ruff check --fix && uv run ruff format
# Shim checks
cd shim && deno task check
License
MIT
Metadata
Release files for localsandbox 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| localsandbox-0.3.2.tar.gz | 36.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| localsandbox-0.3.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 75.1 kB
Release files / localsandbox-0.3.2.tar.gz
| Download URL | localsandbox-0.3.2.tar.gz |
|---|---|
| Size | 36.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c6b25f9868e64db8ca3fe360ba5b1c638d076eda84e7b89bf5363f0bf7d95065
|
|
BLAKE2b-256 checksum How to use checksums |
cdb9784895095d064cf8406865bb630f960d173dfe3bc25f916c988f875ac57d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / localsandbox-0.3.2-py3-none-any.whl
| Download URL | localsandbox-0.3.2-py3-none-any.whl |
|---|---|
| Size | 39.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f825eada16820186ff8056d98244f7273d886700fbeb88546e08dab28035a4d7
|
|
BLAKE2b-256 checksum How to use checksums |
bdb5146a3b5b3fdba6502de33bc240dbbb843c0a388785236c8a6d6a9fb4bcb8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|