diff --git a/.changeset/olive-poets-hammer.md b/.changeset/olive-poets-hammer.md new file mode 100644 index 0000000000..b0a59e8ecb --- /dev/null +++ b/.changeset/olive-poets-hammer.md @@ -0,0 +1,8 @@ +--- +'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/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` 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 new file mode 100644 index 0000000000..bf3f74bd18 --- /dev/null +++ b/.changeset/quiet-desks-secure.md @@ -0,0 +1,5 @@ +--- +'@e2b/desktop-python': minor +--- + +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/.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/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..ec81fec965 100644 --- a/packages/desktop-python/e2b_desktop/main.py +++ b/packages/desktop-python/e2b_desktop/main.py @@ -222,8 +222,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, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, lifecycle: Optional[SandboxLifecycle] = None, @@ -244,8 +244,8 @@ 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 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 @@ -263,7 +263,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/src/api/schema.gen.ts b/packages/js-sdk/src/api/schema.gen.ts index 15c75cc10f..5c5668cdd4 100644 --- a/packages/js-sdk/src/api/schema.gen.ts +++ b/packages/js-sdk/src/api/schema.gen.ts @@ -46,7 +46,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: { @@ -167,7 +168,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: { @@ -1645,7 +1647,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; @@ -2114,6 +2211,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 @@ -2273,6 +2380,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/index.ts b/packages/js-sdk/src/sandbox/index.ts index fd6c50d1d1..f9964e2b16 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' @@ -75,7 +74,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 @@ -318,7 +316,7 @@ export class Sandbox extends SandboxApi { const sandboxInfo = await this.createSandbox( template, - apiOpts?.timeoutMs ?? this.defaultSandboxTimeoutMs, + apiOpts?.timeoutMs, apiOpts ) @@ -434,8 +432,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 28a2183a0d..c144863534 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' @@ -529,8 +528,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. - * - * @default true */ keepMemory?: boolean } @@ -545,16 +542,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. - * - * @default 1 */ count?: number /** * Timeout for the forked sandboxes in **milliseconds**. * Maximum time a sandbox can be kept alive is 24 hours (86_400_000 milliseconds) for Pro users and 1 hour (3_600_000 milliseconds) for Hobby users. - * - * @default 300_000 // 5 minutes */ timeoutMs?: number } @@ -606,22 +599,16 @@ 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 */ timeoutMs?: number /** - * Secure all traffic coming to the sandbox controller with auth token - * - * @default true + * @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]`. - * - * @default true */ allowInternetAccess?: boolean @@ -699,8 +686,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 @@ -758,8 +743,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). - * - * @default 'desc' */ order?: SandboxListOrder @@ -1532,7 +1515,7 @@ export class SandboxApi extends ClientFactory { }, }, body: { - memory: apiOpts?.keepMemory ?? true, + memory: apiOpts?.keepMemory, }, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) @@ -1659,7 +1642,7 @@ export class SandboxApi extends ClientFactory { protected static async createSandbox( template: string, - timeoutMs: number, + timeoutMs?: number, opts?: SandboxOpts ) { // onTimeout accepts a bare action (`'pause'` / `'kill'`) or the object form @@ -1718,14 +1701,14 @@ 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: timeoutToSeconds(timeoutMs), - secure: opts?.secure ?? true, - allow_internet_access: opts?.allowInternetAccess ?? true, + timeout: + timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), + allow_internet_access: opts?.allowInternetAccess, network: buildNetworkBody(opts?.network, iam), iam, autoPause: onTimeoutConfigured ? action === 'pause' : undefined, @@ -1747,7 +1730,7 @@ export class SandboxApi extends ClientFactory { const apiOpts = this.resolveOpts(opts) const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) - const res = await client.api.POST('/sandboxes', { + const res = await client.api.POST('/v2/sandboxes', { body, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), }) @@ -1775,14 +1758,10 @@ 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) { - throw new InvalidArgumentError('count must be at least 1') - } - const apiOpts = this.resolveOpts(opts) const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) @@ -1794,7 +1773,8 @@ export class SandboxApi extends ClientFactory { }, }, body: { - timeout: timeoutToSeconds(timeoutMs), + timeout: + timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), count, }, signal: config.getSignal(apiOpts?.requestTimeoutMs, apiOpts?.signal), @@ -1847,7 +1827,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 // A nullish value is not a choice of restore, matching every other nullish // option. Any other value outside the union never reaches the API — it is @@ -1864,14 +1844,15 @@ export class SandboxApi extends ClientFactory { const config = new ConnectionConfig(apiOpts) const client = new ApiClient(config) - const res = await client.api.POST('/sandboxes/{sandboxID}/connect', { + const res = await client.api.POST('/v2/sandboxes/{sandboxID}/connect', { params: { path: { sandboxID: sandboxId, }, }, body: { - timeout: timeoutToSeconds(timeoutMs), + timeout: + timeoutMs === undefined ? undefined : timeoutToSeconds(timeoutMs), memory: onResume === 'reboot' ? false : undefined, }, 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 5b8aba70ea..d04bb62cc7 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 minFreeDiskMb?: number } diff --git a/packages/js-sdk/src/template/index.ts b/packages/js-sdk/src/template/index.ts index 12ba6caeb6..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) ) @@ -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, minFreeDiskMb: options.minFreeDiskMb, }, config.getSignal(undefined, options.signal) diff --git a/packages/js-sdk/src/template/types.ts b/packages/js-sdk/src/template/types.ts index b608670be4..654575b5df 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. - * @default 2 */ cpuCount?: number /** * Amount of memory in MB allocated to the sandbox. - * @default 1024 */ memoryMB?: number /** 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/js-sdk/tests/sandbox/abortSignal.test.ts b/packages/js-sdk/tests/sandbox/abortSignal.test.ts index 54d0e8b0f2..e1ee32ff0a 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 new file mode 100644 index 0000000000..91459cc6d7 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/apiDefaults.test.ts @@ -0,0 +1,136 @@ +import { afterAll, afterEach, beforeAll, expect, test } from 'vitest' +import { http, HttpResponse } from 'msw' + +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 = setupMockApi( + http.post(apiUrl('/v2/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('/v2/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([ + { + 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 + lastConnectBody = undefined + server.resetHandlers() +}) + +test('Sandbox.create omits timeout 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 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, + timeoutMs: 60_000, + allowInternetAccess: false, + }) + + expect(lastCreateBody?.timeout).toBe(60) + 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) +}) + +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/js-sdk/tests/sandbox/egressProxy.test.ts b/packages/js-sdk/tests/sandbox/egressProxy.test.ts index b54a058c02..e4c137055a 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 = setupMockApi( - 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/fork.test.ts b/packages/js-sdk/tests/sandbox/fork.test.ts index b33959e4c9..4821333e16 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', @@ -98,9 +98,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/js-sdk/tests/sandbox/iam.test.ts b/packages/js-sdk/tests/sandbox/iam.test.ts index d38e262930..662a78a465 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 = setupMockApi( - 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 fb3636d6df..2368be959a 100644 --- a/packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts +++ b/packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts @@ -8,7 +8,7 @@ import { setupMockApi } from '../mockApi' let lastCreateBody: Record | undefined const server = setupMockApi( - 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 3b6541a298..bf6b6e256a 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 = setupMockApi( - 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 96985ff727..c423dcdecf 100644 --- a/packages/js-sdk/tests/sandbox/onResumeRequest.test.ts +++ b/packages/js-sdk/tests/sandbox/onResumeRequest.test.ts @@ -8,7 +8,7 @@ import { setupMockApi } from '../mockApi' let lastConnectBody: Record | undefined const server = setupMockApi( - 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 e8647031fd..3c71c8eb7f 100644 --- a/packages/js-sdk/tests/sandbox/secure.test.ts +++ b/packages/js-sdk/tests/sandbox/secure.test.ts @@ -17,12 +17,6 @@ async function expectedSignature(raw: string): Promise { } describe('secure sandbox', () => { - sandboxTest.override({ - sandboxOpts: { - secure: true, - }, - }) - sandboxTest.skipIf(isDebug)( 'test access file with signing', async ({ sandbox }) => { diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index b4d0e37a3b..a2c09b6c81 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/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/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/e2b/api/client/api/sandboxes/post_sandboxes.py b/packages/python-sdk/e2b/api/client/api/sandboxes/post_sandboxes.py index 5bce9d8b33..3dec505418 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 @@ -85,7 +85,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): @@ -116,7 +116,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): @@ -142,7 +142,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): @@ -171,7 +171,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 7ecdccf34f..8f7337d1c7 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 @@ -99,7 +99,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): @@ -133,7 +134,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): @@ -162,7 +164,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): @@ -194,7 +197,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 3c1f982e4f..8aba151880 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 @@ -86,6 +88,7 @@ "BuildLogEntry", "BuildStatusReason", "ConnectSandbox", + "ConnectSandboxV2", "DeleteTemplateTagsRequest", "Error", "GCPRegistry", @@ -98,6 +101,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 372a1c409f..f58da89a43 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -587,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. """ @@ -844,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 da6bdc1f3b..a5eac92642 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -175,8 +175,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, @@ -191,11 +191,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**. 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: 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]``). :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 @@ -227,7 +227,6 @@ async def create( timeout=timeout, metadata=metadata, envs=envs, - secure=secure, allow_internet_access=allow_internet_access, mcp=mcp, network=network, @@ -414,8 +413,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**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -453,8 +452,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**. + :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 @@ -490,8 +489,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**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -786,13 +785,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. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -802,14 +801,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. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -818,13 +817,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. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -838,7 +837,7 @@ async def pause( @overload async def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @@ -846,14 +845,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: """ @@ -1150,8 +1149,7 @@ async def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: bool, - allow_internet_access: bool, + allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -1173,10 +1171,9 @@ 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, 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 8fd0dcb3fc..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,9 +25,9 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( - ConnectSandbox, + ConnectSandboxV2, Error, - NewSandbox, + NewSandboxV2, SandboxSnapshotRequest, SandboxTimeoutRequest, SandboxForkRequest, @@ -39,7 +39,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, @@ -86,7 +85,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) :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. """ @@ -210,11 +209,10 @@ 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, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -232,24 +230,25 @@ 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, 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, + 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, ) api_client = get_api_client(config) - res = await post_sandboxes.asyncio_detailed( + res = await post_v2_sandboxes.asyncio_detailed( body=body, client=api_client, ) @@ -407,7 +406,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)) @@ -416,7 +415,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: @@ -444,21 +445,16 @@ 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: - raise InvalidArgumentException("count must be at least 1") - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) 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: @@ -532,17 +528,15 @@ async def _cls_connect( on_resume: SandboxOnResume = "restore", **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)) 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=ConnectSandbox( - 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 3a9e172f34..5dfae376fe 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -171,8 +171,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, @@ -187,11 +187,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**. 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: 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]``). :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 @@ -223,7 +223,6 @@ def create( timeout=timeout, metadata=metadata, envs=envs, - secure=secure, allow_internet_access=allow_internet_access, mcp=mcp, network=network, @@ -409,8 +408,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**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -448,8 +447,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**. + :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 @@ -485,8 +484,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**. + :param count: Number of forked sandboxes to create. :return: List with one entry per requested fork — a sandbox instance or an exception @@ -784,13 +783,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. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -800,14 +799,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. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -816,13 +815,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. :return: `True` if the sandbox got paused, `False` if the sandbox was already paused """ @@ -836,7 +835,7 @@ def pause( @overload def beta_pause( self, - keep_memory: bool = True, + keep_memory: Optional[bool] = None, **opts: Unpack[ApiParams], ) -> bool: ... @@ -844,14 +843,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: """ @@ -1146,8 +1145,7 @@ def _create( timeout: Optional[int], metadata: Optional[Dict[str, str]], envs: Optional[Dict[str, str]], - secure: bool, - allow_internet_access: bool, + allow_internet_access: Optional[bool], mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -1169,10 +1167,9 @@ 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, 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 61c668ce04..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,9 +25,9 @@ ) from e2b.api.client.api.templates import delete_templates_template_id from e2b.api.client.models import ( - ConnectSandbox, + ConnectSandboxV2, Error, - NewSandbox, + NewSandboxV2, SandboxSnapshotRequest, SandboxTimeoutRequest, SandboxForkRequest, @@ -38,7 +38,6 @@ from e2b.api.client.types import UNSET, Unset from e2b.connection_config import ApiParams, ConnectionConfig from e2b.exceptions import ( - InvalidArgumentException, NotFoundException, SandboxException, SandboxNotFoundException, @@ -85,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) :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. """ @@ -209,11 +208,10 @@ 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, mcp: Optional[McpServer] = None, network: Optional[SandboxNetworkOpts] = None, iam: Optional[SandboxIamOpts] = None, @@ -231,24 +229,25 @@ 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, 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, + 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, ) api_client = get_api_client(config) - res = post_sandboxes.sync_detailed( + res = post_v2_sandboxes.sync_detailed( body=body, client=api_client, ) @@ -347,16 +346,14 @@ def _cls_connect( on_resume: SandboxOnResume = "restore", **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( + res = post_v_2_sandboxes_sandbox_id_connect.sync_detailed( sandbox_id, client=api_client, - body=ConnectSandbox( - timeout=timeout, + body=ConnectSandboxV2( + timeout=timeout if timeout is not None else UNSET, memory=resolve_connect_memory(on_resume), ), ) @@ -402,21 +399,16 @@ 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: - raise InvalidArgumentException("count must be at least 1") - config = ConnectionConfig(logger=logger, **cls._resolve_api_params(**opts)) api_client = get_api_client(config) 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: @@ -539,7 +531,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)) @@ -548,7 +540,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 8c8733b621..8f0ffc205c 100644 --- a/packages/python-sdk/e2b/template_async/build_api.py +++ b/packages/python-sdk/e2b/template_async/build_api.py @@ -49,8 +49,8 @@ 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], min_free_disk_mb: Optional[int], ): res = await post_v3_templates.asyncio_detailed( @@ -58,8 +58,8 @@ async def request_build( 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, min_free_disk_mb=( min_free_disk_mb if min_free_disk_mb is not None else UNSET ), @@ -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 f21f07f5a5..a5f144cc4d 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -38,8 +38,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, min_free_disk_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, @@ -52,8 +52,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. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param min_free_disk_mb: Requested minimum free space after the build steps, in MiB. Growth is best effort and the filesystem is never shrunk. Omit to use the team default or set to 0 to request no growth. :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 @@ -205,8 +205,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, min_free_disk_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, @@ -219,8 +219,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. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param min_free_disk_mb: Requested minimum free space after the build steps, in MiB. Growth is best effort and the filesystem is never shrunk. Omit to use the team default or set to 0 to request no growth. :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 @@ -311,8 +311,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, min_free_disk_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, @@ -325,8 +325,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. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param min_free_disk_mb: Requested minimum free space after the build steps, in MiB. Growth is best effort and the filesystem is never shrunk. Omit to use the team default or set to 0 to request no growth. :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID @@ -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 e931c03b38..36d915e388 100644 --- a/packages/python-sdk/e2b/template_sync/build_api.py +++ b/packages/python-sdk/e2b/template_sync/build_api.py @@ -48,8 +48,8 @@ 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], min_free_disk_mb: Optional[int], ): res = post_v3_templates.sync_detailed( @@ -57,8 +57,8 @@ def request_build( 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, min_free_disk_mb=( min_free_disk_mb if min_free_disk_mb is not None else UNSET ), @@ -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 7316422f0e..d6c8b46c5b 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -38,8 +38,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, min_free_disk_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, @@ -52,8 +52,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. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param min_free_disk_mb: Requested minimum free space after the build steps, in MiB. Growth is best effort and the filesystem is never shrunk. Omit to use the team default or set to 0 to request no growth. :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 @@ -205,8 +205,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, min_free_disk_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, @@ -219,8 +219,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. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param min_free_disk_mb: Requested minimum free space after the build steps, in MiB. Growth is best effort and the filesystem is never shrunk. Omit to use the team default or set to 0 to request no growth. :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 @@ 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, min_free_disk_mb: Optional[int] = None, skip_cache: bool = False, on_build_logs: Optional[Callable[[LogEntry], None]] = None, @@ -326,8 +326,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. + :param memory_mb: Amount of memory in MB allocated to the sandbox. :param min_free_disk_mb: Requested minimum free space after the build steps, in MiB. Growth is best effort and the filesystem is never shrunk. Omit to use the team default or set to 0 to request no growth. :param skip_cache: If True, forces a complete rebuild ignoring cache :return: BuildInfo containing the template ID and build ID @@ -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/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_fork.py b/packages/python-sdk/tests/async/sandbox_async/test_fork.py index 19f79be4f4..1d2e52e0a4 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() @@ -79,8 +79,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/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/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/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) 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..1ab0b5779f --- /dev/null +++ b/packages/python-sdk/tests/shared/sandbox/test_api_defaults.py @@ -0,0 +1,230 @@ +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_v2_sandboxes, + post_v_2_sandboxes_sandbox_id_connect, + 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_v2_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_v2_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, + allow_internet_access=False, + ) + + assert body["timeout"] == 60 + 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 +): + 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, + allow_internet_access=False, + ) + + assert body["timeout"] == 60 + 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) + + 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 + + +def _sync_connect_body(monkeypatch, api_key: str, **kwargs) -> Dict[str, Any]: + request = Mock(return_value=_created_sandbox()) + monkeypatch.setattr(post_v_2_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_v_2_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 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/shared/template/test_api_defaults.py b/packages/python-sdk/tests/shared/template/test_api_defaults.py new file mode 100644 index 0000000000..c35a25f11f --- /dev/null +++ b/packages/python-sdk/tests/shared/template/test_api_defaults.py @@ -0,0 +1,72 @@ +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, min_free_disk_mb=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, min_free_disk_mb=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 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." 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_fork.py b/packages/python-sdk/tests/sync/sandbox_sync/test_fork.py index bf0486f0a0..797c1fc34b 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() @@ -76,8 +76,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) 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/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] 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( diff --git a/spec/openapi.yml b/spec/openapi.yml index 8c43992469..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 @@ -957,6 +964,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: 1 + 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: >- @@ -1028,6 +1088,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: 1 + 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: @@ -2248,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 @@ -2399,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] @@ -2436,7 +2861,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: [] @@ -2473,9 +2899,9 @@ paths: $ref: "#/components/responses/504" /v2/sandboxes: - get: - summary: List sandboxes (v2) - description: List all 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: [] @@ -2485,14 +2911,52 @@ paths: AdminTeamAuth: [] - AdminJWTAuth: [] AdminTeamAuth: [] - parameters: - - name: metadata - in: query - description: Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. - required: false - schema: - type: string - - name: state + 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) + x-api-group: list + description: List all sandboxes + tags: [sandboxes] + security: + - ApiKeyAuth: [] + - AuthProviderBearerAuth: [] + AuthProviderTeamAuth: [] + - AdminApiKeyAuth: [] + AdminTeamAuth: [] + - AdminJWTAuth: [] + AdminTeamAuth: [] + parameters: + - name: metadata + in: query + description: Metadata query used to filter the sandboxes (e.g. "user=abc&app=prod"). Each key and values must be URL encoded. + required: false + schema: + type: string + - name: state in: query description: Filter sandboxes by one or more states required: false @@ -2549,6 +3013,7 @@ paths: /sandboxes/metrics: get: summary: List sandbox metrics + x-api-group: list description: List metrics for given sandboxes tags: [sandboxes] security: @@ -2943,7 +3408,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: [] @@ -2991,6 +3457,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 @@ -3130,6 +3650,7 @@ paths: /snapshots: get: summary: List snapshots + x-api-group: list description: List all snapshots for the team tags: [snapshots] security: @@ -3217,6 +3738,7 @@ paths: /v2/templates: get: summary: List templates (v2) + x-api-group: list description: List all templates tags: [templates] security: @@ -3302,6 +3824,7 @@ paths: /templates: get: summary: List templates + x-api-group: list description: List all templates deprecated: true tags: [templates] @@ -3339,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: @@ -3681,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: @@ -4007,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: @@ -4117,6 +4643,7 @@ paths: /volumes: get: summary: List team volumes + x-api-group: list description: List all team volumes tags: [volumes] security: @@ -4237,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: @@ -4643,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