## Summary Adds an `E2B` client to both SDKs so a process can talk to several API keys / domains / deployments without going through environment variables. The client binds a connection config once and exposes the resource surfaces off it; the named top-level exports are untouched and keep reading the environment. Nothing existing changes (changeset is `minor`): the default export is still `Sandbox`, `Template(...)` keeps working, and `E2B` is a new named export. Two follow-ups are tracked for v3: making `E2B` the default export ([SDK-341](https://linear.app/e2b/issue/SDK-341/sdk-v3-js-make-e2b-the-default-export-instead-of-sandbox)) and dropping the `Template` Proxy in favour of `new Template()` ([SDK-342](https://linear.app/e2b/issue/SDK-342/sdk-v3-js-drop-the-template-proxy-require-new-template)). ```ts import { E2B } from 'e2b' const { Sandbox, Volume, Template, Secret } = new E2B({ apiKey: 'e2b_***', domain: 'e2b.dev', }) const sandbox = await Sandbox.create() const volume = await Volume.create('my-volume') const exists = await Template.exists('my-template') await Template.build(Template().fromPythonImage('3'), 'my-env') await Secret.create('openai-api-key', 'sk-***') // Per-call options still win over the client's options. await Sandbox.create({ apiKey: 'e2b_other***' }) ``` ```python from e2b import E2B client = E2B(api_key="e2b_***", domain="e2b.dev") Sandbox, Volume, Template = client.Sandbox, client.Volume, client.Template Secret = client.Secret sandbox = Sandbox.create() volume = Volume.create("my-volume") exists = Template.exists("my-template") secret = Secret.create("openai-api-key", "sk-***") # Async variants are exposed too. AsyncSandbox, AsyncTemplate = client.AsyncSandbox, client.AsyncTemplate async_sandbox = await AsyncSandbox.create() await AsyncTemplate.exists("my-template") ``` ### Mechanism `client.Sandbox` / `client.Volume` / `client.Template` / `client.Secret` (plus the `Async*` variants in Python) are per-client subclasses of the real classes, carrying the bound opts as class-level state. Nothing process-global is mutated, so clients are isolated from each other and from the default path, and `cls`/`this` dispatch is preserved (`create` on a client class returns an instance of that client class). ```ts // sandboxApi.ts / volume/index.ts / template/index.ts / secret.ts — one hook per class hierarchy protected static readonly boundOpts?: ConnectionOpts // undefined on the base classes protected static resolveOpts<T extends ConnectionOpts>(opts?: T) { return ConnectionConfig.mergeOpts(this.boundOpts, opts) // { ...bound, ...definedPerCall } } // every static method that built a config from raw opts now does - const config = new ConnectionConfig(opts) + const apiOpts = this.resolveOpts(opts) + const config = new ConnectionConfig(apiOpts) ``` ```py # sandbox/main.py, volume_sync.py, volume_async.py, template_{sync,async}/main.py, secret/base.py _bound_api_params: ApiParams = {} # empty on the base classes @classmethod def _resolve_api_params(cls, **opts: Unpack[ApiParams]) -> ApiParams: return merge_api_params(cls._bound_api_params, opts) - config = ConnectionConfig(**opts) + config = ConnectionConfig(**cls._resolve_api_params(**opts)) ``` `Template` used to be a factory function whose statics were pre-bound to `TemplateBase`, which left no class for a client to subclass. It is now the `TemplateBase` class itself, wrapped in a `Proxy` whose only trap makes it callable without `new`, so `Template(...)` keeps working (no breaking change) while `client.Template` is a plain subclass like Sandbox/Volume and `Template.build(...)` resolves `this` naturally: ```ts export function callableTemplate<T extends typeof TemplateBase>(cls: T) { return new Proxy(cls, { apply: (target, _this, args) => new target(...args) }) } export const Template = callableTemplate(TemplateBase) // Template() still returns a builder this.Template = callableTemplate(class extends TemplateBase { boundOpts }) // client.Template ``` Because the trap only intercepts calls, `new Template()`, statics, `instanceof` and subclassing all go straight to the class, and the builder's default file context (`getCallerDirectory()`) still resolves to the user's frame (the trap's frame is inside the SDK and filtered like the old factory's). Two side effects of routing everything through the hook: - Static methods that resolved config off the base class had to move to `this`/`cls`: `SandboxApi.createSandbox(...)` → `this.createSandbox(...)` in JS, `new Volume(...)` → `new this(...)`, and several Python `@staticmethod`s (`SandboxApi.list`, `_cls_list_snapshots`, `delete_snapshot`, `Volume._class_get_info` / `_class_list` / `destroy`, and the `Secret` operations) became `@classmethod`s. Behavior for the top-level classes is unchanged since their bound opts are empty. - `DualMethod.__get__` (the descriptor behind `Volume.get_info` / `Volume.list` working both on the class and on instances) now binds the class-level function to the accessed class, so `client.Volume.list()` sees the subclass' bound params instead of `Volume`'s. Per-call values explicitly set to `undefined` / `None` are dropped when merging, so they fall back to the client's opts rather than clearing them into the env-var path. ### Tests `packages/js-sdk/tests/client.test.ts` (MSW) and `packages/python-sdk/tests/test_client.py` (local HTTP server, sync + async) cover: the client's API key/domain being used instead of the env vars, per-call precedence, rebinding the class (`const S = client.Sandbox`), rebound `client.Template`, the client template builder producing the same Dockerfile as the top-level one, two clients staying isolated, generated-subclass instances, `client.Secret` (sync + async) using the bound config, the top-level classes still using the env config with empty bound opts and the default export still being `Sandbox`. Link to Devin session: https://app.devin.ai/sessions/772afa048b814ad784b5dde0a599df46 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
3.6 KiB
What is E2B?
E2B is an open-source infrastructure that allows you to run AI-generated code in secure isolated sandboxes in the cloud. To start and control sandboxes, use our JavaScript SDK or Python SDK.
Run your first Sandbox
1. Install SDK
pip install e2b
2. Get your E2B API key
E2B_API_KEY=e2b_***
3. Start a sandbox and run commands
from e2b import Sandbox
with Sandbox.create() as sandbox:
result = sandbox.commands.run('echo "Hello from E2B!"')
print(result.stdout) # Hello from E2B!
4. Bind the configuration to a client
The top-level Sandbox, AsyncSandbox, Volume, AsyncVolume, Template, AsyncTemplate and Secret exports read their configuration from the environment variables. To use an explicit configuration — e.g. several API keys or domains in one process — create an E2B client and use the resource classes it exposes:
from e2b import E2B
client = E2B(api_key="e2b_***", domain="e2b.dev")
sandbox = client.Sandbox.create()
volume = client.Volume.create("my-volume")
exists = client.Template.exists("my-template")
secret = client.Secret.create("openai-api-key", "sk-***")
# The async variants are exposed as well.
async_sandbox = await client.AsyncSandbox.create()
# The classes can be assigned and used like the top-level ones.
Sandbox = client.Sandbox
paginator = Sandbox.list()
Per-call params still take precedence over the client's params, and clients are isolated from each other and from the env-configured top-level exports.
5. Code execution with Code Interpreter
If you need run_code(), install the Code Interpreter SDK:
pip install e2b-code-interpreter
from e2b_code_interpreter import Sandbox
with Sandbox.create() as sandbox:
execution = sandbox.run_code("x = 1; x += 1; x")
print(execution.text) # outputs 2
6. Check docs
Visit E2B documentation.
7. E2B cookbook
Visit our Cookbook to get inspired by examples with different LLMs and AI frameworks.
