Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/olive-poets-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'e2b': minor
'@e2b/python-sdk': minor
---

Remove SDK-side defaults from API request payloads so the API defaults apply when options are omitted. Sandbox create/fork no longer preset a 5-minute timeout, fork no longer presets `count: 1`, create no longer presets `secure` and `allow_internet_access`, pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged.
8 changes: 3 additions & 5 deletions packages/js-sdk/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { createConnectTransport } from '@connectrpc/connect-web'
import {
ConnectionConfig,
ConnectionOpts,
DEFAULT_SANDBOX_TIMEOUT_MS,
defaultUsername,
Username,
} from '../connectionConfig'
Expand Down Expand Up @@ -76,7 +75,6 @@ export interface SandboxUrlOpts {
export class Sandbox extends SandboxApi {
protected static readonly defaultTemplate: string = 'base'
protected static readonly defaultMcpTemplate: string = 'mcp-gateway'
protected static readonly defaultSandboxTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

/**
* Module for interacting with the sandbox filesystem
Expand Down Expand Up @@ -317,7 +315,7 @@ export class Sandbox extends SandboxApi {

const sandboxInfo = await this.createSandbox(
template,
apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs,
apiOpts?.timeoutMs,
apiOpts
)

Expand Down Expand Up @@ -433,8 +431,8 @@ export class Sandbox extends SandboxApi {

const results = await this.forkSandbox(
sandboxId,
apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs,
apiOpts?.count ?? 1,
apiOpts?.timeoutMs,
apiOpts?.count,
apiOpts
)

Expand Down
35 changes: 18 additions & 17 deletions packages/js-sdk/src/sandbox/sandboxApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ export interface SandboxPauseOpts extends SandboxApiOpts {
* persisted (a filesystem-only snapshot); resuming such a sandbox cold-boots
* (reboots) it from disk, losing running processes and open connections.
*
* @default true
* When not set, the API default (currently a full memory snapshot) applies.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
*/
keepMemory?: boolean
}
Expand All @@ -530,15 +530,15 @@ export interface SandboxForkOpts extends ConnectionOpts {
* regardless of count. Each fork succeeds or fails independently; the
* outcome of each is reported in its entry of the returned array.
*
* @default 1
* When not set, the API default (currently 1) applies.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
*/
count?: number

/**
* Timeout for the forked sandboxes in **milliseconds**.
* Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users.
*
* @default 300_000 // 5 minutes
* When not set, the API default timeout applies.
*/
timeoutMs?: number
}
Expand Down Expand Up @@ -591,21 +591,21 @@ export interface SandboxOpts extends ConnectionOpts {
* Timeout for the sandbox in **milliseconds**.
* Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users.
*
* @default 300_000 // 5 minutes
* When not set, the API default timeout applies.
*/
timeoutMs?: number

/**
* Secure all traffic coming to the sandbox controller with auth token
*
* @default true
* When not set, the API default (currently enabled) applies.
*/
secure?: boolean

/**
* Allow sandbox to access the internet. If set to `False`, it works the same as setting network `denyOut` to `[0.0.0.0/0]`.
*
* @default true
* When not set, the API default (currently allowed) applies.
*/
allowInternetAccess?: boolean

Expand Down Expand Up @@ -714,8 +714,7 @@ export interface SandboxListOpts extends Omit<SandboxApiOpts, 'signal'> {
/**
* Sort order of the list of sandboxes by start time, applied across the
* whole result set before pagination (not within a page).
*
* @default 'desc'
* When not set, the API default (currently `'desc'`, newest first) applies.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
*/
order?: SandboxListOrder

Expand Down Expand Up @@ -1475,7 +1474,7 @@ export class SandboxApi extends ClientFactory {
},
},
body: {
memory: apiOpts?.keepMemory ?? true,
memory: apiOpts?.keepMemory,
},
signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal),
})
Expand Down Expand Up @@ -1602,7 +1601,7 @@ export class SandboxApi extends ClientFactory {

protected static async createSandbox(
template: string,
timeoutMs: number,
timeoutMs?: number,
opts?: SandboxOpts
) {
const apiOpts = this.resolveOpts(opts)
Expand Down Expand Up @@ -1656,9 +1655,10 @@ export class SandboxApi extends ClientFactory {
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,
timeout:
timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs),
secure: opts?.secure,
Comment thread
mishushakov marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: HIGH

The SDK now omits secure unless callers set it explicitly (secure: opts?.secure), which removes the prior secure-by-default behavior at sandbox creation.

Impact: Callers that rely on defaults can unintentionally create sandboxes with weaker controller access protection while backend defaulting is not universally guaranteed, allowing unauthorized controller interaction when the endpoint is reachable.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit e9963a5. Configure here.

allow_internet_access: opts?.allowInternetAccess,
network: buildNetworkBody(opts?.network, iam),
iam,
autoPause: onTimeoutConfigured ? action === 'pause' : undefined,
Expand Down Expand Up @@ -1705,11 +1705,11 @@ export class SandboxApi extends ClientFactory {

protected static async forkSandbox(
sandboxId: string,
timeoutMs: number,
count: number,
timeoutMs?: number,
count?: number,
opts?: SandboxApiOpts
): Promise<SandboxForkResponse[]> {
if (count < 1) {
if (count !== undefined && count < 1) {
throw new InvalidArgumentError('count must be at least 1')
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

Expand All @@ -1724,7 +1724,8 @@ export class SandboxApi extends ClientFactory {
},
},
body: {
timeout: timeoutToSeconds(timeoutMs),
timeout:
timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs),
count,
},
signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal),
Expand Down
4 changes: 2 additions & 2 deletions packages/js-sdk/src/template/buildApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
type RequestBuildInput = {
name: string
tags?: string[]
cpuCount: number
memoryMB: number
cpuCount?: number
memoryMB?: number
}

type GetFileUploadLinkInput = {
Expand Down
4 changes: 2 additions & 2 deletions packages/js-sdk/src/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1073,14 +1073,14 @@
tags: responseTags,
} = await requestBuild(
client,
{
name,
tags: options.tags,
cpuCount: options.cpuCount ?? 2,
memoryMB: options.memoryMB ?? 1024,
cpuCount: options.cpuCount,
memoryMB: options.memoryMB,
},
config.getSignal(undefined, options.signal)
)

Check warning on line 1083 in packages/js-sdk/src/template/index.ts

View check run for this annotation

Claude / Claude Code Review

Missing test coverage for template build cpuCount/memoryMB omission

Removing SDK-side defaults from `cpuCount`/`memoryMB` changed `requestBuild` in `packages/js-sdk/src/template/index.ts` and `request_build` in both Python `template_sync/build_api.py` / `template_async/build_api.py`, but no test asserts these fields are actually omitted from the template build request body when unset — unlike the new omission tests added for sandbox create/fork/pause in `apiDefaults.test.ts` and `test_api_defaults.py`. This is a coverage gap, not a runtime defect (JSON seriali
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

options.onBuildLogs?.(
new LogEntry(
Expand Down
4 changes: 2 additions & 2 deletions packages/js-sdk/src/template/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ export type BasicBuildOptions = {
tags?: string[]
/**
* Number of CPUs allocated to the sandbox.
* @default 2
* When not set, the API default applies.
*/
cpuCount?: number
/**
* Amount of memory in MB allocated to the sandbox.
* @default 1024
* When not set, the API default applies.
*/
memoryMB?: number
/**
Expand Down
105 changes: 105 additions & 0 deletions packages/js-sdk/tests/sandbox/apiDefaults.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { afterAll, afterEach, beforeAll, expect, test } from 'vitest'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'

import { Sandbox } from '../../src'
import { TEST_API_KEY, apiUrl } from '../setup'

let lastCreateBody: Record<string, unknown> | undefined
let lastForkBody: Record<string, unknown> | undefined
let lastPauseBody: Record<string, unknown> | undefined

const server = setupServer(
http.post(apiUrl('/sandboxes'), async ({ request }) => {
lastCreateBody = (await request.json()) as Record<string, unknown>
return HttpResponse.json({
sandboxID: 'test-sandbox-id',
templateID: 'base',
envdVersion: '0.2.4',
})
}),
http.post(apiUrl('/sandboxes/:sandboxID/fork'), async ({ request }) => {
lastForkBody = (await request.json()) as Record<string, unknown>
return HttpResponse.json([
{
sandbox: {
sandboxID: 'forked-sandbox-id',
templateID: 'base',
envdVersion: '0.2.4',
},
},
])
}),
http.post(apiUrl('/sandboxes/:sandboxID/pause'), async ({ request }) => {
lastPauseBody = (await request.json()) as Record<string, unknown>
return new HttpResponse(null, { status: 204 })
})
)

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))

afterAll(() => server.close())

afterEach(() => {
lastCreateBody = undefined
lastForkBody = undefined
lastPauseBody = undefined
server.resetHandlers()
})

test('Sandbox.create omits timeout, secure and allow_internet_access when unset', async () => {
await Sandbox.create('base', { apiKey: TEST_API_KEY })

expect(lastCreateBody).toBeDefined()
expect(lastCreateBody).not.toHaveProperty('timeout')
expect(lastCreateBody).not.toHaveProperty('secure')
expect(lastCreateBody).not.toHaveProperty('allow_internet_access')
})

test('Sandbox.create sends explicit timeout, secure and allow_internet_access', async () => {
await Sandbox.create('base', {
apiKey: TEST_API_KEY,
timeoutMs: 60_000,
secure: false,
allowInternetAccess: false,
})

expect(lastCreateBody?.timeout).toBe(60)
expect(lastCreateBody?.secure).toBe(false)
expect(lastCreateBody?.allow_internet_access).toBe(false)
})

test('Sandbox.fork omits timeout and count when unset', async () => {
await Sandbox.fork('test-sandbox-id', { apiKey: TEST_API_KEY })

expect(lastForkBody).toBeDefined()
expect(lastForkBody).not.toHaveProperty('timeout')
expect(lastForkBody).not.toHaveProperty('count')
})

test('Sandbox.fork sends explicit timeout and count', async () => {
await Sandbox.fork('test-sandbox-id', {
apiKey: TEST_API_KEY,
timeoutMs: 60_000,
count: 2,
})

expect(lastForkBody?.timeout).toBe(60)
expect(lastForkBody?.count).toBe(2)
})

test('Sandbox.pause omits memory when keepMemory is unset', async () => {
await Sandbox.pause('test-sandbox-id', { apiKey: TEST_API_KEY })

expect(lastPauseBody).toBeDefined()
expect(lastPauseBody).not.toHaveProperty('memory')
})

test('Sandbox.pause sends an explicit keepMemory', async () => {
await Sandbox.pause('test-sandbox-id', {
apiKey: TEST_API_KEY,
keepMemory: false,
})

expect(lastPauseBody?.memory).toBe(false)
})
Loading
Loading