Skip to content

feat(sdk): add an E2B client to the Code Interpreter SDKs - #1783

Open
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1787774000-code-interpreter-e2b-client
Open

feat(sdk): add an E2B client to the Code Interpreter SDKs#1783
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1787774000-code-interpreter-e2b-client

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an E2B client to @e2b/code-interpreter and e2b-code-interpreter, so the API key/domain can be bound explicitly instead of coming from the environment. packages/js-sdk and packages/python-sdk are untouched: the binding is local to this package and relies only on the existing ClientFactory class state (boundOpts / _bound_api_params) the downstream Sandbox already inherits.

import { E2B } from '@e2b/code-interpreter'

const client = new E2B({ apiKey, domain })
const sandbox = await client.Sandbox.create() // code-interpreter-v1, bound config
await sandbox.runCode('x = 1; x += 1; x')
from e2b_code_interpreter import E2B

client = E2B(api_key=..., domain=...)
sandbox = client.Sandbox.create()
sandbox.run_code("x = 1; x += 1; x")

The mechanism, per package:

// src/client.ts — Code Interpreter Sandbox is bound here…
this.Sandbox = class extends Sandbox {
  protected static override readonly boundOpts = boundOpts
}
// …the non-Code-Interpreter resources come from the core client.
const core = new CoreE2B(boundOpts)
this.Volume = core.Volume
this.Template = core.Template
this.Secret = core.Secret
# client.py
def _bind(cls, api_params):
    return type(cls.__name__, (cls,), {"_bound_api_params": api_params})

self.Sandbox = _bind(Sandbox, api_params)
self.AsyncSandbox = _bind(AsyncSandbox, api_params)
core = CoreE2B(**api_params)  # Volume/Template/Secret (+ async variants)

Options are copied (and the nested headers / apiHeaders maps snapshotted, signal dropped) so later mutations of the caller's object can't change what's bound.

Precedence is the core one, unchanged: per-call options > client options > env; explicit undefined/None does not clear a bound value. Clients are isolated from each other and from the env-configured top-level exports.

Also: the new JS suite serves a mocked API from a local node:http server, which workerd can't listen on, so it's excluded from the Cloudflare runtime leg (the Node/bun/deno legs keep running it).

Link to Devin session: https://app.devin.ai/sessions/ec1af68649fd4880a1cb27cb51d819e9
Open in Devin Desktop: https://app.devin.ai/desktop/session/ec1af68649fd4880a1cb27cb51d819e9?variant=devin
Requested by: @mishushakov

Move the configuration binding onto the resource classes in the core, so
the core and the downstream clients share one implementation.

Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@cla-bot cla-bot Bot added the cla-signed label Aug 27, 2026
@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ecd493b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@e2b/code-interpreter Minor
@e2b/code-interpreter-python Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 4f31712. Download artifacts from this workflow run.

JS SDK (e2b@2.46.2-devin-1787774000-code-interpreter-e2b-client.0):

npm install ./e2b-2.46.2-devin-1787774000-code-interpreter-e2b-client.0.tgz

CLI (@e2b/cli@2.18.1-devin-1787774000-code-interpreter-e2b-client.0):

npm install ./e2b-cli-2.18.1-devin-1787774000-code-interpreter-e2b-client.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.7.3-devin-1787774000-code-interpreter-e2b-client.0):

npm install ./e2b-code-interpreter-2.7.3-devin-1787774000-code-interpreter-e2b-client.0.tgz

Desktop JS SDK (@e2b/desktop@2.3.4-devin-1787774000-code-interpreter-e2b-client.0):

npm install ./e2b-desktop-2.3.4-devin-1787774000-code-interpreter-e2b-client.0.tgz

Python SDK (e2b==2.46.1+devin.1787774000.code.interpreter.e2b.client):

pip install ./e2b-2.46.1+devin.1787774000.code.interpreter.e2b.client-py3-none-any.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.9.2+devin.1787774000.code.interpreter.e2b.client):

pip install ./e2b_code_interpreter-2.9.2+devin.1787774000.code.interpreter.e2b.client-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.4.5+devin.1787774000.code.interpreter.e2b.client):

pip install ./e2b_desktop-2.4.5+devin.1787774000.code.interpreter.e2b.client-py3-none-any.whl

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TASTE.md review (sdk-harness) of the new E2B client plumbing.

Checked: cross-language parity (T-1, T-2), API shape and option-type conventions (T-3, T-19, T-22, T-23), module/entry-point structure (T-54), configuration precedence and immutability (T-49–T-51), naming (T-9, T-12).

