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:
bashexport 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 works on it too (fork, snapshot, ports, run_code). The key needs sandboxes:read, sandboxes:write and sandboxes:run.
Clients
bashcurl -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>.
bashnpm install e2b@2.62.0
tsimport { 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();
bashpip install e2b==2.62.0
pythonfrom e2b import Sandboxsandbox = 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()
The same microVM through the B3OS CLI, which speaks the native API:
bashb3os sandbox create --template base --timeout-s 300b3os 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. 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):
bashpip install "ag2[openai]==1.3.1" "openai==3.45.0" "e2b==2.62.0"
pythonimport asyncioimport osfrom ag2 import Agentfrom ag2.config import OpenAICompletionsConfigfrom ag2.extensions.e2b import E2BEnvironmentfrom ag2.tools import SandboxShellToolasync 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:
textrun_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/Linuxrun_shell_command uname -a > /home/user/proof.txtrun_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/LinuxThe 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 works
| SDK call | On B3OS |
|---|---|
Sandbox.create | Templates base (the default), code-interpreter and agent (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). 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 |
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
/workspacewhatevercwdit names: the SDK runs each command through a login shell (the probe shows the login profile'scd /workspacetaking effect). E2B runs it incwd, else the user's home. Use absolute paths, orcdin the command. - Each command leads its own session, so
setsidforks and exits0at once, and output written after that is lost. Usesetsid -w, or nosetsid.
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 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.connectmints 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.connectgives them their own. - It grants what
sandboxes:rungrants 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 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.