Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/shiny-clients-bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@e2b/code-interpreter': minor
'@e2b/code-interpreter-python': minor
e2b: patch
'@e2b/python-sdk': patch
---

Add an `E2B` client to the Code Interpreter SDKs that binds the connection configuration explicitly, so the API key and domain no longer have to come from the environment variables: `new E2B({ apiKey, domain }).Sandbox.create()` in JavaScript and `E2B(api_key=..., domain=...).Sandbox.create()` in Python. The client exposes the package's own `Sandbox` (and `AsyncSandbox` in Python) together with the core `Volume`, `Template` and `Secret` resources, per-call options still take precedence, and multiple clients are isolated from each other and from the env-configured top-level exports.

Binding the configuration to a class now lives on the resource classes themselves (internally), so the core and the downstream clients share one implementation instead of each package generating bound subclasses on its own.
28 changes: 26 additions & 2 deletions packages/code-interpreter-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,32 @@ const execution = await sbx.runCode('x+=1; x')
console.log(execution.text) // outputs 2
```

### 4. Check docs
### 4. Bind the configuration to a client

The top-level `Sandbox` export reads its 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 resources it exposes:

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

const client = new E2B({ apiKey: 'e2b_***', domain: 'e2b.dev' })

const sbx = await client.Sandbox.create()
const execution = await sbx.runCode('x = 1; x += 1; x')

// The core resources are bound to the client's configuration as well.
const volume = await client.Volume.create('my-volume')
const exists = await client.Template.exists('my-template')
await client.Secret.create('openai-api-key', 'sk-***')

// The classes can be destructured and used like the top-level ones.
const { Sandbox } = client
const paginator = Sandbox.list()
```

Per-call options still take precedence over the client's options, and clients are isolated from each other and from the env-configured top-level exports.

