Skip to main content

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 with statement

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, Path objects (read at creation), or bytes for binary files. All paths should use the /data prefix.
  • snapshot: Restore from a previously exported snapshot (mutually exclusive with files).
  • cwd: Initial working directory (default: /data).
  • preset: Execution limits preset (STRICT, NORMAL, or PERMISSIVE).

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 code
  • FileNotFoundError: File/directory not found (with .path attribute)
  • PermissionError: Permission denied (with .path attribute)
  • ExecutionLimitError: Execution limits exceeded
  • SubprocessCrashed: 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 identifier
  • name: Tool name (e.g., "bash" or "python")
  • started_at: Unix timestamp when command started
  • completed_at: Unix timestamp when command finished
  • parameters: Dict with command/cwd (bash) or codeLength/cwd (python)
  • result: Dict with exitCode
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)

Source distribution for localsandbox 0.3.2
File Size Uploaded
localsandbox-0.3.2.tar.gz 36.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for localsandbox 0.3.2
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.3.2 This release

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page