Skip to content

feat(sdk): sidecars on sandbox create, info and list - #1872

Draft
tomassrnka wants to merge 17 commits into
mainfrom
impl-d039/sdk-sidecars
Draft

feat(sdk): sidecars on sandbox create, info and list#1872
tomassrnka wants to merge 17 commits into
mainfrom
impl-d039/sdk-sidecars

Conversation

@tomassrnka

Copy link
Copy Markdown
Member

Summary

Adds the sidecar plane (DES-039 / IMPL-D039 W6) to the JS SDK, the Python SDK and the CLI.

  • Create: sidecars on Sandbox.create (JS SandboxOpts.sidecars: SidecarAttachment[]; Python keyword-only sidecars: Optional[List[SidecarAttachment]] on sync and async create). Each attachment names a catalog entry — iron-proxy (proxy role), valkey (Valkey, Redis-compatible cache), sqlite (libsql-server over HTTP), iroh (peer-to-peer tunnel) — with optional version, entry-specific config, and secrets slots holding ${e2b.secrets.<name>} references. The body is built from the known keys only; an empty list, null/None or an omitted option leaves the field out.
  • Info and list: SandboxInfo.sidecars (JS SidecarInfo[], Python List[SidecarInfo]) from inspect and list — entry, version, role, class, state, name, address, ports, last error; empty when the API sends none. state is an open string on the wire; the SDK unions document the known values.
  • Errors: sidecar_* semantic codes read from the API Error.error_code, case-sensitive. The 400 codes (sidecar_unknown_entry, sidecar_deprecated_entry, sidecar_limit, sidecar_one_proxy, sidecar_config_invalid, sidecar_secret_missing, sidecar_rule_collision, sidecar_egress_conflict, sidecar_flag_off) surface as InvalidArgumentError / InvalidArgumentException; sidecar_failed (create), sidecar_version_unavailable (409, resume) and sidecar_snapshot_mismatch (500, resume) stay SandboxError / SandboxException. All keep the code and the API message and carry the HTTP status. Applied on create, connect/resume and network update.
  • CLI: e2b sandbox list gains a SIDECARS column (entry:state); e2b sandbox info prints a sidecar table (entry, version, role, class, state, name, address, ports, last error truncated to 60 chars).
  • Spec: SidecarAttachment, SidecarInfo, NewSandbox.sidecars (maxItems 4), sidecars on Sandbox, SandboxDetail and ListedSandbox; both clients regenerated (pnpm -C packages/js-sdk generate:api, uv run make generate-api).

Usage:

const sandbox = await Sandbox.create({
  sidecars: [
    { entry: 'valkey' },
    { entry: 'iron-proxy', secrets: { upstream: '${e2b.secrets.openai-key}' } },
  ],
})
const { sidecars } = await sandbox.getInfo()
sandbox = Sandbox.create(sidecars=[{"entry": "valkey"}, {"entry": "sqlite"}])
for sidecar in sandbox.get_info().sidecars:
    print(sidecar.entry, sidecar.state, sidecar.name)

Changesets: e2b minor, @e2b/python-sdk minor, @e2b/cli minor.

Verification

Run on this branch (Node v22.23.2, pnpm 10.34.5, uv 0.12.13, Python 3.10.21):

Gate Command Result
lint (as lint.yml) pnpm install --frozen-lockfile && (cd packages/python-sdk && uv sync --locked) && pnpm run lint && pnpm run format && git status --porcelain green, porcelain empty
typecheck (as typecheck.yml) pnpm run typecheck green (tsc per JS package, ty check per Python package)
js-sdk unit pnpm -C packages/js-sdk build && npx vitest run --project unit tests/sandbox/sidecars.test.ts tests/sandbox/egressProxy.test.ts tests/api/handleApiError.test.ts 54 passed (26 in sidecars.test.ts, msw-mocked)
js-sdk full pnpm -C packages/js-sdk test 513 passed; 254 failed — the identical set to main at 67c2e07 with no E2B_API_KEY (integration suites and the chromium project)
python-sdk unit cd packages/python-sdk && uv run ruff check . && uv run ruff format --check . && uv run ty check && uv run pytest -q tests/shared tests/test_api_exception.py green; 48 in tests/shared/sandbox/test_sidecars.py
python-sdk full uv build && uv run pytest --numprocesses=4 tests 844 passed; every failure/error is httpx.ConnectError in the sync/async integration trees (no API access)
cli pnpm -C packages/cli build && pnpm -C packages/cli test 125 passed; template/create.test.ts throws at beforeAll for the missing E2B_API_KEY
cli targeted npx vitest run tests/commands/sandbox/info.test.ts tests/commands/sandbox/list.test.ts tests/utils/table.test.ts 17 passed

