> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# E2B compatibility

Run code written for the e2b SDKs on B3OS microVMs by changing three environment variables; what works, what is refused, and how the envd access token is kept.

B3OS serves E2B's sandbox API and envd, the daemon inside each sandbox, at `/compat/e2b`, with E2B's field names, status codes and error bodies. Code written for the e2b 2.62 SDKs (TypeScript and Python) creates B3OS microVMs and runs commands and file calls in them with three environment variables changed:

```bash
export E2B_API_KEY="$B3OS_API_KEY"
export E2B_API_URL="https://beta-api.b3os.org/compat/e2b"
export E2B_SANDBOX_URL="https://beta-api.b3os.org/compat/e2b/envd"
```

The sandboxes underneath are the native ones: a sandbox created here is listed, billed and limited like any other, and the [native API](/sandboxes) works on it too (fork, snapshot, ports, `run_code`). The key needs `sandboxes:read`, `sandboxes:write` and `sandboxes:run`.

## Clients

**curl**

```bash
curl -s https://beta-api.b3os.org/compat/e2b/v2/sandboxes \
  -H "X-API-KEY: $B3OS_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"templateID": "base", "timeout": 300, "metadata": {"run": "e2b"}}'
```

The answer holds `sandboxID` (the B3OS ID in its DNS-safe form, `sbx-…`) and `envdAccessToken`, which envd calls send as `X-Access-Token` with `E2b-Sandbox-Id: <sandboxID>`.

**TypeScript**

```bash
npm install e2b@2.62.0
```

```ts
import { Sandbox } from "e2b";

const sandbox = await Sandbox.create({ timeoutMs: 300_000 });
const result = await sandbox.commands.run('echo "Hello from E2B Sandbox!"');
console.log(result.stdout);
await sandbox.files.write("/home/user/hello.txt", "Hello");
console.log(await sandbox.files.read("/home/user/hello.txt"));
await sandbox.kill();
```

**Python**

```bash
pip install e2b==2.62.0
```

```python
from e2b import Sandbox

sandbox = Sandbox.create(timeout=300)
result = sandbox.commands.run('echo "Hello from E2B Sandbox!"')
print(result.stdout)
sandbox.files.write("/home/user/hello.txt", "Hello")
print(sandbox.files.read("/home/user/hello.txt"))
sandbox.kill()
```

**CLI**

The same microVM through the B3OS CLI, which speaks the native API:

```bash
b3os sandbox create --template base --timeout-s 300
b3os sandbox exec sbx_01m5t68q1b590rj801nbzsqzkk -- echo "Hello from B3OS"
```

Where it stands: the e2b 2.62.0 TypeScript and Python quickstarts in the repository's `bench/sandboxes/e2b` (create, commands with exit codes and streamed output, files, the process calls, list, get, timeout, pause, connect, kill, and the refusals below) passed against `beta-api.b3os.org` on 2026-11-02 with only the three variables set: all 23 TypeScript steps and all 11 Python steps (`bench/sandboxes/results/20261102T2058Z-e2b-quickstart-prod`).

## Frameworks

These agent frameworks run on B3OS through their own E2B backend, with the three variables above and the agent's model on the node's [OpenAI-compatible API](/compat/openai). In a run against `beta-api.b3os.org` on 2026-11-14, from the node's own host, each framework's tool loop on `b3os/qwen3.5-9b` ran `uname -a` in a sandbox, wrote the output to `/home/user/proof.txt`, read it back and answered with its first word (`bench/sandboxes/results/20261114T101650Z-e2b-frameworks-prod`). AG2 and Google ADK met the pass rule in `bench/sandboxes/e2b/frameworks/README.md` on the second attempt: in the first, the model's first call was `uname -a > /home/user/proof.txt`, and the rule counts `uname` only when its own result shows `Linux`.

| Framework | Version | On B3OS | Settings |
|---|---|---|---|
| AG2 | `ag2` 1.3.1 | `E2BEnvironment` behind `SandboxShellTool`: the agent's shell commands | The three variables. Model: `OpenAICompletionsConfig(base_url=...)` |
| Google ADK | `google-adk` 2.13.0 | `E2BEnvironment` behind `EnvironmentToolset`: `Execute`, `WriteFile`, `ReadFile` | The three variables. Model: `LiteLlm(model="openai/b3os/qwen3.5-9b", api_base=...)` on `litellm` 1.106.2; the run needed the `litellm[mcp]` extra. The run's `adk.py` adds a wire log, a metadata tag (a wrapper on `AsyncSandbox.create`) and `LITELLM_LOCAL_MODEL_COST_MAP=True`, so LiteLLM uses its bundled price list instead of fetching one from GitHub; none of the three changes where the sandbox points |