### 5. Check docs
Visit [E2B documentation](https://docs.e2b.dev/?utm_source=npm&utm_medium=referral&utm_campaign=readme&utm_content=code-interpreter).

### 5. E2B cookbook
### 6. E2B cookbook
Visit our [Cookbook](https://github.com/e2b-dev/e2b-cookbook/tree/main) to get inspired by examples with different LLMs and AI frameworks.
73 changes: 73 additions & 0 deletions packages/code-interpreter-js/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ConnectionOpts, Secret, Template, Volume } from 'e2b'

import { Sandbox } from './sandbox'

/**
* Connection options bound to an {@link E2B} client.
*
* Same as {@link ConnectionOpts} without `signal`, which cancels a single
* request and therefore can only be passed per call.
*/
export type E2BClientOpts = Omit<ConnectionOpts, 'signal'>

/**
* E2B client with an explicitly bound connection configuration.
*
* The resources exposed by the client ({@link E2B.Sandbox},
* {@link E2B.Volume}, {@link E2B.Template}, {@link E2B.Secret}) behave exactly
* like the top-level `Sandbox` / `Volume` / `Template` / `Secret` exports,
* except the options passed to the client are used as the defaults instead of
* the environment variables.
* Per-call options still take precedence over the client's options.
*
* Multiple clients are fully isolated from each other and from the top-level
* env-configured exports.
*
* @example
* ```ts
* import { E2B } from '@e2b/code-interpreter'
*
* const client = new E2B({ apiKey: 'e2b_...', domain: 'e2b.dev' })
*
* const sandbox = await client.Sandbox.create()
* const execution = await sandbox.runCode('x = 1; x += 1; x')
* ```
*/
export class E2B {
/**
* Code Interpreter `Sandbox` class bound to this client's connection
* configuration.
*/
readonly Sandbox: typeof Sandbox

/**
* `Volume` class bound to this client's connection configuration.
*/
readonly Volume: typeof Volume

/**
* `Template` bound to this client's connection configuration. Both the
* builder (`client.Template()`) and the statics
* (`client.Template.build(...)`, `client.Template.exists(...)`, …) work like
* the top-level `Template`.
*/
readonly Template: typeof Template

/**
* `Secret` class bound to this client's connection configuration.
*/
readonly Secret: typeof Secret

/**
* Create a new client with the connection options bound to it.
*
* @param opts connection options used as the defaults for every call made
* through this client's resource classes.
*/
constructor(opts?: E2BClientOpts) {
this.Sandbox = Sandbox.withOpts(opts)
this.Volume = Volume.withOpts(opts)
this.Template = Template.withOpts(opts)
this.Secret = Secret.withOpts(opts)
}
}
1 change: 1 addition & 0 deletions packages/code-interpreter-js/src/index.ts
Original file line number Diff line number Diff line change
@@ -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.

export { Sandbox } from './sandbox'
export type {
Context,
Expand Down
227 changes: 227 additions & 0 deletions packages/code-interpreter-js/tests/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import {
createServer,
IncomingMessage,
Server,
ServerResponse,
} from 'node:http'
import { AddressInfo } from 'node:net'
import { afterAll, assert, beforeAll, beforeEach, test } from 'vitest'

import { E2B, Sandbox, Volume } from '../src'

const API_KEY_A = `e2b_${'a'.repeat(40)}`
const API_KEY_B = `e2b_${'b'.repeat(40)}`
const ENV_API_KEY = `e2b_${'e'.repeat(40)}`

const DOMAIN_A = 'client-a.test'
const DOMAIN_B = 'client-b.test'
const DOMAIN_ENV = 'env.test'

interface RecordedRequest {
path: string
apiKey?: string
body: unknown
}

let requests: RecordedRequest[] = []
let server: Server
let apiUrl: string

const lastRequest = () => requests[requests.length - 1]
const apiKeys = () => requests.map((request) => request.apiKey)

async function handler(req: IncomingMessage, res: ServerResponse) {
const chunks: Buffer[] = []
for await (const chunk of req) {
chunks.push(chunk as Buffer)
}
const raw = Buffer.concat(chunks).toString()

requests.push({
path: req.url ?? '',
apiKey: (req.headers['x-api-key'] as string | undefined) ?? undefined,
body: raw ? JSON.parse(raw) : undefined,
})

const respond = (status: number, body: unknown) => {
res.writeHead(status, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(body))
}

const path = req.url ?? ''
if (path.startsWith('/sandboxes')) {
respond(201, {
sandboxID: 'test-sandbox-id',
templateID: 'base',
envdVersion: '0.2.4',
})
} else if (path.startsWith('/volumes')) {
respond(201, {
volumeID: 'test-volume-id',
name: 'test-volume',
token: 'test-volume-token',
})
} else if (path.startsWith('/templates/aliases/')) {
respond(200, { aliases: [], templateID: 'test-template-id' })
} else if (path.startsWith('/secrets')) {
respond(201, {
secretID: 'test-secret-id',
name: 'test-secret',
currentVersion: 1,
metadata: {},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
} else {
respond(404, { code: 404, message: 'not found' })
}
}

const envBackup: Record<string, string | undefined> = {}
const envOverrides: Record<string, string | undefined> = {
E2B_API_KEY: ENV_API_KEY,
E2B_DOMAIN: DOMAIN_ENV,
E2B_API_URL: undefined,
E2B_SANDBOX_URL: undefined,
E2B_DEBUG: undefined,
}

beforeAll(async () => {
for (const [key, value] of Object.entries(envOverrides)) {
envBackup[key] = process.env[key]
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}

server = createServer(handler)
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
apiUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
})

afterAll(async () => {
await new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve()))
)

for (const [key, value] of Object.entries(envBackup)) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
})

beforeEach(() => {
requests = []
})

test('client.Sandbox.create uses the client config instead of env vars', async () => {
const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })

const sandbox = await client.Sandbox.create()

Check failure on line 125 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > client.Sandbox.create uses the client config instead of env vars

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:125:19 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }

assert.deepEqual(apiKeys(), [API_KEY_A])
assert.equal(sandbox.sandboxDomain, DOMAIN_A)
// The Code Interpreter template is still the default.
assert.equal(
(lastRequest().body as { templateID: string }).templateID,
'code-interpreter-v1'
)
})

test('client.Sandbox instances keep the Code Interpreter API', async () => {
const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })

assert.notEqual(client.Sandbox, Sandbox)
assert.isTrue(client.Sandbox.prototype instanceof Sandbox)

const sandbox = await client.Sandbox.create()

Check failure on line 142 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > client.Sandbox instances keep the Code Interpreter API

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:142:19 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }

assert.instanceOf(sandbox, Sandbox)
assert.instanceOf(sandbox, client.Sandbox)
assert.isFunction(sandbox.runCode)
assert.isFunction(sandbox.createCodeContext)
})

test('per-call options take precedence over the client config', async () => {
const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })

const sandbox = await client.Sandbox.create({

Check failure on line 153 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > per-call options take precedence over the client config

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:153:19 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }
apiKey: API_KEY_B,
domain: DOMAIN_B,
})

assert.deepEqual(apiKeys(), [API_KEY_B])
assert.equal(sandbox.sandboxDomain, DOMAIN_B)
})

test('per-call options explicitly set to undefined keep the client config', async () => {
const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })

