Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 12 additions & 0 deletions .changeset/shiny-clients-bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@e2b/code-interpreter': minor
'@e2b/code-interpreter-python': minor
'@e2b/desktop': minor
'@e2b/desktop-python': minor
e2b: patch
'@e2b/python-sdk': patch
---

Add an `E2B` client 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 the Code Interpreter Python SDK) 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'
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
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()

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()

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({
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({
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()
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()

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()

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')
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()

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