The AG2 lines (the run's `bench/sandboxes/e2b/frameworks/ag2.py` adds a wire log, a metadata tag and, after the answer, a verify read: `cat /home/user/proof.txt` through the same `E2BEnvironment`):

```bash
pip install "ag2[openai]==1.3.1" "openai==3.45.0" "e2b==2.62.0"
```

```python
import asyncio
import os

from ag2 import Agent
from ag2.config import OpenAICompletionsConfig
from ag2.extensions.e2b import E2BEnvironment
from ag2.tools import SandboxShellTool


async def main():
    env = E2BEnvironment(sandbox_timeout=300)
    agent = Agent(
        "agent",
        "You work in a remote Linux sandbox through your tools. Use a tool for every step; never guess an output.",
        config=OpenAICompletionsConfig(
            model="b3os/qwen3.5-9b",
            api_key=os.environ["B3OS_API_KEY"],
            base_url="https://beta-api.b3os.org/v1/infer",
        ),
        tools=[SandboxShellTool(env)],
    )
    try:
        reply = await agent.ask(
            "Run uname -a in the sandbox, write the output to /home/user/proof.txt, "
            "read it back, and answer with its first word."
        )
        print(reply.body)
    finally:
        await env.aclose()


asyncio.run(main())
```

The model's tool calls, what each returned, and the answer, from `ag2.json` in that run:

```text
run_shell_command  uname -a
  Linux sbx-01m7cryqkcvhw3sb5pbp4gynph 6.1.188 #1 SMP PREEMPT_DYNAMIC Fri Oct 23 16:17:47 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
run_shell_command  uname -a > /home/user/proof.txt
run_shell_command  cat /home/user/proof.txt
  Linux sbx-01m7cryqkcvhw3sb5pbp4gynph 6.1.188 #1 SMP PREEMPT_DYNAMIC Fri Oct 23 16:17:47 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
The first word of the output is **Linux**.
```

Pydantic AI Harness and TanStack AI finished the same task, but on B3OS they lose exit codes: see [What is refused](#what-is-refused).

## What works

| SDK call | On B3OS |
|---|---|
| `Sandbox.create` | Templates `base` (the default), `code-interpreter` and `agent` ([templates](/sandboxes#templates)), with `envs`, `metadata`, `timeoutMs` and `lifecycle.onTimeout: "pause"`. A retry with the SDK's `Idempotency-Key` returns the same sandbox |
| `Sandbox.list`, `Sandbox.getInfo` | Running and paused sandboxes, with E2B's `metadata` and `state` filters and `nextToken` pages |
| `setTimeout`, `pause`, `Sandbox.connect`, `kill` | As in E2B: `connect` resumes a paused sandbox and returns a new envd access token |
| `commands.run`, `commands.start`, `background: true`, `onStdout`, `onStderr` | A process belongs to the sandbox, so closing the stream leaves it running; `commands.connect` streams it again from its start |
| `commands.list`, `commands.kill` | Processes started through envd |
| `files.read`, `files.write`, `files.writeFiles`, `files.list` (with `depth` up to 8), `files.exists`, `files.getInfo`, `files.makeDir`, `files.rename`, `files.remove` | Relative paths start at the user's home (`/home/user`; `/root` with `user: "root"`) |
| `network.allowOut` with `denyOut: ["0.0.0.0/0"]`, or `allowInternetAccess: false` | B3OS egress: only the named hosts (`example.com`, `*.example.com`, `host:port`), or nothing |

## What is refused

A create, pause or connect option B3OS cannot honor answers `400` naming the field ([`unsupported_option`](/errors#unsupported_option)). A body member B3OS does not read is accepted and ignored, so a field a later SDK release adds does not fail the call.

| Option | Instead |
|---|---|
| `mcp` | Run your MCP server as a process in the sandbox (`commands.run` with `background: true`) |
| `lifecycle.autoResume` | Resume a paused sandbox with `Sandbox.connect` |
| `volumeMounts` | Keep files in the sandbox and save it with a [snapshot](/sandboxes/snapshots) |
| `iam`, and `network` fields other than `allowOut` and `denyOut` | Not in the beta |
| `onTimeout.mode: "filesystem"`, `pause` without memory, `connect` with `onResume: "reboot"` | A paused sandbox always keeps its memory |

envd refuses two options with Connect's `invalid_argument`, naming the option, and one with `400`:

| Option | Instead |
|---|---|
| `commands.run` with `stdin: true` | Pass input in the command (a here-document or a file) |
| `sandbox.pty` | Open a terminal with the native `GET /v1/sandboxes/{sandbox_id}/pty` |
| `files.write` with gzip (`400`) | Send the file uncompressed |

Two envd behaviors differ from E2B's (`envd-probe.json` in the frameworks run):

- A command from the Python SDK runs in `/workspace` whatever `cwd` it names: the SDK runs each command through a login shell (the probe shows the login profile's `cd /workspace` taking effect). E2B runs it in `cwd`, else the user's home. Use absolute paths, or `cd` in the command.
- Each command leads its own session, so `setsid` forks and exits `0` at once, and output written after that is lost. Use `setsid -w`, or no `setsid`.

Two frameworks trip on these and on the stdin refusal, so they run in part:

| Framework | What works | What is refused |
|---|---|---|
| Pydantic AI Harness, `pydantic-ai-harness[e2b]` 0.64.0 | `Shell`'s `run_command` ran the command, wrote the file with `>` and read it with `cat` | The backend's `run` reports exit code `0` for `exit 5` (`envd-probe.json`, `harness_run_exit_5`). `FileSystem`'s `write_file` answers "`/home/user/proof.txt` is outside the project root `/workspace`", also with `E2BSandbox(working_dir="/home/user")`, and the harness's file read raises "FIFO reads are not supported" for a plain file |
| TanStack AI, `@tanstack/ai-sandbox-e2b` 1.0.0 | The task ran through three tools the bench wrote, `exec`, `write_file` and `read_file`, thin wrappers over the handle's `process.exec`, `fs.write` and `fs.read`, because TanStack ships no exec tool for a text adapter. `fs.write`, `fs.read`, and `process.exec` of a command that prints at once work | `process.exec` runs `exec setsid bash -c`, so `exit 5` reports exit code `0` and `sleep 1; echo late` returns no output. `process.spawn` opens stdin and gets `[invalid_argument] stdin: true is not supported by B3OS envd; pass input in the command (a here-document or a file)` |

Template builds (`Template.build`, `POST /v3/templates`) answer [`501 not_implemented`](/errors#not_implemented) with the alternative: start from a built-in template, install what you need, and create from a snapshot. `files.watchDir` and `getMetrics` are not served and answer `404` in E2B's shape.

## The envd access token

envd calls carry no API key, only the sandbox's envd access token, so B3OS keeps it under these rules:

- It is `b3os_et_` followed by 32 random bytes. The node keeps only its SHA-256 in the sandbox's record; nothing else stores it.
- Only the create and connect answers return it, never a list or get answer or the native API.
- It works while the sandbox is running or paused, for at most an hour from issue, and while the key that minted it is not revoked. Kill, delete and the timeout end it at once.
- `Sandbox.connect` mints a new one, bound to the connecting key; the previous one keeps working for a minute, then never again.
- Forks and sandboxes restored from a snapshot start without a token: `Sandbox.connect` gives them their own.
- It grants what `sandboxes:run` grants on that one sandbox (commands and files), as the key that minted it.

Every refused envd call gets the same `401` with [`envd_token_invalid`](/errors#envd_token_invalid) in the message, whether the token is missing, wrong, expired or rotated out, or the sandbox is missing, ended or another organization's.

E2B's signed file URLs (`downloadUrl` and `uploadUrl` in TypeScript, `download_url` and `upload_url` in Python) are not supported: the node keeps no token it could check a signature with, so a request carrying `signature` or `signature_expiration` gets that `401`. Read and write files through `files.read` and `files.write`.

Plan limits apply as to native sandboxes: see [sandbox limits](/sandboxes/limits).