Skip to content
Closed
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
8 changes: 8 additions & 0 deletions .changeset/shiny-clients-bind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@e2b/code-interpreter': minor
'@e2b/code-interpreter-python': minor
'@e2b/desktop': minor
'@e2b/desktop-python': minor
---

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.
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.
86 changes: 86 additions & 0 deletions packages/code-interpreter-js/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ConnectionOpts, E2B as CoreE2B, 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) {
// Options are copied so later mutations of the caller's object cannot
// change the bound configuration. `signal` is dropped rather than only
// typed away, since it cancels a single request and a caller passing a
// wider-typed object (or plain JS) would otherwise bind it to every call.
const boundOpts: E2BClientOpts = { ...(opts ?? {}) }
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
delete (boundOpts as ConnectionOpts).signal

this.Sandbox = class extends Sandbox {
protected static override readonly boundOpts = boundOpts
}

// The resources that are not specific to the Code Interpreter are bound by
// the core client.
const core = new CoreE2B(boundOpts)
this.Volume = core.Volume
this.Template = core.Template
this.Secret = core.Secret
}
}
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)
})
Loading
Loading