const sandbox = await client.Sandbox.create({

Check failure on line 165 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > per-call options explicitly set to undefined keep the client config

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:165:19 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }
apiKey: undefined,
domain: undefined,
})

assert.deepEqual(apiKeys(), [API_KEY_A])
assert.equal(sandbox.sandboxDomain, DOMAIN_A)
})

test('two clients with different configs stay isolated', async () => {
const clientA = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })
const clientB = new E2B({ apiKey: API_KEY_B, domain: DOMAIN_B, apiUrl })

const sandboxA = await clientA.Sandbox.create()

Check failure on line 178 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > two clients with different configs stay isolated

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:178:20 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }
const sandboxB = await clientB.Sandbox.create()

assert.deepEqual(apiKeys(), [API_KEY_A, API_KEY_B])
assert.equal(sandboxA.sandboxDomain, DOMAIN_A)
assert.equal(sandboxB.sandboxDomain, DOMAIN_B)
})

test('mutating the options object does not change the bound config', async () => {
const opts = { apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl }
const client = new E2B(opts)
opts.apiKey = API_KEY_B

await client.Sandbox.create()

Check failure on line 191 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > mutating the options object does not change the bound config

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:191:3 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }

assert.deepEqual(apiKeys(), [API_KEY_A])
})

test('client.Sandbox can be rebound to a variable', async () => {
const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })
const S = client.Sandbox

const sandbox = await S.create()

Check failure on line 200 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > client.Sandbox can be rebound to a variable

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:200:19 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }

assert.deepEqual(apiKeys(), [API_KEY_A])
assert.equal(sandbox.sandboxDomain, DOMAIN_A)
})

test('the core resources are bound to the client config as well', async () => {
const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })

const volume = await client.Volume.create('test-volume')

Check failure on line 209 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > the core resources are bound to the client config as well

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.create ../js-sdk/src/volume/index.ts:129:17 ❯ tests/client.test.ts:209:18 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }
assert.instanceOf(volume, Volume)
assert.deepEqual(apiKeys(), [API_KEY_A])

assert.isTrue(await client.Template.exists('test-template'))
await client.Secret.create('test-secret', 'value')

assert.deepEqual(apiKeys(), [API_KEY_A, API_KEY_A, API_KEY_A])
})

test('the top-level Sandbox keeps using the environment configuration', async () => {
const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A, apiUrl })
await client.Sandbox.create()

Check failure on line 221 in packages/code-interpreter-js/tests/client.test.ts

View workflow job for this annotation

GitHub Actions / Production / Code Interpreter JS SDK Tests / Code Interpreter JS SDK - cloudflare

[cloudflare] tests/client.test.ts > the top-level Sandbox keeps using the environment configuration

Error: Network connection lost. ❯ coreFetch ../../node_modules/.pnpm/openapi-fetch@0.14.1/node_modules/openapi-fetch/src/index.js:162:20 ❯ _Class.createSandbox ../js-sdk/src/sandbox/sandboxApi.ts:1689:17 ❯ _Class.create ../js-sdk/src/sandbox/index.ts:325:35 ❯ tests/client.test.ts:221:3 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { remote: true, retryable: true }

const sandbox = await Sandbox.create({ apiUrl })

assert.equal(lastRequest().apiKey, ENV_API_KEY)
assert.equal(sandbox.sandboxDomain, DOMAIN_ENV)
})
31 changes: 29 additions & 2 deletions packages/code-interpreter-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,35 @@ with Sandbox.create() as sandbox:
print(execution.text) # outputs 2
```

### 4. Check docs
### 4. Bind the configuration to a client

The top-level `Sandbox` and `AsyncSandbox` 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:

```py
from e2b_code_interpreter import E2B

client = E2B(api_key="e2b_***", domain="e2b.dev")

with client.Sandbox.create() as sandbox:
execution = sandbox.run_code("x = 1; x += 1; x")

# The async variant is exposed as well.
async_sandbox = await client.AsyncSandbox.create()

# The core resources are bound to the client's configuration as well.
volume = client.Volume.create("my-volume")
exists = client.Template.exists("my-template")
secret = client.Secret.create("openai-api-key", "sk-***")

# 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. Check docs
Visit [E2B documentation](https://docs.e2b.dev/?utm_source=pypi&utm_medium=referral&utm_campaign=readme&utm_content=code-interpreter).

### 5. E2B cookbook
### 6. E2B cookbook
Visit our [Cookbook](https://github.com/e2b-dev/e2b-cookbook/tree/main) to get inspired by examples with different LLMs and AI frameworks.
Loading
Loading