Not run here: test:bun, test:deno, test:cf (runtimes not installed) and generated_files (see below).

Risk & rollout

  • spec/openapi.yml in this repo is a Copybara mirror of e2b-dev/runtime at spec/runtime-ref. This PR edits the mirror directly, so the generated_files check stays red until the runtime mirror carries the same schema (landing in belt first) and spec/runtime-ref is bumped — at which point the regenerated clients must be byte-identical to what is committed here. Land order: belt → runtime mirror → pin bump → this PR.
  • Wire-compatible additions only: every new field is optional on read and omitted from requests unless set; a sandbox without sidecars serialises exactly as before. SidecarInfo.state is an open string so a state a newer API adds cannot break get_info() / list().
  • Python create() keeps its positional parameters: sidecars sits after logger and is keyword-only.
  • Requires the team's sandbox-sidecars feature server-side; without it the API answers sidecar_flag_off and nothing else in the create path changes.

Follow-ups

  • Python SidecarInfo.role / class_ are typed as closed Literal unions mirroring the generated enums; if the API ever widens them, the generated SidecarInfoRole / SidecarInfoClass enums raise on parse like state used to. Open them the same way if that becomes a possibility.
  • The JS SDK validates the shape of sidecars (array of objects with a string entry) but not the secrets values (Python checks str→str); the API rejects malformed references either way.
  • Catalog membership, the four-sidecar cap and the one-proxy rule are validated server-side only, by design (same policy as egressProxy reachability).

Add SidecarAttachment and SidecarInfo to the monorepo copy of the OpenAPI
spec, NewSandbox.sidecars (maxItems 4) and a sidecars array on Sandbox,
SandboxDetail and ListedSandbox, then regenerate the JS schema and the
Python client models. Shapes follow IMPL-D039 00-overview; ListedSandbox
is an addition (recorded in qa.md QA7) so the list endpoint can carry the
sidecar state the CLI column and R9 need.

The tracked spec is Copybara-synced from e2b-dev/runtime at spec/runtime-ref,
so the generated-files CI check stays red until the upstream pin advances
past belt's W3 change; the edit is a stand-in for that fetch.
Add the `sidecars` create option (SidecarAttachment: entry, version,
config, secrets) serialised into NewSandbox.sidecars from the known keys
only, and `sidecars` on SandboxInfo from both inspect and list
(SidecarInfo: entry, version, role, class, state, name, address, ports,
lastError; empty list when the API sends none).

Sidecar rejections carry a SIDECAR_* semantic code in the Error body's
error_code: 400s become InvalidArgumentError, SIDECAR_FAILED stays a
SandboxError, and both keep the code and the API message (which names
the entry) in the message and the HTTP status on statusCode. The same
mapping covers SIDECAR_RULE_COLLISION on updateNetwork.

The count limit is left to the API (SIDECAR_LIMIT); the generated
tuple type from maxItems is cast rather than re-validated client-side.
Add the `sidecars` create option (SidecarAttachment TypedDict: entry,
version, config, secrets) on Sandbox.create and AsyncSandbox.create,
built into NewSandbox.sidecars from the known keys only, and
SandboxInfo.sidecars (SidecarInfo dataclass: entry, version, role,
class_, state, name, address, ports, last_error) from both inspect and
list, empty when the API sends none. `class_` carries the wire's `class`,
the same rename the generated client uses.

SIDECAR_* rejections are read off the Error body's error_code: 400s
raise InvalidArgumentException, SIDECAR_FAILED stays a SandboxException,
both keep the code and the API message (which names the entry) and the
HTTP status. The same mapping covers SIDECAR_RULE_COLLISION on
update_network; a 404 there still wins. Any other body falls through to
the existing handle_api_exception.
`sandbox list` gains a SIDECARS column (entry:state, comma-separated,
empty when none) and `sandbox info` prints the sandbox's sidecars as a
kubectl-style table (entry, version, role, class, state, name, address,
ports), indented under its label and omitted when the list is empty.
The JSON output of both carries the field unchanged.