The core design holds up well: required-vs-optional shape is right (new E2B(opts?) / E2B(**opts), T-3), per-call options taking precedence keeps the explicit → env → default chain intact (T-49), the bound options are snapshotted rather than aliased (T-51), and hoisting the binding into ClientFactory so both SDKs and both downstream packages share one implementation is exactly the consistency T-1/T-2 ask for.

4 violations, all in the wiring rather than the design:

  • 2× T-54 (entry-point/export hygiene)
  • 1× T-23 (inline option type in a signature)
  • 1× T-1a/T-22 (withOpts vs _with_params naming parity)

Not tied to a changed line: E2BClientParams in packages/code-interpreter-python/e2b_code_interpreter/client.py (and its e2b counterpart it mirrors) breaks T-22, which asks for the Opts suffix in both languages — E2BClientOpts in JS, so E2BClientOpts in Python too. Since the name is already shipped in the core e2b package, I'm not asking for it here alone; renaming both together in one change is the fix, and doing it now while the downstream client is still unreleased is cheaper than later. Same root cause as the _with_params comment below.

@@ -1,5 +1,6 @@
export * from 'e2b'

export { E2B, type E2BClientOpts } from './client'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-54 — one flat entry point, and in JS "runtime values use export and type-only names use export type — never mixed". This line mixes the class and the type alias in one statement; every other export in this file (and in js-sdk/src/index.ts) keeps them apart.

Suggested change
export { E2B, type E2BClientOpts } from './client'
export { E2B } from './client'
export type { E2BClientOpts } from './client'

Note the same mixed line already exists at packages/js-sdk/src/index.ts:160 from the earlier client PR — worth fixing there in this stack too rather than propagating it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — export { E2B } and export type { E2BClientOpts } are now separate statements (4b90c98). Left packages/js-sdk/src/index.ts alone: this PR no longer touches the core SDKs at all.

Template,
Volume,
)
from e2b.connection_config import ApiParams

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-54 — "one flat entry point per package … no subpath exports". Reaching into e2b.connection_config from a different package pins this client to the core's internal module layout, and it isn't necessary: ApiParams is already in e2b.__all__ (packages/python-sdk/e2b/__init__.py). The JS sibling gets this right (import { ConnectionOpts, … } from 'e2b').

Suggested change
from e2b.connection_config import ApiParams
from e2b import ApiParams

