Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental stateless JavaScript SDK #313

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/js_rc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: JS SDK Release Candidate

on:
pull_request:

permissions:
contents: write

jobs:
release:
name: Release Candidate
if: ${{ contains( github.event.pull_request.labels.*.name, 'js-rc') }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/js-sdk

steps:
- name: Checkout Repo
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}

- uses: pnpm/action-setup@v3

- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: pnpm

- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Release Candidate
run: |
npm version prerelease --preid=${{ github.head_ref }}
npm publish --tag rc
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Commit new versions
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -am "[skip ci] Release new versions" || exit 0
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion packages/js-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "e2b",
"version": "0.12.1",
"version": "0.12.2-stateless-sdk.7",
"description": "E2B SDK that give agents cloud environments",
"homepage": "https://e2b.dev",
"license": "MIT",
Expand Down
17 changes: 17 additions & 0 deletions packages/js-sdk/src/api/schema.gen.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export { Process, ProcessMessage, ProcessOutput } from './sandbox/process'
export type { ProcessManager } from './sandbox/process'
export type { EnvVars } from './sandbox/envVars'
export { runCode, CodeRuntime } from './runCode' // Export CodeRuntime enum as value, not as type, so it can be actually used in consumer code
import { Sandbox } from './sandbox/index'
import { Sandbox } from './sandbox'

import { DataAnalysis } from './templates/dataAnalysis'
export { DataAnalysis as CodeInterpreter }
Expand All @@ -32,3 +32,6 @@ export type { Action } from './sandbox/index'

export { Sandbox }
export default Sandbox

// Experimental
export * as experimental_stateless from './sandbox/stateless'
47 changes: 23 additions & 24 deletions packages/js-sdk/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type DownloadFileFormat =
| 'blob'
| 'buffer'
| 'arraybuffer'
| 'text';
| 'text'