formatTable is split out of renderTable so the info view can embed the
table lines in its own output; renderTable keeps printing them.
Drop the enum on SidecarInfo.state in the spec (known values listed in
the description; role and class keep their closed enums) and regenerate
both clients, so a state a newer api adds no longer makes the generated
Python model raise ValueError and take get_info()/list() down with it.
The SDK-level SidecarState unions stay as documentation and admit any
string, the SandboxIamTokenType pattern.

sidecar_api_exception also swallows a non-UTF-8 error body (ValueError
covers JSONDecodeError and UnicodeDecodeError) instead of raising from
inside the error path. Review round 1, fix 1 + optional.
An empty list, null/None or an omitted option all leave `sidecars` out of
NewSandbox, matching how volumeMounts is sent; shape validation still runs
first so a non-list is rejected before the request. Review round 1, fix 2.
A failed sidecar shows its reason; values longer than 60 characters are
cut to 59 plus an ellipsis. Review round 1, fix 3.
The api's sidecar codes follow every existing error_code
(sidecar_unknown_entry, sidecar_deprecated_entry, sidecar_limit,
sidecar_one_proxy, sidecar_config_invalid, sidecar_secret_missing,
sidecar_rule_collision, sidecar_flag_off; sidecar_failed on the create
failure). Both mappers match the `sidecar_` prefix case-sensitively; an
uppercase variant falls through to the generic mapping, and a test in
each SDK pins that. Review round 1, architect contract decision.
The api rejects a proxy-role sidecar attached together with a service
sidecar whose egress is `sandbox` with sidecar_egress_conflict. The
prefix match already covers it; the mapper docs now list all nine 400
codes and a parametrized test in each SDK pins each one to the
argument error.
The catalog has four entries; the SidecarAttachment docs and the
changeset now name sqlite (libsql-server over HTTP on port 8080) and
iroh (peer-to-peer tunnel: publish/connect pipes, tickets.json polled
until ready, optional node_secret slot) next to iron-proxy and redis.
Lifecycle wording is unchanged pending the architect's final text.
Operator decision 2026-09-11: the ephemeral class is withdrawn. A sidecar
is paused and snapshotted with the sandbox, comes back as it was on
resume (data included), is forked with it and terminated with it; a
crashed sidecar is restarted once from its clean image and then reported
failed while the sandbox keeps running. A forked sandbox's iroh sidecar
starts with a fresh peer identity.

SidecarAttachment and SidecarClass docs in both SDKs, the create() param
docs, and the changeset carry that text; SidecarClass keeps both wire
values for stability with every catalog entry reported stateful. Test
fixtures report stateful, so the CLI table expectations shift one column.
The connect/resume endpoint can now reject with
sidecar_version_unavailable (409: the catalog version the sidecar was
snapshotted with has been removed; the sandbox stays paused) and
sidecar_snapshot_mismatch (500: a stored sidecar snapshot has no matching
declaration). Route the connect response through the sidecar mapper in
JS and both Python variants so the code and status are preserved; with
no conflict error type in either SDK both stay SandboxError /
SandboxException. Docs list them; tests pin both codes and the 404 path.
sidecars had been inserted before the existing positional logger
parameter of Sandbox.create / AsyncSandbox.create (and _create), which
shifted positional callers. It now follows logger behind a `*`, so the
twelve pre-existing positional parameters bind as before and sidecars
can only be passed by keyword; tests cover both bindings in each variant.
from_client_sidecars guarded ports against Unset only, so a wire null
raised TypeError inside get_info()/list(); it is now guarded like
address and last_error. The forcing test fails on the previous code.
build_sidecars_body passed config and secrets straight to dict(), so a
list or string escaped as a bare ValueError that named nothing, against
the helper's own docstring. config must be a mapping and secrets a
mapping of str to str; either failure is InvalidArgumentException naming
sidecars[i].config or sidecars[i].secrets. The five forcing cases fail
on the previous code (ValueError / TypeError instead).
Operator decision 2026-09-12: the cache ships as Valkey (BSD-3,
wire-compatible); the catalog entry is `valkey` at
valkey.sidecar.e2b.local:6379 with no `redis` alias. Renamed in the
SidecarAttachment docs and examples, the create() param docs, error
message examples, the changeset, every JS/Python/CLI test fixture, and
the spec's entry description (clients regenerated). Each SDK's
SidecarAttachment doc says "Valkey (Redis-compatible)" once so a search
for redis still lands.
@changeset-bot