(Or fold ApiParams into the from e2b import (...) block above and drop this line.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — now from e2b import ApiParams (4b90c98).

Comment thread packages/js-sdk/src/connectionConfig.ts Outdated
* @hidden
* @hide
*/
static withOpts<T>(this: T, opts?: Omit<ConnectionOpts, 'signal'>): T {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-23 — "the option type is always named and exported from the entry point, never an inline intersection in the signature". Omit<ConnectionOpts, 'signal'> is spelled inline here, again in TemplateBase.withOpts (packages/js-sdk/src/template/index.ts), and a third time in packages/code-interpreter-js/src/client.ts — while the identical shape already has a name, E2BClientOpts. Three inline copies mean a future change to what is bindable has to be found by grep instead of following the type.

Declare the alias here in connectionConfig.ts (client.ts imports from this module, so it can't live there without a cycle) and re-export it unchanged from client.ts / index.ts, then:

Suggested change
static withOpts<T>(this: T, opts?: Omit<ConnectionOpts, 'signal'>): T {
static withOpts<T>(this: T, opts?: E2BClientOpts): T {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obsolete: ClientFactory.withOpts is gone. packages/js-sdk and packages/python-sdk are untouched now; the Code Interpreter client binds its own Sandbox locally (anonymous subclass carrying boundOpts) and delegates Volume/Template/Secret to the core E2B client.

"""

@classmethod
def _with_params(cls, **api_params: Unpack[ApiParams]) -> Type[Self]:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-1a / T-22 — the three surfaces "mirror each other 1:1 in names and semantics, differing only by language idiom", and option-shaped things carry the Opts suffix in both languages. This is the same operation as JS ClientFactory.withOpts, but the names diverge twice over: withOpts_with_params swaps Opts for params and changes visibility (public + @internal in JS, underscore-private in Python), so the shared vocabulary downstream packages have to learn is different per language.

Pick one spelling and mirror it — withOptswith_opts if it stays part of the (documented-as-internal) cross-package surface, _withOpts_with_opts if it should be private in both. Same for the parameter name (api_paramsopts) and _bound_api_paramsboundOpts. Not suggesting a patch inline because the rename touches the body and the callers in e2b/client.py, e2b_code_interpreter/client.py, and the tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obsolete: _with_params is gone — the core Python SDK is untouched; binding is a local _bind(cls, api_params) helper in e2b_code_interpreter/client.py. On E2BClientParams vs E2BClientOpts: keeping E2BClientParams here because that's the name the released core e2b package already uses for the same type, and renaming would mean changing the core SDK, which is explicitly out of scope for this PR. Happy to do the parity rename (core + both downstream packages) in a follow-up.

devin-ai-integration Bot and others added 2 commits August 27, 2026 21:22
…re SDKs untouched

Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
@mishushakov

Copy link
Copy Markdown
Member

can you implement taste suggestions if sill relevant

Split the type-only export out of the runtime one, import ApiParams from the
e2b entry point instead of its module, and drop the computed __all__.

Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
@mishushakov
mishushakov marked this pull request as ready for review August 28, 2026 14:24
@mishushakov
mishushakov self-requested a review as a code owner August 28, 2026 14:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b90c9848b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

from typing import Dict, Type, TypeVar, cast

from e2b import ApiParams
from e2b import E2B as CoreE2B

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require the core SDK version that provides E2B

When an environment resolves any currently allowed e2b version from 2.39.1 through 2.43.x, this import raises ImportError because the core E2B client was introduced in 2.44.0. Since e2b_code_interpreter.__init__ imports this module unconditionally, even existing imports such as from e2b_code_interpreter import Sandbox stop working in those valid dependency environments; raise the e2b lower bound in packages/code-interpreter-python/pyproject.toml to the first compatible release.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the floor needed raising — fixed. E2B and ApiParams both first shipped in e2b 2.44.0 (release commit 33195ae, changeset 5759f17), while the floor was >=2.39.1, so a resolution to 2.39.1–2.43.x would ImportError on import e2b_code_interpreter. Now e2b>=2.44.0,<3.0.0 (ecd493b); uv.lock is unchanged since the workspace resolves e2b as an editable path dependency. Same bump applied to packages/desktop-python in #1784.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — a straightforward, symmetric extension of the existing js-sdk/python-sdk E2B client mechanism (from PR #1720, unmodified here) into the Code Interpreter packages.

What was reviewed: the new client.ts/client.py binding logic (defensive copy of opts, headers/apiHeaders snapshot, signal drop in JS with no Python equivalent needed), the Sandbox/AsyncSandbox subclassing via boundOpts/_bound_api_params against the pre-existing ClientFactory/mergeOpts base in js-sdk, and the Cloudflare vitest exclusion (justified — the test spins up a node:http server workerd can't bind). Tests in both packages cover precedence, isolation between clients, and mutation-safety of bound options/headers.

Extended reasoning...

Overview

The PR adds an E2B client class to both @ e2b/code-interpreter (JS) and e2b-code-interpreter (Python) that lets callers bind connection config (API key, domain, etc.) explicitly instead of relying on environment variables. It reuses a mechanism already merged into packages/js-sdk and packages/python-sdk in a prior PR (#1720): a ClientFactory base class with a boundOpts/_bound_api_params static field and a mergeOpts helper that gives per-call options precedence over bound options over env vars. This PR's new code just subclasses Sandbox/AsyncSandbox with the bound options and delegates Volume/Template/Secret (and Python's async variants) to a bound core E2B client instance — no changes to js-sdk or python-sdk themselves, confirmed by git log showing boundOpts/ClientFactory/mergeOpts predate this diff.

Security risks

None identified. This is client-side configuration binding with no new network surface, no crypto, no auth bypass. The defensive copying of the options object and its nested headers/apiHeaders maps (both JS and Python) correctly prevents a caller from mutating the bound config after client construction, which is verified by dedicated tests in both suites.

Level of scrutiny

Moderate — this touches the public SDK surface (hence the changeset) and needs JS/Python parity per CLAUDE.md, but the underlying mechanism was already reviewed and merged separately, and this diff is a mechanical, low-risk application of that pattern to a downstream package. I checked JS/Python parity point by point (subclassing approach, defensive copying, precedence semantics, signal handling) and found it consistent; the asymmetry in signal handling (dropped explicitly in JS via delete, no analog needed in Python since ApiParams/E2BClientParams has no signal field at all) is correctly reasoned, not an oversight.

Other factors

Test coverage is thorough on both sides (244 lines JS, 236 lines Python) covering precedence, cross-client isolation, isolation from env-configured top-level exports, and mutation-safety. The Cloudflare vitest config exclusion is narrowly scoped and well-commented, with the Node/bun/deno legs still running the excluded test. The inline review comments left by the bot's own earlier pass appear addressed by the subsequent commit (4b90c98) predating the human comment from mishushakov, and there is no outstanding CHANGES_REQUESTED review from a third party. No bugs were reported by the automated bug hunter for this diff.

@mishushakov

Copy link
Copy Markdown
Member

can you check codex comment do we need to raise min version ?

The client imports E2B and ApiParams from the e2b entry point, both of which
first shipped in 2.44.0.

Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant