From 1c4dc53bca14822ab236ac1fc4277f9dd957a86c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:25:15 +0000 Subject: [PATCH 01/20] Remove SDK-side defaults from API request payloads Co-Authored-By: mish@e2b.dev --- .changeset/olive-poets-hammer.md | 6 + packages/js-sdk/src/sandbox/index.ts | 8 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 35 ++-- packages/js-sdk/src/template/buildApi.ts | 4 +- packages/js-sdk/src/template/index.ts | 4 +- packages/js-sdk/src/template/types.ts | 4 +- .../js-sdk/tests/sandbox/apiDefaults.test.ts | 105 ++++++++++ packages/python-sdk/e2b/sandbox_async/main.py | 46 ++--- .../e2b/sandbox_async/sandbox_api.py | 34 ++-- packages/python-sdk/e2b/sandbox_sync/main.py | 46 ++--- .../e2b/sandbox_sync/sandbox_api.py | 34 ++-- .../e2b/template_async/build_api.py | 8 +- .../python-sdk/e2b/template_async/main.py | 24 +-- .../python-sdk/e2b/template_sync/build_api.py | 8 +- packages/python-sdk/e2b/template_sync/main.py | 24 +-- .../tests/shared/sandbox/test_api_defaults.py | 179 ++++++++++++++++++ 16 files changed, 431 insertions(+), 138 deletions(-) create mode 100644 .changeset/olive-poets-hammer.md create mode 100644 packages/js-sdk/tests/sandbox/apiDefaults.test.ts create mode 100644 packages/python-sdk/tests/shared/sandbox/test_api_defaults.py diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md new file mode 100644 index 0000000000..053a52d8e2 --- /dev/null +++ b/.changeset/olive-poets-hammer.md @@ -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. diff --git a/packages/js-sdk/src/sandbox/index.ts b/packages/js-sdk/src/sandbox/index.ts index f22de94e07..26f30a4e74 100644 --- a/packages/js-sdk/src/sandbox/index.ts +++ b/packages/js-sdk/src/sandbox/index.ts @@ -3,7 +3,6 @@ import { createConnectTransport } from '@connectrpc/connect-web' import { ConnectionConfig, ConnectionOpts, - DEFAULT_SANDBOX_TIMEOUT_MS, defaultUsername, Username, } from '../connectionConfig' @@ -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 /** * Module for interacting with the sandbox filesystem @@ -317,7 +315,7 @@ export class Sandbox extends SandboxApi { const sandboxInfo = await this.createSandbox( template, - apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs, + apiOpts?.timeoutMs, apiOpts ) @@ -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 ) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 3af6b1bace..37730597bd 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -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. */ keepMemory?: boolean } @@ -530,7 +530,7 @@ 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. */ count?: number @@ -538,7 +538,7 @@ export interface SandboxForkOpts extends ConnectionOpts { * 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 } @@ -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 @@ -714,8 +714,7 @@ export interface SandboxListOpts extends Omit { /** * 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. */ order?: SandboxListOrder @@ -1475,7 +1474,7 @@ export class SandboxApi extends ClientFactory { }, }, body: { - memory: apiOpts?.keepMemory ?? true, + memory: apiOpts?.keepMemory, }, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) @@ -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) @@ -1656,9 +1655,10 @@ export class SandboxApi extends ClientFactory { metadata: opts?.metadata, mcp: opts?.mcp as Record | 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, + allow_internet_access: opts?.allowInternetAccess, network: buildNetworkBody(opts?.network, iam), iam, autoPause: onTimeoutConfigured ? action === 'pause' : undefined, @@ -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 { - if (count < 1) { + if (count !== undefined && count < 1) { throw new InvalidArgumentError('count must be at least 1') } @@ -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), diff --git a/packages/js-sdk/src/template/buildApi.ts b/packages/js-sdk/src/template/buildApi.ts index f79321166e..11153db5aa 100644 --- a/packages/js-sdk/src/template/buildApi.ts +++ b/packages/js-sdk/src/template/buildApi.ts @@ -19,8 +19,8 @@ import { type RequestBuildInput = { name: string tags?: string[] - cpuCount: number - memoryMB: number + cpuCount?: number + memoryMB?: number } type GetFileUploadLinkInput = { diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index 1b712c18ae..b8611a3f03 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -1076,8 +1076,8 @@ export class TemplateBase { name, tags: options.tags, - cpuCount: options.cpuCount ?? 2, - memoryMB: options.memoryMB ?? 1024, + cpuCount: options.cpuCount, + memoryMB: options.memoryMB, }, config.getSignal(undefined, options.signal) ) diff --git a/packages/js-sdk/src/template/types.ts b/packages/js-sdk/src/template/types.ts index 87ff44441d..8a65112d31 100644 --- a/packages/js-sdk/src/template/types.ts +++ b/packages/js-sdk/src/template/types.ts @@ -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 /** diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts new file mode 100644 index 0000000000..098e3b2d10 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -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 | undefined +let lastForkBody: Record | undefined +let lastPauseBody: Record | undefined + +const server = setupServer( + http.post(apiUrl('/sandboxes'), async ({ request }) => { + lastCreateBody = (await request.json()) as Record + 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 + 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 + 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) +}) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 3196008757..2b0ed8a50b 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -172,8 +172,8 @@ async def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, - secure: bool = True, - allow_internet_access: bool = True, + secure: Optional[bool] = None, + allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -188,11 +188,11 @@ async def create( By default, the sandbox is created from the default `base` sandbox template. :param template: Sandbox template name or ID - :param timeout: Timeout for the sandbox in **seconds**, default to 300 seconds. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. + :param timeout: Timeout for the sandbox in **seconds**. When not set, the API default timeout applies. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it, defaults to `True`. - :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. + :param secure: Envd is secured with access token and cannot be used without it. When not set, the API default (currently enabled) applies. + :param allow_internet_access: Allow sandbox to access the internet. When not set, the API default (currently allowed) applies. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}``. Registered tokens are exposed to ``network.rules`` ``transform`` callables as ``ctx.iam.tokens[name]`` placeholders, which the egress proxy resolves per request @@ -377,8 +377,8 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies + :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies :return: List with one entry per requested fork — a sandbox instance or an exception @@ -416,8 +416,8 @@ async def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies + :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -453,8 +453,8 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies + :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies :return: List with one entry per requested fork — a sandbox instance or an exception @@ -749,13 +749,13 @@ async def get_metrics( @overload async def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -765,14 +765,14 @@ async def pause( @staticmethod async def pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox specified by sandbox ID. :param sandbox_id: Sandbox ID - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -781,13 +781,13 @@ async def pause( @class_method_variant("_cls_pause") async def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. Defaults to `True` (full memory snapshot). + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. When not set, the API default (currently a full memory snapshot) applies. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -801,7 +801,7 @@ async def pause( @overload async def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @@ -809,14 +809,14 @@ async def beta_pause( @staticmethod async def beta_pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @class_method_variant("_cls_pause") async def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ @@ -1110,8 +1110,8 @@ async def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: bool, - allow_internet_access: bool, + secure: Optional[bool], + allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -1133,7 +1133,7 @@ async def _create( else: response = await SandboxApi._create_sandbox( template=template or cls.default_template, - timeout=timeout or cls.default_sandbox_timeout, + timeout=timeout, metadata=metadata, env_vars=envs, secure=secure, diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index b6a208d6a8..e66b1f603b 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -84,7 +84,7 @@ def list( :param query: Filter the list of sandboxes by metadata, state, start time, or template, e.g. `SandboxQuery(metadata={"key": "value"})` or `SandboxQuery(state=[SandboxState.RUNNING])` :param limit: Maximum number of sandboxes to return per page :param next_token: Token for pagination - :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), defaults to `"desc"` (newest first) + :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), when not set, the API default (currently `"desc"`, newest first) applies :return: An `AsyncSandboxPaginator` that yields pages of sandboxes (running and paused by default). Iterate pages via `await paginator.next_items()` while `paginator.has_next` is True. """ @@ -208,11 +208,11 @@ async def _cls_update_network( async def _create_sandbox( cls, template: str, - timeout: int, - allow_internet_access: bool, + timeout: Optional[int], + allow_internet_access: Optional[bool], metadata: Optional[Dict[str, str]], env_vars: Optional[Dict[str, str]], - secure: bool, + secure: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -236,11 +236,13 @@ async def _create_sandbox( auto_pause_memory=lifecycle_body.auto_pause_memory, auto_resume=lifecycle_body.auto_resume, metadata=metadata or {}, - timeout=timeout, + timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure, - allow_internet_access=allow_internet_access, + secure=secure if secure is not None else UNSET, + allow_internet_access=( + allow_internet_access if allow_internet_access is not None else UNSET + ), network=SandboxNetworkConfig(**network_body) if network_body else UNSET, iam=iam_body or UNSET, volume_mounts=volume_mounts if volume_mounts else UNSET, @@ -405,7 +407,7 @@ async def _cls_delete_snapshot( async def _cls_pause( cls, sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: config = ConnectionConfig(**cls._resolve_api_params(**opts)) @@ -414,7 +416,9 @@ async def _cls_pause( res = await post_sandboxes_sandbox_id_pause.asyncio_detailed( sandbox_id, client=api_client, - body=SandboxPauseRequest(memory=keep_memory), + body=SandboxPauseRequest( + memory=keep_memory if keep_memory is not None else UNSET + ), ) if res.status_code == 404: @@ -442,12 +446,7 @@ async def _cls_fork( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> List[Union[SandboxCreateResponse, Exception]]: - timeout = ( - timeout if timeout is not None else SandboxBase.default_sandbox_timeout - ) - count = count if count is not None else 1 - - if count < 1: + if count is not None and count < 1: raise InvalidArgumentException("count must be at least 1") config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) @@ -456,7 +455,10 @@ async def _cls_fork( res = await post_sandboxes_sandbox_id_fork.asyncio_detailed( sandbox_id, client=api_client, - body=SandboxForkRequest(timeout=timeout, count=count), + body=SandboxForkRequest( + timeout=timeout if timeout is not None else UNSET, + count=count if count is not None else UNSET, + ), ) if res.status_code == 404: diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 2ebda95aab..f68db89b18 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -168,8 +168,8 @@ def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, - secure: bool = True, - allow_internet_access: bool = True, + secure: Optional[bool] = None, + allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -184,11 +184,11 @@ def create( By default, the sandbox is created from the default `base` sandbox template. :param template: Sandbox template name or ID - :param timeout: Timeout for the sandbox in **seconds**, default to 300 seconds. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. + :param timeout: Timeout for the sandbox in **seconds**. When not set, the API default timeout applies. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it, defaults to `True`. - :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. + :param secure: Envd is secured with access token and cannot be used without it. When not set, the API default (currently enabled) applies. + :param allow_internet_access: Allow sandbox to access the internet. When not set, the API default (currently allowed) applies. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}``. Registered tokens are exposed to ``network.rules`` ``transform`` callables as ``ctx.iam.tokens[name]`` placeholders, which the egress proxy resolves per request @@ -372,8 +372,8 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies + :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies :return: List with one entry per requested fork — a sandbox instance or an exception @@ -411,8 +411,8 @@ def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies + :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -448,8 +448,8 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**, defaults to 300 seconds - :param count: Number of forked sandboxes to create, defaults to 1 + :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies + :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies :return: List with one entry per requested fork — a sandbox instance or an exception @@ -747,13 +747,13 @@ def get_metrics( @overload def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -763,14 +763,14 @@ def pause( @staticmethod def pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox specified by sandbox ID. :param sandbox_id: Sandbox ID - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. Defaults to `True`. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -779,13 +779,13 @@ def pause( @class_method_variant("_cls_pause") def pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. Defaults to `True` (full memory snapshot). + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. When not set, the API default (currently a full memory snapshot) applies. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -799,7 +799,7 @@ def pause( @overload def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @@ -807,14 +807,14 @@ def beta_pause( @staticmethod def beta_pause( sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @class_method_variant("_cls_pause") def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: """ @@ -1106,8 +1106,8 @@ def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: bool, - allow_internet_access: bool, + secure: Optional[bool], + allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -1129,7 +1129,7 @@ def _create( else: response = SandboxApi._create_sandbox( template=template or cls.default_template, - timeout=timeout or cls.default_sandbox_timeout, + timeout=timeout, metadata=metadata, env_vars=envs, secure=secure, diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index ff8dc4a3e8..84fc8b8688 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -83,7 +83,7 @@ def list( :param query: Filter the list of sandboxes by metadata, state, start time, or template, e.g. `SandboxQuery(metadata={"key": "value"})` or `SandboxQuery(state=[SandboxState.RUNNING])` :param limit: Maximum number of sandboxes to return per page :param next_token: Token for pagination - :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), defaults to `"desc"` (newest first) + :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), when not set, the API default (currently `"desc"`, newest first) applies :return: A `SandboxPaginator` that yields pages of sandboxes (running and paused by default). Iterate pages via `paginator.next_items()` while `paginator.has_next` is True. """ @@ -207,11 +207,11 @@ def _cls_update_network( def _create_sandbox( cls, template: str, - timeout: int, - allow_internet_access: bool, + timeout: Optional[int], + allow_internet_access: Optional[bool], metadata: Optional[Dict[str, str]], env_vars: Optional[Dict[str, str]], - secure: bool, + secure: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -235,11 +235,13 @@ def _create_sandbox( auto_pause_memory=lifecycle_body.auto_pause_memory, auto_resume=lifecycle_body.auto_resume, metadata=metadata or {}, - timeout=timeout, + timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure, - allow_internet_access=allow_internet_access, + secure=secure if secure is not None else UNSET, + allow_internet_access=( + allow_internet_access if allow_internet_access is not None else UNSET + ), network=SandboxNetworkConfig(**network_body) if network_body else UNSET, iam=iam_body or UNSET, volume_mounts=volume_mounts if volume_mounts else UNSET, @@ -395,12 +397,7 @@ def _cls_fork( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> List[Union[SandboxCreateResponse, Exception]]: - timeout = ( - timeout if timeout is not None else SandboxBase.default_sandbox_timeout - ) - count = count if count is not None else 1 - - if count < 1: + if count is not None and count < 1: raise InvalidArgumentException("count must be at least 1") config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) @@ -409,7 +406,10 @@ def _cls_fork( res = post_sandboxes_sandbox_id_fork.sync_detailed( sandbox_id, client=api_client, - body=SandboxForkRequest(timeout=timeout, count=count), + body=SandboxForkRequest( + timeout=timeout if timeout is not None else UNSET, + count=count if count is not None else UNSET, + ), ) if res.status_code == 404: @@ -532,7 +532,7 @@ def _cls_delete_snapshot( def _cls_pause( cls, sandbox_id: str, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: config = ConnectionConfig(**cls._resolve_api_params(**opts)) @@ -541,7 +541,9 @@ def _cls_pause( res = post_sandboxes_sandbox_id_pause.sync_detailed( sandbox_id, client=api_client, - body=SandboxPauseRequest(memory=keep_memory), + body=SandboxPauseRequest( + memory=keep_memory if keep_memory is not None else UNSET + ), ) if res.status_code == 404: diff --git a/packages/python-sdk/e2b/template_async/build_api.py b/packages/python-sdk/e2b/template_async/build_api.py index 9a822cd516..b51494a093 100644 --- a/packages/python-sdk/e2b/template_async/build_api.py +++ b/packages/python-sdk/e2b/template_async/build_api.py @@ -49,16 +49,16 @@ async def request_build( client: AuthenticatedClient, name: str, tags: Optional[List[str]], - cpu_count: int, - memory_mb: int, + cpu_count: Optional[int], + memory_mb: Optional[int], ): res = await post_v3_templates.asyncio_detailed( client=client, body=TemplateBuildRequestV3( name=name, tags=tags if tags else UNSET, - cpu_count=cpu_count, - memory_mb=memory_mb, + cpu_count=cpu_count if cpu_count is not None else UNSET, + memory_mb=memory_mb if memory_mb is not None else UNSET, ), ) diff --git a/packages/python-sdk/e2b/template_async/main.py b/packages/python-sdk/e2b/template_async/main.py index 250d8aea32..1e88d9a92e 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -37,8 +37,8 @@ async def _build( template: TemplateClass, name: str, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, request_timeout: Optional[float] = None, @@ -50,8 +50,8 @@ async def _build( :param template: The template to build :param name: Name for the template :param tags: Optional tags for the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies + :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process """ @@ -196,8 +196,8 @@ async def build( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -209,8 +209,8 @@ async def build( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies + :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process @@ -299,8 +299,8 @@ async def build_in_background( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -312,8 +312,8 @@ async def build_in_background( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies + :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID diff --git a/packages/python-sdk/e2b/template_sync/build_api.py b/packages/python-sdk/e2b/template_sync/build_api.py index 2bc918ab2f..0fcd4ddc67 100644 --- a/packages/python-sdk/e2b/template_sync/build_api.py +++ b/packages/python-sdk/e2b/template_sync/build_api.py @@ -47,16 +47,16 @@ def request_build( client: AuthenticatedClient, name: str, tags: Optional[List[str]], - cpu_count: int, - memory_mb: int, + cpu_count: Optional[int], + memory_mb: Optional[int], ): res = post_v3_templates.sync_detailed( client=client, body=TemplateBuildRequestV3( name=name, tags=tags if tags else UNSET, - cpu_count=cpu_count, - memory_mb=memory_mb, + cpu_count=cpu_count if cpu_count is not None else UNSET, + memory_mb=memory_mb if memory_mb is not None else UNSET, ), ) diff --git a/packages/python-sdk/e2b/template_sync/main.py b/packages/python-sdk/e2b/template_sync/main.py index 9d397b41b4..af13f70959 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -37,8 +37,8 @@ def _build( template: TemplateClass, name: str, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, request_timeout: Optional[float] = None, @@ -50,8 +50,8 @@ def _build( :param template: The template to build :param name: Name for the template :param tags: Optional tags for the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies + :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process """ @@ -196,8 +196,8 @@ def build( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -209,8 +209,8 @@ def build( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies + :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process @@ -300,8 +300,8 @@ def build_in_background( *, alias: Optional[str] = None, tags: Optional[List[str]] = None, - cpu_count: int = 2, - memory_mb: int = 1024, + cpu_count: Optional[int] = None, + memory_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, **opts: Unpack[ApiParams], @@ -313,8 +313,8 @@ def build_in_background( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox - :param memory_mb: Amount of memory in MB allocated to the sandbox + :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies + :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py new file mode 100644 index 0000000000..a76beb2e99 --- /dev/null +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -0,0 +1,179 @@ +from types import SimpleNamespace +from typing import Any, Dict +from unittest.mock import AsyncMock, Mock + +from e2b import AsyncSandbox, Sandbox +from e2b.api.client.api.sandboxes import ( + post_sandboxes, + post_sandboxes_sandbox_id_fork, + post_sandboxes_sandbox_id_pause, +) +from e2b.api.client.models import Sandbox as SandboxModel + + +def _created_sandbox(): + return SimpleNamespace( + status_code=200, + parsed=SandboxModel( + client_id="client-id", + envd_version="0.2.4", + sandbox_id="sbx-test", + template_id="template-id", + ), + ) + + +def _sync_create_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes, "sync_detailed", request) + + Sandbox.create(api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_create_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes, "asyncio_detailed", request) + + await AsyncSandbox.create(api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_create_omits_api_owned_fields_when_unset(monkeypatch, test_api_key): + body = _sync_create_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "secure" not in body + assert "allow_internet_access" not in body + + +def test_create_sends_explicit_values(monkeypatch, test_api_key): + body = _sync_create_body( + monkeypatch, + test_api_key, + timeout=60, + secure=False, + allow_internet_access=False, + ) + + assert body["timeout"] == 60 + assert body["secure"] is False + assert body["allow_internet_access"] is False + + +async def test_async_create_omits_api_owned_fields_when_unset( + monkeypatch, test_api_key +): + body = await _async_create_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "secure" not in body + assert "allow_internet_access" not in body + + +async def test_async_create_sends_explicit_values(monkeypatch, test_api_key): + body = await _async_create_body( + monkeypatch, + test_api_key, + timeout=60, + secure=False, + allow_internet_access=False, + ) + + assert body["timeout"] == 60 + assert body["secure"] is False + assert body["allow_internet_access"] is False + + +def _sync_fork_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=SimpleNamespace(status_code=200, parsed=[])) + monkeypatch.setattr(post_sandboxes_sandbox_id_fork, "sync_detailed", request) + + Sandbox.fork("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_fork_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=SimpleNamespace(status_code=200, parsed=[])) + monkeypatch.setattr(post_sandboxes_sandbox_id_fork, "asyncio_detailed", request) + + await AsyncSandbox.fork("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_fork_omits_timeout_and_count_when_unset(monkeypatch, test_api_key): + body = _sync_fork_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "count" not in body + + +def test_fork_sends_explicit_timeout_and_count(monkeypatch, test_api_key): + body = _sync_fork_body(monkeypatch, test_api_key, timeout=60, count=2) + + assert body["timeout"] == 60 + assert body["count"] == 2 + + +async def test_async_fork_omits_timeout_and_count_when_unset( + monkeypatch, test_api_key +): + body = await _async_fork_body(monkeypatch, test_api_key) + + assert "timeout" not in body + assert "count" not in body + + +async def test_async_fork_sends_explicit_timeout_and_count(monkeypatch, test_api_key): + body = await _async_fork_body(monkeypatch, test_api_key, timeout=60, count=2) + + assert body["timeout"] == 60 + assert body["count"] == 2 + + +def _sync_pause_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=SimpleNamespace(status_code=204, parsed=None)) + monkeypatch.setattr(post_sandboxes_sandbox_id_pause, "sync_detailed", request) + + Sandbox.pause("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_pause_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=SimpleNamespace(status_code=204, parsed=None)) + monkeypatch.setattr(post_sandboxes_sandbox_id_pause, "asyncio_detailed", request) + + await AsyncSandbox.pause("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_pause_omits_memory_when_keep_memory_unset(monkeypatch, test_api_key): + body = _sync_pause_body(monkeypatch, test_api_key) + + assert "memory" not in body + + +def test_pause_sends_explicit_keep_memory(monkeypatch, test_api_key): + body = _sync_pause_body(monkeypatch, test_api_key, keep_memory=False) + + assert body["memory"] is False + + +async def test_async_pause_omits_memory_when_keep_memory_unset( + monkeypatch, test_api_key +): + body = await _async_pause_body(monkeypatch, test_api_key) + + assert "memory" not in body + + +async def test_async_pause_sends_explicit_keep_memory(monkeypatch, test_api_key): + body = await _async_pause_body(monkeypatch, test_api_key, keep_memory=False) + + assert body["memory"] is False From b92ce2b99d5cc9a34c1a4a7b99440c2f8dbe299b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:19:45 +0000 Subject: [PATCH 02/20] Fix ruff formatting in test_api_defaults.py Co-Authored-By: mish@e2b.dev --- packages/python-sdk/tests/shared/sandbox/test_api_defaults.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py index a76beb2e99..9aba290689 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -119,9 +119,7 @@ def test_fork_sends_explicit_timeout_and_count(monkeypatch, test_api_key): assert body["count"] == 2 -async def test_async_fork_omits_timeout_and_count_when_unset( - monkeypatch, test_api_key -): +async def test_async_fork_omits_timeout_and_count_when_unset(monkeypatch, test_api_key): body = await _async_fork_body(monkeypatch, test_api_key) assert "timeout" not in body From 996c67914ac477c2ec79978a4025297ae4c98d52 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:24:45 +0000 Subject: [PATCH 03/20] Keep SDK-side secure=true default for sandbox creation Co-Authored-By: mish@e2b.dev --- .changeset/olive-poets-hammer.md | 2 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 4 ++-- packages/js-sdk/tests/sandbox/apiDefaults.test.ts | 4 ++-- packages/python-sdk/e2b/sandbox_async/main.py | 2 +- packages/python-sdk/e2b/sandbox_async/sandbox_api.py | 2 +- packages/python-sdk/e2b/sandbox_sync/main.py | 2 +- packages/python-sdk/e2b/sandbox_sync/sandbox_api.py | 2 +- packages/python-sdk/tests/shared/sandbox/test_api_defaults.py | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md index 053a52d8e2..60390a1477 100644 --- a/.changeset/olive-poets-hammer.md +++ b/.changeset/olive-poets-hammer.md @@ -3,4 +3,4 @@ '@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. +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 `allow_internet_access` (sandboxes remain `secure` by default), pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 37730597bd..6e4a96a9d9 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -598,7 +598,7 @@ export interface SandboxOpts extends ConnectionOpts { /** * Secure all traffic coming to the sandbox controller with auth token * - * When not set, the API default (currently enabled) applies. + * @default true */ secure?: boolean @@ -1657,7 +1657,7 @@ export class SandboxApi extends ClientFactory { envVars: opts?.envs, timeout: timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), - secure: opts?.secure, + secure: opts?.secure ?? true, allow_internet_access: opts?.allowInternetAccess, network: buildNetworkBody(opts?.network, iam), iam, diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts index 098e3b2d10..cc8ce3526a 100644 --- a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -47,12 +47,12 @@ afterEach(() => { server.resetHandlers() }) -test('Sandbox.create omits timeout, secure and allow_internet_access when unset', async () => { +test('Sandbox.create omits timeout and allow_internet_access when unset and defaults secure to true', async () => { await Sandbox.create('base', { apiKey: TEST_API_KEY }) expect(lastCreateBody).toBeDefined() expect(lastCreateBody).not.toHaveProperty('timeout') - expect(lastCreateBody).not.toHaveProperty('secure') + expect(lastCreateBody?.secure).toBe(true) expect(lastCreateBody).not.toHaveProperty('allow_internet_access') }) diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 2b0ed8a50b..5480917680 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -191,7 +191,7 @@ async def create( :param timeout: Timeout for the sandbox in **seconds**. When not set, the API default timeout applies. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it. When not set, the API default (currently enabled) applies. + :param secure: Envd is secured with access token and cannot be used without it. Defaults to `True`. :param allow_internet_access: Allow sandbox to access the internet. When not set, the API default (currently allowed) applies. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index e66b1f603b..525aae8446 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -239,7 +239,7 @@ async def _create_sandbox( timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure if secure is not None else UNSET, + secure=secure if secure is not None else True, allow_internet_access=( allow_internet_access if allow_internet_access is not None else UNSET ), diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index f68db89b18..dd0f51dff1 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -187,7 +187,7 @@ def create( :param timeout: Timeout for the sandbox in **seconds**. When not set, the API default timeout applies. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it. When not set, the API default (currently enabled) applies. + :param secure: Envd is secured with access token and cannot be used without it. Defaults to `True`. :param allow_internet_access: Allow sandbox to access the internet. When not set, the API default (currently allowed) applies. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 84fc8b8688..33f835ae93 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -238,7 +238,7 @@ def _create_sandbox( timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure if secure is not None else UNSET, + secure=secure if secure is not None else True, allow_internet_access=( allow_internet_access if allow_internet_access is not None else UNSET ), diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py index 9aba290689..e9f9f7a6b9 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -45,7 +45,7 @@ def test_create_omits_api_owned_fields_when_unset(monkeypatch, test_api_key): body = _sync_create_body(monkeypatch, test_api_key) assert "timeout" not in body - assert "secure" not in body + assert body["secure"] is True assert "allow_internet_access" not in body @@ -69,7 +69,7 @@ async def test_async_create_omits_api_owned_fields_when_unset( body = await _async_create_body(monkeypatch, test_api_key) assert "timeout" not in body - assert "secure" not in body + assert body["secure"] is True assert "allow_internet_access" not in body From ef24b6424273d61dead16b928e77866f232c3c1a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:27:28 +0000 Subject: [PATCH 04/20] Remove redundant API-default doc mentions Co-Authored-By: mish@e2b.dev --- packages/js-sdk/src/sandbox/sandboxApi.ts | 11 ---------- packages/js-sdk/src/template/types.ts | 2 -- packages/python-sdk/e2b/sandbox_async/main.py | 22 +++++++++---------- packages/python-sdk/e2b/sandbox_sync/main.py | 22 +++++++++---------- .../python-sdk/e2b/template_async/main.py | 12 +++++----- packages/python-sdk/e2b/template_sync/main.py | 12 +++++----- 6 files changed, 34 insertions(+), 47 deletions(-) diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 6e4a96a9d9..04fc4effa8 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -513,8 +513,6 @@ export interface SandboxPauseOpts extends SandboxApiOpts { * When `false`, the in-memory state is dropped and only the filesystem is * persisted (a filesystem-only snapshot); resuming such a sandbox cold-boots * (reboots) it from disk, losing running processes and open connections. - * - * When not set, the API default (currently a full memory snapshot) applies. */ keepMemory?: boolean } @@ -529,16 +527,12 @@ export interface SandboxForkOpts extends ConnectionOpts { * All forks boot from the same snapshot — the snapshot is captured once * regardless of count. Each fork succeeds or fails independently; the * outcome of each is reported in its entry of the returned array. - * - * When not set, the API default (currently 1) applies. */ 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. - * - * When not set, the API default timeout applies. */ timeoutMs?: number } @@ -590,8 +584,6 @@ 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. - * - * When not set, the API default timeout applies. */ timeoutMs?: number @@ -604,8 +596,6 @@ export interface SandboxOpts extends ConnectionOpts { /** * Allow sandbox to access the internet. If set to `False`, it works the same as setting network `denyOut` to `[0.0.0.0/0]`. - * - * When not set, the API default (currently allowed) applies. */ allowInternetAccess?: boolean @@ -714,7 +704,6 @@ export interface SandboxListOpts extends Omit { /** * Sort order of the list of sandboxes by start time, applied across the * whole result set before pagination (not within a page). - * When not set, the API default (currently `'desc'`, newest first) applies. */ order?: SandboxListOrder diff --git a/packages/js-sdk/src/template/types.ts b/packages/js-sdk/src/template/types.ts index 8a65112d31..e7b7c70efd 100644 --- a/packages/js-sdk/src/template/types.ts +++ b/packages/js-sdk/src/template/types.ts @@ -34,12 +34,10 @@ export type BasicBuildOptions = { tags?: string[] /** * Number of CPUs allocated to the sandbox. - * When not set, the API default applies. */ cpuCount?: number /** * Amount of memory in MB allocated to the sandbox. - * When not set, the API default applies. */ memoryMB?: number /** diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 5480917680..4257fbb511 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -188,11 +188,11 @@ async def create( By default, the sandbox is created from the default `base` sandbox template. :param template: Sandbox template name or ID - :param timeout: Timeout for the sandbox in **seconds**. When not set, the API default timeout applies. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. + :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox :param secure: Envd is secured with access token and cannot be used without it. Defaults to `True`. - :param allow_internet_access: Allow sandbox to access the internet. When not set, the API default (currently allowed) applies. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. + :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}``. Registered tokens are exposed to ``network.rules`` ``transform`` callables as ``ctx.iam.tokens[name]`` placeholders, which the egress proxy resolves per request @@ -377,8 +377,8 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies - :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -416,8 +416,8 @@ async def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies - :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -453,8 +453,8 @@ async def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies - :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -755,7 +755,7 @@ async def pause( """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -772,7 +772,7 @@ async def pause( Pause the sandbox specified by sandbox ID. :param sandbox_id: Sandbox ID - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -787,7 +787,7 @@ async def pause( """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. When not set, the API default (currently a full memory snapshot) applies. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index dd0f51dff1..208a7049dc 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -184,11 +184,11 @@ def create( By default, the sandbox is created from the default `base` sandbox template. :param template: Sandbox template name or ID - :param timeout: Timeout for the sandbox in **seconds**. When not set, the API default timeout applies. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. + :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox :param secure: Envd is secured with access token and cannot be used without it. Defaults to `True`. - :param allow_internet_access: Allow sandbox to access the internet. When not set, the API default (currently allowed) applies. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. + :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). :param iam: Sandbox workload identity configuration. A non-empty ``tokens`` map enables workload identity for the sandbox; token definitions can be created with :meth:`Secret.iam_token`. Example: ``{"tokens": {"aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID")}}``. Registered tokens are exposed to ``network.rules`` ``transform`` callables as ``ctx.iam.tokens[name]`` placeholders, which the egress proxy resolves per request @@ -372,8 +372,8 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies - :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -411,8 +411,8 @@ def fork( (e.g. 429 to `RateLimitException`). :param sandbox_id: Sandbox ID - :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies - :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :param logger: Logger used for request and response logging for the forked sandboxes. Accepts any standard library `logging.Logger`. When omitted, no request/response logging is emitted. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -448,8 +448,8 @@ def fork( error codes map to the same exception classes as other API errors (e.g. 429 to `RateLimitException`). - :param timeout: Timeout for the forked sandboxes in **seconds**. When not set, the API default timeout applies - :param count: Number of forked sandboxes to create. When not set, the API default (currently 1) applies + :param timeout: Timeout for the forked sandboxes in **seconds**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -753,7 +753,7 @@ def pause( """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -770,7 +770,7 @@ def pause( Pause the sandbox specified by sandbox ID. :param sandbox_id: Sandbox ID - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. When not set, the API default (currently a full memory snapshot) applies. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -785,7 +785,7 @@ def pause( """ Pause the sandbox. - :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. When not set, the API default (currently a full memory snapshot) applies. + :param keep_memory: When `False`, the in-memory state is dropped and only the filesystem is persisted (no memory snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ diff --git a/packages/python-sdk/e2b/template_async/main.py b/packages/python-sdk/e2b/template_async/main.py index 1e88d9a92e..531cb0fbcb 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -50,8 +50,8 @@ async def _build( :param template: The template to build :param name: Name for the template :param tags: Optional tags for the template - :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies - :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process """ @@ -209,8 +209,8 @@ async def build( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies - :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process @@ -312,8 +312,8 @@ async def build_in_background( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies - :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID diff --git a/packages/python-sdk/e2b/template_sync/main.py b/packages/python-sdk/e2b/template_sync/main.py index af13f70959..af302fbd0c 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -50,8 +50,8 @@ def _build( :param template: The template to build :param name: Name for the template :param tags: Optional tags for the template - :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies - :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process """ @@ -209,8 +209,8 @@ def build( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies - :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :param on_build_logs: Callback function to receive build logs during the build process @@ -313,8 +313,8 @@ def build_in_background( :param name: Template name in 'name' or 'name:tag' format :param alias: (Deprecated) Alias name for the template. Use name instead. :param tags: Optional additional tags to assign to the template - :param cpu_count: Number of CPUs allocated to the sandbox. When not set, the API default applies - :param memory_mb: Amount of memory in MB allocated to the sandbox. When not set, the API default applies + :param cpu_count: Number of CPUs allocated to the sandbox. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID From b984e34ecfd246e421c135f0f0eafc6cd47b6265 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:44:13 +0200 Subject: [PATCH 05/20] fix(sdk): match JS and Python on malformed and null egress proxy input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BYOP surface from #1688 diverged for callers that bypass the types. Python raised InvalidArgumentException on a proxy without a string address; JS rebuilt the body from the known fields, so `egressProxy` passed as a bare string sent `{}` and the caller got an API error naming a field they never left out. Mirror the guard in buildEgressProxyBody, the way buildIamBody already does for untyped token maps. Both SDKs also forwarded a null/None username or password as a JSON null, which the API rejects — `{"username": os.environ.get(...)}` on an unset variable is the way that happens. Read it as "no credentials", the same reading both already gave `egressProxy: null` itself, and normalize a null username coming back out of getInfo so SandboxEgressProxyInfo.username cannot be a null its type forbids. The get_info example published in both CHANGELOGs for 2.41.0 subscripts `info.network["egress_proxy"]`, which KeyErrors on every sandbox without a proxy — SandboxNetworkInfo is total=False and the key is only set when one is configured. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/egress-proxy-untyped-callers.md | 43 +++++++++++ packages/js-sdk/CHANGELOG.md | 2 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 35 +++++---- .../js-sdk/tests/sandbox/egressProxy.test.ts | 74 +++++++++++++++++-- packages/python-sdk/CHANGELOG.md | 2 +- .../python-sdk/e2b/sandbox/sandbox_api.py | 16 ++-- .../tests/shared/sandbox/test_egress_proxy.py | 35 +++++++++ 7 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 .changeset/egress-proxy-untyped-callers.md diff --git a/.changeset/egress-proxy-untyped-callers.md b/.changeset/egress-proxy-untyped-callers.md new file mode 100644 index 0000000000..efd7eaab53 --- /dev/null +++ b/.changeset/egress-proxy-untyped-callers.md @@ -0,0 +1,43 @@ +--- +'e2b': patch +'@e2b/python-sdk': patch +--- + +Bring the JS and Python halves of `network.egressProxy` / `network["egress_proxy"]` back in line for callers that bypass the types, and stop `null` credentials from reaching the wire. + +A malformed proxy now raises `InvalidArgumentError` / `InvalidArgumentException` in both SDKs. Before, only Python did; JS rebuilt the body from the known fields, so a proxy passed as a bare string sent `{}` and the caller got an API error about a field they never left out: + +```ts +// Now: InvalidArgumentError, naming the option you typed. +// Before: sent `"egressProxy": {}` and failed at the API. +await Sandbox.create({ + network: { egressProxy: 'proxy.example.com:1080' as never }, +}) +``` + +A `username` or `password` that is `null` / `None` is treated as "no credentials" rather than serialized as a JSON null the API rejects — the same reading both SDKs already gave `egressProxy: null` itself: + +```ts +await Sandbox.create({ + network: { + egressProxy: { + address: 'proxy.example.com:1080', + // Unset in the environment; the proxy takes no credentials. + username: process.env.PROXY_USER, + }, + }, +}) +``` + +```python +Sandbox.create( + network={ + "egress_proxy": { + "address": "proxy.example.com:1080", + "username": os.environ.get("PROXY_USER"), + }, + }, +) +``` + +`getInfo` / `get_info` normalizes a `null` `username` the same way, so `SandboxEgressProxyInfo.username` is `undefined` / an absent key rather than a null that its type says cannot be there. diff --git a/packages/js-sdk/CHANGELOG.md b/packages/js-sdk/CHANGELOG.md index c0002e840f..9b7e102b25 100644 --- a/packages/js-sdk/CHANGELOG.md +++ b/packages/js-sdk/CHANGELOG.md @@ -267,7 +267,7 @@ ```python info = sandbox.get_info() - print(info.network["egress_proxy"]) + print(info.network.get("egress_proxy")) # {'address': 'proxy.example.com:1080', 'username': 'proxy-user'} ``` diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 04fc4effa8..918d4c656e 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -1009,20 +1009,30 @@ function resolveRulesForBody( /** * Rebuild the proxy config from the known fields so stray properties on the * caller's object never reach the wire and a later mutation of it cannot alter - * the in-flight request. Validation is the server's — it is the only side that - * can tell whether the address resolves, and to where. + * the in-flight request. Address reachability is the server's — it is the only + * side that can tell whether the address resolves, and to where. */ function buildEgressProxyBody( egressProxy: SandboxEgressProxyOpts ): components['schemas']['SandboxEgressProxyConfig'] { + // Re-check at runtime for callers that bypass the type — rebuilding from the + // known fields drops an address that isn't there, and the API error for the + // resulting `{}` names neither the option the caller typed nor the mistake. + // Python raises `InvalidArgumentException` on the same input. + if (typeof egressProxy.address !== 'string') { + throw new InvalidArgumentError( + "network egressProxy must be an object with a string 'address' " + + "(e.g. 'proxy.example.com:1080')." + ) + } + return { address: egressProxy.address, - ...(egressProxy.username !== undefined - ? { username: egressProxy.username } - : {}), - ...(egressProxy.password !== undefined - ? { password: egressProxy.password } - : {}), + // `!= null` so a credential read out of an unset environment variable + // reads as "no credentials" rather than reaching the wire as JSON null, + // which the API rejects. Same reasoning as `egressProxy: null` itself. + ...(egressProxy.username != null ? { username: egressProxy.username } : {}), + ...(egressProxy.password != null ? { password: egressProxy.password } : {}), } } @@ -1066,8 +1076,9 @@ function buildNetworkEgress( /** * Map the wire proxy config into the SDK-owned shape: `password` is dropped - * because the API never returns it, and the wire's `null` for "no proxy" is - * normalized so the union never reaches a consumer. + * because the API never returns it, and the wire's `null` — for "no proxy" and + * for an anonymous proxy's `username` alike — is normalized so it never reaches + * a consumer typed to see `undefined`. */ function fromApiEgressProxy( egressProxy: components['schemas']['SandboxEgressProxyConfig'] | undefined @@ -1078,9 +1089,7 @@ function fromApiEgressProxy( return { address: egressProxy.address, - ...(egressProxy.username !== undefined - ? { username: egressProxy.username } - : {}), + ...(egressProxy.username != null ? { username: egressProxy.username } : {}), } } diff --git a/packages/js-sdk/tests/sandbox/egressProxy.test.ts b/packages/js-sdk/tests/sandbox/egressProxy.test.ts index b861fd2df7..d6087bc77e 100644 --- a/packages/js-sdk/tests/sandbox/egressProxy.test.ts +++ b/packages/js-sdk/tests/sandbox/egressProxy.test.ts @@ -2,7 +2,7 @@ import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import { Sandbox } from '../../src' +import { InvalidArgumentError, Sandbox } from '../../src' import { TEST_API_KEY, apiUrl } from '../setup' const sandboxId = 'test-sandbox-id' @@ -101,14 +101,64 @@ test('Sandbox.create combines the egress proxy with allow and deny lists', async }) }) -test('Sandbox.create omits the egress proxy when not provided', async () => { +test.for([ + ['omitted', { allowOut: ['api.example.com'] }], + // Untyped callers spell "no proxy" as null; Python treats an explicit None + // the same way. + ['null', { egressProxy: null }], +])( + 'Sandbox.create omits the egress proxy when it is %s', + async ([, network]: [string, Record]) => { + await Sandbox.create('base', { + apiKey: TEST_API_KEY, + network, + }) + + expect(lastCreateBody?.network).toBeDefined() + expect(lastCreateBody?.network).not.toHaveProperty('egressProxy') + } +) + +test.for([ + // An empty object is falsy but present — it must not silently disable + // tunneling. Match Python: fail loudly. + ['empty', {}], + ['missing-address', { username: 'proxy-user' }], + ['non-string-address', { address: 1080 }], + ['string', 'proxy.example.com:1080'], +])( + 'Sandbox.create rejects a %s egress proxy', + async ([, egressProxy]: [string, unknown]) => { + // Rebuilding from the known fields drops an address that isn't there, so + // without this the caller gets an API error about a `{}` they never wrote. + await expect( + Sandbox.create('base', { + apiKey: TEST_API_KEY, + network: { egressProxy } as never, + }) + ).rejects.toThrow(InvalidArgumentError) + + expect(lastCreateBody).toBeUndefined() + } +) + +test('Sandbox.create omits credentials that are null', async () => { + // `{ username: process.env.PROXY_USER }` on an unset variable is the way + // this happens; a JSON null is rejected by the API. await Sandbox.create('base', { apiKey: TEST_API_KEY, - network: { allowOut: ['api.example.com'] }, + network: { + egressProxy: { + address: 'proxy.example.com:1080', + username: null, + password: undefined, + } as never, + }, }) - expect(lastCreateBody?.network).toBeDefined() - expect(lastCreateBody?.network).not.toHaveProperty('egressProxy') + expect(lastCreateBody?.network.egressProxy).toEqual({ + address: 'proxy.example.com:1080', + }) }) test('Sandbox.create strips unknown egress proxy properties', async () => { @@ -199,6 +249,20 @@ test('getInfo drops a password the API unexpectedly returns', async () => { }) }) +test('getInfo drops a null username', async () => { + // `username?: string` says absence is `undefined`, so a null from the wire + // has to be normalized rather than handed to a consumer. + sandboxNetwork = { + egressProxy: { address: 'proxy.example.com:1080', username: null }, + } + + const info = await Sandbox.getInfo(sandboxId, { apiKey: TEST_API_KEY }) + + expect(info.network?.egressProxy).toEqual({ + address: 'proxy.example.com:1080', + }) +}) + test.for([ ['omitted', {}], ['null', { egressProxy: null }], diff --git a/packages/python-sdk/CHANGELOG.md b/packages/python-sdk/CHANGELOG.md index c19b593dee..5b1b2895fd 100644 --- a/packages/python-sdk/CHANGELOG.md +++ b/packages/python-sdk/CHANGELOG.md @@ -243,7 +243,7 @@ ```python info = sandbox.get_info() - print(info.network["egress_proxy"]) + print(info.network.get("egress_proxy")) # {'address': 'proxy.example.com:1080', 'username': 'proxy-user'} ``` diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index d53c84915a..a9177f07e8 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -671,9 +671,12 @@ def _build_egress_proxy( ) body = ClientSandboxEgressProxyConfig(address=egress_proxy["address"]) - if "username" in egress_proxy: + # `is not None` so a credential read out of an unset environment variable + # reads as "no credentials" rather than reaching the wire as JSON null, + # which the API rejects. Same reasoning as ``"egress_proxy": None`` itself. + if egress_proxy.get("username") is not None: body.username = egress_proxy["username"] - if "password" in egress_proxy: + if egress_proxy.get("password") is not None: body.password = egress_proxy["password"] return body @@ -899,15 +902,16 @@ def _from_client_egress_proxy( ) -> Optional[SandboxEgressProxyInfo]: """ Map the wire proxy config into the SDK-owned shape: ``password`` is dropped - because the API never returns it, and the wire's ``None`` for "no proxy" - becomes an absent key. + because the API never returns it, and the wire's ``None`` — for "no proxy" + and for an anonymous proxy's ``username`` alike — becomes an absent key. """ if not isinstance(egress_proxy, ClientSandboxEgressProxyConfig): return None result: SandboxEgressProxyInfo = {"address": egress_proxy.address} - if not isinstance(egress_proxy.username, Unset): - result["username"] = egress_proxy.username + username = egress_proxy.username + if not isinstance(username, Unset) and username is not None: + result["username"] = username return result diff --git a/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py b/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py index e097ee1711..182167699b 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py +++ b/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py @@ -85,6 +85,25 @@ def test_create_rejects_a_malformed_egress_proxy(egress_proxy): build_network_config(cast(Any, {"egress_proxy": egress_proxy})) +def test_create_omits_credentials_that_are_none(): + # ``{"username": os.environ.get("PROXY_USER")}`` on an unset variable is the + # way this happens; a JSON null is rejected by the API. + body = build_network_config( + cast( + Any, + { + "egress_proxy": { + "address": "proxy.example.com:1080", + "username": None, + "password": None, + }, + }, + ) + ) + assert body is not None + assert body["egress_proxy"].to_dict() == {"address": "proxy.example.com:1080"} + + def test_create_strips_unknown_egress_proxy_keys(): # An untyped caller can copy an extra key out of a config file; the API # rejects unknown properties. @@ -145,6 +164,22 @@ def test_get_info_reports_the_active_egress_proxy_without_the_password(): } +def test_get_info_drops_a_none_username(): + # ``username`` is ``NotRequired[str]``, so absence is a missing key — a None + # from the wire has to be normalized rather than handed to a caller. + info = from_client_network_config( + SandboxNetworkConfig( + egress_proxy=ClientSandboxEgressProxyConfig( + address="proxy.example.com:1080", + username=cast(Any, None), + ) + ) + ) + + assert info is not None + assert info["egress_proxy"] == {"address": "proxy.example.com:1080"} + + @pytest.mark.parametrize( "egress_proxy", [ From 6d991cb64bef0e249b2738566569fd4cf89dbd62 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:43:55 +0000 Subject: [PATCH 06/20] Revert "fix(sdk): match JS and Python on malformed and null egress proxy input" This reverts commit b984e34ecfd246e421c135f0f0eafc6cd47b6265. --- .changeset/egress-proxy-untyped-callers.md | 43 ----------- packages/js-sdk/CHANGELOG.md | 2 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 35 ++++----- .../js-sdk/tests/sandbox/egressProxy.test.ts | 74 ++----------------- packages/python-sdk/CHANGELOG.md | 2 +- .../python-sdk/e2b/sandbox/sandbox_api.py | 16 ++-- .../tests/shared/sandbox/test_egress_proxy.py | 35 --------- 7 files changed, 26 insertions(+), 181 deletions(-) delete mode 100644 .changeset/egress-proxy-untyped-callers.md diff --git a/.changeset/egress-proxy-untyped-callers.md b/.changeset/egress-proxy-untyped-callers.md deleted file mode 100644 index efd7eaab53..0000000000 --- a/.changeset/egress-proxy-untyped-callers.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'e2b': patch -'@e2b/python-sdk': patch ---- - -Bring the JS and Python halves of `network.egressProxy` / `network["egress_proxy"]` back in line for callers that bypass the types, and stop `null` credentials from reaching the wire. - -A malformed proxy now raises `InvalidArgumentError` / `InvalidArgumentException` in both SDKs. Before, only Python did; JS rebuilt the body from the known fields, so a proxy passed as a bare string sent `{}` and the caller got an API error about a field they never left out: - -```ts -// Now: InvalidArgumentError, naming the option you typed. -// Before: sent `"egressProxy": {}` and failed at the API. -await Sandbox.create({ - network: { egressProxy: 'proxy.example.com:1080' as never }, -}) -``` - -A `username` or `password` that is `null` / `None` is treated as "no credentials" rather than serialized as a JSON null the API rejects — the same reading both SDKs already gave `egressProxy: null` itself: - -```ts -await Sandbox.create({ - network: { - egressProxy: { - address: 'proxy.example.com:1080', - // Unset in the environment; the proxy takes no credentials. - username: process.env.PROXY_USER, - }, - }, -}) -``` - -```python -Sandbox.create( - network={ - "egress_proxy": { - "address": "proxy.example.com:1080", - "username": os.environ.get("PROXY_USER"), - }, - }, -) -``` - -`getInfo` / `get_info` normalizes a `null` `username` the same way, so `SandboxEgressProxyInfo.username` is `undefined` / an absent key rather than a null that its type says cannot be there. diff --git a/packages/js-sdk/CHANGELOG.md b/packages/js-sdk/CHANGELOG.md index 9b7e102b25..c0002e840f 100644 --- a/packages/js-sdk/CHANGELOG.md +++ b/packages/js-sdk/CHANGELOG.md @@ -267,7 +267,7 @@ ```python info = sandbox.get_info() - print(info.network.get("egress_proxy")) + print(info.network["egress_proxy"]) # {'address': 'proxy.example.com:1080', 'username': 'proxy-user'} ``` diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 918d4c656e..04fc4effa8 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -1009,30 +1009,20 @@ function resolveRulesForBody( /** * Rebuild the proxy config from the known fields so stray properties on the * caller's object never reach the wire and a later mutation of it cannot alter - * the in-flight request. Address reachability is the server's — it is the only - * side that can tell whether the address resolves, and to where. + * the in-flight request. Validation is the server's — it is the only side that + * can tell whether the address resolves, and to where. */ function buildEgressProxyBody( egressProxy: SandboxEgressProxyOpts ): components['schemas']['SandboxEgressProxyConfig'] { - // Re-check at runtime for callers that bypass the type — rebuilding from the - // known fields drops an address that isn't there, and the API error for the - // resulting `{}` names neither the option the caller typed nor the mistake. - // Python raises `InvalidArgumentException` on the same input. - if (typeof egressProxy.address !== 'string') { - throw new InvalidArgumentError( - "network egressProxy must be an object with a string 'address' " + - "(e.g. 'proxy.example.com:1080')." - ) - } - return { address: egressProxy.address, - // `!= null` so a credential read out of an unset environment variable - // reads as "no credentials" rather than reaching the wire as JSON null, - // which the API rejects. Same reasoning as `egressProxy: null` itself. - ...(egressProxy.username != null ? { username: egressProxy.username } : {}), - ...(egressProxy.password != null ? { password: egressProxy.password } : {}), + ...(egressProxy.username !== undefined + ? { username: egressProxy.username } + : {}), + ...(egressProxy.password !== undefined + ? { password: egressProxy.password } + : {}), } } @@ -1076,9 +1066,8 @@ function buildNetworkEgress( /** * Map the wire proxy config into the SDK-owned shape: `password` is dropped - * because the API never returns it, and the wire's `null` — for "no proxy" and - * for an anonymous proxy's `username` alike — is normalized so it never reaches - * a consumer typed to see `undefined`. + * because the API never returns it, and the wire's `null` for "no proxy" is + * normalized so the union never reaches a consumer. */ function fromApiEgressProxy( egressProxy: components['schemas']['SandboxEgressProxyConfig'] | undefined @@ -1089,7 +1078,9 @@ function fromApiEgressProxy( return { address: egressProxy.address, - ...(egressProxy.username != null ? { username: egressProxy.username } : {}), + ...(egressProxy.username !== undefined + ? { username: egressProxy.username } + : {}), } } diff --git a/packages/js-sdk/tests/sandbox/egressProxy.test.ts b/packages/js-sdk/tests/sandbox/egressProxy.test.ts index d6087bc77e..b861fd2df7 100644 --- a/packages/js-sdk/tests/sandbox/egressProxy.test.ts +++ b/packages/js-sdk/tests/sandbox/egressProxy.test.ts @@ -2,7 +2,7 @@ import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' import { http, HttpResponse } from 'msw' import { setupServer } from 'msw/node' -import { InvalidArgumentError, Sandbox } from '../../src' +import { Sandbox } from '../../src' import { TEST_API_KEY, apiUrl } from '../setup' const sandboxId = 'test-sandbox-id' @@ -101,64 +101,14 @@ test('Sandbox.create combines the egress proxy with allow and deny lists', async }) }) -test.for([ - ['omitted', { allowOut: ['api.example.com'] }], - // Untyped callers spell "no proxy" as null; Python treats an explicit None - // the same way. - ['null', { egressProxy: null }], -])( - 'Sandbox.create omits the egress proxy when it is %s', - async ([, network]: [string, Record]) => { - await Sandbox.create('base', { - apiKey: TEST_API_KEY, - network, - }) - - expect(lastCreateBody?.network).toBeDefined() - expect(lastCreateBody?.network).not.toHaveProperty('egressProxy') - } -) - -test.for([ - // An empty object is falsy but present — it must not silently disable - // tunneling. Match Python: fail loudly. - ['empty', {}], - ['missing-address', { username: 'proxy-user' }], - ['non-string-address', { address: 1080 }], - ['string', 'proxy.example.com:1080'], -])( - 'Sandbox.create rejects a %s egress proxy', - async ([, egressProxy]: [string, unknown]) => { - // Rebuilding from the known fields drops an address that isn't there, so - // without this the caller gets an API error about a `{}` they never wrote. - await expect( - Sandbox.create('base', { - apiKey: TEST_API_KEY, - network: { egressProxy } as never, - }) - ).rejects.toThrow(InvalidArgumentError) - - expect(lastCreateBody).toBeUndefined() - } -) - -test('Sandbox.create omits credentials that are null', async () => { - // `{ username: process.env.PROXY_USER }` on an unset variable is the way - // this happens; a JSON null is rejected by the API. +test('Sandbox.create omits the egress proxy when not provided', async () => { await Sandbox.create('base', { apiKey: TEST_API_KEY, - network: { - egressProxy: { - address: 'proxy.example.com:1080', - username: null, - password: undefined, - } as never, - }, + network: { allowOut: ['api.example.com'] }, }) - expect(lastCreateBody?.network.egressProxy).toEqual({ - address: 'proxy.example.com:1080', - }) + expect(lastCreateBody?.network).toBeDefined() + expect(lastCreateBody?.network).not.toHaveProperty('egressProxy') }) test('Sandbox.create strips unknown egress proxy properties', async () => { @@ -249,20 +199,6 @@ test('getInfo drops a password the API unexpectedly returns', async () => { }) }) -test('getInfo drops a null username', async () => { - // `username?: string` says absence is `undefined`, so a null from the wire - // has to be normalized rather than handed to a consumer. - sandboxNetwork = { - egressProxy: { address: 'proxy.example.com:1080', username: null }, - } - - const info = await Sandbox.getInfo(sandboxId, { apiKey: TEST_API_KEY }) - - expect(info.network?.egressProxy).toEqual({ - address: 'proxy.example.com:1080', - }) -}) - test.for([ ['omitted', {}], ['null', { egressProxy: null }], diff --git a/packages/python-sdk/CHANGELOG.md b/packages/python-sdk/CHANGELOG.md index 5b1b2895fd..c19b593dee 100644 --- a/packages/python-sdk/CHANGELOG.md +++ b/packages/python-sdk/CHANGELOG.md @@ -243,7 +243,7 @@ ```python info = sandbox.get_info() - print(info.network.get("egress_proxy")) + print(info.network["egress_proxy"]) # {'address': 'proxy.example.com:1080', 'username': 'proxy-user'} ``` diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index a9177f07e8..d53c84915a 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -671,12 +671,9 @@ def _build_egress_proxy( ) body = ClientSandboxEgressProxyConfig(address=egress_proxy["address"]) - # `is not None` so a credential read out of an unset environment variable - # reads as "no credentials" rather than reaching the wire as JSON null, - # which the API rejects. Same reasoning as ``"egress_proxy": None`` itself. - if egress_proxy.get("username") is not None: + if "username" in egress_proxy: body.username = egress_proxy["username"] - if egress_proxy.get("password") is not None: + if "password" in egress_proxy: body.password = egress_proxy["password"] return body @@ -902,16 +899,15 @@ def _from_client_egress_proxy( ) -> Optional[SandboxEgressProxyInfo]: """ Map the wire proxy config into the SDK-owned shape: ``password`` is dropped - because the API never returns it, and the wire's ``None`` — for "no proxy" - and for an anonymous proxy's ``username`` alike — becomes an absent key. + because the API never returns it, and the wire's ``None`` for "no proxy" + becomes an absent key. """ if not isinstance(egress_proxy, ClientSandboxEgressProxyConfig): return None result: SandboxEgressProxyInfo = {"address": egress_proxy.address} - username = egress_proxy.username - if not isinstance(username, Unset) and username is not None: - result["username"] = username + if not isinstance(egress_proxy.username, Unset): + result["username"] = egress_proxy.username return result diff --git a/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py b/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py index 182167699b..e097ee1711 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py +++ b/packages/python-sdk/tests/shared/sandbox/test_egress_proxy.py @@ -85,25 +85,6 @@ def test_create_rejects_a_malformed_egress_proxy(egress_proxy): build_network_config(cast(Any, {"egress_proxy": egress_proxy})) -def test_create_omits_credentials_that_are_none(): - # ``{"username": os.environ.get("PROXY_USER")}`` on an unset variable is the - # way this happens; a JSON null is rejected by the API. - body = build_network_config( - cast( - Any, - { - "egress_proxy": { - "address": "proxy.example.com:1080", - "username": None, - "password": None, - }, - }, - ) - ) - assert body is not None - assert body["egress_proxy"].to_dict() == {"address": "proxy.example.com:1080"} - - def test_create_strips_unknown_egress_proxy_keys(): # An untyped caller can copy an extra key out of a config file; the API # rejects unknown properties. @@ -164,22 +145,6 @@ def test_get_info_reports_the_active_egress_proxy_without_the_password(): } -def test_get_info_drops_a_none_username(): - # ``username`` is ``NotRequired[str]``, so absence is a missing key — a None - # from the wire has to be normalized rather than handed to a caller. - info = from_client_network_config( - SandboxNetworkConfig( - egress_proxy=ClientSandboxEgressProxyConfig( - address="proxy.example.com:1080", - username=cast(Any, None), - ) - ) - ) - - assert info is not None - assert info["egress_proxy"] == {"address": "proxy.example.com:1080"} - - @pytest.mark.parametrize( "egress_proxy", [ From aa59e1f68c8c71ed003a35d5bde139bb444a635b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:52:18 +0000 Subject: [PATCH 07/20] Pin explicit 300s timeout in test sandbox fixtures Fixture sandboxes previously inherited the SDK's 300s create default; after removing SDK-side defaults they would fall back to the API's 15s default, making long-running integration tests flaky. Co-Authored-By: mish@e2b.dev --- packages/js-sdk/tests/setup.ts | 1 + packages/python-sdk/tests/conftest.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 91d427c3cc..90a1774788 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -82,6 +82,7 @@ export const sandboxTest = base.extend({ async ({ sandboxTestId, sandboxOpts }, use) => { const sandbox = await Sandbox.create(template, { metadata: { sandboxTestId }, + timeoutMs: 300_000, ...sandboxOpts, }) onTestFailed(() => { diff --git a/packages/python-sdk/tests/conftest.py b/packages/python-sdk/tests/conftest.py index fa8f4a0949..9847f460e7 100644 --- a/packages/python-sdk/tests/conftest.py +++ b/packages/python-sdk/tests/conftest.py @@ -72,6 +72,7 @@ def sandbox_factory(request, template, sandbox_test_id): def factory(*, template_name: str = template, **kwargs): metadata = kwargs.setdefault("metadata", dict()) metadata.setdefault("sandbox_test_id", sandbox_test_id) + kwargs.setdefault("timeout", 300) sandbox = Sandbox.create(template_name, **kwargs) @@ -99,6 +100,7 @@ async def async_sandbox_factory(request, template, sandbox_test_id): async def factory(*, template_name: str = template, **kwargs): metadata = kwargs.setdefault("metadata", dict()) metadata.setdefault("sandbox_test_id", sandbox_test_id) + kwargs.setdefault("timeout", 300) sandbox = await AsyncSandbox.create(template_name, **kwargs) sandboxes.append(sandbox) From e4d7b95e8c47b50fe3f1304073502b9c5e79dbd6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:15:36 +0000 Subject: [PATCH 08/20] Address review: add template build payload tests, mirror connect timeout default, simplify order docs Co-Authored-By: mish@e2b.dev --- .changeset/olive-poets-hammer.md | 2 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 4 +- .../js-sdk/tests/template/apiDefaults.test.ts | 52 ++++++++++++++ .../e2b/sandbox_async/sandbox_api.py | 2 +- .../e2b/sandbox_sync/sandbox_api.py | 2 +- .../shared/template/test_api_defaults.py | 68 +++++++++++++++++++ 6 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 packages/js-sdk/tests/template/apiDefaults.test.ts create mode 100644 packages/python-sdk/tests/shared/template/test_api_defaults.py diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md index 60390a1477..b5715e5fb8 100644 --- a/.changeset/olive-poets-hammer.md +++ b/.changeset/olive-poets-hammer.md @@ -3,4 +3,4 @@ '@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 `allow_internet_access` (sandboxes remain `secure` by default), pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. +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 `allow_internet_access` (sandboxes remain `secure` by default), pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. `connect` keeps the SDK's 5-minute default because the API requires the `timeout` field in the connect request. diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 04fc4effa8..5523263d9b 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -1160,6 +1160,8 @@ function buildNetworkUpdateBody( } } export class SandboxApi extends ClientFactory { + protected static readonly defaultSandboxTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS + protected constructor() { super() } @@ -1767,7 +1769,7 @@ export class SandboxApi extends ClientFactory { opts?: SandboxConnectOpts ) { const apiOpts = this.resolveOpts(opts) - const timeoutMs = apiOpts?.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS + const timeoutMs = apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) diff --git a/packages/js-sdk/tests/template/apiDefaults.test.ts b/packages/js-sdk/tests/template/apiDefaults.test.ts new file mode 100644 index 0000000000..0c1fcbb7f7 --- /dev/null +++ b/packages/js-sdk/tests/template/apiDefaults.test.ts @@ -0,0 +1,52 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +import { ApiClient } from '../../src/api' +import { ConnectionConfig } from '../../src/connectionConfig' +import { requestBuild } from '../../src/template/buildApi' +import { TEST_API_KEY, apiUrl } from '../setup' + +let lastBuildBody: Record | undefined + +const server = setupServer( + http.post(apiUrl('/v3/templates'), async ({ request }) => { + lastBuildBody = (await request.json()) as Record + return HttpResponse.json({ + templateID: 'test-template-id', + buildID: 'test-build-id', + }) + }) +) + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +afterAll(() => server.close()) + +afterEach(() => { + lastBuildBody = undefined + server.resetHandlers() +}) + +function client() { + return new ApiClient(new ConnectionConfig({ apiKey: TEST_API_KEY })) +} + +test('template build request omits cpuCount and memoryMB when unset', async () => { + await requestBuild(client(), { name: 'test-template' }) + + expect(lastBuildBody).toBeDefined() + expect(lastBuildBody).not.toHaveProperty('cpuCount') + expect(lastBuildBody).not.toHaveProperty('memoryMB') +}) + +test('template build request sends explicit cpuCount and memoryMB', async () => { + await requestBuild(client(), { + name: 'test-template', + cpuCount: 1, + memoryMB: 512, + }) + + expect(lastBuildBody?.cpuCount).toBe(1) + expect(lastBuildBody?.memoryMB).toBe(512) +}) diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index 525aae8446..41dd446df2 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -84,7 +84,7 @@ def list( :param query: Filter the list of sandboxes by metadata, state, start time, or template, e.g. `SandboxQuery(metadata={"key": "value"})` or `SandboxQuery(state=[SandboxState.RUNNING])` :param limit: Maximum number of sandboxes to return per page :param next_token: Token for pagination - :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), when not set, the API default (currently `"desc"`, newest first) applies + :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page) :return: An `AsyncSandboxPaginator` that yields pages of sandboxes (running and paused by default). Iterate pages via `await paginator.next_items()` while `paginator.has_next` is True. """ diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 33f835ae93..f950595ace 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -83,7 +83,7 @@ def list( :param query: Filter the list of sandboxes by metadata, state, start time, or template, e.g. `SandboxQuery(metadata={"key": "value"})` or `SandboxQuery(state=[SandboxState.RUNNING])` :param limit: Maximum number of sandboxes to return per page :param next_token: Token for pagination - :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page), when not set, the API default (currently `"desc"`, newest first) applies + :param order: Sort order of the list of sandboxes by start time, applied across the whole result set before pagination (not within a page) :return: A `SandboxPaginator` that yields pages of sandboxes (running and paused by default). Iterate pages via `paginator.next_items()` while `paginator.has_next` is True. """ diff --git a/packages/python-sdk/tests/shared/template/test_api_defaults.py b/packages/python-sdk/tests/shared/template/test_api_defaults.py new file mode 100644 index 0000000000..da061fbf90 --- /dev/null +++ b/packages/python-sdk/tests/shared/template/test_api_defaults.py @@ -0,0 +1,68 @@ +from types import SimpleNamespace +from typing import Any, Dict +from unittest.mock import AsyncMock, Mock + +from e2b.api.client.api.templates import post_v3_templates +from e2b.api.client.models import TemplateRequestResponseV3 +from e2b.template_async.build_api import request_build as async_request_build +from e2b.template_sync.build_api import request_build as sync_request_build + + +def _build_response(): + return SimpleNamespace( + status_code=200, + parsed=TemplateRequestResponseV3( + template_id="template-id", + build_id="build-id", + public=False, + names=[], + tags=[], + aliases=[], + ), + ) + + +def _sync_build_body(monkeypatch, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=_build_response()) + monkeypatch.setattr(post_v3_templates, "sync_detailed", request) + + sync_request_build(Mock(), name="test-template", tags=None, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_build_body(monkeypatch, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=_build_response()) + monkeypatch.setattr(post_v3_templates, "asyncio_detailed", request) + + await async_request_build(Mock(), name="test-template", tags=None, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_build_omits_cpu_and_memory_when_unset(monkeypatch): + body = _sync_build_body(monkeypatch, cpu_count=None, memory_mb=None) + + assert "cpuCount" not in body + assert "memoryMB" not in body + + +def test_build_sends_explicit_cpu_and_memory(monkeypatch): + body = _sync_build_body(monkeypatch, cpu_count=1, memory_mb=512) + + assert body["cpuCount"] == 1 + assert body["memoryMB"] == 512 + + +async def test_async_build_omits_cpu_and_memory_when_unset(monkeypatch): + body = await _async_build_body(monkeypatch, cpu_count=None, memory_mb=None) + + assert "cpuCount" not in body + assert "memoryMB" not in body + + +async def test_async_build_sends_explicit_cpu_and_memory(monkeypatch): + body = await _async_build_body(monkeypatch, cpu_count=1, memory_mb=512) + + assert body["cpuCount"] == 1 + assert body["memoryMB"] == 512 From a06670b02a314a9bbf862b92df70c82e716f8a05 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:55:53 +0000 Subject: [PATCH 09/20] Drop SDK-side connect timeout and secure defaults Co-Authored-By: mish@e2b.dev --- .changeset/olive-poets-hammer.md | 2 +- packages/js-sdk/src/sandbox/sandboxApi.ts | 17 +++---- .../js-sdk/tests/sandbox/apiDefaults.test.ts | 30 +++++++++++- .../python-sdk/e2b/sandbox/sandbox_api.py | 16 +++++++ packages/python-sdk/e2b/sandbox_async/main.py | 2 +- .../e2b/sandbox_async/sandbox_api.py | 8 ++-- packages/python-sdk/e2b/sandbox_sync/main.py | 2 +- .../e2b/sandbox_sync/sandbox_api.py | 8 ++-- .../tests/shared/sandbox/test_api_defaults.py | 47 ++++++++++++++++++- 9 files changed, 104 insertions(+), 28 deletions(-) diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md index b5715e5fb8..9050d567f2 100644 --- a/.changeset/olive-poets-hammer.md +++ b/.changeset/olive-poets-hammer.md @@ -3,4 +3,4 @@ '@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 `allow_internet_access` (sandboxes remain `secure` by default), pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. `connect` keeps the SDK's 5-minute default because the API requires the `timeout` field in the connect request. +Remove SDK-side defaults from API request payloads so the API defaults apply when options are omitted. Sandbox create/fork/connect no longer preset a 5-minute timeout, fork no longer presets `count: 1`, create no longer presets `secure: true` or `allow_internet_access`, pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. Note: until the API-side defaults for `secure` and connect `timeout` are deployed, omitting them changes behavior (sandboxes are created unsecured and connect requests without a timeout are rejected). diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 5523263d9b..4c39d98ef9 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -3,7 +3,6 @@ import { ClientFactory, ConnectionConfig, ConnectionOpts, - DEFAULT_SANDBOX_TIMEOUT_MS, } from '../connectionConfig' import { compareVersions } from 'compare-versions' import { ALL_TRAFFIC } from './network' @@ -589,8 +588,6 @@ export interface SandboxOpts extends ConnectionOpts { /** * Secure all traffic coming to the sandbox controller with auth token - * - * @default true */ secure?: boolean @@ -663,8 +660,6 @@ export type SandboxConnectOpts = ConnectionOpts & { * Timeout for the sandbox in **milliseconds**. * For running sandboxes, the timeout will update only if the new timeout is longer than the existing one. * 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 */ timeoutMs?: number } @@ -1160,8 +1155,6 @@ function buildNetworkUpdateBody( } } export class SandboxApi extends ClientFactory { - protected static readonly defaultSandboxTimeoutMs = DEFAULT_SANDBOX_TIMEOUT_MS - protected constructor() { super() } @@ -1648,7 +1641,7 @@ export class SandboxApi extends ClientFactory { envVars: opts?.envs, timeout: timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), - secure: opts?.secure ?? true, + secure: opts?.secure, allow_internet_access: opts?.allowInternetAccess, network: buildNetworkBody(opts?.network, iam), iam, @@ -1769,7 +1762,7 @@ export class SandboxApi extends ClientFactory { opts?: SandboxConnectOpts ) { const apiOpts = this.resolveOpts(opts) - const timeoutMs = apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs + const timeoutMs = apiOpts?.timeoutMs const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) @@ -1780,9 +1773,11 @@ export class SandboxApi extends ClientFactory { sandboxID: sandboxId, }, }, + // TODO: drop the cast once the API spec makes `timeout` optional body: { - timeout: timeoutToSeconds(timeoutMs), - }, + timeout: + timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), + } as components['schemas']['ConnectSandbox'], signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts index cc8ce3526a..0783744318 100644 --- a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -8,6 +8,7 @@ import { TEST_API_KEY, apiUrl } from '../setup' let lastCreateBody: Record | undefined let lastForkBody: Record | undefined let lastPauseBody: Record | undefined +let lastConnectBody: Record | undefined const server = setupServer( http.post(apiUrl('/sandboxes'), async ({ request }) => { @@ -18,6 +19,14 @@ const server = setupServer( envdVersion: '0.2.4', }) }), + http.post(apiUrl('/sandboxes/:sandboxID/connect'), async ({ request }) => { + lastConnectBody = (await request.json()) as Record + 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 return HttpResponse.json([ @@ -44,15 +53,16 @@ afterEach(() => { lastCreateBody = undefined lastForkBody = undefined lastPauseBody = undefined + lastConnectBody = undefined server.resetHandlers() }) -test('Sandbox.create omits timeout and allow_internet_access when unset and defaults secure to true', async () => { +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?.secure).toBe(true) + expect(lastCreateBody).not.toHaveProperty('secure') expect(lastCreateBody).not.toHaveProperty('allow_internet_access') }) @@ -103,3 +113,19 @@ test('Sandbox.pause sends an explicit keepMemory', async () => { expect(lastPauseBody?.memory).toBe(false) }) + +test('Sandbox.connect omits timeout when unset', async () => { + await Sandbox.connect('test-sandbox-id', { apiKey: TEST_API_KEY }) + + expect(lastConnectBody).toBeDefined() + expect(lastConnectBody).not.toHaveProperty('timeout') +}) + +test('Sandbox.connect sends an explicit timeout', async () => { + await Sandbox.connect('test-sandbox-id', { + apiKey: TEST_API_KEY, + timeoutMs: 60_000, + }) + + expect(lastConnectBody?.timeout).toBe(60) +}) diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index d53c84915a..9b80d05163 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -18,6 +18,7 @@ from typing_extensions import NotRequired, Unpack from e2b.api.client.models import ( + ConnectSandbox, ListedSandbox, SandboxDetail, SandboxState, @@ -73,6 +74,21 @@ from e2b.paginator import PaginatorBase +class ConnectSandboxBody(ConnectSandbox): + """Connect request body that omits `timeout` when not provided so the + API default applies. The generated model still requires `timeout`; + remove this once the spec makes it optional.""" + + def __init__(self, timeout: Optional[int] = None): + super().__init__(timeout=cast(int, timeout)) + + def to_dict(self) -> Dict[str, Any]: + result = super().to_dict() + if result["timeout"] is None: + del result["timeout"] + return result + + class GitHubMcpServerConfig(TypedDict): """ Configuration for a GitHub-based MCP server. diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 4257fbb511..ed578f746e 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -191,7 +191,7 @@ async def create( :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it. Defaults to `True`. + :param secure: Envd is secured with access token and cannot be used without it. :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index 41dd446df2..035053e3cb 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -25,7 +25,6 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( - ConnectSandbox, Error, NewSandbox, SandboxSnapshotRequest, @@ -48,6 +47,7 @@ from e2b.sandbox.main import SandboxBase from e2b.sandbox.sandbox_api import ( build_network_update_body, + ConnectSandboxBody, McpServer, SandboxIamOpts, SandboxInfo, @@ -239,7 +239,7 @@ async def _create_sandbox( timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure if secure is not None else True, + secure=secure if secure is not None else UNSET, allow_internet_access=( allow_internet_access if allow_internet_access is not None else UNSET ), @@ -530,8 +530,6 @@ async def _cls_connect( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> SandboxCreateResponse: - timeout = timeout or SandboxBase.default_sandbox_timeout - # Sandbox is not running, resume it config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) @@ -539,7 +537,7 @@ async def _cls_connect( res = await post_sandboxes_sandbox_id_connect.asyncio_detailed( sandbox_id, client=api_client, - body=ConnectSandbox(timeout=timeout), + body=ConnectSandboxBody(timeout=timeout), ) if res.status_code == 404: diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 208a7049dc..31567e9909 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -187,7 +187,7 @@ def create( :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it. Defaults to `True`. + :param secure: Envd is secured with access token and cannot be used without it. :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index f950595ace..10eee48f14 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -25,7 +25,6 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( - ConnectSandbox, Error, NewSandbox, SandboxSnapshotRequest, @@ -47,6 +46,7 @@ from e2b.sandbox.main import SandboxBase from e2b.sandbox.sandbox_api import ( build_network_update_body, + ConnectSandboxBody, McpServer, SandboxIamOpts, SandboxInfo, @@ -238,7 +238,7 @@ def _create_sandbox( timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure if secure is not None else True, + secure=secure if secure is not None else UNSET, allow_internet_access=( allow_internet_access if allow_internet_access is not None else UNSET ), @@ -345,15 +345,13 @@ def _cls_connect( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> SandboxCreateResponse: - timeout = timeout or SandboxBase.default_sandbox_timeout - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) res = post_sandboxes_sandbox_id_connect.sync_detailed( sandbox_id, client=api_client, - body=ConnectSandbox(timeout=timeout), + body=ConnectSandboxBody(timeout=timeout), ) if res.status_code == 404: diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py index e9f9f7a6b9..785f8d6b4b 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -5,6 +5,7 @@ from e2b import AsyncSandbox, Sandbox from e2b.api.client.api.sandboxes import ( post_sandboxes, + post_sandboxes_sandbox_id_connect, post_sandboxes_sandbox_id_fork, post_sandboxes_sandbox_id_pause, ) @@ -45,7 +46,7 @@ def test_create_omits_api_owned_fields_when_unset(monkeypatch, test_api_key): body = _sync_create_body(monkeypatch, test_api_key) assert "timeout" not in body - assert body["secure"] is True + assert "secure" not in body assert "allow_internet_access" not in body @@ -69,7 +70,7 @@ async def test_async_create_omits_api_owned_fields_when_unset( body = await _async_create_body(monkeypatch, test_api_key) assert "timeout" not in body - assert body["secure"] is True + assert "secure" not in body assert "allow_internet_access" not in body @@ -175,3 +176,45 @@ async def test_async_pause_sends_explicit_keep_memory(monkeypatch, test_api_key) body = await _async_pause_body(monkeypatch, test_api_key, keep_memory=False) assert body["memory"] is False + + +def _sync_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "sync_detailed", request) + + Sandbox.connect("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +async def _async_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = AsyncMock(return_value=_created_sandbox()) + monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "asyncio_detailed", request) + + await AsyncSandbox.connect("sbx-test", api_key=api_key, **kwargs) + + return request.call_args.kwargs["body"].to_dict() + + +def test_connect_omits_timeout_when_unset(monkeypatch, test_api_key): + body = _sync_connect_body(monkeypatch, test_api_key) + + assert "timeout" not in body + + +def test_connect_sends_explicit_timeout(monkeypatch, test_api_key): + body = _sync_connect_body(monkeypatch, test_api_key, timeout=60) + + assert body["timeout"] == 60 + + +async def test_async_connect_omits_timeout_when_unset(monkeypatch, test_api_key): + body = await _async_connect_body(monkeypatch, test_api_key) + + assert "timeout" not in body + + +async def test_async_connect_sends_explicit_timeout(monkeypatch, test_api_key): + body = await _async_connect_body(monkeypatch, test_api_key, timeout=60) + + assert body["timeout"] == 60 From e9963a5dd7d46c6c1bc020984d926f0ea90a983a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:30:44 +0000 Subject: [PATCH 10/20] Remove client-side fork count validation per T-52 Co-Authored-By: mish@e2b.dev --- .changeset/witty-parrots-decide.md | 6 ++++++ packages/js-sdk/src/sandbox/sandboxApi.ts | 4 ---- packages/js-sdk/tests/sandbox/fork.test.ts | 10 ++-------- packages/python-sdk/e2b/sandbox_async/sandbox_api.py | 4 ---- packages/python-sdk/e2b/sandbox_sync/sandbox_api.py | 4 ---- .../python-sdk/tests/async/sandbox_async/test_fork.py | 7 +------ .../python-sdk/tests/sync/sandbox_sync/test_fork.py | 7 +------ 7 files changed, 10 insertions(+), 32 deletions(-) create mode 100644 .changeset/witty-parrots-decide.md diff --git a/.changeset/witty-parrots-decide.md b/.changeset/witty-parrots-decide.md new file mode 100644 index 0000000000..8b7793a719 --- /dev/null +++ b/.changeset/witty-parrots-decide.md @@ -0,0 +1,6 @@ +--- +'e2b': patch +'@e2b/python-sdk': patch +--- + +Remove client-side validation of the fork `count` argument. The API validates the requested fork count and rejects invalid values. diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index 4c39d98ef9..761b7a734d 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -1693,10 +1693,6 @@ export class SandboxApi extends ClientFactory { count?: number, opts?: SandboxApiOpts ): Promise { - if (count !== undefined && count < 1) { - throw new InvalidArgumentError('count must be at least 1') - } - const apiOpts = this.resolveOpts(opts) const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) diff --git a/packages/js-sdk/tests/sandbox/fork.test.ts b/packages/js-sdk/tests/sandbox/fork.test.ts index da3d30b9a6..20e8a227a7 100644 --- a/packages/js-sdk/tests/sandbox/fork.test.ts +++ b/packages/js-sdk/tests/sandbox/fork.test.ts @@ -1,8 +1,8 @@ import { assert, expect, test } from 'vitest' -import { sandboxTest, isDebug, TEST_API_KEY } from '../setup.js' +import { sandboxTest, isDebug } from '../setup.js' import { Sandbox } from '../../src' -import { InvalidArgumentError, SandboxNotFoundError } from '../../src/errors' +import { SandboxNotFoundError } from '../../src/errors' sandboxTest.skipIf(isDebug)('fork a sandbox', async ({ sandbox }) => { await sandbox.files.write('/home/user/state.txt', 'state before fork') @@ -86,9 +86,3 @@ test.skipIf(isDebug)('fork a killed sandbox fails', async () => { await expect(sandbox.fork()).rejects.toThrowError(SandboxNotFoundError) }) - -test('fork with count lower than 1 fails', async () => { - await expect( - Sandbox.fork('sbx-test', { count: 0, apiKey: TEST_API_KEY }) - ).rejects.toThrowError(InvalidArgumentError) -}) diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index 035053e3cb..8866dafffe 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -38,7 +38,6 @@ from e2b.api.client_async import get_api_client from e2b.connection_config import ApiParams, ConnectionConfig from e2b.exceptions import ( - InvalidArgumentException, NotFoundException, SandboxException, SandboxNotFoundException, @@ -446,9 +445,6 @@ async def _cls_fork( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> List[Union[SandboxCreateResponse, Exception]]: - if count is not None and count < 1: - raise InvalidArgumentException("count must be at least 1") - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 10eee48f14..0f9806d878 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -37,7 +37,6 @@ from e2b.api.client.types import UNSET, Unset from e2b.connection_config import ApiParams, ConnectionConfig from e2b.exceptions import ( - InvalidArgumentException, NotFoundException, SandboxException, SandboxNotFoundException, @@ -395,9 +394,6 @@ def _cls_fork( logger: Optional[logging.Logger] = None, **opts: Unpack[ApiParams], ) -> List[Union[SandboxCreateResponse, Exception]]: - if count is not None and count < 1: - raise InvalidArgumentException("count must be at least 1") - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) diff --git a/packages/python-sdk/tests/async/sandbox_async/test_fork.py b/packages/python-sdk/tests/async/sandbox_async/test_fork.py index 19f8c7ef23..ab15402dc7 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_fork.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_fork.py @@ -1,7 +1,7 @@ import pytest from e2b import AsyncSandbox -from e2b.exceptions import InvalidArgumentException, SandboxNotFoundException +from e2b.exceptions import SandboxNotFoundException @pytest.mark.skip_debug() @@ -76,8 +76,3 @@ async def test_fork_killed_sandbox(async_sandbox_factory): with pytest.raises(SandboxNotFoundException): await sandbox.fork() - - -async def test_fork_invalid_count(): - with pytest.raises(InvalidArgumentException): - await AsyncSandbox.fork("sbx-test", count=0) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py b/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py index 7159a53102..903431591b 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py @@ -1,7 +1,7 @@ import pytest from e2b import Sandbox -from e2b.exceptions import InvalidArgumentException, SandboxNotFoundException +from e2b.exceptions import SandboxNotFoundException @pytest.mark.skip_debug() @@ -73,8 +73,3 @@ def test_fork_killed_sandbox(sandbox_factory): with pytest.raises(SandboxNotFoundException): sandbox.fork() - - -def test_fork_invalid_count(): - with pytest.raises(InvalidArgumentException): - Sandbox.fork("sbx-test", count=0) From c5ed9b12fb80359f6945a5f7654f6983dc6658ad Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:53:13 +0000 Subject: [PATCH 11/20] feat(sdk): create and connect sandboxes through the v2 API endpoints Sync the v2 create/connect routes into spec/openapi.yml and regenerate the JS and Python API clients. Sandbox.create posts to /v2/sandboxes (NewSandboxV2, no secure field: envd access is always secured) and Sandbox.connect posts to /v2/sandboxes/{id}/connect with an optional body, so omitted timeouts fall back to the API's 300s default. Drops the secure option from create and the Python ConnectSandboxBody shim. Co-Authored-By: mish@e2b.dev --- .changeset/olive-poets-hammer.md | 4 +- packages/js-sdk/src/api/schema.gen.ts | 143 ++++++++++- packages/js-sdk/src/sandbox/sandboxApi.ts | 15 +- .../js-sdk/tests/sandbox/abortSignal.test.ts | 2 +- .../js-sdk/tests/sandbox/apiDefaults.test.ts | 10 +- .../js-sdk/tests/sandbox/egressProxy.test.ts | 2 +- .../tests/sandbox/files/signing.test.ts | 6 - packages/js-sdk/tests/sandbox/iam.test.ts | 2 +- .../tests/sandbox/lifecycleRequest.test.ts | 2 +- .../tests/sandbox/networkTransform.test.ts | 2 +- .../tests/sandbox/onResumeRequest.test.ts | 2 +- packages/js-sdk/tests/sandbox/secure.test.ts | 6 - .../client/api/sandboxes/post_sandboxes.py | 8 +- .../post_sandboxes_sandbox_id_connect.py | 12 +- .../client/api/sandboxes/post_v2_sandboxes.py | 196 +++++++++++++++ .../post_v_2_sandboxes_sandbox_id_connect.py | 221 ++++++++++++++++ .../e2b/api/client/models/__init__.py | 4 + .../api/client/models/connect_sandbox_v2.py | 73 ++++++ .../e2b/api/client/models/new_sandbox_v2.py | 236 ++++++++++++++++++ .../python-sdk/e2b/sandbox/sandbox_api.py | 24 +- packages/python-sdk/e2b/sandbox_async/main.py | 5 - .../e2b/sandbox_async/sandbox_api.py | 20 +- packages/python-sdk/e2b/sandbox_sync/main.py | 5 - .../e2b/sandbox_sync/sandbox_api.py | 20 +- .../async/sandbox_async/files/test_write.py | 2 +- .../tests/async/sandbox_async/test_connect.py | 6 +- .../tests/async/sandbox_async/test_create.py | 12 +- .../tests/async/sandbox_async/test_network.py | 2 +- .../tests/async/sandbox_async/test_secure.py | 4 +- .../tests/shared/sandbox/test_api_defaults.py | 18 +- .../shared/sandbox/test_lifecycle_request.py | 12 +- .../shared/sandbox/test_on_resume_request.py | 20 +- .../sync/sandbox_sync/files/test_watch.py | 2 +- .../sync/sandbox_sync/files/test_write.py | 2 +- .../tests/sync/sandbox_sync/test_connect.py | 6 +- .../tests/sync/sandbox_sync/test_create.py | 12 +- .../tests/sync/sandbox_sync/test_network.py | 4 +- .../tests/sync/sandbox_sync/test_secure.py | 4 +- spec/openapi.yml | 169 ++++++++++++- 39 files changed, 1140 insertions(+), 155 deletions(-) create mode 100644 packages/python-sdk/e2b/api/client/api/sandboxes/post_v2_sandboxes.py create mode 100644 packages/python-sdk/e2b/api/client/api/sandboxes/post_v_2_sandboxes_sandbox_id_connect.py create mode 100644 packages/python-sdk/e2b/api/client/models/connect_sandbox_v2.py create mode 100644 packages/python-sdk/e2b/api/client/models/new_sandbox_v2.py diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md index 9050d567f2..c7cb947a41 100644 --- a/.changeset/olive-poets-hammer.md +++ b/.changeset/olive-poets-hammer.md @@ -3,4 +3,6 @@ '@e2b/python-sdk': minor --- -Remove SDK-side defaults from API request payloads so the API defaults apply when options are omitted. Sandbox create/fork/connect no longer preset a 5-minute timeout, fork no longer presets `count: 1`, create no longer presets `secure: true` or `allow_internet_access`, pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. Note: until the API-side defaults for `secure` and connect `timeout` are deployed, omitting them changes behavior (sandboxes are created unsecured and connect requests without a timeout are rejected). +Remove SDK-side defaults from API request payloads so the API defaults apply when options are omitted. Sandbox create/fork/connect no longer preset a 5-minute timeout, fork no longer presets `count: 1`, create no longer presets `allow_internet_access`, pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. + +Sandbox create and connect now use the v2 API endpoints (`POST /v2/sandboxes`, `POST /v2/sandboxes/{id}/connect`), which default `timeout` to 5 minutes and always secure envd access. The `secure` option on `Sandbox.create` has been removed: every sandbox is secured. diff --git a/packages/js-sdk/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index 8943b3385e..134deccc08 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -45,7 +45,8 @@ export interface paths { put?: never; /** * Create sandbox - * @description Create a sandbox from the template + * @deprecated + * @description Create a sandbox from the template. Use POST /v2/sandboxes instead. */ post: { parameters: { @@ -163,7 +164,8 @@ export interface paths { put?: never; /** * Connect sandbox - * @description Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. + * @deprecated + * @description Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. Use POST /v2/sandboxes/{sandboxID}/connect instead. */ post: { parameters: { @@ -1720,7 +1722,102 @@ export interface paths { }; }; put?: never; - post?: never; + /** + * Create sandbox (v2) + * @description Create a sandbox from the template. All system communication with the sandbox is secured. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NewSandboxV2"]; + }; + }; + responses: { + /** @description The sandbox was created successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Sandbox"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 429: components["responses"]["429"]; + 500: components["responses"]["500"]; + 503: components["responses"]["503"]; + 504: components["responses"]["504"]; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/sandboxes/{sandboxID}/connect": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Connect sandbox (v2) + * @description Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. The request body is optional; an omitted timeout defaults to 300 seconds. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + sandboxID: components["parameters"]["sandboxID"]; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ConnectSandboxV2"]; + }; + }; + responses: { + /** @description The sandbox was already running */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Sandbox"]; + }; + }; + /** @description The sandbox was resumed successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Sandbox"]; + }; + }; + 400: components["responses"]["400"]; + 401: components["responses"]["401"]; + 404: components["responses"]["404"]; + 409: components["responses"]["409"]; + 429: components["responses"]["429"]; + 500: components["responses"]["500"]; + 503: components["responses"]["503"]; + 504: components["responses"]["504"]; + }; + }; delete?: never; options?: never; head?: never; @@ -2209,6 +2306,16 @@ export interface components { */ timeout: number; }; + ConnectSandboxV2: { + /** @description Defaults to true. When false and the sandbox is paused, resume from disk state only: the sandbox cold-boots fresh and any memory in the snapshot is ignored, never modified or deleted. Disk state has crash-recovery semantics — writes not flushed before the pause may be lost. A no-op for snapshots that contain no memory. Rejected with an error in environments where this capability is not enabled, never silently downgraded to a memory restore. */ + memory?: boolean; + /** + * Format: int32 + * @description Timeout in seconds from the current time after which the sandbox should expire + * @default 300 + */ + timeout?: number; + }; /** * Format: int32 * @description CPU cores for the sandbox @@ -2368,6 +2475,36 @@ export interface components { timeout?: number; volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; }; + /** @description Sandbox creation request. All system communication with the sandbox is always secured; the template's envd version must support secured access. */ + NewSandboxV2: { + /** @description Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut to 0.0.0.0/0 in the network config. */ + allow_internet_access?: boolean; + /** + * @description Automatically pauses the sandbox after the timeout + * @default false + */ + autoPause?: boolean; + /** + * @description Controls the snapshot kind taken when the sandbox auto-pauses on timeout (only relevant when autoPause is true). When false, the auto-pause drops the in-memory state and persists only the filesystem (a filesystem-only snapshot); resuming it cold-boots (reboots) the sandbox from disk. Such a snapshot cannot be auto-resumed by traffic and must be resumed explicitly, so it cannot be combined with autoResume. Defaults to true (full memory snapshot). + * @default true + */ + autoPauseMemory?: boolean; + autoResume?: components["schemas"]["SandboxAutoResumeConfig"]; + envVars?: components["schemas"]["EnvVars"]; + iam?: components["schemas"]["SandboxIam"]; + mcp?: components["schemas"]["Mcp"]; + metadata?: components["schemas"]["SandboxMetadata"]; + network?: components["schemas"]["SandboxNetworkConfig"]; + /** @description Identifier of the required template */ + templateID: string; + /** + * Format: int32 + * @description Time to live for the sandbox in seconds. + * @default 300 + */ + timeout?: number; + volumeMounts?: components["schemas"]["SandboxVolumeMount"][]; + }; NewSecret: { metadata?: components["schemas"]["SecretMetadata"]; /** @description Name of the secret, unique within the project. Names are lower-cased before storage and returned in that canonical form; the sec_ prefix is reserved for secret identifiers. */ diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index a44751fd53..234540d014 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -601,11 +601,6 @@ export interface SandboxOpts extends ConnectionOpts { */ timeoutMs?: number - /** - * Secure all traffic coming to the sandbox controller with auth token - */ - 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]`. */ @@ -1697,14 +1692,13 @@ export class SandboxApi extends ClientFactory { // against the workload tokens this request registers. const iam = buildIamBody(opts?.iam) - const body: components['schemas']['NewSandbox'] = { + const body: components['schemas']['NewSandboxV2'] = { templateID: template, metadata: opts?.metadata, mcp: opts?.mcp as Record | undefined, envVars: opts?.envs, timeout: timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), - secure: opts?.secure, allow_internet_access: opts?.allowInternetAccess, network: buildNetworkBody(opts?.network, iam), iam, @@ -1724,7 +1718,7 @@ export class SandboxApi extends ClientFactory { ) } - const res = await client.api.POST('/sandboxes', { + const res = await client.api.POST('/v2/sandboxes', { body, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) @@ -1838,18 +1832,17 @@ export class SandboxApi extends ClientFactory { ) } - const res = await client.api.POST('/sandboxes/{sandboxID}/connect', { + const res = await client.api.POST('/v2/sandboxes/{sandboxID}/connect', { params: { path: { sandboxID: sandboxId, }, }, - // TODO: drop the cast once the API spec makes `timeout` optional body: { timeout: timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), memory: onResume === 'reboot' ? false : undefined, - } as components['schemas']['ConnectSandbox'], + }, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) diff --git a/packages/js-sdk/tests/sandbox/abortSignal.test.ts b/packages/js-sdk/tests/sandbox/abortSignal.test.ts index 2aa767ce28..8fc98080e9 100644 --- a/packages/js-sdk/tests/sandbox/abortSignal.test.ts +++ b/packages/js-sdk/tests/sandbox/abortSignal.test.ts @@ -20,7 +20,7 @@ function holdUntilAborted(signal: AbortSignal): Promise { } const restHandlers = [ - http.post(apiUrl('/sandboxes'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes'), async ({ request }) => { await holdUntilAborted(request.signal) return HttpResponse.json({}) }), diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts index 0783744318..3aa66698c2 100644 --- a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -11,7 +11,7 @@ let lastPauseBody: Record | undefined let lastConnectBody: Record | undefined const server = setupServer( - http.post(apiUrl('/sandboxes'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes'), async ({ request }) => { lastCreateBody = (await request.json()) as Record return HttpResponse.json({ sandboxID: 'test-sandbox-id', @@ -19,7 +19,7 @@ const server = setupServer( envdVersion: '0.2.4', }) }), - http.post(apiUrl('/sandboxes/:sandboxID/connect'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes/:sandboxID/connect'), async ({ request }) => { lastConnectBody = (await request.json()) as Record return HttpResponse.json({ sandboxID: 'test-sandbox-id', @@ -57,7 +57,7 @@ afterEach(() => { server.resetHandlers() }) -test('Sandbox.create omits timeout, secure and allow_internet_access when unset', async () => { +test('Sandbox.create omits timeout and allow_internet_access when unset', async () => { await Sandbox.create('base', { apiKey: TEST_API_KEY }) expect(lastCreateBody).toBeDefined() @@ -66,16 +66,14 @@ test('Sandbox.create omits timeout, secure and allow_internet_access when unset' expect(lastCreateBody).not.toHaveProperty('allow_internet_access') }) -test('Sandbox.create sends explicit timeout, secure and allow_internet_access', async () => { +test('Sandbox.create sends explicit timeout 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) }) diff --git a/packages/js-sdk/tests/sandbox/egressProxy.test.ts b/packages/js-sdk/tests/sandbox/egressProxy.test.ts index a807ed34f3..f3799a0bca 100644 --- a/packages/js-sdk/tests/sandbox/egressProxy.test.ts +++ b/packages/js-sdk/tests/sandbox/egressProxy.test.ts @@ -12,7 +12,7 @@ let lastUpdateBody: Record | undefined let sandboxNetwork: Record | undefined const server = setupServer( - http.post(apiUrl('/sandboxes'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes'), async ({ request }) => { lastCreateBody = (await request.json()) as Record return HttpResponse.json({ sandboxID: sandboxId, diff --git a/packages/js-sdk/tests/sandbox/files/signing.test.ts b/packages/js-sdk/tests/sandbox/files/signing.test.ts index 3f65cdcfa2..8d319c02c1 100644 --- a/packages/js-sdk/tests/sandbox/files/signing.test.ts +++ b/packages/js-sdk/tests/sandbox/files/signing.test.ts @@ -3,12 +3,6 @@ import { assert, describe } from 'vitest' import { sandboxTest, isDebug } from '../../setup' describe('file signing', () => { - sandboxTest.override({ - sandboxOpts: { - secure: true, - }, - }) - sandboxTest.skipIf(isDebug)( 'test access file with expired signing', async ({ sandbox }) => { diff --git a/packages/js-sdk/tests/sandbox/iam.test.ts b/packages/js-sdk/tests/sandbox/iam.test.ts index 4cf768afc8..1c5159009f 100644 --- a/packages/js-sdk/tests/sandbox/iam.test.ts +++ b/packages/js-sdk/tests/sandbox/iam.test.ts @@ -11,7 +11,7 @@ const RUNTIME_PROBED_PROPS = ['toJSON', 'then', 'toString', 'valueOf'] let lastCreateBody: Record | undefined const server = setupServer( - http.post(apiUrl('/sandboxes'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes'), async ({ request }) => { lastCreateBody = (await request.json()) as Record return HttpResponse.json({ sandboxID: 'test-sandbox-id', diff --git a/packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts b/packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts index 12fe5d5f0f..1a83ec5707 100644 --- a/packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts +++ b/packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts @@ -8,7 +8,7 @@ import { TEST_API_KEY, apiUrl } from '../setup' let lastCreateBody: Record | undefined const server = setupServer( - http.post(apiUrl('/sandboxes'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes'), async ({ request }) => { lastCreateBody = (await request.json()) as Record return HttpResponse.json({ sandboxID: 'test-sandbox-id', diff --git a/packages/js-sdk/tests/sandbox/networkTransform.test.ts b/packages/js-sdk/tests/sandbox/networkTransform.test.ts index 9be7b3ab46..39f61fccff 100644 --- a/packages/js-sdk/tests/sandbox/networkTransform.test.ts +++ b/packages/js-sdk/tests/sandbox/networkTransform.test.ts @@ -11,7 +11,7 @@ let lastCreateBody: Record | undefined let lastUpdateBody: Record | undefined const server = setupServer( - http.post(apiUrl('/sandboxes'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes'), async ({ request }) => { lastCreateBody = (await request.json()) as Record return HttpResponse.json({ sandboxID: sandboxId, diff --git a/packages/js-sdk/tests/sandbox/onResumeRequest.test.ts b/packages/js-sdk/tests/sandbox/onResumeRequest.test.ts index 4f3edcd10b..814562b340 100644 --- a/packages/js-sdk/tests/sandbox/onResumeRequest.test.ts +++ b/packages/js-sdk/tests/sandbox/onResumeRequest.test.ts @@ -8,7 +8,7 @@ import { TEST_API_KEY, apiUrl } from '../setup' let lastConnectBody: Record | undefined const server = setupServer( - http.post(apiUrl('/sandboxes/:sandboxID/connect'), async ({ request }) => { + http.post(apiUrl('/v2/sandboxes/:sandboxID/connect'), async ({ request }) => { lastConnectBody = (await request.json()) as Record return HttpResponse.json({ sandboxID: 'test-sandbox-id', diff --git a/packages/js-sdk/tests/sandbox/secure.test.ts b/packages/js-sdk/tests/sandbox/secure.test.ts index 90a8712941..7a2c2ab689 100644 --- a/packages/js-sdk/tests/sandbox/secure.test.ts +++ b/packages/js-sdk/tests/sandbox/secure.test.ts @@ -4,12 +4,6 @@ import { sandboxTest, isDebug } from '../setup' import { randomUUID, createHash } from 'node:crypto' describe('secure sandbox', () => { - sandboxTest.override({ - sandboxOpts: { - secure: true, - }, - }) - sandboxTest.skipIf(isDebug)( 'test access file with signing', async ({ sandbox }) => { diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes.py index 56f5dc1557..702245ea1a 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes.py @@ -81,7 +81,7 @@ def sync_detailed( ) -> Response[Union[Error, Sandbox]]: """Create sandbox - Create a sandbox from the template + Create a sandbox from the template. Use POST /v2/sandboxes instead. Args: body (NewSandbox): @@ -112,7 +112,7 @@ def sync( ) -> Optional[Union[Error, Sandbox]]: """Create sandbox - Create a sandbox from the template + Create a sandbox from the template. Use POST /v2/sandboxes instead. Args: body (NewSandbox): @@ -138,7 +138,7 @@ async def asyncio_detailed( ) -> Response[Union[Error, Sandbox]]: """Create sandbox - Create a sandbox from the template + Create a sandbox from the template. Use POST /v2/sandboxes instead. Args: body (NewSandbox): @@ -167,7 +167,7 @@ async def asyncio( ) -> Optional[Union[Error, Sandbox]]: """Create sandbox - Create a sandbox from the template + Create a sandbox from the template. Use POST /v2/sandboxes instead. Args: body (NewSandbox): diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py index b2562c449a..c0493605da 100644 --- a/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py @@ -95,7 +95,8 @@ def sync_detailed( ) -> Response[Union[Error, Sandbox]]: """Connect sandbox - Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. Use + POST /v2/sandboxes/{sandboxID}/connect instead. Args: sandbox_id (str): @@ -129,7 +130,8 @@ def sync( ) -> Optional[Union[Error, Sandbox]]: """Connect sandbox - Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. Use + POST /v2/sandboxes/{sandboxID}/connect instead. Args: sandbox_id (str): @@ -158,7 +160,8 @@ async def asyncio_detailed( ) -> Response[Union[Error, Sandbox]]: """Connect sandbox - Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. Use + POST /v2/sandboxes/{sandboxID}/connect instead. Args: sandbox_id (str): @@ -190,7 +193,8 @@ async def asyncio( ) -> Optional[Union[Error, Sandbox]]: """Connect sandbox - Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. Use + POST /v2/sandboxes/{sandboxID}/connect instead. Args: sandbox_id (str): diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_v2_sandboxes.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_v2_sandboxes.py new file mode 100644 index 0000000000..e2c75ac7d3 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_v2_sandboxes.py @@ -0,0 +1,196 @@ +from http import HTTPStatus +from typing import Any, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error import Error +from ...models.new_sandbox_v2 import NewSandboxV2 +from ...models.sandbox import Sandbox +from ...types import Response + + +def _get_kwargs( + *, + body: NewSandboxV2, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v2/sandboxes", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Error, Sandbox]]: + if response.status_code == 201: + response_201 = Sandbox.from_dict(response.json()) + + return response_201 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + if response.status_code == 429: + response_429 = Error.from_dict(response.json()) + + return response_429 + if response.status_code == 500: + response_500 = Error.from_dict(response.json()) + + return response_500 + if response.status_code == 503: + response_503 = Error.from_dict(response.json()) + + return response_503 + if response.status_code == 504: + response_504 = Error.from_dict(response.json()) + + return response_504 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Error, Sandbox]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: NewSandboxV2, +) -> Response[Union[Error, Sandbox]]: + """Create sandbox (v2) + + Create a sandbox from the template. All system communication with the sandbox is secured. + + Args: + body (NewSandboxV2): Sandbox creation request. All system communication with the sandbox + is always secured; the template's envd version must support secured access. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, Sandbox]] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: NewSandboxV2, +) -> Optional[Union[Error, Sandbox]]: + """Create sandbox (v2) + + Create a sandbox from the template. All system communication with the sandbox is secured. + + Args: + body (NewSandboxV2): Sandbox creation request. All system communication with the sandbox + is always secured; the template's envd version must support secured access. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, Sandbox] + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: NewSandboxV2, +) -> Response[Union[Error, Sandbox]]: + """Create sandbox (v2) + + Create a sandbox from the template. All system communication with the sandbox is secured. + + Args: + body (NewSandboxV2): Sandbox creation request. All system communication with the sandbox + is always secured; the template's envd version must support secured access. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, Sandbox]] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: NewSandboxV2, +) -> Optional[Union[Error, Sandbox]]: + """Create sandbox (v2) + + Create a sandbox from the template. All system communication with the sandbox is secured. + + Args: + body (NewSandboxV2): Sandbox creation request. All system communication with the sandbox + is always secured; the template's envd version must support secured access. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, Sandbox] + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/packages/python-sdk/e2b/api/client/api/sandboxes/post_v_2_sandboxes_sandbox_id_connect.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_v_2_sandboxes_sandbox_id_connect.py new file mode 100644 index 0000000000..bfe96fee1b --- /dev/null +++ b/packages/python-sdk/e2b/api/client/api/sandboxes/post_v_2_sandboxes_sandbox_id_connect.py @@ -0,0 +1,221 @@ +from http import HTTPStatus +from typing import Any, Optional, Union + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.connect_sandbox_v2 import ConnectSandboxV2 +from ...models.error import Error +from ...models.sandbox import Sandbox +from ...types import Response + + +def _get_kwargs( + sandbox_id: str, + *, + body: ConnectSandboxV2, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": f"/v2/sandboxes/{sandbox_id}/connect", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Optional[Union[Error, Sandbox]]: + if response.status_code == 200: + response_200 = Sandbox.from_dict(response.json()) + + return response_200 + if response.status_code == 201: + response_201 = Sandbox.from_dict(response.json()) + + return response_201 + if response.status_code == 400: + response_400 = Error.from_dict(response.json()) + + return response_400 + if response.status_code == 401: + response_401 = Error.from_dict(response.json()) + + return response_401 + if response.status_code == 404: + response_404 = Error.from_dict(response.json()) + + return response_404 + if response.status_code == 409: + response_409 = Error.from_dict(response.json()) + + return response_409 + if response.status_code == 429: + response_429 = Error.from_dict(response.json()) + + return response_429 + if response.status_code == 500: + response_500 = Error.from_dict(response.json()) + + return response_500 + if response.status_code == 503: + response_503 = Error.from_dict(response.json()) + + return response_503 + if response.status_code == 504: + response_504 = Error.from_dict(response.json()) + + return response_504 + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: Union[AuthenticatedClient, Client], response: httpx.Response +) -> Response[Union[Error, Sandbox]]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + sandbox_id: str, + *, + client: AuthenticatedClient, + body: ConnectSandboxV2, +) -> Response[Union[Error, Sandbox]]: + """Connect sandbox (v2) + + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. The + request body is optional; an omitted timeout defaults to 300 seconds. + + Args: + sandbox_id (str): + body (ConnectSandboxV2): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, Sandbox]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + sandbox_id: str, + *, + client: AuthenticatedClient, + body: ConnectSandboxV2, +) -> Optional[Union[Error, Sandbox]]: + """Connect sandbox (v2) + + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. The + request body is optional; an omitted timeout defaults to 300 seconds. + + Args: + sandbox_id (str): + body (ConnectSandboxV2): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, Sandbox] + """ + + return sync_detailed( + sandbox_id=sandbox_id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + sandbox_id: str, + *, + client: AuthenticatedClient, + body: ConnectSandboxV2, +) -> Response[Union[Error, Sandbox]]: + """Connect sandbox (v2) + + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. The + request body is optional; an omitted timeout defaults to 300 seconds. + + Args: + sandbox_id (str): + body (ConnectSandboxV2): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Union[Error, Sandbox]] + """ + + kwargs = _get_kwargs( + sandbox_id=sandbox_id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + sandbox_id: str, + *, + client: AuthenticatedClient, + body: ConnectSandboxV2, +) -> Optional[Union[Error, Sandbox]]: + """Connect sandbox (v2) + + Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. The + request body is optional; an omitted timeout defaults to 300 seconds. + + Args: + sandbox_id (str): + body (ConnectSandboxV2): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Union[Error, Sandbox] + """ + + return ( + await asyncio_detailed( + sandbox_id=sandbox_id, + client=client, + body=body, + ) + ).parsed diff --git a/packages/python-sdk/e2b/api/client/models/__init__.py b/packages/python-sdk/e2b/api/client/models/__init__.py index d2acfa930d..492970ddaf 100644 --- a/packages/python-sdk/e2b/api/client/models/__init__.py +++ b/packages/python-sdk/e2b/api/client/models/__init__.py @@ -7,6 +7,7 @@ from .build_log_entry import BuildLogEntry from .build_status_reason import BuildStatusReason from .connect_sandbox import ConnectSandbox +from .connect_sandbox_v2 import ConnectSandboxV2 from .delete_template_tags_request import DeleteTemplateTagsRequest from .error import Error from .gcp_registry import GCPRegistry @@ -19,6 +20,7 @@ from .logs_source import LogsSource from .mcp_type_0 import McpType0 from .new_sandbox import NewSandbox +from .new_sandbox_v2 import NewSandboxV2 from .new_secret import NewSecret from .new_volume import NewVolume from .order_direction import OrderDirection @@ -88,6 +90,7 @@ "BuildLogEntry", "BuildStatusReason", "ConnectSandbox", + "ConnectSandboxV2", "DeleteTemplateTagsRequest", "Error", "GCPRegistry", @@ -100,6 +103,7 @@ "LogsSource", "McpType0", "NewSandbox", + "NewSandboxV2", "NewSecret", "NewVolume", "OrderDirection", diff --git a/packages/python-sdk/e2b/api/client/models/connect_sandbox_v2.py b/packages/python-sdk/e2b/api/client/models/connect_sandbox_v2.py new file mode 100644 index 0000000000..dd3da84ef4 --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/connect_sandbox_v2.py @@ -0,0 +1,73 @@ +from collections.abc import Mapping +from typing import Any, TypeVar, Union + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ConnectSandboxV2") + + +@_attrs_define +class ConnectSandboxV2: + """ + Attributes: + timeout (Union[Unset, int]): Timeout in seconds from the current time after which the sandbox should expire + Default: 300. + memory (Union[Unset, bool]): Defaults to true. When false and the sandbox is paused, resume from disk state + only: the sandbox cold-boots fresh and any memory in the snapshot is ignored, never modified or deleted. Disk + state has crash-recovery semantics — writes not flushed before the pause may be lost. A no-op for snapshots that + contain no memory. Rejected with an error in environments where this capability is not enabled, never silently + downgraded to a memory restore. + """ + + timeout: Union[Unset, int] = 300 + memory: Union[Unset, bool] = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + timeout = self.timeout + + memory = self.memory + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if timeout is not UNSET: + field_dict["timeout"] = timeout + if memory is not UNSET: + field_dict["memory"] = memory + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + timeout = d.pop("timeout", UNSET) + + memory = d.pop("memory", UNSET) + + connect_sandbox_v2 = cls( + timeout=timeout, + memory=memory, + ) + + connect_sandbox_v2.additional_properties = d + return connect_sandbox_v2 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/api/client/models/new_sandbox_v2.py b/packages/python-sdk/e2b/api/client/models/new_sandbox_v2.py new file mode 100644 index 0000000000..1c37dbcccb --- /dev/null +++ b/packages/python-sdk/e2b/api/client/models/new_sandbox_v2.py @@ -0,0 +1,236 @@ +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, Union, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.mcp_type_0 import McpType0 + from ..models.sandbox_auto_resume_config import SandboxAutoResumeConfig + from ..models.sandbox_iam import SandboxIam + from ..models.sandbox_network_config import SandboxNetworkConfig + from ..models.sandbox_volume_mount import SandboxVolumeMount + + +T = TypeVar("T", bound="NewSandboxV2") + + +@_attrs_define +class NewSandboxV2: + """Sandbox creation request. All system communication with the sandbox is always secured; the template's envd version + must support secured access. + + Attributes: + template_id (str): Identifier of the required template + timeout (Union[Unset, int]): Time to live for the sandbox in seconds. Default: 300. + auto_pause (Union[Unset, bool]): Automatically pauses the sandbox after the timeout Default: False. + auto_pause_memory (Union[Unset, bool]): Controls the snapshot kind taken when the sandbox auto-pauses on timeout + (only relevant when autoPause is true). When false, the auto-pause drops the in-memory state and persists only + the filesystem (a filesystem-only snapshot); resuming it cold-boots (reboots) the sandbox from disk. Such a + snapshot cannot be auto-resumed by traffic and must be resumed explicitly, so it cannot be combined with + autoResume. Defaults to true (full memory snapshot). Default: True. + auto_resume (Union[Unset, SandboxAutoResumeConfig]): Auto-resume configuration for paused sandboxes. + allow_internet_access (Union[Unset, bool]): Allow sandbox to access the internet. When set to false, it behaves + the same as specifying denyOut to 0.0.0.0/0 in the network config. + network (Union[Unset, SandboxNetworkConfig]): + metadata (Union[Unset, Any]): + env_vars (Union[Unset, Any]): + mcp (Union['McpType0', None, Unset]): MCP configuration for the sandbox + iam (Union[Unset, SandboxIam]): Sandbox workload identity configuration. A non-empty, valid tokens map enables + workload identity for the sandbox. + volume_mounts (Union[Unset, list['SandboxVolumeMount']]): + """ + + template_id: str + timeout: Union[Unset, int] = 300 + auto_pause: Union[Unset, bool] = False + auto_pause_memory: Union[Unset, bool] = True + auto_resume: Union[Unset, "SandboxAutoResumeConfig"] = UNSET + allow_internet_access: Union[Unset, bool] = UNSET + network: Union[Unset, "SandboxNetworkConfig"] = UNSET + metadata: Union[Unset, Any] = UNSET + env_vars: Union[Unset, Any] = UNSET + mcp: Union["McpType0", None, Unset] = UNSET + iam: Union[Unset, "SandboxIam"] = UNSET + volume_mounts: Union[Unset, list["SandboxVolumeMount"]] = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.mcp_type_0 import McpType0 + + template_id = self.template_id + + timeout = self.timeout + + auto_pause = self.auto_pause + + auto_pause_memory = self.auto_pause_memory + + auto_resume: Union[Unset, dict[str, Any]] = UNSET + if not isinstance(self.auto_resume, Unset): + auto_resume = self.auto_resume.to_dict() + + allow_internet_access = self.allow_internet_access + + network: Union[Unset, dict[str, Any]] = UNSET + if not isinstance(self.network, Unset): + network = self.network.to_dict() + + metadata = self.metadata + + env_vars = self.env_vars + + mcp: Union[None, Unset, dict[str, Any]] + if isinstance(self.mcp, Unset): + mcp = UNSET + elif isinstance(self.mcp, McpType0): + mcp = self.mcp.to_dict() + else: + mcp = self.mcp + + iam: Union[Unset, dict[str, Any]] = UNSET + if not isinstance(self.iam, Unset): + iam = self.iam.to_dict() + + volume_mounts: Union[Unset, list[dict[str, Any]]] = UNSET + if not isinstance(self.volume_mounts, Unset): + volume_mounts = [] + for volume_mounts_item_data in self.volume_mounts: + volume_mounts_item = volume_mounts_item_data.to_dict() + volume_mounts.append(volume_mounts_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "templateID": template_id, + } + ) + if timeout is not UNSET: + field_dict["timeout"] = timeout + if auto_pause is not UNSET: + field_dict["autoPause"] = auto_pause + if auto_pause_memory is not UNSET: + field_dict["autoPauseMemory"] = auto_pause_memory + if auto_resume is not UNSET: + field_dict["autoResume"] = auto_resume + if allow_internet_access is not UNSET: + field_dict["allow_internet_access"] = allow_internet_access + if network is not UNSET: + field_dict["network"] = network + if metadata is not UNSET: + field_dict["metadata"] = metadata + if env_vars is not UNSET: + field_dict["envVars"] = env_vars + if mcp is not UNSET: + field_dict["mcp"] = mcp + if iam is not UNSET: + field_dict["iam"] = iam + if volume_mounts is not UNSET: + field_dict["volumeMounts"] = volume_mounts + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.mcp_type_0 import McpType0 + from ..models.sandbox_auto_resume_config import SandboxAutoResumeConfig + from ..models.sandbox_iam import SandboxIam + from ..models.sandbox_network_config import SandboxNetworkConfig + from ..models.sandbox_volume_mount import SandboxVolumeMount + + d = dict(src_dict) + template_id = d.pop("templateID") + + timeout = d.pop("timeout", UNSET) + + auto_pause = d.pop("autoPause", UNSET) + + auto_pause_memory = d.pop("autoPauseMemory", UNSET) + + _auto_resume = d.pop("autoResume", UNSET) + auto_resume: Union[Unset, SandboxAutoResumeConfig] + if isinstance(_auto_resume, Unset): + auto_resume = UNSET + else: + auto_resume = SandboxAutoResumeConfig.from_dict(_auto_resume) + + allow_internet_access = d.pop("allow_internet_access", UNSET) + + _network = d.pop("network", UNSET) + network: Union[Unset, SandboxNetworkConfig] + if isinstance(_network, Unset): + network = UNSET + else: + network = SandboxNetworkConfig.from_dict(_network) + + metadata = d.pop("metadata", UNSET) + + env_vars = d.pop("envVars", UNSET) + + def _parse_mcp(data: object) -> Union["McpType0", None, Unset]: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_mcp_type_0 = McpType0.from_dict(data) + + return componentsschemas_mcp_type_0 + except: # noqa: E722 + pass + return cast(Union["McpType0", None, Unset], data) + + mcp = _parse_mcp(d.pop("mcp", UNSET)) + + _iam = d.pop("iam", UNSET) + iam: Union[Unset, SandboxIam] + if isinstance(_iam, Unset): + iam = UNSET + else: + iam = SandboxIam.from_dict(_iam) + + volume_mounts = [] + _volume_mounts = d.pop("volumeMounts", UNSET) + for volume_mounts_item_data in _volume_mounts or []: + volume_mounts_item = SandboxVolumeMount.from_dict(volume_mounts_item_data) + + volume_mounts.append(volume_mounts_item) + + new_sandbox_v2 = cls( + template_id=template_id, + timeout=timeout, + auto_pause=auto_pause, + auto_pause_memory=auto_pause_memory, + auto_resume=auto_resume, + allow_internet_access=allow_internet_access, + network=network, + metadata=metadata, + env_vars=env_vars, + mcp=mcp, + iam=iam, + volume_mounts=volume_mounts, + ) + + new_sandbox_v2.additional_properties = d + return new_sandbox_v2 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index f0c4610d0f..a321bb5054 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -18,7 +18,6 @@ from typing_extensions import NotRequired, Unpack from e2b.api.client.models import ( - ConnectSandbox, ListedSandbox, SandboxDetail, SandboxState, @@ -74,25 +73,6 @@ from e2b.paginator import PaginatorBase -class ConnectSandboxBody(ConnectSandbox): - """Connect request body that omits `timeout` when not provided so the - API default applies. The generated model still requires `timeout`; - remove this once the spec makes it optional.""" - - def __init__( - self, - timeout: Optional[int] = None, - memory: Union[Unset, bool] = UNSET, - ): - super().__init__(timeout=cast(int, timeout), memory=memory) - - def to_dict(self) -> Dict[str, Any]: - result = super().to_dict() - if result["timeout"] is None: - del result["timeout"] - return result - - class GitHubMcpServerConfig(TypedDict): """ Configuration for a GitHub-based MCP server. @@ -607,7 +587,7 @@ class SandboxInfoLifecycle(TypedDict): def resolve_connect_memory( on_resume: Optional["SandboxOnResume"], ) -> Union[Unset, bool]: - """Resolve ``on_resume`` into ``ConnectSandbox.memory``. + """Resolve ``on_resume`` into ``ConnectSandboxV2.memory``. ``"restore"`` is the API's own default, so it travels as an omitted field. """ @@ -864,7 +844,7 @@ def build_iam_config( @dataclass(frozen=True) class SandboxLifecycleBody: - """Lifecycle fields of a create-sandbox request, as ``NewSandbox`` takes them.""" + """Lifecycle fields of a create-sandbox request, as ``NewSandboxV2`` takes them.""" auto_pause: Union[Unset, bool] auto_pause_memory: Union[Unset, bool] diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index d2007e645c..8b5a87435f 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -175,7 +175,6 @@ async def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, - secure: Optional[bool] = None, allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, @@ -194,7 +193,6 @@ async def create( :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it. :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). @@ -227,7 +225,6 @@ async def create( timeout=timeout, metadata=metadata, envs=envs, - secure=secure, allow_internet_access=allow_internet_access, mcp=mcp, network=network, @@ -1138,7 +1135,6 @@ async def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: Optional[bool], allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, @@ -1164,7 +1160,6 @@ async def _create( timeout=timeout, metadata=metadata, env_vars=envs, - secure=secure, allow_internet_access=allow_internet_access, mcp=mcp, network=network, diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index e42b68a354..ca0de64176 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -15,8 +15,8 @@ delete_sandboxes_sandbox_id, get_sandboxes_sandbox_id, get_sandboxes_sandbox_id_metrics, - post_sandboxes, - post_sandboxes_sandbox_id_connect, + post_v2_sandboxes, + post_v_2_sandboxes_sandbox_id_connect, post_sandboxes_sandbox_id_fork, post_sandboxes_sandbox_id_pause, post_sandboxes_sandbox_id_snapshots, @@ -25,8 +25,9 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( + ConnectSandboxV2, Error, - NewSandbox, + NewSandboxV2, SandboxSnapshotRequest, SandboxTimeoutRequest, SandboxForkRequest, @@ -46,7 +47,6 @@ from e2b.sandbox.main import SandboxBase from e2b.sandbox.sandbox_api import ( build_network_update_body, - ConnectSandboxBody, McpServer, SandboxIamOpts, SandboxInfo, @@ -213,7 +213,6 @@ async def _create_sandbox( allow_internet_access: Optional[bool], metadata: Optional[Dict[str, str]], env_vars: Optional[Dict[str, str]], - secure: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -231,7 +230,7 @@ async def _create_sandbox( # against the workload tokens this request registers. iam_body = build_iam_config(iam) network_body = build_network_config(network, iam_body) - body = NewSandbox( + body = NewSandboxV2( template_id=template, auto_pause=lifecycle_body.auto_pause, auto_pause_memory=lifecycle_body.auto_pause_memory, @@ -240,7 +239,6 @@ async def _create_sandbox( timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure if secure is not None else UNSET, allow_internet_access=( allow_internet_access if allow_internet_access is not None else UNSET ), @@ -250,7 +248,7 @@ async def _create_sandbox( ) api_client = get_api_client(config) - res = await post_sandboxes.asyncio_detailed( + res = await post_v2_sandboxes.asyncio_detailed( body=body, client=api_client, ) @@ -534,11 +532,11 @@ async def _cls_connect( config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) - res = await post_sandboxes_sandbox_id_connect.asyncio_detailed( + res = await post_v_2_sandboxes_sandbox_id_connect.asyncio_detailed( sandbox_id, client=api_client, - body=ConnectSandboxBody( - timeout=timeout, + body=ConnectSandboxV2( + timeout=timeout if timeout is not None else UNSET, memory=resolve_connect_memory(on_resume), ), ) diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 87e4ccb523..8efec4fc31 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -171,7 +171,6 @@ def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, - secure: Optional[bool] = None, allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, @@ -190,7 +189,6 @@ def create( :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it. :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). @@ -223,7 +221,6 @@ def create( timeout=timeout, metadata=metadata, envs=envs, - secure=secure, allow_internet_access=allow_internet_access, mcp=mcp, network=network, @@ -1134,7 +1131,6 @@ def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: Optional[bool], allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, @@ -1160,7 +1156,6 @@ def _create( timeout=timeout, metadata=metadata, env_vars=envs, - secure=secure, allow_internet_access=allow_internet_access, mcp=mcp, network=network, diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 2e3b27cc1b..07f65f5e3f 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -15,8 +15,8 @@ delete_sandboxes_sandbox_id, get_sandboxes_sandbox_id, get_sandboxes_sandbox_id_metrics, - post_sandboxes, - post_sandboxes_sandbox_id_connect, + post_v2_sandboxes, + post_v_2_sandboxes_sandbox_id_connect, post_sandboxes_sandbox_id_fork, post_sandboxes_sandbox_id_pause, post_sandboxes_sandbox_id_snapshots, @@ -25,8 +25,9 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( + ConnectSandboxV2, Error, - NewSandbox, + NewSandboxV2, SandboxSnapshotRequest, SandboxTimeoutRequest, SandboxForkRequest, @@ -45,7 +46,6 @@ from e2b.sandbox.main import SandboxBase from e2b.sandbox.sandbox_api import ( build_network_update_body, - ConnectSandboxBody, McpServer, SandboxIamOpts, SandboxInfo, @@ -212,7 +212,6 @@ def _create_sandbox( allow_internet_access: Optional[bool], metadata: Optional[Dict[str, str]], env_vars: Optional[Dict[str, str]], - secure: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -230,7 +229,7 @@ def _create_sandbox( # against the workload tokens this request registers. iam_body = build_iam_config(iam) network_body = build_network_config(network, iam_body) - body = NewSandbox( + body = NewSandboxV2( template_id=template, auto_pause=lifecycle_body.auto_pause, auto_pause_memory=lifecycle_body.auto_pause_memory, @@ -239,7 +238,6 @@ def _create_sandbox( timeout=timeout if timeout is not None else UNSET, env_vars=env_vars or {}, mcp=cast(Any, mcp) or UNSET, - secure=secure if secure is not None else UNSET, allow_internet_access=( allow_internet_access if allow_internet_access is not None else UNSET ), @@ -249,7 +247,7 @@ def _create_sandbox( ) api_client = get_api_client(config) - res = post_sandboxes.sync_detailed( + res = post_v2_sandboxes.sync_detailed( body=body, client=api_client, ) @@ -351,11 +349,11 @@ def _cls_connect( config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) - res = post_sandboxes_sandbox_id_connect.sync_detailed( + res = post_v_2_sandboxes_sandbox_id_connect.sync_detailed( sandbox_id, client=api_client, - body=ConnectSandboxBody( - timeout=timeout, + body=ConnectSandboxV2( + timeout=timeout if timeout is not None else UNSET, memory=resolve_connect_memory(on_resume), ), ) diff --git a/packages/python-sdk/tests/async/sandbox_async/files/test_write.py b/packages/python-sdk/tests/async/sandbox_async/files/test_write.py index dd065133a0..fb72b9692d 100644 --- a/packages/python-sdk/tests/async/sandbox_async/files/test_write.py +++ b/packages/python-sdk/tests/async/sandbox_async/files/test_write.py @@ -127,7 +127,7 @@ async def test_write_with_secured_envd(async_sandbox_factory): filename = f"non_existing_dir_{uuid.uuid4()}/test_write.txt" content = "This should succeed too." - sbx = await async_sandbox_factory(timeout=30, secure=True) + sbx = await async_sandbox_factory(timeout=30) assert await sbx.is_running() assert sbx._envd_version is not None diff --git a/packages/python-sdk/tests/async/sandbox_async/test_connect.py b/packages/python-sdk/tests/async/sandbox_async/test_connect.py index 51ec2b6860..3349681513 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_connect.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_connect.py @@ -5,7 +5,7 @@ import pytest from e2b import AsyncSandbox -from e2b.api.client.api.sandboxes import post_sandboxes_sandbox_id_connect +from e2b.api.client.api.sandboxes import post_v_2_sandboxes_sandbox_id_connect from e2b.api.client.models import Sandbox as SandboxModel import e2b.sandbox_async.main as sandbox_async_main @@ -24,7 +24,7 @@ async def test_connect(async_sandbox_factory): async def test_connect_with_secure(async_sandbox_factory): dir_name = f"test_directory_{uuid.uuid4()}" - sbx = await async_sandbox_factory(timeout=10, secure=True) + sbx = await async_sandbox_factory(timeout=10) assert await sbx.is_running() sbx_connection = await AsyncSandbox.connect(sbx.sandbox_id) @@ -134,7 +134,7 @@ async def test_connect_normalizes_unset_tokens(monkeypatch, test_api_key): return_value=SimpleNamespace(status_code=200, parsed=model) ) monkeypatch.setattr( - post_sandboxes_sandbox_id_connect, "asyncio_detailed", mock_request + post_v_2_sandboxes_sandbox_id_connect, "asyncio_detailed", mock_request ) sbx = await AsyncSandbox.connect("sbx-test", debug=False, api_key=test_api_key) diff --git a/packages/python-sdk/tests/async/sandbox_async/test_create.py b/packages/python-sdk/tests/async/sandbox_async/test_create.py index 79bcabe3ef..476d41680b 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_create.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_create.py @@ -7,7 +7,7 @@ from e2b import AsyncSandbox, SandboxException, SandboxQuery, SandboxState, Secret from e2b.api.client.models import ( - NewSandbox, + NewSandboxV2, SandboxAutoResumeConfig, ) from e2b.api.client.types import UNSET @@ -102,7 +102,7 @@ async def test_mcp_gateway_start_failure_kills_created_sandbox(template): def test_create_payload_serializes_auto_resume_enabled(): - body = NewSandbox( + body = NewSandboxV2( template_id="template-id", auto_pause=True, auto_resume=SandboxAutoResumeConfig(enabled=True), @@ -113,7 +113,7 @@ def test_create_payload_serializes_auto_resume_enabled(): def test_create_payload_deserializes_auto_resume_enabled(): - body = NewSandbox.from_dict( + body = NewSandboxV2.from_dict( { "templateID": "template-id", "autoPause": False, @@ -135,7 +135,7 @@ def test_create_payload_serializes_iam_tokens(): ) assert iam is not None - body = NewSandbox(template_id="template-id", iam=iam) + body = NewSandboxV2(template_id="template-id", iam=iam) assert body.to_dict()["iam"] == { "tokens": { @@ -156,7 +156,7 @@ def test_create_payload_serializes_secret_iam_token(): ) assert iam is not None - body = NewSandbox(template_id="template-id", iam=iam) + body = NewSandboxV2(template_id="template-id", iam=iam) assert body.to_dict()["iam"] == { "tokens": { @@ -170,7 +170,7 @@ def test_create_payload_omits_iam_when_not_provided_or_empty(): assert build_iam_config({}) is None assert build_iam_config({"tokens": {}}) is None - body = NewSandbox(template_id="template-id", iam=UNSET) + body = NewSandboxV2(template_id="template-id", iam=UNSET) assert "iam" not in body.to_dict() diff --git a/packages/python-sdk/tests/async/sandbox_async/test_network.py b/packages/python-sdk/tests/async/sandbox_async/test_network.py index 671b8b3c7d..66d7e3a07d 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_network.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_network.py @@ -140,7 +140,7 @@ async def test_allow_takes_precedence_over_deny(async_sandbox_factory): async def test_allow_public_traffic_false(async_sandbox_factory): """Test that sandbox with allow_public_traffic=False requires traffic access token.""" async_sandbox = await async_sandbox_factory( - secure=True, network=SandboxNetworkOpts(allow_public_traffic=False) + network=SandboxNetworkOpts(allow_public_traffic=False) ) # Verify the sandbox was created successfully and has a traffic access token diff --git a/packages/python-sdk/tests/async/sandbox_async/test_secure.py b/packages/python-sdk/tests/async/sandbox_async/test_secure.py index 3ca68dacd4..64261cca56 100644 --- a/packages/python-sdk/tests/async/sandbox_async/test_secure.py +++ b/packages/python-sdk/tests/async/sandbox_async/test_secure.py @@ -5,7 +5,7 @@ @pytest.mark.skip_debug() async def test_start_secured(async_sandbox_factory): - sbx = await async_sandbox_factory(timeout=5, secure=True) + sbx = await async_sandbox_factory(timeout=5) assert await sbx.is_running() assert sbx._envd_version is not None @@ -14,7 +14,7 @@ async def test_start_secured(async_sandbox_factory): @pytest.mark.skip_debug() async def test_connect_to_secured(async_sandbox_factory): - sbx = await async_sandbox_factory(timeout=100, secure=True) + sbx = await async_sandbox_factory(timeout=100) assert await sbx.is_running() assert sbx._envd_version is not None diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py index 785f8d6b4b..182037c3a0 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -4,8 +4,8 @@ from e2b import AsyncSandbox, Sandbox from e2b.api.client.api.sandboxes import ( - post_sandboxes, - post_sandboxes_sandbox_id_connect, + post_v2_sandboxes, + post_v_2_sandboxes_sandbox_id_connect, post_sandboxes_sandbox_id_fork, post_sandboxes_sandbox_id_pause, ) @@ -26,7 +26,7 @@ def _created_sandbox(): def _sync_create_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: request = Mock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes, "sync_detailed", request) + monkeypatch.setattr(post_v2_sandboxes, "sync_detailed", request) Sandbox.create(api_key=api_key, **kwargs) @@ -35,7 +35,7 @@ def _sync_create_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: async def _async_create_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: request = AsyncMock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes, "asyncio_detailed", request) + monkeypatch.setattr(post_v2_sandboxes, "asyncio_detailed", request) await AsyncSandbox.create(api_key=api_key, **kwargs) @@ -55,12 +55,10 @@ def test_create_sends_explicit_values(monkeypatch, test_api_key): monkeypatch, test_api_key, timeout=60, - secure=False, allow_internet_access=False, ) assert body["timeout"] == 60 - assert body["secure"] is False assert body["allow_internet_access"] is False @@ -79,12 +77,10 @@ async def test_async_create_sends_explicit_values(monkeypatch, test_api_key): monkeypatch, test_api_key, timeout=60, - secure=False, allow_internet_access=False, ) assert body["timeout"] == 60 - assert body["secure"] is False assert body["allow_internet_access"] is False @@ -180,7 +176,7 @@ async def test_async_pause_sends_explicit_keep_memory(monkeypatch, test_api_key) def _sync_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: request = Mock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "sync_detailed", request) + monkeypatch.setattr(post_v_2_sandboxes_sandbox_id_connect, "sync_detailed", request) Sandbox.connect("sbx-test", api_key=api_key, **kwargs) @@ -189,7 +185,9 @@ def _sync_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: async def _async_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: request = AsyncMock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "asyncio_detailed", request) + monkeypatch.setattr( + post_v_2_sandboxes_sandbox_id_connect, "asyncio_detailed", request + ) await AsyncSandbox.connect("sbx-test", api_key=api_key, **kwargs) diff --git a/packages/python-sdk/tests/shared/sandbox/test_lifecycle_request.py b/packages/python-sdk/tests/shared/sandbox/test_lifecycle_request.py index 6a736823d7..84f74949e0 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_lifecycle_request.py +++ b/packages/python-sdk/tests/shared/sandbox/test_lifecycle_request.py @@ -5,7 +5,7 @@ import pytest from e2b import AsyncSandbox, Sandbox -from e2b.api.client.api.sandboxes import post_sandboxes +from e2b.api.client.api.sandboxes import post_v2_sandboxes from e2b.api.client.models import Sandbox as SandboxModel from e2b.exceptions import InvalidArgumentException @@ -24,7 +24,7 @@ def _created_sandbox(): def _sync_request_body(monkeypatch, api_key: str, lifecycle) -> Dict[str, Any]: request = Mock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes, "sync_detailed", request) + monkeypatch.setattr(post_v2_sandboxes, "sync_detailed", request) Sandbox.create(api_key=api_key, lifecycle=lifecycle) @@ -33,7 +33,7 @@ def _sync_request_body(monkeypatch, api_key: str, lifecycle) -> Dict[str, Any]: async def _async_request_body(monkeypatch, api_key: str, lifecycle) -> Dict[str, Any]: request = AsyncMock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes, "asyncio_detailed", request) + monkeypatch.setattr(post_v2_sandboxes, "asyncio_detailed", request) await AsyncSandbox.create(api_key=api_key, lifecycle=lifecycle) @@ -214,7 +214,7 @@ def test_create_rejects_an_unrecognized_on_timeout( monkeypatch, test_api_key, on_timeout ): request = Mock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes, "sync_detailed", request) + monkeypatch.setattr(post_v2_sandboxes, "sync_detailed", request) with pytest.raises(InvalidArgumentException): Sandbox.create( @@ -229,7 +229,7 @@ async def test_async_create_rejects_an_unrecognized_on_timeout( monkeypatch, test_api_key, on_timeout ): request = AsyncMock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes, "asyncio_detailed", request) + monkeypatch.setattr(post_v2_sandboxes, "asyncio_detailed", request) with pytest.raises(InvalidArgumentException): await AsyncSandbox.create( @@ -251,7 +251,7 @@ def test_the_error_names_the_field_the_caller_wrote( monkeypatch, test_api_key, on_timeout, expected_field ): request = Mock(return_value=_created_sandbox()) - monkeypatch.setattr(post_sandboxes, "sync_detailed", request) + monkeypatch.setattr(post_v2_sandboxes, "sync_detailed", request) with pytest.raises(InvalidArgumentException) as excinfo: Sandbox.create( diff --git a/packages/python-sdk/tests/shared/sandbox/test_on_resume_request.py b/packages/python-sdk/tests/shared/sandbox/test_on_resume_request.py index 7249a78a39..83f1b132bc 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_on_resume_request.py +++ b/packages/python-sdk/tests/shared/sandbox/test_on_resume_request.py @@ -7,7 +7,7 @@ from e2b import AsyncSandbox, Sandbox from e2b.exceptions import InvalidArgumentException -from e2b.api.client.api.sandboxes import post_sandboxes_sandbox_id_connect +from e2b.api.client.api.sandboxes import post_v_2_sandboxes_sandbox_id_connect from e2b.api.client.models import Sandbox as SandboxModel from e2b.sandbox_async.sandbox_api import SandboxApi as AsyncSandboxApi from e2b.sandbox_sync.sandbox_api import SandboxApi as SyncSandboxApi @@ -29,7 +29,7 @@ def _connected_sandbox(): def _sync_request_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: request = Mock(return_value=_connected_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "sync_detailed", request) + monkeypatch.setattr(post_v_2_sandboxes_sandbox_id_connect, "sync_detailed", request) Sandbox.connect(SANDBOX_ID, api_key=api_key, **kwargs) @@ -38,7 +38,9 @@ def _sync_request_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: async def _async_request_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: request = AsyncMock(return_value=_connected_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "asyncio_detailed", request) + monkeypatch.setattr( + post_v_2_sandboxes_sandbox_id_connect, "asyncio_detailed", request + ) await AsyncSandbox.connect(SANDBOX_ID, api_key=api_key, **kwargs) @@ -87,7 +89,7 @@ async def test_async_connect_sends_memory_only_for_reboot( def test_instance_connect_carries_on_resume(monkeypatch, test_api_key): request = Mock(return_value=_connected_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "sync_detailed", request) + monkeypatch.setattr(post_v_2_sandboxes_sandbox_id_connect, "sync_detailed", request) sandbox = Sandbox.connect(SANDBOX_ID, api_key=test_api_key) @@ -100,7 +102,9 @@ def test_instance_connect_carries_on_resume(monkeypatch, test_api_key): async def test_async_instance_connect_carries_on_resume(monkeypatch, test_api_key): request = AsyncMock(return_value=_connected_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "asyncio_detailed", request) + monkeypatch.setattr( + post_v_2_sandboxes_sandbox_id_connect, "asyncio_detailed", request + ) sandbox = await AsyncSandbox.connect(SANDBOX_ID, api_key=test_api_key) @@ -154,7 +158,7 @@ def test_on_resume_is_keyword_only_on_the_instance_form(sandbox): @pytest.mark.parametrize("value", UNRECOGNIZED) def test_connect_rejects_an_unrecognized_on_resume(monkeypatch, test_api_key, value): request = Mock(return_value=_connected_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "sync_detailed", request) + monkeypatch.setattr(post_v_2_sandboxes_sandbox_id_connect, "sync_detailed", request) with pytest.raises(InvalidArgumentException): Sandbox.connect(SANDBOX_ID, api_key=test_api_key, on_resume=cast(Any, value)) @@ -167,7 +171,9 @@ async def test_async_connect_rejects_an_unrecognized_on_resume( monkeypatch, test_api_key, value ): request = AsyncMock(return_value=_connected_sandbox()) - monkeypatch.setattr(post_sandboxes_sandbox_id_connect, "asyncio_detailed", request) + monkeypatch.setattr( + post_v_2_sandboxes_sandbox_id_connect, "asyncio_detailed", request + ) with pytest.raises(InvalidArgumentException): await AsyncSandbox.connect( diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py index 9159070fa7..e808ac4c9d 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_watch.py @@ -186,7 +186,7 @@ def test_watch_file(sandbox: Sandbox): def test_watch_file_with_secured_envd(sandbox_factory): - sbx = sandbox_factory(timeout=30, secure=True) + sbx = sandbox_factory(timeout=30) sbx.files.watch_dir("/home/user/") sbx.files.write("test_watch.txt", "This file will be watched.") diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py index 5416607410..5b1d77658d 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_write.py @@ -128,7 +128,7 @@ def test_write_with_secured_envd(sandbox_factory): filename = f"non_existing_dir_{uuid.uuid4()}/test_write.txt" content = "This should succeed too." - sbx = sandbox_factory(timeout=30, secure=True) + sbx = sandbox_factory(timeout=30) assert sbx.is_running() assert sbx._envd_version is not None assert sbx._envd_access_token is not None diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_connect.py b/packages/python-sdk/tests/sync/sandbox_sync/test_connect.py index b9d137cb63..91a44dda08 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_connect.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_connect.py @@ -5,7 +5,7 @@ import pytest from e2b import Sandbox -from e2b.api.client.api.sandboxes import post_sandboxes_sandbox_id_connect +from e2b.api.client.api.sandboxes import post_v_2_sandboxes_sandbox_id_connect from e2b.api.client.models import Sandbox as SandboxModel import e2b.sandbox_sync.main as sandbox_sync_main @@ -24,7 +24,7 @@ def test_connect(sandbox_factory): def test_connect_with_secure(sandbox_factory): dir_name = f"test_directory_{uuid.uuid4()}" - sbx = sandbox_factory(timeout=10, secure=True) + sbx = sandbox_factory(timeout=10) assert sbx.is_running() @@ -125,7 +125,7 @@ def test_connect_normalizes_unset_tokens(monkeypatch, test_api_key): ) mock_request = Mock(return_value=SimpleNamespace(status_code=200, parsed=model)) monkeypatch.setattr( - post_sandboxes_sandbox_id_connect, "sync_detailed", mock_request + post_v_2_sandboxes_sandbox_id_connect, "sync_detailed", mock_request ) sbx = Sandbox.connect("sbx-test", debug=False, api_key=test_api_key) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py index c41fa63878..56bb7300ff 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_create.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_create.py @@ -7,7 +7,7 @@ from e2b import Sandbox, SandboxException, SandboxState, Secret from e2b.api.client.models import ( - NewSandbox, + NewSandboxV2, SandboxAutoResumeConfig, ) from e2b.api.client.types import UNSET @@ -98,7 +98,7 @@ def test_mcp_gateway_start_failure_kills_created_sandbox(template): def test_create_payload_serializes_auto_resume_enabled(): - body = NewSandbox( + body = NewSandboxV2( template_id="template-id", auto_pause=True, auto_resume=SandboxAutoResumeConfig(enabled=True), @@ -109,7 +109,7 @@ def test_create_payload_serializes_auto_resume_enabled(): def test_create_payload_deserializes_auto_resume_enabled(): - body = NewSandbox.from_dict( + body = NewSandboxV2.from_dict( { "templateID": "template-id", "autoPause": False, @@ -131,7 +131,7 @@ def test_create_payload_serializes_iam_tokens(): ) assert iam is not None - body = NewSandbox(template_id="template-id", iam=iam) + body = NewSandboxV2(template_id="template-id", iam=iam) assert body.to_dict()["iam"] == { "tokens": { @@ -152,7 +152,7 @@ def test_create_payload_serializes_secret_iam_token(): ) assert iam is not None - body = NewSandbox(template_id="template-id", iam=iam) + body = NewSandboxV2(template_id="template-id", iam=iam) assert body.to_dict()["iam"] == { "tokens": { @@ -166,7 +166,7 @@ def test_create_payload_omits_iam_when_not_provided_or_empty(): assert build_iam_config({}) is None assert build_iam_config({"tokens": {}}) is None - body = NewSandbox(template_id="template-id", iam=UNSET) + body = NewSandboxV2(template_id="template-id", iam=UNSET) assert "iam" not in body.to_dict() diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_network.py b/packages/python-sdk/tests/sync/sandbox_sync/test_network.py index 141ed420a2..4d39765038 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_network.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_network.py @@ -138,9 +138,7 @@ def test_allow_takes_precedence_over_deny(sandbox_factory): @pytest.mark.skip_debug() def test_allow_public_traffic_false(sandbox_factory): """Test that sandbox with allow_public_traffic=False requires traffic access token.""" - sandbox = sandbox_factory( - secure=True, network=SandboxNetworkOpts(allow_public_traffic=False) - ) + sandbox = sandbox_factory(network=SandboxNetworkOpts(allow_public_traffic=False)) # Verify the sandbox was created successfully and has a traffic access token assert sandbox.traffic_access_token is not None diff --git a/packages/python-sdk/tests/sync/sandbox_sync/test_secure.py b/packages/python-sdk/tests/sync/sandbox_sync/test_secure.py index 12bb426b8e..be9097582c 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/test_secure.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/test_secure.py @@ -5,7 +5,7 @@ @pytest.mark.skip_debug() def test_start_secured(sandbox_factory): - sbx = sandbox_factory(timeout=5, secure=True) + sbx = sandbox_factory(timeout=5) assert sbx.is_running() assert sbx._envd_version is not None @@ -14,7 +14,7 @@ def test_start_secured(sandbox_factory): @pytest.mark.skip_debug() def test_connect_to_secured(sandbox_factory): - sbx = sandbox_factory(timeout=5, secure=True) + sbx = sandbox_factory(timeout=5) assert sbx.is_running() assert sbx._envd_version is not None diff --git a/spec/openapi.yml b/spec/openapi.yml index e7787b451f..5a2e7fef97 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -949,6 +949,59 @@ components: items: $ref: "#/components/schemas/SandboxVolumeMount" + NewSandboxV2: + description: >- + Sandbox creation request. All system communication with the sandbox is + always secured; the template's envd version must support secured access. + required: + - templateID + properties: + templateID: + type: string + description: Identifier of the required template + timeout: + type: integer + format: int32 + minimum: 0 + default: 300 + description: Time to live for the sandbox in seconds. + autoPause: + type: boolean + default: false + description: Automatically pauses the sandbox after the timeout + autoPauseMemory: + type: boolean + default: true + description: >- + Controls the snapshot kind taken when the sandbox auto-pauses on + timeout (only relevant when autoPause is true). When false, the + auto-pause drops the in-memory state and persists only the + filesystem (a filesystem-only snapshot); resuming it cold-boots + (reboots) the sandbox from disk. Such a snapshot cannot be + auto-resumed by traffic and must be resumed explicitly, so it cannot + be combined with autoResume. Defaults to true (full memory snapshot). + autoResume: + $ref: "#/components/schemas/SandboxAutoResumeConfig" + allow_internet_access: + type: boolean + description: + Allow sandbox to access the internet. When set to false, it behaves the same as specifying denyOut + to 0.0.0.0/0 in the network config. + network: + $ref: "#/components/schemas/SandboxNetworkConfig" + metadata: + $ref: "#/components/schemas/SandboxMetadata" + envVars: + $ref: "#/components/schemas/EnvVars" + mcp: + $ref: "#/components/schemas/Mcp" + iam: + $ref: "#/components/schemas/SandboxIam" + volumeMounts: + type: array + items: + $ref: "#/components/schemas/SandboxVolumeMount" + SandboxIam: type: object description: >- @@ -1020,6 +1073,25 @@ components: no memory. Rejected with an error in environments where this capability is not enabled, never silently downgraded to a memory restore. + ConnectSandboxV2: + type: object + properties: + timeout: + description: Timeout in seconds from the current time after which the sandbox should expire + type: integer + format: int32 + minimum: 0 + default: 300 + memory: + type: boolean + description: >- + Defaults to true. When false and the sandbox is paused, resume from disk state only: the + sandbox cold-boots fresh and any memory in the snapshot is ignored, never + modified or deleted. Disk state has crash-recovery semantics — writes not + flushed before the pause may be lost. A no-op for snapshots that contain + no memory. Rejected with an error in environments where this capability + is not enabled, never silently downgraded to a memory restore. + SandboxTimeoutRequest: type: object required: @@ -2496,7 +2568,8 @@ paths: $ref: "#/components/responses/500" post: summary: Create sandbox - description: Create a sandbox from the template + description: Create a sandbox from the template. Use POST /v2/sandboxes instead. + deprecated: true tags: [sandboxes] security: - ApiKeyAuth: [] @@ -2531,6 +2604,43 @@ paths: $ref: "#/components/responses/504" /v2/sandboxes: + post: + summary: Create sandbox (v2) + description: Create a sandbox from the template. All system communication with the sandbox is secured. + tags: [sandboxes] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/NewSandboxV2" + responses: + "201": + description: The sandbox was created successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Sandbox" + "401": + $ref: "#/components/responses/401" + "400": + $ref: "#/components/responses/400" + "429": + $ref: "#/components/responses/429" + "500": + $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + "504": + $ref: "#/components/responses/504" get: summary: List sandboxes (v2) description: List all sandboxes @@ -2981,7 +3091,8 @@ paths: /sandboxes/{sandboxID}/connect: post: summary: Connect sandbox - description: Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. + description: Returns sandbox details. If the sandbox is paused, it will be resumed. TTL is only extended. Use POST /v2/sandboxes/{sandboxID}/connect instead. + deprecated: true tags: [sandboxes] security: - ApiKeyAuth: [] @@ -3027,6 +3138,60 @@ paths: "504": $ref: "#/components/responses/504" + /v2/sandboxes/{sandboxID}/connect: + post: + summary: Connect sandbox (v2) + description: >- + Returns sandbox details. If the sandbox is paused, it will be resumed. + TTL is only extended. The request body is optional; an omitted timeout + defaults to 300 seconds. + tags: [sandboxes] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/sandboxID" + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/ConnectSandboxV2" + responses: + "200": + description: The sandbox was already running + content: + application/json: + schema: + $ref: "#/components/schemas/Sandbox" + "201": + description: The sandbox was resumed successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Sandbox" + "400": + $ref: "#/components/responses/400" + "401": + $ref: "#/components/responses/401" + "404": + $ref: "#/components/responses/404" + "409": + $ref: "#/components/responses/409" + "429": + $ref: "#/components/responses/429" + "500": + $ref: "#/components/responses/500" + "503": + $ref: "#/components/responses/503" + "504": + $ref: "#/components/responses/504" + /sandboxes/{sandboxID}/timeout: post: summary: Set sandbox timeout From 9055c26f5e1842ce827bf6e08e3885a040f69635 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:03:02 +0000 Subject: [PATCH 12/20] Drop removed secure option from downstream packages and point client tests at v2 create Co-Authored-By: mish@e2b.dev --- .changeset/quiet-desks-secure.md | 5 +++++ packages/code-interpreter-js/tests/setup.ts | 1 - .../tests/async/test_async_contexts.py | 16 ++++---------- .../code-interpreter-python/tests/conftest.py | 1 - .../tests/sync/test_basic.py | 2 +- .../tests/sync/test_contexts.py | 8 +++---- packages/desktop-js/tests/client.test.ts | 2 +- packages/desktop-python/e2b_desktop/main.py | 7 ++---- packages/desktop-python/tests/test_client.py | 2 +- packages/js-sdk/tests/client.test.ts | 22 +++++++++---------- packages/python-sdk/tests/test_client.py | 2 +- 11 files changed, 30 insertions(+), 38 deletions(-) create mode 100644 .changeset/quiet-desks-secure.md diff --git a/.changeset/quiet-desks-secure.md b/.changeset/quiet-desks-secure.md new file mode 100644 index 0000000000..d70ea0c62c --- /dev/null +++ b/.changeset/quiet-desks-secure.md @@ -0,0 +1,5 @@ +--- +'@e2b/desktop-python': minor +--- + +Remove the `secure` option from `Sandbox.create`: every sandbox is now created through the v2 API, which always secures envd access. `allow_internet_access` is no longer preset to `True`; when omitted, the API default applies. diff --git a/packages/code-interpreter-js/tests/setup.ts b/packages/code-interpreter-js/tests/setup.ts index 5b43801020..d7e4920d7c 100644 --- a/packages/code-interpreter-js/tests/setup.ts +++ b/packages/code-interpreter-js/tests/setup.ts @@ -51,7 +51,6 @@ export const isIntegrationTest = process.env.E2B_INTEGRATION_TEST !== undefined export const secureSandboxTest = sandboxTest.extend({ sandboxOpts: { - secure: true, network: { allowPublicTraffic: false, }, diff --git a/packages/code-interpreter-python/tests/async/test_async_contexts.py b/packages/code-interpreter-python/tests/async/test_async_contexts.py index 0f533f8f84..d470025d91 100644 --- a/packages/code-interpreter-python/tests/async/test_async_contexts.py +++ b/packages/code-interpreter-python/tests/async/test_async_contexts.py @@ -66,9 +66,7 @@ async def test_restart_context(async_sandbox: AsyncSandbox): @pytest.mark.skip_debug async def test_create_context_secure_traffic(async_sandbox_factory): - async_sandbox = await async_sandbox_factory( - secure=True, network={"allow_public_traffic": False} - ) + async_sandbox = await async_sandbox_factory(network={"allow_public_traffic": False}) context = await async_sandbox.create_code_context() contexts = await async_sandbox.list_code_contexts() @@ -81,9 +79,7 @@ async def test_create_context_secure_traffic(async_sandbox_factory): @pytest.mark.skip_debug async def test_remove_context_secure_traffic(async_sandbox_factory): - async_sandbox = await async_sandbox_factory( - secure=True, network={"allow_public_traffic": False} - ) + async_sandbox = await async_sandbox_factory(network={"allow_public_traffic": False}) context = await async_sandbox.create_code_context() await async_sandbox.remove_code_context(context.id) @@ -94,9 +90,7 @@ async def test_remove_context_secure_traffic(async_sandbox_factory): @pytest.mark.skip_debug async def test_list_contexts_secure_traffic(async_sandbox_factory): - async_sandbox = await async_sandbox_factory( - secure=True, network={"allow_public_traffic": False} - ) + async_sandbox = await async_sandbox_factory(network={"allow_public_traffic": False}) contexts = await async_sandbox.list_code_contexts() # default contexts should include python and javascript @@ -107,9 +101,7 @@ async def test_list_contexts_secure_traffic(async_sandbox_factory): @pytest.mark.skip_debug async def test_restart_context_secure_traffic(async_sandbox_factory): - async_sandbox = await async_sandbox_factory( - secure=True, network={"allow_public_traffic": False} - ) + async_sandbox = await async_sandbox_factory(network={"allow_public_traffic": False}) context = await async_sandbox.create_code_context() # set a variable in the context diff --git a/packages/code-interpreter-python/tests/conftest.py b/packages/code-interpreter-python/tests/conftest.py index fbe536667a..1f39f61959 100644 --- a/packages/code-interpreter-python/tests/conftest.py +++ b/packages/code-interpreter-python/tests/conftest.py @@ -29,7 +29,6 @@ def template(): @pytest.fixture() def sandbox_factory(request, template, sandbox_test_id): def factory(*, template_name: str = template, **kwargs): - kwargs.setdefault("secure", False) kwargs.setdefault("timeout", DEFAULT_TEST_SANDBOX_TIMEOUT) metadata = kwargs.setdefault("metadata", dict()) diff --git a/packages/code-interpreter-python/tests/sync/test_basic.py b/packages/code-interpreter-python/tests/sync/test_basic.py index 30f949834f..d2eac37fd2 100644 --- a/packages/code-interpreter-python/tests/sync/test_basic.py +++ b/packages/code-interpreter-python/tests/sync/test_basic.py @@ -10,7 +10,7 @@ def test_basic(sandbox: Sandbox): @pytest.mark.skip_debug def test_secure_access(sandbox_factory): - sandbox = sandbox_factory(secure=True, network={"allow_public_traffic": False}) + sandbox = sandbox_factory(network={"allow_public_traffic": False}) # Create sandbox with public traffic disabled (secure access) result = sandbox.run_code("x =1; x") assert result.text == "1" diff --git a/packages/code-interpreter-python/tests/sync/test_contexts.py b/packages/code-interpreter-python/tests/sync/test_contexts.py index 0b8278f524..2672af7ff1 100644 --- a/packages/code-interpreter-python/tests/sync/test_contexts.py +++ b/packages/code-interpreter-python/tests/sync/test_contexts.py @@ -67,7 +67,7 @@ def test_restart_context(sandbox: Sandbox): # Secure traffic tests (public traffic disabled) @pytest.mark.skip_debug def test_create_context_secure_traffic(sandbox_factory): - sandbox = sandbox_factory(secure=True, network={"allow_public_traffic": False}) + sandbox = sandbox_factory(network={"allow_public_traffic": False}) context = sandbox.create_code_context() contexts = sandbox.list_code_contexts() @@ -80,7 +80,7 @@ def test_create_context_secure_traffic(sandbox_factory): @pytest.mark.skip_debug def test_remove_context_secure_traffic(sandbox_factory): - sandbox = sandbox_factory(secure=True, network={"allow_public_traffic": False}) + sandbox = sandbox_factory(network={"allow_public_traffic": False}) context = sandbox.create_code_context() sandbox.remove_code_context(context.id) @@ -91,7 +91,7 @@ def test_remove_context_secure_traffic(sandbox_factory): @pytest.mark.skip_debug def test_list_contexts_secure_traffic(sandbox_factory): - sandbox = sandbox_factory(secure=True, network={"allow_public_traffic": False}) + sandbox = sandbox_factory(network={"allow_public_traffic": False}) contexts = sandbox.list_code_contexts() # default contexts should include python and javascript @@ -102,7 +102,7 @@ def test_list_contexts_secure_traffic(sandbox_factory): @pytest.mark.skip_debug def test_restart_context_secure_traffic(sandbox_factory): - sandbox = sandbox_factory(secure=True, network={"allow_public_traffic": False}) + sandbox = sandbox_factory(network={"allow_public_traffic": False}) context = sandbox.create_code_context() # set a variable in the context diff --git a/packages/desktop-js/tests/client.test.ts b/packages/desktop-js/tests/client.test.ts index 1960af1c54..1b99c4c337 100644 --- a/packages/desktop-js/tests/client.test.ts +++ b/packages/desktop-js/tests/client.test.ts @@ -51,7 +51,7 @@ async function handler(req: IncomingMessage, res: ServerResponse) { } const path = req.url ?? '' - if (path.startsWith('/sandboxes')) { + if (path.startsWith('/v2/sandboxes')) { respond(201, { sandboxID: 'test-sandbox-id', templateID: 'desktop', diff --git a/packages/desktop-python/e2b_desktop/main.py b/packages/desktop-python/e2b_desktop/main.py index 1962df0142..9f25a4ef6a 100644 --- a/packages/desktop-python/e2b_desktop/main.py +++ b/packages/desktop-python/e2b_desktop/main.py @@ -222,8 +222,7 @@ def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, - secure: bool = True, - allow_internet_access: bool = True, + allow_internet_access: Optional[bool] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, @@ -244,8 +243,7 @@ def create( :param timeout: Timeout for the sandbox in **seconds**, default to 300 seconds. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox - :param secure: Envd is secured with access token and cannot be used without it - :param allow_internet_access: Allow sandbox to access the internet, defaults to `True`. + :param allow_internet_access: Allow sandbox to access the internet :return: A Sandbox instance for the new sandbox @@ -263,7 +261,6 @@ def create( timeout=timeout, metadata=metadata, envs=envs, - secure=secure, allow_internet_access=allow_internet_access, network=network, iam=iam, diff --git a/packages/desktop-python/tests/test_client.py b/packages/desktop-python/tests/test_client.py index 7f89d3433c..2253dc0917 100644 --- a/packages/desktop-python/tests/test_client.py +++ b/packages/desktop-python/tests/test_client.py @@ -57,7 +57,7 @@ def do_POST(self): if length: self.rfile.read(length) - if self.path.startswith("/sandboxes"): + if self.path.startswith("/v2/sandboxes"): self._record_and_respond(201, SANDBOX_RESPONSE) elif self.path.startswith("/volumes"): self._record_and_respond( diff --git a/packages/js-sdk/tests/client.test.ts b/packages/js-sdk/tests/client.test.ts index e49d481cb5..5e4e182874 100644 --- a/packages/js-sdk/tests/client.test.ts +++ b/packages/js-sdk/tests/client.test.ts @@ -143,7 +143,7 @@ test('client.Sandbox.create uses the client config instead of env vars', async ( const sandbox = await client.Sandbox.create() - assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/v2/sandboxes`) assert.equal(lastRequest().apiKey, API_KEY_A) // The bound config is also carried by the created sandbox instance. assert.equal(sandbox.sandboxDomain, DOMAIN_A) @@ -156,7 +156,7 @@ test('client.Sandbox instances are subclass instances of Sandbox', async () => { assert.isTrue(client.Sandbox.prototype instanceof Sandbox) assert.instanceOf(await client.Sandbox.create(), Sandbox) // Class-level defaults are inherited from Sandbox. - assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/v2/sandboxes`) }) test('per-call options take precedence over the client config', async () => { @@ -164,7 +164,7 @@ test('per-call options take precedence over the client config', async () => { await client.Sandbox.create({ apiKey: API_KEY_B, domain: DOMAIN_B }) - assert.equal(lastRequest().url, `https://api.${DOMAIN_B}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_B}/v2/sandboxes`) assert.equal(lastRequest().apiKey, API_KEY_B) }) @@ -239,8 +239,8 @@ test('two clients with different configs stay isolated', async () => { assert.deepEqual( requests.map((r) => [r.url, r.apiKey]), [ - [`https://api.${DOMAIN_A}/sandboxes`, API_KEY_A], - [`https://api.${DOMAIN_B}/sandboxes`, API_KEY_B], + [`https://api.${DOMAIN_A}/v2/sandboxes`, API_KEY_A], + [`https://api.${DOMAIN_B}/v2/sandboxes`, API_KEY_B], ] ) }) @@ -252,14 +252,14 @@ test('mutating the options object does not change the bound config', async () => await client.Sandbox.create() - assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/v2/sandboxes`) }) test('per-call options explicitly set to undefined keep the client config', async () => { const client = new E2B({ apiKey: API_KEY_A, domain: DOMAIN_A }) await client.Sandbox.create({ apiKey: undefined, domain: undefined }) - assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/v2/sandboxes`) assert.equal(lastRequest().apiKey, API_KEY_A) await client.Sandbox.list().nextItems({ domain: undefined }) @@ -283,7 +283,7 @@ test('a signal is not bound to the client', async () => { await client.Sandbox.create() - assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/v2/sandboxes`) }) test('a __proto__ option does not pollute the prototype', async () => { @@ -294,7 +294,7 @@ test('a __proto__ option does not pollute the prototype', async () => { ) assert.isUndefined(({} as Record).polluted) - assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_A}/v2/sandboxes`) }) test('Template statics work detached from the class', async () => { @@ -422,7 +422,7 @@ test('top-level exports keep using the environment configuration', async () => { await client.Sandbox.create() await Sandbox.create() - assert.equal(lastRequest().url, `https://api.${DOMAIN_ENV}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_ENV}/v2/sandboxes`) assert.equal(lastRequest().apiKey, TEST_API_KEY) await Volume.list() @@ -442,6 +442,6 @@ test('the default export is still Sandbox', async () => { await DefaultExport.create() - assert.equal(lastRequest().url, `https://api.${DOMAIN_ENV}/sandboxes`) + assert.equal(lastRequest().url, `https://api.${DOMAIN_ENV}/v2/sandboxes`) assert.equal(lastRequest().apiKey, TEST_API_KEY) }) diff --git a/packages/python-sdk/tests/test_client.py b/packages/python-sdk/tests/test_client.py index 21ad9dbc15..461983f46f 100644 --- a/packages/python-sdk/tests/test_client.py +++ b/packages/python-sdk/tests/test_client.py @@ -65,7 +65,7 @@ def do_POST(self): if length: self.rfile.read(length) - if self.path.startswith("/sandboxes"): + if self.path.startswith("/v2/sandboxes"): self._record_and_respond(201, SANDBOX_RESPONSE) elif self.path.startswith("/volumes"): self._record_and_respond( From 31c28c0174bbcbc4e87190a597d46382843d9fc4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:18:56 +0000 Subject: [PATCH 13/20] Update template build body tests for API-owned cpu/memory defaults Co-Authored-By: mish@e2b.dev --- packages/js-sdk/tests/template/boundOpts.test.ts | 2 -- .../tests/async/template_async/test_bound_api_params.py | 2 +- .../tests/sync/template_sync/test_bound_api_params.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/js-sdk/tests/template/boundOpts.test.ts b/packages/js-sdk/tests/template/boundOpts.test.ts index dae5375497..b6a2397954 100644 --- a/packages/js-sdk/tests/template/boundOpts.test.ts +++ b/packages/js-sdk/tests/template/boundOpts.test.ts @@ -184,8 +184,6 @@ test.each([ expect(buildRequestBodies).toEqual([ { name: 'minimum', - cpuCount: 2, - memoryMB: 1024, ...(expected === undefined ? {} : { minFreeDiskMb: expected }), }, ]) diff --git a/packages/python-sdk/tests/async/template_async/test_bound_api_params.py b/packages/python-sdk/tests/async/template_async/test_bound_api_params.py index 7679618849..8c1bc056f2 100644 --- a/packages/python-sdk/tests/async/template_async/test_bound_api_params.py +++ b/packages/python-sdk/tests/async/template_async/test_bound_api_params.py @@ -205,7 +205,7 @@ async def request(*, client, body): await getattr(AsyncTemplate, method)( Template().from_template("parent"), "minimum", **options ) - expected_body = {"name": "minimum", "cpuCount": 2, "memoryMB": 1024} + expected_body = {"name": "minimum"} if expected is not None: expected_body["minFreeDiskMb"] = expected assert [body.to_dict() for body in bodies] == [expected_body] diff --git a/packages/python-sdk/tests/sync/template_sync/test_bound_api_params.py b/packages/python-sdk/tests/sync/template_sync/test_bound_api_params.py index c0dc1546fc..3472c93877 100644 --- a/packages/python-sdk/tests/sync/template_sync/test_bound_api_params.py +++ b/packages/python-sdk/tests/sync/template_sync/test_bound_api_params.py @@ -179,7 +179,7 @@ def request(*, client, body): monkeypatch.setattr(template_sync_main, "trigger_build", Mock()) monkeypatch.setattr(template_sync_main, "wait_for_build_finish", Mock()) getattr(Template, method)(Template().from_template("parent"), "minimum", **options) - expected_body = {"name": "minimum", "cpuCount": 2, "memoryMB": 1024} + expected_body = {"name": "minimum"} if expected is not None: expected_body["minFreeDiskMb"] = expected assert [body.to_dict() for body in bodies] == [expected_body] From 88e5b44493cab86e866643550937c57d27a22195 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:24:52 +0000 Subject: [PATCH 14/20] Drop removed secure option from sync secured-files tests Co-Authored-By: mish@e2b.dev --- .../tests/sync/sandbox_sync/files/test_secured.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/python-sdk/tests/sync/sandbox_sync/files/test_secured.py b/packages/python-sdk/tests/sync/sandbox_sync/files/test_secured.py index 4fa59694ce..3fa66a58ba 100644 --- a/packages/python-sdk/tests/sync/sandbox_sync/files/test_secured.py +++ b/packages/python-sdk/tests/sync/sandbox_sync/files/test_secured.py @@ -6,7 +6,7 @@ @pytest.mark.skip_debug() def test_download_url_with_signing(sandbox_factory): - sbx = sandbox_factory(timeout=100, secure=True) + sbx = sandbox_factory(timeout=100) file_path = "test_download_url_with_signing.txt" file_content = "This file will be watched." @@ -22,7 +22,7 @@ def test_download_url_with_signing(sandbox_factory): @pytest.mark.skip_debug() def test_download_url_with_signing_and_expiration(sandbox_factory): - sbx = sandbox_factory(timeout=100, secure=True) + sbx = sandbox_factory(timeout=100) file_path = "test_download_url_with_signing.txt" file_content = "This file will be watched." @@ -38,7 +38,7 @@ def test_download_url_with_signing_and_expiration(sandbox_factory): @pytest.mark.skip_debug() def test_download_url_with_expired_signing(sandbox_factory): - sbx = sandbox_factory(timeout=100, secure=True) + sbx = sandbox_factory(timeout=100) file_path = "test_download_url_with_signing.txt" file_content = "This file will be watched." From f34548291074bc6555f13bcb80bf5ecdd19d75cd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:39:54 +0000 Subject: [PATCH 15/20] Keep deprecated secure option accepted and drop logsOffset preset secure stays in the create signatures (JS, Python sync/async, desktop-python) so existing callers don't hit an invalid-argument error, but it is ignored and never serialized into NewSandboxV2. Template.getBuildStatus no longer presets logsOffset=0; when omitted the API default applies. Co-Authored-By: mish@e2b.dev --- .changeset/olive-poets-hammer.md | 2 +- .changeset/quiet-desks-secure.md | 2 +- packages/desktop-python/e2b_desktop/main.py | 2 ++ packages/js-sdk/src/sandbox/sandboxApi.ts | 5 +++++ packages/js-sdk/src/template/index.ts | 4 ++-- packages/js-sdk/tests/sandbox/apiDefaults.test.ts | 7 +++++++ packages/python-sdk/e2b/sandbox_async/main.py | 2 ++ packages/python-sdk/e2b/sandbox_sync/main.py | 2 ++ packages/python-sdk/e2b/template_async/build_api.py | 7 +++++-- packages/python-sdk/e2b/template_async/main.py | 6 +++--- packages/python-sdk/e2b/template_sync/build_api.py | 7 +++++-- packages/python-sdk/e2b/template_sync/main.py | 6 +++--- .../tests/shared/sandbox/test_api_defaults.py | 12 ++++++++++++ 13 files changed, 50 insertions(+), 14 deletions(-) diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md index c7cb947a41..b0a59e8ecb 100644 --- a/.changeset/olive-poets-hammer.md +++ b/.changeset/olive-poets-hammer.md @@ -5,4 +5,4 @@ Remove SDK-side defaults from API request payloads so the API defaults apply when options are omitted. Sandbox create/fork/connect no longer preset a 5-minute timeout, fork no longer presets `count: 1`, create no longer presets `allow_internet_access`, pause no longer presets keeping memory, and template builds no longer preset CPU/memory. Explicitly provided values are still sent unchanged. -Sandbox create and connect now use the v2 API endpoints (`POST /v2/sandboxes`, `POST /v2/sandboxes/{id}/connect`), which default `timeout` to 5 minutes and always secure envd access. The `secure` option on `Sandbox.create` has been removed: every sandbox is secured. +Sandbox create and connect now use the v2 API endpoints (`POST /v2/sandboxes`, `POST /v2/sandboxes/{id}/connect`), which default `timeout` to 5 minutes and always secure envd access. The `secure` option on `Sandbox.create` is deprecated: every sandbox is secured, so the option is still accepted but ignored. diff --git a/.changeset/quiet-desks-secure.md b/.changeset/quiet-desks-secure.md index d70ea0c62c..bf3f74bd18 100644 --- a/.changeset/quiet-desks-secure.md +++ b/.changeset/quiet-desks-secure.md @@ -2,4 +2,4 @@ '@e2b/desktop-python': minor --- -Remove the `secure` option from `Sandbox.create`: every sandbox is now created through the v2 API, which always secures envd access. `allow_internet_access` is no longer preset to `True`; when omitted, the API default applies. +Deprecate the `secure` option of `Sandbox.create` (still accepted, now ignored): every sandbox is created through the v2 API, which always secures envd access. `allow_internet_access` is no longer preset to `True`; when omitted, the API default applies. diff --git a/packages/desktop-python/e2b_desktop/main.py b/packages/desktop-python/e2b_desktop/main.py index 9f25a4ef6a..ec81fec965 100644 --- a/packages/desktop-python/e2b_desktop/main.py +++ b/packages/desktop-python/e2b_desktop/main.py @@ -222,6 +222,7 @@ def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, + secure: Optional[bool] = None, allow_internet_access: Optional[bool] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -243,6 +244,7 @@ def create( :param timeout: Timeout for the sandbox in **seconds**, default to 300 seconds. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox + :param secure: Deprecated — every sandbox secures envd access; accepted for backward compatibility and ignored :param allow_internet_access: Allow sandbox to access the internet :return: A Sandbox instance for the new sandbox diff --git a/packages/js-sdk/src/sandbox/sandboxApi.ts b/packages/js-sdk/src/sandbox/sandboxApi.ts index cf372bdb73..c144863534 100644 --- a/packages/js-sdk/src/sandbox/sandboxApi.ts +++ b/packages/js-sdk/src/sandbox/sandboxApi.ts @@ -602,6 +602,11 @@ export interface SandboxOpts extends ConnectionOpts { */ timeoutMs?: number + /** + * @deprecated Every sandbox secures envd access; this option is accepted for backward compatibility and ignored. + */ + 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]`. */ diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index e7e8fdf12c..39074514d9 100644 --- a/packages/js-sdk/src/template/index.ts +++ b/packages/js-sdk/src/template/index.ts @@ -266,7 +266,7 @@ export class TemplateBase * * @example * ```ts - * const status = await Template.getBuildStatus(data, { logsOffset: 0 }) + * const status = await Template.getBuildStatus(data) * ``` */ static async getBuildStatus( @@ -281,7 +281,7 @@ export class TemplateBase { templateID: data.templateId, buildID: data.buildId, - logsOffset: options?.logsOffset ?? 0, + logsOffset: options?.logsOffset, }, config.getSignal(undefined, options?.signal) ) diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts index 3aa66698c2..1a0c50d800 100644 --- a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -66,6 +66,13 @@ test('Sandbox.create omits timeout and allow_internet_access when unset', async expect(lastCreateBody).not.toHaveProperty('allow_internet_access') }) +test('Sandbox.create ignores the deprecated secure option', async () => { + await Sandbox.create('base', { apiKey: TEST_API_KEY, secure: true }) + + expect(lastCreateBody).toBeDefined() + expect(lastCreateBody).not.toHaveProperty('secure') +}) + test('Sandbox.create sends explicit timeout and allow_internet_access', async () => { await Sandbox.create('base', { apiKey: TEST_API_KEY, diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 8c3018c671..a5eac92642 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -175,6 +175,7 @@ async def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, + secure: Optional[bool] = None, allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, @@ -193,6 +194,7 @@ async def create( :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox + :param secure: Deprecated — every sandbox secures envd access; accepted for backward compatibility and ignored :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 9430c8d8d0..5dfae376fe 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -171,6 +171,7 @@ def create( timeout: Optional[int] = None, metadata: Optional[Dict[str, str]] = None, envs: Optional[Dict[str, str]] = None, + secure: Optional[bool] = None, allow_internet_access: Optional[bool] = None, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, @@ -189,6 +190,7 @@ def create( :param timeout: Timeout for the sandbox in **seconds**. The maximum time a sandbox can be kept alive is 24 hours (86_400 seconds) for Pro users and 1 hour (3_600 seconds) for Hobby users. :param metadata: Custom metadata for the sandbox :param envs: Custom environment variables for the sandbox + :param secure: Deprecated — every sandbox secures envd access; accepted for backward compatibility and ignored :param allow_internet_access: Allow sandbox to access the internet. If set to `False`, it works the same as setting network `deny_out` to `[0.0.0.0/0]`. :param mcp: MCP server to enable in the sandbox :param network: Sandbox network configuration. ``allow_out``/``deny_out`` may also be a callable receiving a :class:`SandboxNetworkSelectorContext` (``ctx.all_traffic``, ``ctx.rules``) and returning a list of strings. Per-host transform rules are nested under ``network.rules``; a rule's ``transform`` may be a callable receiving a :class:`SandboxNetworkTransformContext` of placeholder strings (``ctx.iam.tokens[name]``). diff --git a/packages/python-sdk/e2b/template_async/build_api.py b/packages/python-sdk/e2b/template_async/build_api.py index de46ef09c1..8f0ffc205c 100644 --- a/packages/python-sdk/e2b/template_async/build_api.py +++ b/packages/python-sdk/e2b/template_async/build_api.py @@ -234,13 +234,16 @@ def _map_build_status_reason(reason) -> Optional[BuildStatusReason]: async def get_build_status( - client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int + client: AuthenticatedClient, + template_id: str, + build_id: str, + logs_offset: Optional[int] = None, ) -> TemplateBuildStatusResponse: res = await get_templates_template_id_builds_build_id_status.asyncio_detailed( template_id=encode_path_param(template_id), build_id=build_id, client=client, - logs_offset=logs_offset, + logs_offset=logs_offset if logs_offset is not None else UNSET, ) if res.status_code >= 300: diff --git a/packages/python-sdk/e2b/template_async/main.py b/packages/python-sdk/e2b/template_async/main.py index a7729095e8..a5f144cc4d 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -376,14 +376,14 @@ async def build_in_background( async def get_build_status( cls, build_info: BuildInfo, - logs_offset: int = 0, + logs_offset: Optional[int] = None, **opts: Unpack[ApiParams], ): """ Get the status of a build. :param build_info: Build identifiers returned from build_in_background - :param logs_offset: Offset for fetching logs + :param logs_offset: Offset for fetching logs; when omitted, the API default applies :return: TemplateBuild containing the build status and logs Example @@ -391,7 +391,7 @@ async def get_build_status( from e2b import AsyncTemplate build_info = await AsyncTemplate.build_in_background(template, alias='my-template') - status = await AsyncTemplate.get_build_status(build_info, logs_offset=0) + status = await AsyncTemplate.get_build_status(build_info) ``` """ config = ConnectionConfig(**cls._resolve_api_params(**opts)) diff --git a/packages/python-sdk/e2b/template_sync/build_api.py b/packages/python-sdk/e2b/template_sync/build_api.py index f975cc899e..36d915e388 100644 --- a/packages/python-sdk/e2b/template_sync/build_api.py +++ b/packages/python-sdk/e2b/template_sync/build_api.py @@ -232,13 +232,16 @@ def _map_build_status_reason(reason) -> Optional[BuildStatusReason]: def get_build_status( - client: AuthenticatedClient, template_id: str, build_id: str, logs_offset: int + client: AuthenticatedClient, + template_id: str, + build_id: str, + logs_offset: Optional[int] = None, ) -> TemplateBuildStatusResponse: res = get_templates_template_id_builds_build_id_status.sync_detailed( template_id=encode_path_param(template_id), build_id=build_id, client=client, - logs_offset=logs_offset, + logs_offset=logs_offset if logs_offset is not None else UNSET, ) if res.status_code >= 300: diff --git a/packages/python-sdk/e2b/template_sync/main.py b/packages/python-sdk/e2b/template_sync/main.py index 25d3e30ef3..d6c8b46c5b 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -377,14 +377,14 @@ def build_in_background( def get_build_status( cls, build_info: BuildInfo, - logs_offset: int = 0, + logs_offset: Optional[int] = None, **opts: Unpack[ApiParams], ): """ Get the status of a build. :param build_info: Build identifiers returned from build_in_background - :param logs_offset: Offset for fetching logs + :param logs_offset: Offset for fetching logs; when omitted, the API default applies :return: TemplateBuild containing the build status and logs Example @@ -392,7 +392,7 @@ def get_build_status( from e2b import Template build_info = Template.build_in_background(template, alias='my-template') - status = Template.get_build_status(build_info, logs_offset=0) + status = Template.get_build_status(build_info) ``` """ config = ConnectionConfig(**cls._resolve_api_params(**opts)) diff --git a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py index 182037c3a0..1ab0b5779f 100644 --- a/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -62,6 +62,12 @@ def test_create_sends_explicit_values(monkeypatch, test_api_key): assert body["allow_internet_access"] is False +def test_create_ignores_deprecated_secure(monkeypatch, test_api_key): + body = _sync_create_body(monkeypatch, test_api_key, secure=True) + + assert "secure" not in body + + async def test_async_create_omits_api_owned_fields_when_unset( monkeypatch, test_api_key ): @@ -84,6 +90,12 @@ async def test_async_create_sends_explicit_values(monkeypatch, test_api_key): assert body["allow_internet_access"] is False +async def test_async_create_ignores_deprecated_secure(monkeypatch, test_api_key): + body = await _async_create_body(monkeypatch, test_api_key, secure=True) + + assert "secure" not in body + + def _sync_fork_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: request = Mock(return_value=SimpleNamespace(status_code=200, parsed=[])) monkeypatch.setattr(post_sandboxes_sandbox_id_fork, "sync_detailed", request) From e875d621f1239ff0b2539ea4ca2d16690e1743b5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:49:24 +0000 Subject: [PATCH 16/20] Sync v2 sandbox timeout minimum from belt spec Co-Authored-By: mish@e2b.dev --- spec/openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/openapi.yml b/spec/openapi.yml index 532539d566..4d5363e7b5 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -970,7 +970,7 @@ components: timeout: type: integer format: int32 - minimum: 0 + minimum: 1 default: 300 description: Time to live for the sandbox in seconds. autoPause: @@ -1088,7 +1088,7 @@ components: description: Timeout in seconds from the current time after which the sandbox should expire type: integer format: int32 - minimum: 0 + minimum: 1 default: 300 memory: type: boolean From 973a2da79bfb3887aa195ca536e168bd9aa04cfb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:51:36 +0000 Subject: [PATCH 17/20] Sync v2 sandbox timeout minimum (15s) from belt spec Co-Authored-By: mish@e2b.dev --- spec/openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/openapi.yml b/spec/openapi.yml index 4d5363e7b5..79dd6f0451 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -970,7 +970,7 @@ components: timeout: type: integer format: int32 - minimum: 1 + minimum: 15 default: 300 description: Time to live for the sandbox in seconds. autoPause: @@ -1088,7 +1088,7 @@ components: description: Timeout in seconds from the current time after which the sandbox should expire type: integer format: int32 - minimum: 1 + minimum: 15 default: 300 memory: type: boolean From 0ee39e4bf5b220096d90fc7ead69d8c8469881c2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:55:16 +0000 Subject: [PATCH 18/20] Sync v2 sandbox timeout minimum (1s) from belt spec Co-Authored-By: mish@e2b.dev --- spec/openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/openapi.yml b/spec/openapi.yml index 79dd6f0451..4d5363e7b5 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -970,7 +970,7 @@ components: timeout: type: integer format: int32 - minimum: 15 + minimum: 1 default: 300 description: Time to live for the sandbox in seconds. autoPause: @@ -1088,7 +1088,7 @@ components: description: Timeout in seconds from the current time after which the sandbox should expire type: integer format: int32 - minimum: 15 + minimum: 1 default: 300 memory: type: boolean From e7306ec6655876df5927744f5c49a2612cac548d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:09:16 +0000 Subject: [PATCH 19/20] chore(spec): bump runtime-ref to the exported v2 sandbox create/connect spec Co-Authored-By: mish@e2b.dev --- spec/openapi.yml | 766 +++++++++++++++++++++++++++++++++++++++++++++++ spec/runtime-ref | 2 +- 2 files changed, 767 insertions(+), 1 deletion(-) diff --git a/spec/openapi.yml b/spec/openapi.yml index 4d5363e7b5..7dc37ed62f 100644 --- a/spec/openapi.yml +++ b/spec/openapi.yml @@ -137,6 +137,13 @@ components: Identifier of the secret (sec_ prefixed), or its canonical lower-case name + webhookID: + name: webhookID + in: path + required: true + schema: + type: string + format: uuid headers: XNextToken: description: Cursor to fetch the next page of results, if more exist @@ -2320,6 +2327,351 @@ components: metadata: $ref: "#/components/schemas/SecretMetadata" + SandboxEvent: + description: Sandbox event + required: + - id + - version + - type + - timestamp + - sandboxId + - sandboxExecutionId + - sandboxTemplateId + - sandboxBuildId + - sandboxTeamId + properties: + id: + type: string + format: uuid + description: Event unique identifier + version: + type: string + description: Event structure version + type: + type: string + description: Event name + eventCategory: + type: string + deprecated: true + description: Category of the event (e.g., 'lifecycle', 'process', etc.) + eventLabel: + type: string + deprecated: true + description: Label for the specific event type (e.g., 'sandbox_started', 'process_oom', etc.) + eventData: + type: object + nullable: true + description: Optional JSON data associated with the event + + timestamp: + type: string + format: date-time + description: Timestamp of the event + sandboxId: + type: string + format: string + description: Unique identifier for the sandbox + sandboxExecutionId: + type: string + format: string + description: Unique identifier for the sandbox execution + sandboxTemplateId: + type: string + format: string + description: Unique identifier for the sandbox template + sandboxBuildId: + type: string + format: string + description: Unique identifier for the sandbox build + sandboxTeamId: + type: string + format: uuid + description: Team identifier associated with the sandbox + WebhookCreate: + description: Configuration for registering new webhooks + required: + - name + - url + - events + - signatureSecret + properties: + name: + type: string + url: + type: string + format: uri + events: + type: array + items: + type: string + enabled: + type: boolean + default: true + signatureSecret: + type: string + description: Secret used to sign the webhook payloads + WebhookCreation: + description: Webhook creation response + required: + - id + - name + - createdAt + - teamId + - url + - enabled + - events + properties: + id: + type: string + description: Webhook unique identifier + name: + type: string + description: Webhook user friendly name + createdAt: + type: string + format: date-time + description: Time when the template was created + teamId: + type: string + description: Unique identifier for the team + url: + type: string + format: uri + enabled: + type: boolean + events: + type: array + items: + type: string + WebhookDetail: + description: Webhook detail response + required: + - id + - teamId + - name + - createdAt + - url + - enabled + - events + properties: + id: + type: string + description: Webhook unique identifier + teamId: + type: string + description: Unique identifier for the team + name: + type: string + description: Webhook user friendly name + createdAt: + type: string + format: date-time + description: Time when the template was created + url: + type: string + format: uri + enabled: + type: boolean + events: + type: array + items: + type: string + WebhookConfiguration: + description: Configuration for updating existing webhooks + properties: + enabled: + type: boolean + name: + type: string + description: Webhook user friendly name + url: + type: string + format: uri + events: + type: array + items: + type: string + signatureSecret: + type: string + description: Secret used to sign the webhook payloads + WebhookDelivery: + description: Webhook delivery attempt + required: + - id + - teamId + - webhookId + - eventId + - sandboxId + - eventType + - status + - durationMs + - requestBody + - requestHeaders + - requestUrl + - errorClass + - timestamp + properties: + id: + type: string + format: uuid + description: Delivery attempt identifier + teamId: + type: string + format: uuid + description: Team identifier + webhookId: + type: string + format: uuid + description: Webhook configuration identifier + eventId: + type: string + format: uuid + description: Sandbox event identifier + sandboxId: + type: string + description: Sandbox identifier + eventType: + type: string + description: Sandbox event type + status: + type: string + enum: [success, failed] + description: Delivery attempt status + durationMs: + type: integer + format: int32 + description: Delivery request duration in milliseconds + requestBody: + type: string + description: Serialized webhook request body + requestHeaders: + type: string + description: JSON-encoded request headers with sensitive values redacted + requestUrl: + type: string + format: uri + description: URL attempted for this delivery + responseBody: + type: string + nullable: true + description: Truncated response body, if a response was received + responseHeaders: + type: string + nullable: true + description: JSON-encoded response headers, if a response was received + responseHttpStatusCode: + type: integer + format: int32 + nullable: true + description: HTTP response status code, if a response was received + errorClass: + type: string + nullable: true + enum: + - http_error + - dns_error + - timeout + - transport_error + - request_error + - signature_error + - canceled + description: Machine-readable non-HTTP or HTTP failure class + errorMessage: + type: string + nullable: true + description: Error message for failures without a useful response body + timestamp: + type: string + format: date-time + description: Time when the delivery attempt started + WebhookDeliveryStats: + description: Webhook delivery aggregate stats + required: + - buckets + - total + - failed + - durationMs + properties: + buckets: + type: array + items: + $ref: "#/components/schemas/WebhookDeliveryStatsBucket" + total: + type: integer + format: int64 + failed: + type: integer + format: int64 + durationMs: + $ref: "#/components/schemas/WebhookDeliveryDurationStats" + WebhookDeliveryDurationStats: + description: Webhook delivery duration statistics in milliseconds + required: + - minimum + - average + - maximum + properties: + minimum: + type: number + format: double + average: + type: number + format: double + maximum: + type: number + format: double + WebhookDeliveryStatsBucket: + description: Webhook delivery stats for a time bucket + required: + - timestamp + - total + - failed + - durationMs + properties: + timestamp: + type: string + format: date-time + total: + type: integer + format: int64 + failed: + type: integer + format: int64 + durationMs: + $ref: "#/components/schemas/WebhookDeliveryDurationStats" + WebhookDeliveryGroup: + description: Webhook delivery attempts grouped by sandbox event + required: + - eventId + - eventType + - sandboxId + - attempts + properties: + eventId: + type: string + format: uuid + eventType: + type: string + sandboxId: + type: string + attempts: + type: array + items: + $ref: "#/components/schemas/WebhookDelivery" + WebhookDeliveriesListPayload: + description: Paginated webhook delivery attempts grouped by event + required: + - data + - nextCursor + properties: + data: + type: array + items: + $ref: "#/components/schemas/WebhookDeliveryGroup" + nextCursor: + type: string + nullable: true + description: Cursor to pass to the next list request, or null when there is no next page. tags: - name: templates - name: sandboxes @@ -2471,6 +2823,7 @@ paths: /sandboxes: get: summary: List running sandboxes + x-api-group: list description: List all running sandboxes. Use GET /v2/sandboxes instead. deprecated: true tags: [sandboxes] @@ -2585,6 +2938,7 @@ paths: $ref: "#/components/responses/504" get: summary: List sandboxes (v2) + x-api-group: list description: List all sandboxes tags: [sandboxes] security: @@ -2659,6 +3013,7 @@ paths: /sandboxes/metrics: get: summary: List sandbox metrics + x-api-group: list description: List metrics for given sandboxes tags: [sandboxes] security: @@ -3295,6 +3650,7 @@ paths: /snapshots: get: summary: List snapshots + x-api-group: list description: List all snapshots for the team tags: [snapshots] security: @@ -3382,6 +3738,7 @@ paths: /v2/templates: get: summary: List templates (v2) + x-api-group: list description: List all templates tags: [templates] security: @@ -3467,6 +3824,7 @@ paths: /templates: get: summary: List templates + x-api-group: list description: List all templates deprecated: true tags: [templates] @@ -3504,6 +3862,7 @@ paths: /templates/{templateID}: get: summary: List template builds + x-api-group: list description: List all builds for a template tags: [templates] security: @@ -3846,6 +4205,7 @@ paths: /templates/{templateID}/tags: get: summary: List template tags + x-api-group: list description: List all tags for a template tags: [tags] security: @@ -4172,6 +4532,7 @@ paths: /api-keys: get: summary: List team API keys + x-api-group: list description: List all team API keys tags: [api-keys] security: @@ -4282,6 +4643,7 @@ paths: /volumes: get: summary: List team volumes + x-api-group: list description: List all team volumes tags: [volumes] security: @@ -4402,6 +4764,7 @@ paths: /secrets: get: summary: List project secrets + x-api-group: list description: List the project's secrets. No response carries a secret value. tags: [secrets] security: @@ -4808,3 +5171,406 @@ paths: $ref: "#/components/responses/500" "501": $ref: "#/components/responses/501" + + /events/sandboxes/{sandboxID}: + get: + description: Get sandbox events + tags: [events] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/sandboxID" + - name: offset + in: query + required: false + schema: + type: integer + format: int32 + minimum: 0 + default: 0 + - name: limit + in: query + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 10 + - name: orderAsc + in: query + required: false + schema: + type: boolean + default: false + - name: types + in: query + required: false + style: form + explode: true + schema: + type: array + items: + type: string + description: Filter events to the provided event types + responses: + "200": + description: Successfully returned the sandbox events + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SandboxEvent" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /events/sandboxes: + get: + description: Get all sandbox events for the team associated with the API key + tags: [events] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - name: offset + in: query + required: false + schema: + type: integer + format: int32 + minimum: 0 + default: 0 + - name: limit + in: query + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 10 + - name: orderAsc + in: query + required: false + schema: + type: boolean + default: false + - name: types + in: query + required: false + style: form + explode: true + schema: + type: array + items: + type: string + description: Filter events to the provided event types + responses: + "200": + description: Successfully returned the sandbox events + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SandboxEvent" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /events/webhooks: + post: + description: Register events webhook. + tags: [webhooks] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookCreate" + responses: + "201": + description: Successfully created webhook. + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookCreation" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + get: + description: List registered webhooks. + tags: [webhooks] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + responses: + "200": + description: List of registered webhooks. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/WebhookDetail" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /events/webhooks/{webhookID}: + get: + description: Get a registered webhook. + tags: [webhooks] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/webhookID" + responses: + "200": + description: Successfully returned the webhook configuration. + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookDetail" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + patch: + description: Update a registered webhook configuration. + tags: [webhooks] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/webhookID" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookConfiguration" + responses: + "200": + description: Successfully updated webhook. + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookDetail" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + delete: + description: Delete a registered webhook. + tags: [webhooks] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/webhookID" + responses: + "200": + description: Successfully deleted webhook. + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /events/webhooks/{webhookID}/deliveries: + get: + description: List webhook delivery attempts. + tags: [webhooks] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/webhookID" + - name: cursor + in: query + required: false + schema: + type: string + description: Opaque cursor from the previous response's nextCursor field. + - name: limit + in: query + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 25 + - name: orderAsc + in: query + required: false + schema: + type: boolean + default: false + - name: start + in: query + required: false + schema: + type: string + format: date-time + description: Include deliveries at or after this timestamp. + - name: end + in: query + required: false + schema: + type: string + format: date-time + description: Include deliveries before this timestamp. + - name: deliveryStatus + in: query + required: false + style: form + explode: false + schema: + type: array + items: + type: string + enum: [success, failed] + description: Filter deliveries by delivery status + - name: eventType + in: query + required: false + style: form + explode: false + schema: + type: array + items: + type: string + description: Filter deliveries by event type + responses: + "200": + description: List of webhook delivery attempts grouped by event. + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookDeliveriesListPayload" + "400": + $ref: "#/components/responses/400" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" + + /events/webhooks/{webhookID}/stats: + get: + description: Get webhook delivery aggregate stats. + tags: [webhooks] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - $ref: "#/components/parameters/webhookID" + - name: start + in: query + required: false + schema: + type: string + format: date-time + description: Inclusive stats range start. Defaults to 24 hours ago. + - name: end + in: query + required: false + schema: + type: string + format: date-time + description: Exclusive stats range end. Defaults to now. + responses: + "200": + description: Webhook delivery stats. + content: + application/json: + schema: + $ref: "#/components/schemas/WebhookDeliveryStats" + "404": + $ref: "#/components/responses/404" + "401": + $ref: "#/components/responses/401" + "500": + $ref: "#/components/responses/500" diff --git a/spec/runtime-ref b/spec/runtime-ref index ab915d79e6..d6288fb797 100644 --- a/spec/runtime-ref +++ b/spec/runtime-ref @@ -1 +1 @@ -433d1d5fbbd20c5e2259ee0bf356b7b0a05d46e9 +f5dc6426ef45b6b286ff60c11fc6bba7e00db95e From 11d377de8ec228ae273367796f7281237a0d21ac Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 10:36:52 +0000 Subject: [PATCH 20/20] test(js-sdk): use setupMockApi in apiDefaults test so it runs in the browser suite Co-Authored-By: mish@e2b.dev --- packages/js-sdk/tests/sandbox/apiDefaults.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts index 1a0c50d800..91459cc6d7 100644 --- a/packages/js-sdk/tests/sandbox/apiDefaults.test.ts +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -1,16 +1,16 @@ 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' +import { setupMockApi } from '../mockApi' let lastCreateBody: Record | undefined let lastForkBody: Record | undefined let lastPauseBody: Record | undefined let lastConnectBody: Record | undefined -const server = setupServer( +const server = setupMockApi( http.post(apiUrl('/v2/sandboxes'), async ({ request }) => { lastCreateBody = (await request.json()) as Record return HttpResponse.json({