Repository navigation
Remove SDK-side defaults from API request payloads - #1749
Conversation
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
🦋 Changeset detectedLatest commit: 11d377d The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Package ArtifactsBuilt from 9674edd. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-2.50.1-devin-1787318715-remove-sdk-defaults.0.tgzCLI ( npm install ./e2b-cli-2.20.1-devin-1787318715-remove-sdk-defaults.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.8.1-devin-1787318715-remove-sdk-defaults.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.4.1-devin-1787318715-remove-sdk-defaults.0.tgzPython SDK ( pip install ./e2b-2.50.0+devin.1787318715.remove.sdk.defaults-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.10.0+devin.1787318715.remove.sdk.defaults-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.5.0+devin.1787318715.remove.sdk.defaults-py3-none-any.whl |
There was a problem hiding this comment.
TASTE.md review of the SDK-side default removal.
Checked: parity across JS / sync Python / async Python (T-1, T-2), where defaults live and how they are documented (T-47), server-owns-validation (T-52), absence-is-undefined / Optional[...] = None at the boundary (T-20), and docstring/JSDoc completeness (T-69, T-71, T-72).
The mechanics are clean and applied symmetrically across all three surfaces (UNSET in Python, omitted keys in JS), and the new tests pin both the omit and the explicit-value paths. 4 violations flagged inline, all in the documentation and validation edges rather than the payload change itself.
Not tied to a changed line:
connect/_cls_connectstill applies the SDK-side 300 s default (apiOpts?.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MSinsandboxApi.ts,timeout or SandboxBase.default_sandbox_timeoutin both Pythonsandbox_api.py). After this PRcreate/forkdefer to the API whileconnectdoes not, so the sametimeoutknob has two different "unset" behaviours. Either moveconnectover too or say in the changeset why it keeps a client default.- Several new doc lines restate the server's current value in prose ("currently enabled", "currently allowed", "currently 1", "currently a full memory snapshot"). That is the drift T-47 exists to avoid, without the machine-readable
@defaulttag that made the value greppable. If the value is worth documenting, document it as@default; if it isn't, say only that the SDK omits the field and the API decides.
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
The BYOP surface from #1688 diverged for callers that bypass the types. Python raised InvalidArgumentException on a proxy without a string address; JS rebuilt the body from the known fields, so `egressProxy` passed as a bare string sent `{}` and the caller got an API error naming a field they never left out. Mirror the guard in buildEgressProxyBody, the way buildIamBody already does for untyped token maps. Both SDKs also forwarded a null/None username or password as a JSON null, which the API rejects — `{"username": os.environ.get(...)}` on an unset variable is the way that happens. Read it as "no credentials", the same reading both already gave `egressProxy: null` itself, and normalize a null username coming back out of getInfo so SandboxEgressProxyInfo.username cannot be a null its type forbids. The get_info example published in both CHANGELOGs for 2.41.0 subscripts `info.network["egress_proxy"]`, which KeyErrors on every sandbox without a proxy — SandboxNetworkInfo is total=False and the key is only set when one is configured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oxy input" This reverts commit b984e34.
Fixture sandboxes previously inherited the SDK's 300s create default; after removing SDK-side defaults they would fall back to the API's 15s default, making long-running integration tests flaky. Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
|
check comments |
…out default, simplify order docs Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
|
resolve conflicts |
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
|
Resolved — merged |
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
secure stays in the create signatures (JS, Python sync/async, desktop-python) so existing callers don't hit an invalid-argument error, but it is ignored and never serialized into NewSandboxV2. Template.getBuildStatus no longer presets logsOffset=0; when omitted the API default applies. Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
…ct spec Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
…browser suite Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
…handle setup (#1907) ## Summary Scheduled dead-code audit of `packages/js-sdk/src`, `packages/python-sdk/e2b`, `packages/cli/src`. Tools: knip, `tsc --noUnusedLocals/--noUnusedParameters`, vulture, ruff (`ARG`/`F`), jscpd, then grep across the monorepo, tests and the docs repo. Generated code (`*.gen.ts`, `*_pb.ts`, `*_connect.ts`, `e2b/api/client*`, `e2b/volume/client`, `mcp.d.ts`) was not included. No behaviour change. ### Removed / de-duplicated - **js-sdk `DEFAULT_SANDBOX_TIMEOUT_MS`** (`src/connectionConfig.ts`): #1749 removed its last use when it dropped the SDK-side 300s default. It isn't re-exported from `index.ts` and isn't referenced anywhere in the monorepo. - **python-sdk `Filesystem._envd_api_url`** (`sandbox_sync/filesystem/filesystem.py`, `sandbox_async/filesystem/filesystem.py`): the attribute was set in `__init__` but never read. The `envd_api_url` constructor parameter stays because it is still passed to the RPC and streaming clients. - **js-sdk `Commands.connect` / `Commands.start`** (`src/sandbox/commands/index.ts`): these had the same 20-line `handleProcessStartEvent → new CommandHandle(...)` / `cleanup + handleRpcErrorWithHealthCheck` block (a jscpd clone). It now lives in one private helper: ```ts private async createHandle(events, clearStartTimeout, cleanup, opts?: Pick<CommandStartOpts, 'onStdout' | 'onStderr'>): Promise<CommandHandle> ``` - Patch changeset for `e2b` and `@e2b/python-sdk`. ### Public-API candidates deliberately left in place These have no references inside the repo but users can reach them, so removing them would be a breaking change: - python-sdk `Sandbox.default_sandbox_timeout = 300`: nothing in the SDK reads it since #1749, but it is a public class attribute and `tests/test_client.py` asserts it. - python-sdk `secure=` on `Sandbox.create` and JS `SandboxOpts.secure`: deprecated, accepted and ignored on purpose. - python-sdk `stream_idle_timeout` on sync `Filesystem.read` / `Volume.read_file`, and `request_timeout` on sync streaming `commands.connect` / `pty.create` / `pty.connect`: the docstrings say these are ignored, and they are kept so the signatures match async. - Carried over from #1811 with no change: js-sdk `Sandbox.getMcpUrl()`/`getMcpToken()`, deprecated `SandboxApi.getFullInfo()`, `ConnectionConfig.envdPort`, `TemplateBase.toJSON()`, `LogEntry.toString()`, and exported types referenced only in their own file (`Branded`, `PtyCreateOpts`, `PtyConnectOpts`, `FilesystemRequestOpts`, `FilesystemListOpts`, `WatchOpts`, `SandboxUrlOpts`, `CallableTemplate`, `DockerfileParse*`, `BasicBuildOptions`, `*Registry`, `UploadBody`, `VolumeApiPaths`, `GitHubMcpServer`). - cli: `connectSandbox`, `listSandboxes`, `listSandboxLogs`, `transformTemplateData`, token-refresh helpers, `format.ts` helpers, `User*` / `Column` types. Knip flags these because nothing imports them, but each one is used inside its own module. The superfluous `export` was left alone to keep the diff small. - Not changed: `Pty.kill` / `Pty.connect` in the js-sdk are near-clones of the `Commands` versions, but they sit in a separate class and use the `onData` PTY callback. Sharing that code would need a larger refactor than this mechanical cleanup. ### Verification - `pnpm run lint`, `pnpm run format` and `pnpm run typecheck` pass on all packages. - js-sdk vitest: `tests/sandbox/commands` plus the unit/connectionConfig projects. - python-sdk pytest: `tests/{sync,async}/sandbox_*/files` (123 tests against live sandboxes) plus the offline filesystem/client tests. Link to Devin session: https://app.devin.ai/sessions/d3d4f4a2a02d491184ca8d51b4867404 Open in Devin Desktop: https://app.devin.ai/desktop/session/d3d4f4a2a02d491184ca8d51b4867404?variant=devin Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…setup (#1941) ## Summary Scheduled dead/duplicated-code sweep of `packages/js-sdk/src`, `packages/python-sdk/e2b`, `packages/cli/src`, using knip, `tsc --noUnusedLocals/--noUnusedParameters`, vulture, ruff (`F`/`ARG`) and jscpd. Generated clients (`*.gen.ts`, `*_pb*`, `*_connect*`, `e2b/api/client`, `e2b/volume/client`) were excluded. Every candidate was then grepped across the monorepo, its tests and the docs repo. Behaviour is unchanged. Nothing was removed or renamed in the public API. This run found **no new dead code**. The tools flagged nothing in the JS SDK or CLI beyond what is already listed below. Everything vulture/ruff flagged in Python turned out to be either used or public API. This PR only fixes two duplications that were added or still exist since #1907. Per AGENTS.md, the response-mapping change is applied in JS too. ### De-duplicated - **`SandboxCreateResponse` mapping**: the same 15-line "API `Sandbox` model → `SandboxCreateResponse`" block (narrow `domain` / `envd_access_token` / `traffic_access_token` from `Unset` to `None`) was copied 6 times: `_create_sandbox`, `_cls_connect` and each `_cls_fork` result, in both `sandbox_sync/sandbox_api.py` and `sandbox_async/sandbox_api.py`. It is now one helper, `to_sandbox_create_response(sandbox)`, in the shared `e2b/sandbox/sandbox_api.py`. - **JS equivalent**: `createSandbox`, `connectSandbox` and each `forkSandbox` result in `js-sdk/src/sandbox/sandboxApi.ts` built the same object literal. They now call a module-private `toSandboxCreateResponse(sandbox)`. The internal `SandboxForkResponse` type is now `SandboxCreateResponse | Error`, and neither type is exported. - **`Commands._start` / `Commands.connect`** (sync and async): both ended with the same "read start event → `extract_start_pid` → build `CommandHandle` / on error close stream + `handle_rpc_exception_with_health`" block. It now lives in a private `_create_handle(events, action, ...)`. This is the Python version of the JS `Commands.createHandle` from #1907. The `extract_start_pid` error messages ("start process" / "connect to process") are kept. Patch changeset for `@e2b/python-sdk` and `e2b`. ### Not repeated here (already in open draft PRs) #1766 and #1873 already cover the JS/Python `uploadUrl`/`downloadUrl` signing duplication, the JS volume/secret response tails, `handleProcessStartEvent`/`handleWatchDirStartEvent`, the CLI `init`/`migrate` language list and the CLI's superfluous `export`s. Those were not duplicated here. ### Public-API candidates deliberately left in place - Python `Sandbox.default_sandbox_timeout`: nothing reads it since #1749, but it is a public class attribute and `tests/test_client.py` asserts it. - Python `secure=` on `Sandbox.create` / JS `SandboxOpts.secure`: deprecated, accepted and ignored on purpose. - Python sync `stream_idle_timeout` (`Filesystem.read`, `Volume.read_file`) and `request_timeout` (sync `commands.connect`/`_start`, `pty.create`/`connect`): the docstrings say these are ignored, and they are kept so the signatures match async. - JS `Sandbox.getMcpUrl()`/`getMcpToken()`, deprecated `SandboxApi.getFullInfo()`, `ConnectionConfig.envdPort`, `TemplateBase.toJSON()`, `LogEntry.toString()`; Python `get_mcp_url`, `ConnectionConfig.set_integration` (used by tests/integrations). - JS types that knip reports as unused exports (`Branded`, `PtyCreateOpts`, `PtyConnectOpts`, `FilesystemRequestOpts`, `FilesystemListOpts`, `WatchOpts`, `SandboxUrlOpts`, `McpServer`, `GitHubMcpServer`, `CallableTemplate`, `DockerfileParse*`, `BasicBuildOptions`, `*Registry`, `UploadBody`, `VolumeApiPaths`): they appear in public signatures and the generated SDK reference. - Python `DockerfFileFinalParserInterface`: it is the return type of the `DockerfileParserInterface.set_start_cmd` protocol and appears in the docs. ### Left as-is (not mechanical) - Python `Commands.kill` / `Pty.kill` (and the JS `Commands`/`Pty` equivalents) are near-identical, but they sit on separate public classes. - Python volume `make_dir`/`get_info`/`update_metadata`/`write_file` have similar response tails, but not identical ones: `update_metadata` skips the `VolumeError` check. Merging them would change behaviour. - `Template.build` vs `build_in_background` share signature and docs only. CLI `template delete`/`publish` share a short interactive-selection block with different prompts. ### Verification - `make lint`, `make format`, `make typecheck` (ruff + ty) on python-sdk all pass. - Offline: `tests/shared`, `tests/test_*.py`, sync/async `test_config_propagation` (734 passed). - Live against E2B: sync/async `commands/`, `test_connect`, `test_fork`, `test_create`, `test_snapshot`, `test_secure` (93 passed). - js-sdk: `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` pass. vitest `tests/sandbox/{create,connect,fork,apiDefaults,lifecyclePayload,lifecycleRequest,configPropagation,iam,onResumeRequest,snapshot-api,secure,snapshot,kill}.test.ts` pass. Link to Devin session: https://app.devin.ai/sessions/4e5922ccd81b4c6e83225fa6d158e84b Open in Devin Desktop: https://app.devin.ai/desktop/session/4e5922ccd81b4c6e83225fa6d158e84b?variant=devin <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/e2b-dev/e2b/pull/1941" target="_blank"><picture><source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-devin-review-dark.svg?v=4"><img src="https://static.devin.ai/assets/gh-devin-review-light.svg?v=4" alt="Devin Review"></picture></a> <!-- devin-review-badge-end --> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; the pause call site keeps only the 409 → `paused=False` case. The message is the API's reason followed by what to do (retry; when the reason is an unresponsive guest agent, pause with force takes the snapshot without the health check, its memory may not resume, a reboot resume recovers the disk). A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE` in e2b.exceptions, read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, read the body of a 503 and return it when its `error_code` is non-retryable, so a refusal surfaces after one request; other 503s are retried as before. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests (mocked generated client) cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the reason first and the remedy after; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. test_api_exception covers the central mapping on the code regardless of endpoint and a 503 with another code staying ServiceBusyException. The transport tests assert a coded 503 is returned after one request with its body intact and a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and pytest pass (205 tests across the four touched files).
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, read the body only for a 503 and return it when its `error_code` is non-retryable, so a refusal surfaces after one request; 429 and 502 bodies are not buffered and other 503s are retried as before. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient go through the generated client's real response parsing for 200 with forced True and False, the 204 older-API path (parsed is None) and a coded refusal. test_api_exception covers the central mapping on the code regardless of endpoint, a 503 with another code and a JSON-list 503 body staying ServiceBusyException. The transport tests assert a coded 503 is returned after one request with its body intact and a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (626 tests).
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, read the body only for a 503 whose Content-Length is known and at most 8 KiB, inside a try: a read that fails or times out (the request's read timeout bounds it per chunk) decides nothing and the 503 is retried as before, where an earlier draft let the exception escape after one request; a body carrying the code is returned after one request. 429 and 502 bodies are not buffered. The exception docstring notes that its message has no "503: " prefix (the API message verbatim). `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient go through the generated client's real response parsing for 200 with forced True and False, the 204 older-API path (parsed is None) and a coded refusal, sync and async. test_api_exception covers the central mapping on the code regardless of endpoint, a 503 with another code and a JSON-list 503 body staying ServiceBusyException. The transport tests assert a coded 503 is returned after one request with its body intact, a 503 with another code is still retried, an oversized or unknown-length body is retried without being read, and a 503 whose body read raises ReadTimeout is retried (200 after two requests), sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (632 tests).
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, read the body only for a 503 that is JSON with a known Content-Length of at most 8 KiB, inside a try: a read that fails or times out (the request's read timeout bounds it per chunk) decides nothing and the 503 is retried as before, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; a body carrying the code is returned after one request. 429 and 502 bodies are not buffered. The exception docstring notes that its message has no "503: " prefix (the API message verbatim). `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient go through the generated client's real response parsing for 200 with forced True and False, the 204 older-API path (parsed is None) and a coded refusal, sync and async. test_api_exception covers the central mapping on the code regardless of endpoint, a 503 with another code and a JSON-list 503 body staying ServiceBusyException. The transport tests assert a coded 503 is returned after one request with its body intact, a 503 with another code is still retried, an oversized, unknown-length or non-JSON body is retried without being read, the read time counts towards the delay, and a 503 whose body read raises ReadTimeout is retried (200 after two requests), sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (633 tests).
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, read the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming up to that cap through iter_bytes / aiter_bytes inside a try, so a chunked or HTTP/2 refusal without Content-Length is read too: a read that fails or times out (the request's read timeout bounds it per chunk) or exceeds the cap decides nothing and the 503 is retried as before, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; a body carrying the code is returned after one request with the bytes read as its body (encoding and length headers dropped, the content is decoded), so the caller can still read it. 429 and 502 bodies are not buffered. The exception docstring notes that its message has no "503: " prefix (the API message verbatim). `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient go through the generated client's real response parsing for 200 with forced True and False, the 204 older-API path (parsed is None) and a coded refusal, sync and async. test_api_exception covers the central mapping on the code regardless of endpoint, a 503 with another code and a JSON-list 503 body staying ServiceBusyException. The transport tests assert a coded 503 is returned after one request with its body intact, a 503 with another code is still retried, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a mixed-case Content-Type is matched by media type, the read time counts towards the delay, and a 503 whose body read raises ReadTimeout is retried (200 after two requests), sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (637 tests).
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, read the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming up to that cap through iter_bytes / aiter_bytes inside a try, so a chunked or HTTP/2 refusal without Content-Length is read too: a read that fails or times out (the request's read timeout bounds it per chunk) or exceeds the cap decides nothing and the 503 is retried as before, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; with no retry budget left the 503 is returned as the outcome, as on the JavaScript side, carrying the body read in full or none when the read failed or passed the cap (its stream is consumed; a bodiless 503 is still a busy error); a body carrying the code is returned after one request with the bytes read as its body (encoding and length headers dropped, the content is decoded), so the caller can still read it. 429 and 502 bodies are not buffered. The exception docstring notes that its message has no "503: " prefix (the API message verbatim). `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient go through the generated client's real response parsing for 200 with forced True and False, the 204 older-API path (parsed is None) and a coded refusal, sync and async. test_api_exception covers the central mapping on the code regardless of endpoint, a 503 with another code and a JSON-list 503 body staying ServiceBusyException. The transport tests assert a coded 503 is returned after one request with its body intact, a 503 with another code is still retried, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a mixed-case Content-Type is matched by media type, the read time counts towards the delay, a failed read with no budget left returns the bodiless 503 (sync and async), and a 503 whose body read raises ReadTimeout is retried (200 after two requests), sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (639 tests).
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, peek at the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming it through iter_bytes / aiter_bytes under the existing try with that cap and a wall-clock budget of min(max(delay, 1 s), remaining request budget) measured with the transport's monotonic clock across chunks (each chunk is also bounded by the request's read timeout as pyqwest applies it, none when the request timeout is disabled; a chunk whose wait began within the budget is kept, only waiting for more past it gives up), so a chunked or HTTP/2 refusal without Content-Length is read and a trickling body cannot hold the loop: a read that fails, times out, exceeds the cap or outlives the budget decides nothing and the 503 is closed and retried, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; with no retry budget left, a body read in full is returned as the outcome, a failed read re-raises its error and a partial read raises the retry-budget timeout, never a fabricated body (the API client parses every 503 body as JSON). A refusal is returned after one request as a response rebuilt from the bytes read: the transport's response carries no request yet (httpx attaches it after the transport returns; pyqwest's arrive without one), so the handled request is attached explicitly, Content-Encoding, Transfer-Encoding and Content-Length are dropped (the content is decoded and complete; httpx sets the length), and only plain extensions such as http_version are copied, never pyqwest's get_trailers bound to the closed native response. The earlier draft read response.request and raised RuntimeError on the production transport for every refused pause; the pause tests missed it because their fakes attached request= and bypassed the retry layer. `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient now run through RetryableTransport / AsyncRetryableTransport with responses built as httpx and pyqwest hand them over (no request=): the refusal-through-real-parsing tests fail on the earlier draft with RuntimeError and pass here; they also cover 200 with forced True and False, the 204 older-API path (parsed is None) and, through Sandbox.pause, a refusal body read that times out with no budget left surfacing as the timeout. The transport fakes lost their request= too. The transport tests assert a coded 503 is returned after one request with its body intact, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a trickling body is retried once the peek budget is spent (sync and async), a complete but slow body read with no budget left is returned with its body, a failed read with no budget left re-raises (sync and async), a mixed-case Content-Type is matched by media type, the read time counts towards the delay, the rebuilt refusal drops the transfer headers and native extensions and carries the request, a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (644 tests).
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, peek at the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming it through iter_bytes / aiter_bytes under the existing try with that cap and a wall-clock budget of min(max(delay, 1 s), remaining request budget) measured with the transport's monotonic clock across chunks (each chunk is also bounded by the request's read timeout as pyqwest applies it, none when the request timeout is disabled; a chunk whose wait began within the budget is kept, only waiting for more past it gives up), so a chunked or HTTP/2 refusal without Content-Length is read and a trickling body cannot hold the loop: a read that fails, times out, exceeds the cap or outlives the budget decides nothing and the 503 is closed and retried, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; with no retry budget left, a body read in full is returned as the outcome, one past the byte cap is handed on whole (the peeked chunks followed by the rest of the stream, as before the peek existed and as the JavaScript side does), a failed read re-raises its error and a read cut off by the wall-clock budget raises the retry-budget timeout, never a fabricated body (the API client parses every 503 body as JSON). The peek releases the body generator explicitly when it gives up. A refusal is returned after one request as a response rebuilt from the bytes read: the transport's response carries no request yet (httpx attaches it after the transport returns; pyqwest's arrive without one), so the handled request is attached explicitly, Content-Encoding, Transfer-Encoding and Content-Length are dropped (the content is decoded and complete; httpx sets the length), and only plain extensions such as http_version are copied, never pyqwest's get_trailers bound to the closed native response. The earlier draft read response.request and raised RuntimeError on the production transport for every refused pause; the pause tests missed it because their fakes attached request= and bypassed the retry layer. `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient now run through RetryableTransport / AsyncRetryableTransport with responses built as httpx and pyqwest hand them over (no request=): the refusal-through-real-parsing tests fail on the earlier draft with RuntimeError and pass here; they also cover 200 with forced True and False, the 204 older-API path (parsed is None) and, through Sandbox.pause, a refusal body read that times out with no budget left surfacing as the timeout. The transport fakes lost their request= too. The transport tests assert a coded 503 is returned after one request with its body intact, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a trickling body is retried once the peek budget is spent (sync and async), a complete but slow body read with no budget left is returned with its body, an over-cap body with no budget left surfaces the 503 whole (sync and async), a read cut off by the budget with no budget left is the retry-budget timeout (sync and async, and through Sandbox.pause), a failed read with no budget left re-raises (sync and async), a mixed-case Content-Type is matched by media type, the read time counts towards the delay, the rebuilt refusal drops the transfer headers and native extensions and carries the request, a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (649 tests); `make lint` exits 0 and a second `ruff format` leaves every file unchanged.
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, peek at the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming it through iter_bytes / aiter_bytes under the existing try with that cap and a wall-clock budget of min(max(delay, 1 s), remaining request budget) measured with the transport's monotonic clock across chunks (each chunk is also bounded by the request's read timeout as pyqwest applies it, none when the request timeout is disabled; a chunk whose wait began within the budget is kept, only waiting for more past it gives up), so a chunked or HTTP/2 refusal without Content-Length is read and a trickling body cannot hold the loop: a read that fails, times out, exceeds the cap or outlives the budget decides nothing and the 503 is closed and retried, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; with no retry budget left, the 503 is the outcome as on the JavaScript side: a body read in full is returned as is, one stopped at the byte cap or cut off by the peek budget is handed on whole (the peeked chunks first, then the rest of the stream, through a resumed stream whose close releases the body iterator and the original response), and a failed read re-raises its error, never a fabricated body (the API client parses every 503 body as JSON). While budget remains, the iterator a stopped peek left is closed explicitly before the retry, so nothing is left to garbage collection; the rebuilt response is constructed once, with httpx taking the stream over the content when both are given. A refusal is returned after one request as a response rebuilt from the bytes read: the transport's response carries no request yet (httpx attaches it after the transport returns; pyqwest's arrive without one), so the handled request is attached explicitly, Content-Encoding, Transfer-Encoding and Content-Length are dropped (the content is decoded and complete; httpx sets the length), and only plain extensions such as http_version are copied, never pyqwest's get_trailers bound to the closed native response. The earlier draft read response.request and raised RuntimeError on the production transport for every refused pause; the pause tests missed it because their fakes attached request= and bypassed the retry layer. `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient now run through RetryableTransport / AsyncRetryableTransport with responses built as httpx and pyqwest hand them over (no request=): the refusal-through-real-parsing tests fail on the earlier draft with RuntimeError and pass here; they also cover 200 with forced True and False, the 204 older-API path (parsed is None) and, through Sandbox.pause, a refusal body read that times out with no budget left surfacing as the timeout. The transport fakes lost their request= too. The transport tests assert a coded 503 is returned after one request with its body intact, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a trickling body is retried once the peek budget is spent (sync and async), a complete but slow body read with no budget left is returned with its body (sync and async), an over-cap body with no budget left surfaces the 503 whole with its third chunk read only by the resumed stream and no length or transfer-encoding header (sync and async, and through Sandbox.pause as SandboxPauseRefusedException), a read cut off by the budget with no budget left is handed on whole too (sync and async, and through Sandbox.pause as the typed refusal), a failed read with no budget left re-raises (sync and async), a mixed-case Content-Type is matched by media type, the read time counts towards the delay, the rebuilt refusal drops the transfer headers and native extensions and carries the request, a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (651 tests); `make lint` exits 0 and a second `ruff format` leaves every file unchanged.
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, peek at the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming it through iter_bytes / aiter_bytes under the existing try with that cap and a wall-clock budget of min(max(delay, 1 s), remaining request budget) measured with the transport's monotonic clock across chunks (each chunk is also bounded by the request's read timeout as pyqwest applies it, none when the request timeout is disabled; a chunk whose wait began within the budget is kept, only waiting for more past it gives up), so a chunked or HTTP/2 refusal without Content-Length is read and a trickling body cannot hold the loop: a read that fails, times out, exceeds the cap or outlives the budget decides nothing and the 503 is closed and retried, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; with no retry budget left, the 503 is the outcome as on the JavaScript side: a body read in full is returned as is, one stopped at the byte cap or cut off by the peek budget is handed on whole (the peeked chunks first, then the rest of the stream, through a resumed stream whose close releases the body iterator and the original response), and a failed read re-raises its error, never a fabricated body (the API client parses every 503 body as JSON). While budget remains, the iterator a stopped peek left is closed explicitly before the retry, so nothing is left to garbage collection; the rebuilt response is constructed once, with httpx taking the stream over the content when both are given. A body nested too deep for json.loads (RecursionError, not ValueError) is not a refusal; a BaseException out of the peek (cancellation, KeyboardInterrupt) closes the inner response before propagating, so the connection is not leaked; the resumed stream closes the inner response in a finally and tolerates a body generator a reader is still running (left to that reader). A refusal is returned after one request as a response rebuilt from the bytes read: the transport's response carries no request yet (httpx attaches it after the transport returns; pyqwest's arrive without one), so the handled request is attached explicitly, Content-Encoding, Transfer-Encoding and Content-Length are dropped (the content is decoded and complete; httpx sets the length), and only plain extensions such as http_version are copied, never pyqwest's get_trailers bound to the closed native response. The earlier draft read response.request and raised RuntimeError on the production transport for every refused pause; the pause tests missed it because their fakes attached request= and bypassed the retry layer. `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient now run through RetryableTransport / AsyncRetryableTransport with responses built as httpx and pyqwest hand them over (no request=): the refusal-through-real-parsing tests fail on the earlier draft with RuntimeError and pass here; they also cover 200 with forced True and False, the 204 older-API path (parsed is None) and, through Sandbox.pause, a refusal body read that times out with no budget left surfacing as the timeout. The transport fakes lost their request= too. The transport tests assert a coded 503 is returned after one request with its body intact, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a trickling body is retried once the peek budget is spent (sync and async), a complete but slow body read with no budget left is returned with its body (sync and async), an over-cap body with no budget left surfaces the 503 whole with its third chunk read only by the resumed stream and no length or transfer-encoding header (sync and async, and through Sandbox.pause as SandboxPauseRefusedException), a read cut off by the budget with no budget left is handed on whole too (sync and async, and through Sandbox.pause as the typed refusal), a 3000-deep JSON body is retried (sync and async), cancelling the peek while it awaits a chunk closes the inner response exactly once, and closing the handed-on response while a reader awaits its live tail closes the inner exactly once, a failed read with no budget left re-raises (sync and async), a mixed-case Content-Type is matched by media type, the read time counts towards the delay, the rebuilt refusal drops the transfer headers and native extensions and carries the request, a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (655 tests); `make lint` exits 0 and a second `ruff format` leaves every file unchanged.
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, peek at the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming it through iter_bytes / aiter_bytes under the existing try with that cap and a wall-clock budget of min(max(delay, 1 s), remaining request budget) measured with the transport's monotonic clock across chunks (each chunk is also bounded by the request's read timeout as pyqwest applies it, none when the request timeout is disabled; a chunk whose wait began within the budget is kept, only waiting for more past it gives up), so a chunked or HTTP/2 refusal without Content-Length is read and a trickling body cannot hold the loop: a read that fails, times out, exceeds the cap or outlives the budget decides nothing and the 503 is closed and retried, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; with no retry budget left, the 503 is the outcome as on the JavaScript side: a body read in full is returned as is, one stopped at the byte cap or cut off by the peek budget is handed on whole (the peeked chunks first, then the rest of the stream, through a resumed stream whose close releases the body iterator and the original response), and a failed read re-raises its error, never a fabricated body (the API client parses every 503 body as JSON). While budget remains, the iterator a stopped peek left is closed explicitly before the retry, so nothing is left to garbage collection; the rebuilt response is constructed once, with httpx taking the stream over the content when both are given. A body nested too deep for json.loads (RecursionError, not ValueError) is not a refusal; a BaseException out of the peek (cancellation, KeyboardInterrupt) closes the inner response before propagating, so the connection is not leaked; the resumed stream closes the inner response in a finally and tolerates a body generator a reader is still running (RuntimeError for an async generator, ValueError for a sync one; left to that reader). An ``error_code`` that is not a string (a list or object from a proxy's or a buggy server's body) is not a refusal rather than a TypeError out of the transport. A peek cut off by the budget on what turns out to be the last chunk already holds the whole refusal and returns it without a further retry. The handed-on stream's live tail is bounded by the request deadline: a chunk whose wait would begin past it is not waited for, the inner response is closed and ReadTimeout is raised, so the API client's read ends at the deadline instead of running on chunk by chunk past the request timeout. Because the peek's time counts against the retry delay, a budget exhausted after a peek means the clock is at the deadline, so in practice an over-cap or cut-off body handed on at that point yields what the peek buffered and then the timeout; a body read in full, or buffered chunks that already form the refusal, are returned as content and never reach the tail. The two readers' cross-close tests therefore build the resumed streams directly with the deadline ahead. A refusal is returned after one request as a response rebuilt from the bytes read: the transport's response carries no request yet (httpx attaches it after the transport returns; pyqwest's arrive without one), so the handled request is attached explicitly, Content-Encoding, Transfer-Encoding and Content-Length are dropped (the content is decoded and complete; httpx sets the length), and only plain extensions such as http_version are copied, never pyqwest's get_trailers bound to the closed native response. The earlier draft read response.request and raised RuntimeError on the production transport for every refused pause; the pause tests missed it because their fakes attached request= and bypassed the retry layer. `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient now run through RetryableTransport / AsyncRetryableTransport with responses built as httpx and pyqwest hand them over (no request=): the refusal-through-real-parsing tests fail on the earlier draft with RuntimeError and pass here; they also cover 200 with forced True and False, the 204 older-API path (parsed is None) and, through Sandbox.pause, a refusal body read that times out with no budget left surfacing as the timeout. The transport fakes lost their request= too. The transport tests assert a coded 503 is returned after one request with its body intact, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a trickling body is retried once the peek budget is spent (sync and async), a complete but slow body read with no budget left is returned with its body (sync and async), an over-cap body with no budget left surfaces the 503 whole with its third chunk read only by the resumed stream and no length or transfer-encoding header (sync and async, and through Sandbox.pause as SandboxPauseRefusedException), a read cut off by the budget with no budget left is handed on whole too (sync and async, and through Sandbox.pause as the typed refusal), a 3000-deep JSON body is retried (sync and async), cancelling the peek while it awaits a chunk closes the inner response exactly once, and closing the handed-on response while a reader awaits its live tail closes the inner exactly once (an asyncio task, and a thread inside iter_bytes), a list-valued error_code is retried (sync and async), and a cut-off peek whose buffered chunks form the refusal returns it without retrying and without pulling the tail (sync and async, and through Sandbox.pause), a handed-on tail is cut at the deadline with ReadTimeout and the inner closed once (transport sync and async, and through Sandbox.pause for both the cut-off and the over-cap case), a failed read with no budget left re-raises (sync and async), a mixed-case Content-Type is matched by media type, the read time counts towards the delay, the rebuilt refusal drops the transfer headers and native extensions and carries the request, a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (664 tests); `make lint` exits 0 and a second `ruff format` leaves every file unchanged.
…; SandboxPauseRefusedException for a refused pause What changed: `Sandbox.pause()` and `AsyncSandbox.pause()` (instance and class forms, and the deprecated `beta_pause` forms, matching the JavaScript `betaPause`) take `force: Optional[bool] = None`, sent in the pause request body when set and omitted otherwise, like `mode`, and return the `SandboxPauseResult` dataclass (`paused`, `forced`), exported from `e2b` under its own section, instead of a bool: 200 gives `paused=True, forced=body.forced`, 409 (already paused) `paused=False, forced=False`, and a 204 from an older API `paused=True, forced=False`. A 503 whose body carries `error_code: "sandbox_pause_refused"` is raised as the new `SandboxPauseRefusedException` (exported; `status_code` 503, the `error_code` attribute, subclass of `ServiceBusyException` so existing handlers keep working). The mapping lives in `api_exception_from_code` / `handle_api_exception`, keyed on the body's `error_code` and passed through for errors embedded in fork results too, so any endpoint answering the code gets the typed exception; a non-object JSON body (a list) carries neither field and maps as before; the pause call site keeps only the 409 → `paused=False` case. The message is the API's verbatim: belt's messages already name the reason and what to do for it, so the SDK appends nothing and the general guidance lives in the class docstring. A 503 without the code stays a `ServiceBusyException`, 404 raises `SandboxNotFoundException`, and 400 (force on a filesystem-only pause) is the generic `SandboxException` carrying the status. The code string is one constant, `SANDBOX_PAUSE_REFUSED_ERROR_CODE`, exported from `e2b` and read by the exception class, the mapping and the retry predicate. `RetryableTransport` / `AsyncRetryableTransport`, which retry 503s the same way the JavaScript transport does, peek at the body only for a 503 whose media type is JSON (lower-cased, parameters dropped) and that is not declared larger than 8 KiB, streaming it through iter_bytes / aiter_bytes under the existing try with that cap and a wall-clock budget of min(max(delay, 1 s), remaining request budget) measured with the transport's monotonic clock across chunks (each chunk is also bounded by the request's read timeout as pyqwest applies it, none when the request timeout is disabled; a chunk whose wait began within the budget is kept, only waiting for more past it gives up), so a chunked or HTTP/2 refusal without Content-Length is read and a trickling body cannot hold the loop: a read that fails, times out, exceeds the cap or outlives the budget decides nothing and the 503 is closed and retried, where an earlier draft let the exception escape after one request; the time the read took is subtracted from the following sleep and the deadline re-checked, as the JavaScript side does; with no retry budget left, the 503 is the outcome as on the JavaScript side: a body read in full is returned as is, one stopped at the byte cap or cut off by the peek budget is handed on whole (the peeked chunks first, then the rest of the stream, through a resumed stream whose close releases the body iterator and the original response), and a failed read re-raises its error, never a fabricated body (the API client parses every 503 body as JSON). While budget remains, the iterator a stopped peek left is closed explicitly before the retry, so nothing is left to garbage collection; the rebuilt response is constructed once, with httpx taking the stream over the content when both are given. A body nested too deep for json.loads (RecursionError, not ValueError) is not a refusal; a BaseException out of the peek (cancellation, KeyboardInterrupt) closes the inner response before propagating, so the connection is not leaked; the resumed stream closes the inner response in a finally and tolerates a body generator a reader is still running (RuntimeError for an async generator, ValueError for a sync one; left to that reader). An ``error_code`` that is not a string (a list or object from a proxy's or a buggy server's body) is not a refusal rather than a TypeError out of the transport. A peek cut off by the budget on what turns out to be the last chunk holds one complete JSON document: it is treated as the body read in full (a refusal is returned at once; any other complete API error follows the normal path, retried while budget remains and returned whole when it is exhausted) and the tail is left unread; only a body that does not yet parse is handed on. Without this a busy 503 whose body completed in the cut-off chunk surfaced as ReadTimeout instead of ServiceBusyException. The handed-on stream's live tail is bounded by the request deadline: a chunk whose wait would begin past it is not waited for, the inner response is closed and ReadTimeout is raised, so the API client's read ends at the deadline instead of running on chunk by chunk past the request timeout. Because the peek's time counts against the retry delay, a budget exhausted after a peek means the clock is at the deadline, so in practice an over-cap or cut-off body handed on at that point yields what the peek buffered and then the timeout; a body read in full, or buffered chunks that already form the refusal, are returned as content and never reach the tail. The two readers' cross-close tests therefore build the resumed streams directly with the deadline ahead. A refusal is returned after one request as a response rebuilt from the bytes read: the transport's response carries no request yet (httpx attaches it after the transport returns; pyqwest's arrive without one), so the handled request is attached explicitly, Content-Encoding, Transfer-Encoding and Content-Length are dropped (the content is decoded and complete; httpx sets the length), and only plain extensions such as http_version are copied, never pyqwest's get_trailers bound to the closed native response. The earlier draft read response.request and raised RuntimeError on the production transport for every refused pause; the pause tests missed it because their fakes attached request= and bypassed the retry layer. `@e2b/code-interpreter`, `@e2b/desktop`, `@e2b/code-interpreter-python` and `@e2b/desktop-python` take the same major in the changeset: the JavaScript two re-export `e2b` and subclass its Sandbox, the Python two subclass the Python Sandbox, and `updateInternalDependencies: patch` would otherwise ship the breaking `pause()` in a patch of theirs. The two Python dependents' `e2b` bound is raised from `<3.0.0` to `<4.0.0` so they keep resolving against e2b 3.x (their locks are unchanged: they resolve the workspace e2b). `pnpm changeset status --verbose` lists exactly those six majors and the CLI minor. Docstrings on every overload and on the exception describe the refusal as the node refusing the pause with the sandbox kept running and the reason in the message, and present force as the answer for the unresponsive-guest reason only, with the reboot caveat. The two live tests that asserted the old bool read `.paused`. The changeset is major for both SDKs (minor for the CLI) with the migration note. Why: the API now refuses a memory pause when the sandbox guest agent is unresponsive and keeps the sandbox running; callers need to tell that apart from a busy node (retry) and need the override to snapshot anyway, accepting that the memory may not resume (a resume with `on_resume="reboot"` recovers the disk state). The result object is a breaking change, decided by the operator: `forced` is informative, not an echo of the option (the API reports `forced=False` when a forced request joined a normal pause already in flight). `force` is `Optional[bool]` rather than `bool = False` so an unset flag is omitted from the payload (#1749: no SDK-side defaults in API request bodies). The generated 200 model is imported as `SandboxPauseResultAPI` to keep the public dataclass name. Verification: unit tests with the generated client mocked cover force omitted / true / false / with mode in the body; 200 with forced False and True; forced reported False for a forced request that joined a normal pause; 204 compatibility; 409; the coded refusal raising SandboxPauseRefusedException (a ServiceBusyException) with the API message verbatim; the busy-node refusal; a 503 with no error_code staying ServiceBusyException; 400 carrying the status; 404, for sync and async. Tests over an httpx.MockTransport-backed ApiClient / AsyncApiClient now run through RetryableTransport / AsyncRetryableTransport with responses built as httpx and pyqwest hand them over (no request=): the refusal-through-real-parsing tests fail on the earlier draft with RuntimeError and pass here; they also cover 200 with forced True and False, the 204 older-API path (parsed is None) and, through Sandbox.pause, a refusal body read that times out with no budget left surfacing as the timeout. The transport fakes lost their request= too. The transport tests assert a coded 503 is returned after one request with its body intact, a chunked refusal without Content-Length is returned after one request with its body readable (sync and async), an oversized chunked body and a non-JSON body are retried, a trickling body is retried once the peek budget is spent (sync and async), a complete but slow body read with no budget left is returned with its body (sync and async), an over-cap body with no budget left surfaces the 503 whole with its third chunk read only by the resumed stream and no length or transfer-encoding header (sync and async, and through Sandbox.pause as SandboxPauseRefusedException), a read cut off by the budget with no budget left is handed on whole too (sync and async, and through Sandbox.pause as the typed refusal), a 3000-deep JSON body is retried (sync and async), cancelling the peek while it awaits a chunk closes the inner response exactly once, and closing the handed-on response while a reader awaits its live tail closes the inner exactly once (an asyncio task, and a thread inside iter_bytes), a list-valued error_code is retried (sync and async), and a cut-off peek whose buffered chunks form the refusal returns it without retrying and without pulling the tail (sync and async, and through Sandbox.pause and AsyncSandbox.pause), a busy 503 whose complete body lands in the cut-off chunk is retried while budget remains and surfaces as ServiceBusyException through Sandbox.pause and AsyncSandbox.pause when none is left, the over-cap deadline cut is covered through AsyncSandbox.pause too, a handed-on tail is cut at the deadline with ReadTimeout and the inner closed once (transport sync and async, and through Sandbox.pause for both the cut-off and the over-cap case), a failed read with no budget left re-raises (sync and async), a mixed-case Content-Type is matched by media type, the read time counts towards the delay, the rebuilt refusal drops the transfer headers and native extensions and carries the request, a 503 with another code is still retried, sync and async. `ruff check`, `ruff format --check`, `ty check` and the non-live pytest suite pass (670 tests); `make lint` exits 0 and a second `ruff format` leaves every file unchanged.
Summary
Remove SDK-side defaults from API request payloads (JS + Python sync/async) so omitted options are absent from requests and the API defaults apply. Explicit values — including
false/0— are still serialized unchanged.Fields no longer preset when omitted:
timeout(was 300s),secure(wastrue),allow_internet_accesstimeout(was 300s)POST /sandboxes/{id}/fork:timeout(was 300s),count(was1)POST /sandboxes/{id}/pause:memory(wastrue)cpuCount/memoryMB(were 2 / 1024)Create/connect now use the v2 endpoints from belt #3425 (
spec/openapi.ymlsynced via thespec/runtime-refpin; JS + Python clients regenerated with the pinned generators):Consequently the
secureoption onSandbox.createis deprecated: it stays in the signature (JSSandboxOpts.secure, Python/desktopsecure=) so existing callers keep working, but it is ignored — every sandbox is secured, so the SDK has nothing to send. Downstream packages follow:e2b-desktop(Python) no longer presetsallow_internet_access=True; the code-interpreter test fixtures stop passingsecure. The PythonConnectSandboxBodyshim and the JS cast that worked around the required v1 connecttimeoutare gone too — the generated v2 models already make it optional.Template.getBuildStatus/Template.get_build_statusalso stop presettinglogsOffset: 0— omitted, the query param is left out and the API default applies.Per updated TASTE T-52 (client validation must not mirror backend business rules), the client-side fork
count >= 1pre-validation is also removed — an invalid count now surfaces as the API's own 400 error instead of a client-sideInvalidArgumentError/InvalidArgumentException.Belt #3425 is merged and exported:
spec/runtime-refis bumped to the runtime commit carrying the v2 routes, andmake codegenat that pin reproduces the tracked spec and generated clients byte-for-byte. The live integration suites stay red until the API deploy with the v2 routes reaches staging/production (they 404 on/v2/sandboxesuntil then).Exact-request tests added/updated in
packages/js-sdk/tests/sandbox/apiDefaults.test.ts,packages/js-sdk/tests/template/apiDefaults.test.ts, andpackages/python-sdk/tests/shared/{sandbox,template}/test_api_defaults.py; the msw/monkeypatch-mocked suites now intercept the v2 routes.Usage stays the same; only the outgoing payloads/routes change:
Linear: SDK-346
Link to Devin session: https://app.devin.ai/sessions/1921bb3818604f3a95b0db1272cff3cd
Open in Devin Desktop: https://app.devin.ai/desktop/session/1921bb3818604f3a95b0db1272cff3cd?variant=devin
Requested by: @mishushakov