changeset-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0b69ea7

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

This PR includes changesets to release 3 packages
Name Type
@e2b/cli Minor
e2b Minor
@e2b/python-sdk 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

@cla-bot cla-bot Bot added the cla-signed label Sep 12, 2026
@cursor

cursor Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

PR Summary

Medium Risk
New sandbox create and resume paths accept sidecars tied to egress secrets and network rules; changes are optional on the wire but expand failure modes and feature-gated API behavior.

Overview
Adds optional sidecars on sandbox create in the JS and Python SDKs (catalog entries with entry, version, config, secrets), returns SidecarInfo on get/list, and maps API sidecar_* errors to argument vs sandbox exceptions on create, connect, and network update. OpenAPI and generated clients pick up SidecarAttachment / SidecarInfo. CLI sandbox list shows entry:state in a Sidecars column; sandbox info prints an indented sidecar table via shared formatTable.

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

@github-actions

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from ba587d1. Download artifacts from this workflow run.

JS SDK (e2b@2.49.2-impl-d039-sdk-sidecars.0):

npm install ./e2b-2.49.2-impl-d039-sdk-sidecars.0.tgz

CLI (@e2b/cli@2.19.1-impl-d039-sdk-sidecars.0):

npm install ./e2b-cli-2.19.1-impl-d039-sdk-sidecars.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.8.1-impl-d039-sdk-sidecars.0):

npm install ./e2b-code-interpreter-2.8.1-impl-d039-sdk-sidecars.0.tgz

Desktop JS SDK (@e2b/desktop@2.4.1-impl-d039-sdk-sidecars.0):

npm install ./e2b-desktop-2.4.1-impl-d039-sdk-sidecars.0.tgz

Python SDK (e2b==2.49.1+impl.d039.sdk.sidecars):

pip install ./e2b-2.49.1+impl.d039.sdk.sidecars-py3-none-any.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.10.0+impl.d039.sdk.sidecars):

pip install ./e2b_code_interpreter-2.10.0+impl.d039.sdk.sidecars-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.5.0+impl.d039.sdk.sidecars):

pip install ./e2b_desktop-2.5.0+impl.d039.sdk.sidecars-py3-none-any.whl

@devin-ai-integration devin-ai-integration 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.

TASTE.md compliance review (e2b-dev/sdk-harness) of the sidecar surface added in this PR — JS SandboxOpts.sidecars / SandboxInfo.sidecars, the Python sync + async create(sidecars=…) kwarg and SidecarInfo dataclass, and the sidecar_* error mapping.

Checked: parity (T-1/T-2/T-10), API shape (T-3/T-3a, T-16, T-18, T-19, T-20, T-22/T-23), validation scope (T-52), error mapping and messages (T-57, T-59, T-60, T-62), exports (T-54), docs (T-69–T-72).

3 violations, all with inline comments:

  1. T-60 — the sidecar_* status/code → error-class mapping lives in a new helper that is short-circuited in front of handleApiError / handle_api_exception at six call sites (JS getInfo/create/connect, Python sync + async equivalents) instead of inside the centralized mapper. Commented once per language; the same fix covers every ?? handleApiError(res) / or handle_api_exception(res) site.
  2. T-19SidecarAttachment and SidecarInfo are object shapes declared with type rather than interface; SandboxOpts/SandboxInfo, which they plug into, are interfaces.
  3. T-1SidecarInfo.ports is ports?: number[] (absent when the API omits it) in JS but List[int] = [] in Python, so the same response reads undefined in one SDK and [] in the other. SandboxInfo.sidecars has the same split (sidecars?: in JS, List[SidecarInfo] = [] in Python) even though fromApiSidecars always yields an array — dropping the ? there makes the two match.

