Skip to content

feat(sdk): add sandbox fork to JS and Python SDKs - #1554

Merged
mishushakov merged 6 commits into
mainfrom
sandbox-fork-sdk
Jul 17, 2026
Merged

mishushakov merged 6 commits into
mainfrom
sandbox-fork-sdk

Conversation

@mishushakov

@mishushakov mishushakov commented Jul 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds SDK support for the new POST /sandboxes/{sandboxID}/fork endpoint (e2b-dev/runtime#3202): checkpoint a running sandbox in place (briefly paused, snapshotted with full memory state, and resumed — its ID and expiration stay untouched) and boot count new sandboxes from that snapshot.

  • spec: adds SandboxForkRequest / SandboxForkResult schemas and the /sandboxes/{sandboxID}/fork path (mirroring the infra spec); JS and Python API clients regenerated via make codegen.
  • js-sdk: sandbox.fork(opts) instance method and Sandbox.fork(sandboxId, opts) static method. Returns Promise<Array<Sandbox | Error>> — one entry per requested fork, each either a connected Sandbox instance or an Error describing why that fork failed to start (Promise.allSettled-style, matching the per-fork results of the API). Per-fork error codes go through the same code→class mapping as other API errors (extracted from handleApiError into apiErrorFromCode), so e.g. a per-fork 429 (sandbox limit) surfaces as RateLimitError. SandboxForkOpts extends the full ConnectionOpts (like SandboxConnectOpts), so proxy, logger, apiUrl, etc. work with fork-by-ID. timeoutMs defaults to 5 minutes like create/connect; count defaults to 1 and is validated client-side (InvalidArgumentError for count < 1); a whole-request 404 maps to SandboxNotFoundError (the source sandbox is the missing resource — same semantics as pause/connect/setTimeout), carrying the API error message when present; per-fork 404 error codes map to generic NotFoundError (the missing resource is fork-internal, e.g. the snapshot).
  • python-sdk: sandbox.fork(timeout=..., count=...) / Sandbox.fork(sandbox_id, ...) and the AsyncSandbox equivalents (same @class_method_variant instance/static pattern as connect/pause), returning List[Union[Sandbox, Exception]]. Per-fork errors map through the shared api_exception_from_code (extracted from handle_api_exception). timeout is in seconds per Python SDK convention; an explicit timeout=0 is preserved. Whole-request 404 raises SandboxNotFoundException; per-fork 404 codes map to generic NotFoundException.
  • changesets: minor bumps for e2b and @e2b/python-sdk.

Usage

JS:

const sandbox = await Sandbox.create()

const [fork1, fork2] = await sandbox.fork({ count: 2, timeoutMs: 60_000 })
if (fork1 instanceof Sandbox) {
  await fork1.commands.run('echo "hello from fork"')
}

// or by ID
const forks = await Sandbox.fork(sandbox.sandboxId, { count: 2 })

Python (sync / async):

sandbox = Sandbox.create()

fork1, fork2 = sandbox.fork(count=2, timeout=60)
if isinstance(fork1, Sandbox):
    fork1.commands.run('echo "hello from fork"')

# or by ID
forks = Sandbox.fork(sandbox.sandbox_id, count=2)
sandbox = await AsyncSandbox.create()
fork1, fork2 = await sandbox.fork(count=2)

Notes

  • The JS option is named timeoutMs (milliseconds) to match SandboxOpts.timeoutMs / SandboxConnectOpts.timeoutMs; the API receives seconds via timeoutToSeconds as elsewhere.
  • Failed forks are returned as error values in the array rather than rejected promises, so a partial failure doesn't throw away the successful forks and there are no unhandled-rejection hazards. A per-fork error message includes the API error code only when the API returned one.

Test plan

  • pnpm run format, pnpm run lint, pnpm run typecheck pass at the repo root (ty diagnostics identical to baseline)
  • Offline tests pass: count < 1 → InvalidArgumentError / InvalidArgumentException in JS, Python sync, and Python async; handleApiError suite passes after the apiErrorFromCode extraction (plus a behavior-parity check of the Python handle_api_exception refactor)
  • Integration tests (single fork with FS state inheritance + independence, multi-fork with unique IDs, fork-by-ID, fork of killed sandbox → SandboxNotFoundError) are written but currently fail against prod with 404 because the fork endpoint (feat(api): add sandbox fork endpoint runtime#3202) is not deployed yet — they should pass once it lands.

🤖 Generated with Claude Code

Adds SDK support for the new POST /sandboxes/{sandboxID}/fork endpoint
(e2b-dev/runtime#3202): checkpoint a running sandbox in place and boot
`count` new sandboxes from that snapshot.

- spec: add SandboxForkRequest/SandboxForkResult schemas and the fork
  path; regenerate JS and Python API clients via make codegen
- js-sdk: Sandbox.fork(sandboxId, opts) static and sandbox.fork(opts)
  instance methods returning Promise<Array<Sandbox | SandboxError>>
  (one entry per requested fork, allSettled-style)
- python-sdk: Sandbox.fork(sandbox_id, ...) / sandbox.fork(...) for both
  sync and async SDKs returning List[Union[Sandbox, SandboxException]]
- tests: integration tests for single/multi fork, fork-by-id, killed
  sandbox, plus offline unit tests for result mapping and count
  validation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 572474f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@e2b/python-sdk Minor
e2b Minor

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

@cursor

cursor Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

PR Summary

Medium Risk
New sandbox lifecycle API with in-place checkpointing and partial-success semantics; error-mapping refactor touches shared API error handling in both SDKs.

Overview
Adds POST /sandboxes/{sandboxID}/fork to the OpenAPI spec and regenerates JS/Python clients. JS and Python gain instance and static fork APIs that checkpoint the source sandbox once and return one result per requested fork (Sandbox/AsyncSandbox or Error/Exception), with whole-request failures (e.g. missing sandbox) thrown and per-fork failures returned in the list. Shared helpers apiErrorFromCode and api_exception_from_code centralize status-to-error mapping for HTTP and embedded fork errors. Minor version changesets and integration tests cover fork behavior, multi-fork, and validation.

Reviewed by Cursor Bugbot for commit 572474f. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from ebb9229. Download artifacts from this workflow run.

JS SDK (e2b@2.34.1-sandbox-fork-sdk.0):

npm install ./e2b-2.34.1-sandbox-fork-sdk.0.tgz

CLI (@e2b/cli@2.13.4-sandbox-fork-sdk.0):

npm install ./e2b-cli-2.13.4-sandbox-fork-sdk.0.tgz

Python SDK (e2b==2.33.0+sandbox.fork.sdk):

pip install ./e2b-2.33.0+sandbox.fork.sdk-py3-none-any.whl

Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bc6d730b8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/python-sdk/e2b/sandbox_sync/sandbox_api.py Outdated
Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts Outdated
Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts

@mishushakov mishushakov left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

reviewed

Comment thread packages/js-sdk/src/sandbox/index.ts
Comment thread packages/js-sdk/src/sandbox/index.ts
Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts Outdated
Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts Outdated
Comment thread packages/js-sdk/tests/sandbox/fork.test.ts Outdated
mishushakov and others added 2 commits July 15, 2026 12:26
- map whole-request 404 to generic NotFoundError/NotFoundException
  (the 404 may be route-level, not necessarily a missing sandbox) and
  surface the API error message when present
- don't fabricate a 500 code for per-fork failures missing an error
  body; only include a code when the API returned one
- accept full connection options (proxy, logger, sandboxUrl, apiUrl,
  accessToken) in SandboxForkOpts, matching SandboxConnectOpts
- preserve an explicit timeout=0 in the Python fork paths instead of
  silently replacing it with the 300s default
- drop the mock-based JS unit test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-fork errors previously always produced a bare SandboxError /
SandboxException. Extract the status-code-to-error-class mapping out of
handleApiError / handle_api_exception into apiErrorFromCode /
api_exception_from_code and reuse it for the error objects embedded in
fork results, so e.g. a per-fork 429 (sandbox limit) surfaces as
RateLimitError / RateLimitException. Fork return types widen to
Array<Sandbox | Error> and List[Union[Sandbox, Exception]] accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f7ded9f. Configure here.

Comment thread packages/js-sdk/src/api/index.ts
Comment thread packages/python-sdk/e2b/api/__init__.py
Comment thread packages/js-sdk/tests/sandbox/fork.test.ts
mishushakov and others added 2 commits July 15, 2026 14:38
- detect the whole-request fork 404 from the response status instead of
  the parsed error body, which openapi-fetch leaves unset for empty
  bodies
- map per-fork 404 error codes to NotFoundError/NotFoundException at
  the fork call sites, matching the whole-request 404 (the shared
  apiErrorFromCode/api_exception_from_code intentionally leaves 404 to
  callers — its meaning is call-site-specific, and a global branch
  would hijack 404s from callers passing custom classes like
  BuildError/VolumeError)
- use plain test instead of the sandboxTest fixture for the
  killed-sandbox fork test, which creates its own sandbox

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@matthewlouisbrockman matthewlouisbrockman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

probably main thing we need to fix next is the footgun where when snapshotting times out it kills the sandbox; is edge case but should address this sooner than later

Comment thread packages/js-sdk/src/sandbox/sandboxApi.ts
Comment thread packages/python-sdk/e2b/sandbox_sync/sandbox_api.py Outdated
A 404 from POST /sandboxes/{id}/fork means the source sandbox was not
found — the same semantics as pause/connect/setTimeout/getInfo — so
throw SandboxNotFoundError/SandboxNotFoundException there (message
still taken from the API body when present), consistent with the
sibling sandbox-by-ID operations. Per-fork 404s stay generic
NotFoundError/NotFoundException: they refer to a resource needed to
start that fork, not the source sandbox, which would have failed the
whole request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishushakov
mishushakov enabled auto-merge (squash) July 17, 2026 09:45
@mishushakov
mishushakov merged commit 95e4dc2 into main Jul 17, 2026
30 checks passed
@mishushakov
mishushakov deleted the sandbox-fork-sdk branch July 17, 2026 09:50
mishushakov added a commit that referenced this pull request Sep 17, 2026
## Summary

Exposes `Sandbox.fork` (added to the SDKs in #1554) in the CLI as `e2b
sandbox fork` (alias `sbx fk`).

```
e2b sandbox fork <sandboxID> [-n, --count <count>] [--timeout <seconds>]
```

- Output follows `sandbox create --detach`: one new sandbox ID per line
on stdout, nothing else, so it composes in scripts.
- `count`/`timeoutMs` are only forwarded when the flags are set, so the
SDK/API defaults (1 fork, 300s) apply otherwise. `--timeout` reuses
`parseTimeout` from `create.ts` (now exported) — same 30s minimum.
- The SDK returns `Array<Sandbox | Error>` (`allSettled`-style). The
command prints every successful ID, writes each per-fork error to
stderr, and exits `1` if any fork failed.
- `NotFoundError` on the source sandbox prints `Sandbox <id> wasn't
found`, consistent with the other `sbx` commands.

Linear: SDK-380

### Usage

```sh
$ e2b sbx create base --detach
i2588449rqsrom0wxx5pu

$ e2b sbx fork i2588449rqsrom0wxx5pu -n 2 --timeout 120
iksz0w8nufepy93i6x3k9
iek4cn32hmxwmqmnkgo8n

$ for id in $(e2b sbx fork i2588449rqsrom0wxx5pu -n 3); do e2b sbx exec "$id" -- hostname; done
```

### Testing

- `packages/cli/tests/commands/sandbox/fork.test.ts` (mocked SDK):
default call, count/timeout forwarding, partial failure exit code,
not-found message, count validation.
- Live against the API: created a sandbox, wrote `/tmp/marker`, forked
with `-n 2 --timeout 120`; both forks contained the marker. Forking an
invalid ID exits 1 with the API error.


Link to Devin session:
https://app.devin.ai/sessions/a139e98ad8a04d3dbdd082ed42752a5e
Open in Devin Desktop:
https://app.devin.ai/desktop/session/a139e98ad8a04d3dbdd082ed42752a5e?variant=devin
Requested by: @mishushakov

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mish@e2b.dev <mish@e2b.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants