Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1053eb5
sdk: add lifecycle config and explicit pause/resume APIs
matthewlouisbrockman Feb 18, 2026
6940ddb
tests: update lifecycle and pause/resume coverage
matthewlouisbrockman Feb 18, 2026
3cc4d0b
sdk: keep examples in the js docstrings
matthewlouisbrockman Feb 18, 2026
6872824
try to explain lifecycle props in the format docstring
matthewlouisbrockman Feb 19, 2026
e2d651a
linter
matthewlouisbrockman Feb 19, 2026
16f5233
enums: resumeOn takes "off"
matthewlouisbrockman Feb 19, 2026
a46094d
resumeOn optional in lifecycle
matthewlouisbrockman Feb 19, 2026
65089d9
docs: longer explanation on the SandboxLifecycle docstrings
matthewlouisbrockman Feb 19, 2026
986c81d
changeset
matthewlouisbrockman Feb 19, 2026
a9c906e
rm resume
matthewlouisbrockman Feb 21, 2026
3f6977b
cleanup on rm resume
matthewlouisbrockman Feb 21, 2026
d7de490
missed a resume
matthewlouisbrockman Feb 21, 2026
f1e0ba8
Merge branch 'main' into lifecycle-in-sdk
matthewlouisbrockman Feb 21, 2026
039f079
lifecycle config mirrors networking
matthewlouisbrockman Feb 21, 2026
67e8298
Merge branch 'main' into lifecycle-in-sdk
matthewlouisbrockman Feb 21, 2026
fd2764a
fix: review flagged needing to validate options before running thru t…
matthewlouisbrockman Feb 21, 2026
066a464
move validateCreateOotions
matthewlouisbrockman Feb 21, 2026
8058d8f
make it codegen compatible
matthewlouisbrockman Feb 21, 2026
57a5086
openapi back to before
matthewlouisbrockman Feb 23, 2026
97bfc71
use autoResume: bool instead of old resumeOn: any/off
matthewlouisbrockman Feb 23, 2026
320be8e
feat(sdk): add lifecycle back to create opts and transform to autoPau…
matthewlouisbrockman Feb 23, 2026
ff36ac3
api mapping is internal for now
matthewlouisbrockman Feb 24, 2026
d7389df
refactor: use typed codegen models for autoResume instead of raw dict…
matthewlouisbrockman Feb 24, 2026
9b4c636
refactor(python-sdk): extract shared validate_lifecycle helper
matthewlouisbrockman Feb 24, 2026
cf1e611
lint: need run format not just run lint
matthewlouisbrockman Feb 24, 2026
1195b99
linter didn't need to change the test_stacktrace
matthewlouisbrockman Feb 24, 2026
f1c0b2a
bugbot fix: use api provided sandbox id for connect rather than the u…
matthewlouisbrockman Feb 24, 2026
fa2bb09
only set policy for lifecycle when autoresume is actually set.
matthewlouisbrockman Feb 24, 2026
b19c775
don't pass negative autoresume policy in python when not not set either
matthewlouisbrockman Feb 24, 2026
df58ad4
always send auto_resume False unless otherwise indicated
matthewlouisbrockman Feb 24, 2026
daaa8e6
Merge branch 'main' into lifecycle-in-sdk
matthewlouisbrockman Feb 24, 2026
a3fd14f
Merge branch 'main' into lifecycle-in-sdk
matthewlouisbrockman Feb 24, 2026
3267a46
share auto resume policy helper
matthewlouisbrockman Feb 24, 2026
1a8306f
rm the _policy
matthewlouisbrockman Feb 24, 2026
d057951
linter
matthewlouisbrockman Feb 24, 2026
bd2c505
remove lifecycle combination validation
matthewlouisbrockman Feb 24, 2026
8e43a25
effective_auto_pause rn to should_auto_pause
matthewlouisbrockman Feb 24, 2026
0d22776
lint: formatter
matthewlouisbrockman Feb 24, 2026
d7cd50c
js e2e test for pause/autoresume params
matthewlouisbrockman Feb 24, 2026
4885cab
mocks to real tests for autoresume on python
matthewlouisbrockman Feb 24, 2026
ba065e3
Merge branch 'main' into lifecycle-in-sdk
matthewlouisbrockman Feb 27, 2026
cff6e50
rm unused get_auto_resume_policy
matthewlouisbrockman Feb 28, 2026
c279d38
did indeed need the auto resume policy
matthewlouisbrockman Feb 28, 2026
504de2c
rm redundant check
matthewlouisbrockman Feb 28, 2026
616ffb9
new api spec with enabled
matthewlouisbrockman Mar 2, 2026
f15223a
keeping the autoresume config as a boolean on the API now isntead of …
matthewlouisbrockman Mar 2, 2026
fcfaf65
linter
matthewlouisbrockman Mar 2, 2026
f20f06c
run codegen
matthewlouisbrockman Mar 2, 2026
515f9b7
Merge branch 'main' into lifecycle-in-sdk
matthewlouisbrockman Mar 4, 2026
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
6 changes: 6 additions & 0 deletions .changeset/bold-times-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@e2b/python-sdk': minor
'e2b': minor
---

Adds lifecycle prop to control pausing and auto-resume
11 changes: 5 additions & 6 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.

1 change: 1 addition & 0 deletions packages/js-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type {
SandboxListOpts,
SandboxPaginator,
SandboxNetworkOpts,
SandboxLifecycle,
SnapshotInfo,
SnapshotListOpts,
SnapshotPaginator,
Expand Down
17 changes: 14 additions & 3 deletions packages/js-sdk/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export class Sandbox extends SandboxApi {
* const sameSandbox = await sandbox.connect()
* ```
*/
async connect(opts?: SandboxBetaCreateOpts): Promise<this> {
async connect(opts?: SandboxConnectOpts): Promise<this> {
await SandboxApi.connectSandbox(this.sandboxId, opts)

return this
Expand Down Expand Up @@ -571,13 +571,24 @@ export class Sandbox extends SandboxApi {
}

/**
* @beta This feature is in beta and may change in the future.
*
* Pause a sandbox by its ID.
*
* @param opts connection options.
*
* @returns sandbox ID that can be used to resume the sandbox.
*
* @example
* ```ts
* const sandbox = await Sandbox.create()
* await sandbox.pause()
* ```
*/
async pause(opts?: ConnectionOpts): Promise<boolean> {
return await SandboxApi.pause(this.sandboxId, opts)
}

/**
* @deprecated Use {@link Sandbox.pause} instead.
*/
async betaPause(opts?: ConnectionOpts): Promise<boolean> {
return await SandboxApi.betaPause(this.sandboxId, opts)
Expand Down
87 changes: 75 additions & 12 deletions packages/js-sdk/src/sandbox/sandboxApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ export type SandboxNetworkOpts = {
maskRequestHost?: string
}

export type SandboxLifecycle = {
/**
* Action to take when sandbox timeout is reached.
* @default "kill"
*/
onTimeout: 'pause' | 'kill'

/**
* Auto-resume enabled flag.
* @default false
* Can be `true` only when `onTimeout` is `pause`.
*/
autoResume?: boolean
}

/**
* Options for request to the Sandbox API.
*/
Expand Down Expand Up @@ -133,10 +148,17 @@ export interface SandboxOpts extends ConnectionOpts {
* Sandbox URL. Used for local development
*/
sandboxUrl?: string

/**
* Sandbox lifecycle configuration.
*/
lifecycle?: SandboxLifecycle
}

export type SandboxBetaCreateOpts = SandboxOpts & {
/**
* @deprecated Use `lifecycle.onTimeout = "pause"` instead.
*
* Automatically pause the sandbox after the timeout expires.
* @default false
*/
Expand Down Expand Up @@ -330,6 +352,26 @@ export interface SandboxMetrics {
diskTotal: number
}

function getLifecycle(
opts?: Pick<SandboxBetaCreateOpts, 'lifecycle' | 'autoPause'>
): SandboxLifecycle {
if (opts?.lifecycle) {
return opts.lifecycle
}

if (opts?.autoPause) {
return {
onTimeout: 'pause',
autoResume: false,
}
}

return {
onTimeout: 'kill',
autoResume: false,
}
}

export class SandboxApi {
protected constructor() {}

Expand Down Expand Up @@ -529,7 +571,7 @@ export class SandboxApi {
*
* @returns `true` if the sandbox got paused, `false` if the sandbox was already paused.
*/
static async betaPause(
static async pause(
sandboxId: string,
opts?: SandboxApiOpts
): Promise<boolean> {
Expand Down Expand Up @@ -562,6 +604,16 @@ export class SandboxApi {
return true
}

/**
* @deprecated Use {@link SandboxApi.pause} instead.
*/
static async betaPause(
sandboxId: string,
opts?: SandboxApiOpts
): Promise<boolean> {
return this.pause(sandboxId, opts)
}

/**
* Create a snapshot from a sandbox.
*
Expand Down Expand Up @@ -659,19 +711,30 @@ export class SandboxApi {
) {
const config = new ConnectionConfig(opts)
const client = new ApiClient(config)
const lifecycle = getLifecycle(opts)
Comment thread
matthewlouisbrockman marked this conversation as resolved.
const autoPause = lifecycle.onTimeout === 'pause'
const autoResumeEnabled =
lifecycle.onTimeout === 'pause'
? (lifecycle.autoResume ?? false)
: undefined

const body: components['schemas']['NewSandbox'] = {
templateID: template,
metadata: opts?.metadata,
mcp: opts?.mcp as Record<string, unknown> | undefined,
envVars: opts?.envs,
timeout: timeoutToSeconds(timeoutMs),
secure: opts?.secure ?? true,
allow_internet_access: opts?.allowInternetAccess ?? true,
network: opts?.network,
...(autoPause !== undefined ? { autoPause } : {}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Conditional autoPause !== undefined is always true

Low Severity

The autoPause variable is computed as lifecycle.onTimeout === 'pause', which always produces a boolean — never undefined. The conditional autoPause !== undefined on the spread is therefore always true, making the ternary dead code. This was likely written by analogy with the autoResumePolicy check below (where undefined IS a real possibility), but it's misleading here since a reader might incorrectly assume there's a path where autoPause is intentionally excluded from the body.

Additional Locations (1)

Fix in Cursor Fix in Web

...(autoResumeEnabled !== undefined
? { autoResume: { enabled: autoResumeEnabled } }
: {}),
}
Comment thread
cursor[bot] marked this conversation as resolved.

const res = await client.api.POST('/sandboxes', {
body: {
autoPause: opts?.autoPause ?? false,
templateID: template,
metadata: opts?.metadata,
mcp: opts?.mcp as Record<string, unknown> | undefined,
envVars: opts?.envs,
timeout: timeoutToSeconds(timeoutMs),
secure: opts?.secure ?? true,
allow_internet_access: opts?.allowInternetAccess ?? true,
network: opts?.network,
},
body,
Comment thread
cursor[bot] marked this conversation as resolved.
signal: config.getSignal(opts?.requestTimeoutMs),
})

Expand Down
4 changes: 2 additions & 2 deletions packages/js-sdk/tests/api/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { sandboxTest, isDebug } from '../setup.js'
import { Sandbox } from '../../src'

sandboxTest.skipIf(isDebug)('pause sandbox', async ({ sandbox }) => {
await Sandbox.betaPause(sandbox.sandboxId)
await Sandbox.pause(sandbox.sandboxId)
assert.isFalse(
await sandbox.isRunning(),
'Sandbox should not be running after pause'
)
})

sandboxTest.skipIf(isDebug)('resume sandbox', async ({ sandbox }) => {
await Sandbox.betaPause(sandbox.sandboxId)
await Sandbox.pause(sandbox.sandboxId)
assert.isFalse(
await sandbox.isRunning(),
'Sandbox should not be running after pause'
Expand Down
11 changes: 11 additions & 0 deletions packages/js-sdk/tests/sandbox/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ test.skipIf(isDebug)('connect', async () => {
}
})

sandboxTest.skipIf(isDebug)(
'connect resumes paused sandbox',
async ({ sandbox }) => {
await sandbox.pause()
assert.isFalse(await sandbox.isRunning())

const resumed = await Sandbox.connect(sandbox.sandboxId)
assert.isTrue(await resumed.isRunning())
}
)

sandboxTest.skipIf(isDebug)(
'connect to non-running sandbox',
async ({ sandbox }) => {
Expand Down
63 changes: 63 additions & 0 deletions packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { assert, test } from 'vitest'

import { Sandbox } from '../../src'
import { isDebug, template, wait } from '../setup.js'

test.skipIf(isDebug)(
'auto-pause without auto-resume requires connect to wake',
async () => {
const sandbox = await Sandbox.create(template, {
timeoutMs: 3_000,
lifecycle: {
onTimeout: 'pause',
autoResume: false,
},
})

try {
await wait(5_000)

assert.equal((await sandbox.getInfo()).state, 'paused')
assert.isFalse(await sandbox.isRunning())

await sandbox.connect()

assert.equal((await sandbox.getInfo()).state, 'running')
assert.isTrue(await sandbox.isRunning())
} finally {
await sandbox.kill().catch(() => {})
}
},
60_000
)

test.skipIf(isDebug)(
'auto-resume wakes paused sandbox on http request',
async () => {
const sandbox = await Sandbox.create(template, {
timeoutMs: 3_000,
lifecycle: {
onTimeout: 'pause',
autoResume: true,
},
})

try {
await sandbox.commands.run('python3 -m http.server 8000', {
background: true,
})

await wait(5_000)

const url = `https://${sandbox.getHost(8000)}`
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) })

