A python SDK for a agentfs + just-bash + pyodide agent sandbox.
Project description
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file localsandbox-0.3.1.tar.gz.
File metadata
- Download URL: localsandbox-0.3.1.tar.gz
- Upload date:
- Size: 36.0 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f86d905c385204a6b851dcdc9094dcbf04ccd088385e4f8e911523b1850508a4
|
|
| MD5 |
e120fd43ee2951e0768964cf65946135
|
|
| BLAKE2b-256 |
7d2d5ad516f56540b2b481274abc111db6377b4acf640e0069cd9a5ce5df7c98
|
File details
Details for the file localsandbox-0.3.1-py3-none-any.whl.
File metadata
- Download URL: localsandbox-0.3.1-py3-none-any.whl
- Upload date:
- Size: 38.9 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a419236067d38891ee4dafc8c99f2d83e36cd2b792023dc35ea47c06da8f581a
|
|
| MD5 |
da052c3b6bbe83b9c1f2b93b6d9fc62d
|
|
| BLAKE2b-256 |
aa320331237f1b242593f232ea1c0394a1851b7cb3ff7fb6ec34fa96a39cc16d
|