export interface SandboxOpts extends SandboxConnectionOpts {
onScanPorts?: ScanOpenPortsHandler;
Expand All @@ -37,7 +37,7 @@ export interface Action<S extends Sandbox = Sandbox, T = {

/**
* E2B cloud sandbox gives your agent a full cloud development environment that's sandboxed.
*
*
* That means:
* - Access to Linux OS
* - Using filesystem (create, list, and delete files and dirs)
Expand All @@ -47,16 +47,16 @@ export interface Action<S extends Sandbox = Sandbox, T = {
*
* Check usage docs - https://e2b.dev/docs/sandbox/overview
*
* These cloud sandboxes are meant to be used for agents. Like a sandboxed playgrounds, where the agent can do whatever it wants.
*
* These cloud sandboxes are meant to be used for agents. Like a sandboxed playgrounds, where the agent can do whatever it wants.
*
* Use the {@link Sandbox.create} method to create a new sandbox.
*
*
* @example
* ```ts
* import { Sandbox } from '@e2b/sdk'
*
*
* const sandbox = await Sandbox.create()
*
*
* await sandbox.close()
* ```
*/
Expand All @@ -81,7 +81,7 @@ export class Sandbox extends SandboxConnection {

/**
* Use `Sandbox.create()` instead.
*
*
* @hidden
* @hide
* @internal
Expand Down Expand Up @@ -412,8 +412,7 @@ export class Sandbox extends SandboxConnection {
startAndWait: async (optsOrCmd: string | ProcessOpts) => {
const opts = typeof optsOrCmd === 'string' ? { cmd: optsOrCmd } : optsOrCmd
const process = await this.process.start(opts)
const out = await process.wait()
return out
return process.wait(opts.timeout)
}
}

Expand All @@ -428,7 +427,7 @@ export class Sandbox extends SandboxConnection {
* If a file with the same name already exists, it will be overwritten.
*/
get fileURL() {
const protocol = this.getProtocol('http', this.opts.__debug_devEnv !== "local")
const protocol = this.getProtocol('http', this.opts.__debug_devEnv !== 'local')
const hostname = this.getHostname(this.opts.__debug_port || ENVD_PORT)
return `${protocol}://${hostname}${FILE_ROUTE}`
}
Expand Down Expand Up @@ -463,7 +462,7 @@ export class Sandbox extends SandboxConnection {
* ```
* @constructs Sandbox
*/
static async create<S extends typeof Sandbox>(this: S): Promise<InstanceType<S>>;
static async create<S extends typeof Sandbox>(this: S): Promise<InstanceType<S>>
/**
* Creates a new Sandbox from the template with the specified ID.
* @param template Sandbox template ID or name
Expand All @@ -474,7 +473,7 @@ export class Sandbox extends SandboxConnection {
* const sandbox = await Sandbox.create("sandboxTemplateID")
* ```
*/
static async create<S extends typeof Sandbox>(this: S, template: string): Promise<InstanceType<S>>;
static async create<S extends typeof Sandbox>(this: S, template: string): Promise<InstanceType<S>>
/**
* Creates a new Sandbox from the specified options.
* @param opts Sandbox options
Expand All @@ -488,7 +487,7 @@ export class Sandbox extends SandboxConnection {
* })
* ```
*/
static async create<S extends typeof Sandbox>(this: S, opts: SandboxOpts): Promise<InstanceType<S>>;
static async create<S extends typeof Sandbox>(this: S, opts: SandboxOpts): Promise<InstanceType<S>>
static async create(optsOrTemplate?: string | SandboxOpts) {
const opts: SandboxOpts | undefined = typeof optsOrTemplate === 'string' ? { template: optsOrTemplate } : optsOrTemplate
const sandbox = new Sandbox(opts)
Expand All @@ -506,14 +505,14 @@ export class Sandbox extends SandboxConnection {
* ```ts
* const sandbox = await Sandbox.create()
* const sandboxID = sandbox.id
*
*
* await sandbox.keepAlive(300 * 1000)
* await sandbox.close()
*
*
* const reconnectedSandbox = await Sandbox.reconnect(sandboxID)
* ```
*/
static async reconnect<S extends typeof Sandbox>(this: S, sandboxID: string): Promise<InstanceType<S>>;
static async reconnect<S extends typeof Sandbox>(this: S, sandboxID: string): Promise<InstanceType<S>>
/**
* Reconnects to an existing Sandbox.
* @param opts Sandbox options
Expand All @@ -523,16 +522,16 @@ export class Sandbox extends SandboxConnection {
* ```ts
* const sandbox = await Sandbox.create()
* const sandboxID = sandbox.id
*
*
* await sandbox.keepAlive(300 * 1000)
* await sandbox.close()
*
*
* const reconnectedSandbox = await Sandbox.reconnect({
* sandboxID,
* })
* ```
*/
static async reconnect<S extends typeof Sandbox>(this: S, opts: Omit<SandboxOpts, 'id' | 'template'> & { sandboxID: string }): Promise<InstanceType<S>>;
static async reconnect<S extends typeof Sandbox>(this: S, opts: Omit<SandboxOpts, 'id' | 'template'> & { sandboxID: string }): Promise<InstanceType<S>>
static async reconnect<S extends typeof Sandbox>(this: S, sandboxIDorOpts: string | Omit<SandboxOpts, 'id' | 'template'> & { sandboxID: string }): Promise<InstanceType<S>> {
let id: string
let opts: SandboxOpts
Expand All @@ -544,7 +543,7 @@ export class Sandbox extends SandboxConnection {
opts = sandboxIDorOpts
}

const sandboxIDAndClientID = id.split("-")
const sandboxIDAndClientID = id.split('-')
const sandboxID = sandboxIDAndClientID[0]
const clientID = sandboxIDAndClientID[1]
opts.__sandbox = { sandboxID, clientID, templateID: 'unknown' }
Expand All @@ -569,7 +568,7 @@ export class Sandbox extends SandboxConnection {
* sandbox.addAction('readFile', (sandbox, args) => sandbox.filesystem.read(args.path))
* ```
*/
addAction<T = { [name: string]: any }>(action: Action<this, T>): this;
addAction<T = { [name: string]: any }>(action: Action<this, T>): this
/**
* Add a new action with a specified name.
*
Expand All @@ -589,7 +588,7 @@ export class Sandbox extends SandboxConnection {
* sandbox.addAction(readFile)
* ```
*/
addAction<T = { [name: string]: any }>(name: string, action: Action<this, T>): this;
addAction<T = { [name: string]: any }>(name: string, action: Action<this, T>): this
addAction<T = { [name: string]: any }>(actionOrName: string | Action<this, T>, action?: Action<this, T>): this {
if (typeof actionOrName === 'string') {
if (!action) throw new Error('Action is required')
Expand Down Expand Up @@ -736,7 +735,7 @@ export class Sandbox extends SandboxConnection {
cwd: '/',
})
} catch (err) {
this.logger.debug?.("start command not started", err)
this.logger.debug?.('start command not started', err)
}
}
}
38 changes: 34 additions & 4 deletions packages/js-sdk/src/sandbox/sandboxConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ const listSandboxes = withAPIKey(
const createSandbox = withAPIKey(
api.path('/sandboxes').method('post').create(),
)
const killSandbox = withAPIKey(
api.path('/sandboxes/{sandboxID}').method('delete').create(),
)
const refreshSandbox = withAPIKey(
api.path('/sandboxes/{sandboxID}/refreshes').method('post').create(),
)
Expand Down Expand Up @@ -185,6 +188,34 @@ export class SandboxConnection {
}
}

/**
* List all running sandboxes
* @param sandboxID ID of the sandbox to kill
* @param apiKey API key to use for authentication. If not provided, the `E2B_API_KEY` environment variable will be used.
*/
static async kill(sandboxID: string, apiKey?: string): Promise<void> {
const id = sandboxID.split('-')[0]
apiKey = getApiKey(apiKey)
try {
await killSandbox(apiKey, {sandboxID: id})
} catch (e) {
if (e instanceof killSandbox.Error) {
const error = e.getActualType()
if (error.status === 401) {
throw new Error(
`Error killing sandbox (${sandboxID}) - (${error.status}) unauthenticated: ${error.data.message}`,
)
}
if (error.status === 500) {
throw new Error(
`Error killing sandbox (${sandboxID}) - (${error.status}) server error: ${error.data.message}`,
)
}
}
throw e
}
}

/**
* Keep the sandbox alive for the specified duration.
*
Expand Down Expand Up @@ -527,21 +558,20 @@ export class SandboxConnection {
try {
// eslint-disable-next-line no-constant-condition
while (true) {
await wait(SANDBOX_REFRESH_PERIOD)

if (!this.isOpen) {
this.logger.debug?.(
`Cannot refresh sandbox ${this.id} - it was closed`,
)
return
}

await wait(SANDBOX_REFRESH_PERIOD)

try {
this.logger.debug?.(`Refreshed sandbox "${sandboxID}"`)

await refreshSandbox(this.apiKey, {
sandboxID, duration: 0,
})
this.logger.debug?.(`Refreshed sandbox "${sandboxID}"`)
} catch (e) {
if (e instanceof refreshSandbox.Error) {
const error = e.getActualType()
Expand Down