assert.equal(res.status, 200)
assert.equal((await sandbox.getInfo()).state, 'running')
assert.isTrue(await sandbox.isRunning())
} finally {
await sandbox.kill().catch(() => {})
}
},
60_000
)
12 changes: 6 additions & 6 deletions packages/js-sdk/tests/sandbox/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sandboxTest.skipIf(isDebug)(
async ({ sandbox }) => {
assert.isTrue(await sandbox.isRunning())

await sandbox.betaPause()
await sandbox.pause()

assert.isFalse(await sandbox.isRunning())

Expand Down Expand Up @@ -35,7 +35,7 @@ describe('pause and resume with env vars', () => {
assert.equal(cmd.exitCode, 0)
assert.equal(cmd.stdout.trim(), 'sfisback')

await sandbox.betaPause()
await sandbox.pause()

assert.isFalse(await sandbox.isRunning())

Expand Down Expand Up @@ -68,7 +68,7 @@ sandboxTest.skipIf(isDebug)(
const readContent = await sandbox.files.read(filename)
assert.equal(readContent, content)

await sandbox.betaPause()
await sandbox.pause()
assert.isFalse(await sandbox.isRunning())

await sandbox.connect()
Expand All @@ -87,7 +87,7 @@ sandboxTest.skipIf(isDebug)(
const cmd = await sandbox.commands.run('sleep 3600', { background: true })
const expectedPid = cmd.pid

await sandbox.betaPause()
await sandbox.pause()
assert.isFalse(await sandbox.isRunning())

await sandbox.connect()
Expand Down Expand Up @@ -121,7 +121,7 @@ sandboxTest.skipIf(isDebug)(
const exists = await sandbox.files.exists(filename)
assert.isFalse(exists)

await sandbox.betaPause()
await sandbox.pause()
assert.isFalse(await sandbox.isRunning())

await sandbox.connect()
Expand Down Expand Up @@ -151,7 +151,7 @@ sandboxTest.skipIf(isDebug)(
const response1 = await fetch(`https://${url}`)
assert.equal(response1.status, 200)

await sandbox.betaPause()
await sandbox.pause()
assert.isFalse(await sandbox.isRunning())

await sandbox.connect()
Expand Down
2 changes: 2 additions & 0 deletions packages/python-sdk/e2b/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
McpServer,
SandboxInfo,
SandboxMetrics,
SandboxLifecycle,
SandboxNetworkOpts,
SandboxQuery,
SandboxState,
Expand Down Expand Up @@ -154,6 +155,7 @@
"FileType",
# Network
"SandboxNetworkOpts",
"SandboxLifecycle",
"ALL_TRAFFIC",
# Snapshot
"SnapshotInfo",
Expand Down
2 changes: 0 additions & 2 deletions packages/python-sdk/e2b/api/client/models/__init__.py

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

Loading