What looks right: sidecars is keyword-only from day one in Python (T-3a) and lives in the trailing options object in JS (T-3); options are a TypedDict and results a @dataclass (T-16); generated ClientSidecar* models are mapped at the boundary (T-18); client-side checks are shape-only and leave catalog/limit/proxy rules to the API (T-52); 400s go to InvalidArgumentError/InvalidArgumentException and the rest stay in the SandboxError hierarchy (T-57/T-59); all new names are exported from index.ts / __all__ (T-54).

})

const err = handleApiError(res)
const err = sidecarApiError(res) ?? handleApiError(res)

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.

T-60 — HTTP status → error class mapping is centralized in handleApiError (api/index.ts), not scattered across call sites. sidecarApiError is a second mapper spliced in front of the central one at three sites in this file (here, getInfo L1705, connect/resume L2090), so a future call site that adds sidecars — or a new *_code family — has to remember the ?? chain.

Compliant form: teach handleApiError to read error_code (it already has the body and status) and dispatch sidecar_* there, leaving every call site as the unchanged single call:

Suggested change
const err = sidecarApiError(res) ?? handleApiError(res)
const err = handleApiError(res)

(Same applies to the Python sidecar_api_exception(res) or handle_api_exception(res) sites — see the comment in sandbox_sync/sandbox_api.py.)


if res.status_code >= 300:
raise handle_api_exception(res)
raise sidecar_api_exception(res) or handle_api_exception(res)

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.

T-60 — status → exception mapping is centralized in handle_api_exception (e2b/api/__init__.py), not decided per call site. This or chain is repeated at three sites in this file (L209, L263, L374) and mirrored in sandbox_async/sandbox_api.py. Move the sidecar_* error_code dispatch into handle_api_exception and keep the call sites as they were:

Suggested change
raise sidecar_api_exception(res) or handle_api_exception(res)
raise handle_api_exception(res)

* })
* ```
*/
export type SidecarAttachment = {

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.

T-19 — object shapes are interfaces (SandboxOpts, SandboxInfo); type is reserved for unions and intersections (SandboxState, SidecarRole). SidecarAttachment is an extensible options shape that sits inside SandboxOpts, so it follows the interface convention:

Suggested change
export type SidecarAttachment = {
export interface SidecarAttachment {

* A sidecar attached to a sandbox, as returned by the sandbox info and list
* endpoints.
*/
export type SidecarInfo = {

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.

T-19 — result shapes are interfaces like the SandboxInfo they are embedded in; type is for unions/intersections.

Suggested change
export type SidecarInfo = {
export interface SidecarInfo {

/** Address of the sidecar inside the sandbox network. */
address?: string
/** Ports the sidecar listens on. */
ports?: number[]

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.

T-1 — the JS and Python surfaces mirror each other in semantics, not just names. Python SidecarInfo.ports is List[int] = field(default_factory=list) and _from_client_sidecar normalizes an omitted/null wire value to [], while here ports is optional and fromApiSidecars leaves it undefined. The same response therefore reads [] in Python and undefined in JS. Pick one — given volumeMounts and SandboxInfo.sidecars already normalize to [], the array is the consistent choice:

Suggested change
ports?: number[]
ports: number[]

and have fromApiSidecars emit ports: sidecar.ports ?? []. (SandboxInfo.sidecars?: SidecarInfo[] has the same split against Python's List[SidecarInfo] = []fromApiSidecars always returns an array, so the ? can go.)

@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 using high effort and found 1 potential issue.

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 0b69ea7. Configure here.

...(sidecar.ports !== undefined ? { ports: sidecar.ports } : {}),
...(sidecar.lastError !== undefined
? { lastError: sidecar.lastError }
: {}),

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.

Null sidecar fields leak through mapping

Medium Severity

fromApiSidecars keeps wire null on address, ports, and lastError because it only skips undefined. That puts null on SidecarInfo (typed as optional, not nullable) and truncate then reads .length on lastError, so e2b sandbox info can throw when the API sends null for those fields.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: JS SDK: prefer undefined over null for absent values

Reviewed by Cursor Bugbot for commit 0b69ea7. Configure here.

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.

1 participant