# sandboxio > One secure Python API for running AI-agent code in any sandbox (Docker, E2B, an in-process fake). Async-first on anyio with a sync facade; deny-by-default egress; mandatory timeouts; isolation tiers reported and enforceable; stable error codes; offline testing with FakeBackend. Canonical usage is `import sandboxio`, unaliased. `SBX` is the short code (error codes `SBX_E1002`, env vars `SBX_DEBUG`, fixture `sbx_fake`). Copy-paste commands use `sandboxio`, never the `sbx` alias. `.native` is outside the semver contract. The specification in `docs/spec/` is normative and wins over every other document. ## Start here - [README](../README.md): what it is and runnable examples - [Examples](../examples/README.md): complete runnable programs, one concept each, all executed by CI - [Quickstart](quickstart.md): install to a sandboxed run in five minutes - [AGENTS.md snippet](reference/agents-snippet.md): paste-ready rules for coding assistants in a project that uses sandboxio ## How-to - [Docker](how-to/docker.md): images with dependencies, offline wheelhouses, reaper, allowlists refused - [E2B](how-to/e2b.md): API key, allowlists, stateful contexts, rich outputs - [Offline testing](how-to/offline-testing.md): sbx_fake fixture, scripting, register() so DSNs resolve to the fake - [Audit and tracing](how-to/observability.md): LoggingSink, FileSink, QueueSink, OTel execute_tool spans, redaction - [CI](how-to/ci.md): copy-paste GitHub Actions workflow, fake on PRs and Docker on main - [Integrations](how-to/integrations.md): LangGraph tool, OpenAI Agents tool, MCP server and its container image - [Operations](how-to/operations.md): sandboxio doctor, sandboxio reap, SBX_* variables, exit codes ## Explanation - [Isolation tiers](explanation/isolation-tiers.md): CONTAINER is not a boundary; MICROVM is the floor for untrusted code - [Deny by default](explanation/deny-by-default.md): why egress is off and what it costs - [Why errors have codes](explanation/error-codes.md): stable codes, hints that are fixes, no builtin inheritance - [Version policy](explanation/version-policy.md): what breaks, what a deprecation window is, why `.native` is exempt ## Reference - [Error codes](errors/README.md): every SBX_E code, generated from the source - [Public API spec](spec/03-public-api.md): create(), sync facade, stability contract - [Ports spec](spec/02-ports.md): Backend, AsyncSandbox, Process, AsyncFileSystem, ReapableBackend - [Domain model spec](spec/01-domain-model.md): value objects, Capability, IsolationTier - [Errors spec](spec/04-errors.md): tree and rendering - [Security policy spec](spec/05-security-policy.md): defaults, network, secrets, tenancy - [Observability spec](spec/06-observability.md): the operation record, sinks, spans - [Configuration spec](spec/07-configuration.md): DSN grammar, typed config, routing file - [Adapter contract spec](spec/08-adapter-contract.md): what every adapter must do - [Integrations spec](spec/09-integrations.md): LangGraph, OpenAI Agents, MCP server - [CLI spec](spec/10-cli.md): doctor, reap, demo ## Optional - [Architecture decision records](adr/README.md): why each decision was made - [Hazards](hazards.md): what can go wrong and the tripwires - [Build order](build-order.md): what is built, in what order, with exit criteria - [Full text](llms-full.txt): every page above concatenated, for one-shot context loading ============================================================================== # FILE: README.md ============================================================================== # sandboxio **One secure Python API for running AI-agent code in any sandbox.** Swap Docker ↔ E2B with one line; no network by default; test your agent tools offline with the built-in fake. [![CI](https://github.com/bit-agents/sandboxio/actions/workflows/ci.yml/badge.svg)](https://github.com/bit-agents/sandboxio/actions/workflows/ci.yml) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](docs/adr/0015-python-version-floor.md) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) > [!WARNING] > **Pre-alpha. The core, the contract suite, `FakeBackend`, the Docker and E2B adapters, the > CLI and the integrations exist and pass their gates; nothing is released.** > > The public API is not stable, nothing is published to PyPI, and no version is suitable for > any use. What *is* stable enough to build against is [`docs/spec/`](docs/spec/), which is > normative and CI-enforced. > > Watch the repo if you want the v0.1 announcement. Do not depend on this yet. ## Why this exists Every sandbox provider ships its own SDK with its own shape, so agent code that runs on one is rewritten for the next. The framework layers each solve it captively — LangChain's backends only help inside LangChain. sandboxio is the framework-agnostic substrate underneath, with the security posture that agent execution actually needs. - **Security is the headline, not an add-on.** Deny-by-default egress, mandatory timeouts, isolation-tier reporting and audit hooks are all in v0.1 — not a later hardening pass. - **No lowest common denominator.** One-backend features stay reachable through `Capability` flags and `.native` instead of being sanded off. - **Tiny, auditable core.** `anyio` and `typing-extensions` only; every backend SDK sits behind an extra and is imported lazily. - **Offline-testable.** `FakeBackend` and a pytest fixture, so your agent's tools have tests that need no Docker, no network and no provider account. - **Provider churn is absorbed publicly.** Upstream breaking changes are tracked, absorbed and written down. ## Quickstart Every Python block below is executed by CI exactly as written ([`tests/test_readme_examples.py`](tests/test_readme_examples.py)); a copied example that does not run is a P0 bug. The Docker-backed lines run against the built-in fake in CI, so they are the same code you would run — only the backend differs. The commands below are what v0.1 will install. Today the name on PyPI holds a `0.0.0` placeholder that carries no code, so `uv add sandboxio` does not yet get you a library. `uvx sandboxio demo` is the documented entry point — never the `sbx` alias, which resolves to an unrelated PyPI package ([ADR-0014](docs/adr/0014-project-name.md)). ```bash uv add "sandboxio[docker]" # from v0.1 uvx sandboxio demo # create, run, stream, prove egress is denied, tear down sandboxio doctor # what is installed, reachable and missing — no secrets printed ``` Run code in a sandbox. `create()` with no arguments is local Docker; nothing leaves the sandbox unless you say so. ```python import sandboxio async with await sandboxio.create() as sb: # zero-config, local Docker res = await sb.run_code("print('hello')") print(res.stdout) # hello res = await sb.run(["python", "--version"], timeout=30) print(res.exit_code, res.stdout.strip()) ``` Swap the backend with one line. The code that uses the sandbox does not change. ```python import sandboxio async def summarise(sb: sandboxio.protocols.AsyncSandbox) -> str: await sb.files.write("/work/input.txt", "3 4\n") res = await sb.run_code("a, b = open('/work/input.txt').read().split(); print(int(a) * int(b))") res.raise_for_status() # ExecutionError on a non-zero exit return res.stdout.strip() async with await sandboxio.create("docker://python:3.12-slim") as sb: print(await summarise(sb)) async with await sandboxio.create("e2b://code-interpreter-v1") as sb: # needs E2B_API_KEY print(await summarise(sb)) ``` Stream output while a long command runs, and rely on the timeout: a sandbox timeout is `sandboxio.SandboxTimeout`, never the builtin `TimeoutError`. ```python import sandboxio async with await sandboxio.create() as sb: async with sb.stream(["sh", "-c", "echo start; sleep 1; echo done"], timeout=30) as proc: async for chunk in proc: print(chunk.stream, chunk.data.decode(), end="") res = await proc.wait() try: await sb.run(["sleep", "999"], timeout=1) except sandboxio.SandboxTimeout as exc: print(exc.code) # SBX_E1302 — stable, documented ``` Sync code gets the same surface through `create_sync()`. One sandbox is one portal thread; use the async API for heavy concurrency. ```python import sandboxio with sandboxio.create_sync("docker://python:3.12-slim", timeout=60) as sb: print(sb.run_code("print(6 * 7)").stdout) ``` Test your agent's tools offline. `sbx_fake` is a pytest fixture installed with the package; it executes nothing, records everything, and passes the same contract suite as the real backends. ```python import pytest from sandboxio import ExecResult @pytest.mark.anyio async def test_my_tool_reads_the_pandas_version(sbx_fake): sbx_fake.on_run_code(match="import pandas", returns=ExecResult(0, "2.2.1\n", "")) res = await sbx_fake.sandbox.run_code("import pandas; print(pandas.__version__)") assert "2.2.1" in res.stdout assert sbx_fake.calls[0].network.egress == "deny" # the default, recorded ``` Full surface, including capability discovery, `require_isolation`, audit sinks and spans: [`docs/quickstart.md`](docs/quickstart.md) and the normative [`docs/spec/03-public-api.md`](docs/spec/03-public-api.md). ## Backends | Backend | Status | Isolation tier | |---------|--------|----------------| | Docker | adapter built, unreleased | `CONTAINER` — [containers share the host kernel](https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-a-container/) | | E2B | adapter built, unreleased | `MICROVM` — [Firecracker microVM, its own kernel](https://e2b.dev/security) | | `FakeBackend` | built, unreleased | n/a — in-process, for tests | | Modal | planned for v0.1.1 | `GVISOR` — [containerised and virtualised using gVisor](https://modal.com/docs/guide/security) | Every tier above is the mechanism the provider documents for itself, read on **2026-09-20**. None of those three pages carries its own revision date, so that is the date it was read, not a date the provider published; re-verification is a [quarterly item](docs/runbook.md). What the tiers mean — and why `GVISOR` is defence in depth rather than VM equivalence — is in [`docs/explanation/isolation-tiers.md`](docs/explanation/isolation-tiers.md). Third-party adapters are first-class: the adapter contract is [specified](docs/spec/08-adapter-contract.md) and enforced by a shared contract suite. ## Integrations - **LangGraph / LangChain** — `sandboxio.integrations.langgraph.make_code_tool()` returns a native `BaseTool` (`sandboxio[langgraph]`). - **OpenAI Agents SDK** — `sandboxio.integrations.openai_agents.make_code_tool()` returns a native `FunctionTool` (`sandboxio[openai-agents]`). - **MCP** — `python -m sandboxio.mcp --backend docker://python:3.12-slim` serves `run_python`, `run_command`, file tools and `sandbox_info` (`sandboxio[mcp]`), also as a container image. ## Documentation **[docs.sandboxio.dev](https://docs.sandboxio.dev)** — the Markdown under [`docs/`](docs/) is its source and stays the source, so nothing below moves. Read it here or there: - [`examples/`](examples/) — complete programs, one concept each: hello world, swapping backends, streaming, proving egress is denied, agent tools. Each one is executed by CI. - [`docs/quickstart.md`](docs/quickstart.md) — five minutes from install to a sandboxed run. - [`docs/how-to/`](docs/how-to/) — Docker images and offline wheelhouses, E2B keys and allowlists, offline testing with the fake, audit sinks and tracing, CI. - [`docs/explanation/`](docs/explanation/) — isolation tiers, deny-by-default, why errors have codes, and the [version policy](docs/explanation/version-policy.md). - [`docs/errors/`](docs/errors/README.md) — every error code, generated from the source. - [`docs/spec/`](docs/spec/) — the normative specification. This is the contract. - [`docs/adr/`](docs/adr/) — why each decision was made, including the ones that look arbitrary. - [`docs/llms.txt`](docs/llms.txt) — for coding assistants; `llms-full.txt` beside it. ## Security Isolation tiers are reported, not assumed: `CONTAINER` is not a security boundary against hostile code, and sandboxio says so rather than implying otherwise. Report vulnerabilities privately — see [`SECURITY.md`](SECURITY.md). A report that a *declared control did not apply* is the highest-severity class this project has. ## Contributing Read [`CONTRIBUTING.md`](CONTRIBUTING.md). Contributions need a DCO `Signed-off-by` line; there is no CLA. ## License Code is [MIT](LICENSE). Prose in `docs/` is [CC BY 4.0](LICENSE-DOCS); code samples inside those documents are MIT ([ADR-0026](docs/adr/0026-docs-license-cc-by.md)). ============================================================================== # FILE: examples/README.md ============================================================================== # Examples Complete programs, one concept each. Every one of them is executed by CI ([`tests/test_examples.py`](../tests/test_examples.py)) with the Docker and E2B backends swapped for the in-process fake, so an example that stopped working fails our build rather than your first attempt. | Example | Shows | Needs | |---------|-------|-------| | [`01_hello_sandbox.py`](01_hello_sandbox.py) | create → `run_code` → `run` → teardown | Docker | | [`02_swap_backends.py`](02_swap_backends.py) | one function, two providers, one line changed | Docker, `E2B_API_KEY` | | [`03_stream_and_timeout.py`](03_stream_and_timeout.py) | `stream()`, and `SandboxTimeout` (`SBX_E1302`) stopping a runaway | Docker | | [`04_egress_denied.py`](04_egress_denied.py) | deny-by-default egress, proven, then the explicit opt-out | Docker | | [`05_langgraph_agent.py`](05_langgraph_agent.py) | a LangGraph agent whose tool is a sandbox | Docker, `langgraph`, `ANTHROPIC_API_KEY` | | [`06_openai_agents.py`](06_openai_agents.py) | the same through the OpenAI Agents SDK | Docker, `OPENAI_API_KEY` | | [`test_my_tool.py`](test_my_tool.py) | testing your agent's tools with `sbx_fake` | nothing | ## Running them From a clone, with the repository's own environment: ```bash uv run --extra docker examples/01_hello_sandbox.py uv run pytest examples/test_my_tool.py ``` The two agent examples run their sandboxio half with no API key and print why they stopped, so you can see the tool work before you spend a token: ```bash uv sync --all-extras uv run examples/05_langgraph_agent.py ``` Every example assumes a running Docker daemon and the `python:3.12-slim` image. If something is off, `sandboxio doctor` says what is installed, reachable and missing — without printing a single secret. ## What they deliberately do not show - **Per-host allowlists on Docker.** `NetworkPolicy(allow=...)` is refused there with `SBX_E1101`, because Docker cannot enforce it and silently ignoring it would be a lie about a security control. E2B can; see [`docs/spec/05-security-policy.md`](../docs/spec/05-security-policy.md). - **Tasks rather than concepts.** Image preparation, offline wheelhouses, E2B keys, audit sinks, tracing and CI each have a page under [`docs/how-to/`](../docs/how-to/). ============================================================================== # FILE: docs/quickstart.md ============================================================================== # Quickstart Five minutes from a clean machine to code running in a sandbox that cannot reach the network. You need Python 3.11+ and, for the Docker backend, a running Docker daemon. Everything here also runs against the in-process fake, which needs neither. ## 1. Prove the install works ```bash uvx sandboxio demo ``` The demo creates a sandbox, runs code, streams output, tries to reach the network from inside — and reports that the attempt was denied — then tears the sandbox down. The first run pulls the `python:3.12-slim` image (about 130 MB, on the host, outside the sandbox's network policy). A warm run finishes in about two seconds. If Docker is not installed, the demo prints the exact install command and exits `1`. When something is off, ask the doctor. It prints credential variable *names*, never values, and makes no provider API call: ```bash sandboxio doctor sandboxio doctor --json # paste-friendly, for bug reports ``` ## 2. Add it to a project From v0.1 onwards — the name currently holds a `0.0.0` placeholder with no code in it: ```bash uv add "sandboxio[docker]" # or "sandboxio[e2b]" — the core has no backend built in ``` The base package depends on `anyio` and `typing-extensions` only. Every backend SDK lives behind an extra and is imported the first time that backend is used. ## 3. Run code ```python import sandboxio async with await sandboxio.create() as sb: res = await sb.run_code("print('hello')") print(res.stdout) ``` `create()` with no arguments is local Docker with the defaults every backend applies: | Default | Value | Change it with | |---------|-------|----------------| | Network | egress denied | `network=NetworkPolicy(egress="allow")` or an allowlist on E2B | | Timeout | 300 s for the sandbox and every call | `timeout=` on `create()`, `run()`, `run_code()`, `stream()` | | Resources | modest CPU and memory caps | `resources=Resources(memory_mb=2048)` | | Labels | none | `metadata={"tenant_id": ..., "session_id": ...}` | `timeout=None` on a call means "inherit the sandbox timeout", never "unbounded"; `timeout=None` on `create()` is refused. ## 4. Swap the backend ```python import sandboxio sb = await sandboxio.create("docker://python:3.12-slim") sb = await sandboxio.create("e2b://code-interpreter-v1") # needs E2B_API_KEY sb = await sandboxio.create("fake://") # tests; executes nothing ``` The DSN is `://[