Commit Graph

5071 Commits

Author SHA1 Message Date
github-actions[bot] e139fc38e0 [skip ci] Release new versions @e2b/cli@2.17.1 @e2b/python-sdk@2.45.1 2026-08-21 15:08:58 +00:00
devin-ai-integration[bot] bc14dd0158 feat(cli): render tables in kubectl style (#1748)
## Summary
Replaces `console-table-printer` with a small custom renderer
(`src/utils/table.ts`) that mimics [kubectl's
tableprinter](https://github.com/kubernetes/cli-runtime/blob/master/pkg/printers/tableprinter.go):
uppercase headers, left-aligned columns padded to the widest cell with a
3-space gap, no borders, colors, titles, or truncation.

```ts
renderTable(items, [{ header: 'Sandbox ID', value: (row) => row.sandboxId }, ...])
```

Applies to `e2b sandbox list`, `e2b sandbox snapshot list`, and `e2b
template list`. Section titles ("Sandboxes", "Snapshots", "Sandbox
templates") and the `maxLen: 20` truncation of template IDs/names are
dropped, matching kubectl output. JSON output is unchanged.

Example:
```
$ e2b sandbox list
SANDBOX ID                                        TEMPLATE ID   ALIAS   STARTED AT            END AT                STATE     VCPUS   RAM MIB   ENVD VERSION   METADATA
i8mz0kj9r7z2sflqmnkq2-e25b6bcb                    rki5dems9wqfm4r03t7g  8/21/2026, 1:10:05 PM  8/21/2026, 1:15:05 PM  Running   2       512       0.2.9          {}
```

`console-table-printer` dependency removed. Unit tests for the renderer
added in `tests/utils/table.test.ts`.

Link to Devin session:
https://app.devin.ai/sessions/05d46079372f4a5caafd9441e3ecd72b
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>
2026-08-21 17:02:43 +02:00
devin-ai-integration[bot] 29794d0d56 Route volume-api generation through redocly filtering (#1731)
## Summary

Follow-up to #1728 (per review:
https://github.com/e2b-dev/E2B/pull/1728#discussion_r3822169921): the
volume-content client generation now bundles through redocly first, like
the envd pipeline, so future `x-internal: true` operations in the synced
spec are dropped before SDK schemas are generated.

- `redocly.yaml`: new `volume` API rooted at
`spec/openapi-volumecontent.yml` with the same `filter-out: x-internal`
+ `remove-unused-components` decorators as `envd`.
- js-sdk `generate:volume-api` and python-sdk `generate-volume-api` now
run `redocly bundle volume -o spec/openapi_generated.volume.yml` before
their generators (the bundle output is gitignored like the other
`openapi_generated.*.yml` files).

Regenerated output: the Python volume client is byte-identical (the spec
has no `x-internal` operations today); the JS `schema.gen.ts` only loses
four response components (400/401/403/409) that no operation referenced,
dropped by `remove-unused-components`.

Now that #1728 is merged, this is rebased onto `main` (single commit,
codegen plumbing only) and ready for review.

Link to Devin session:
https://app.devin.ai/sessions/175095f75cbe42df8710718a1ff2a6a3
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>
2026-08-21 14:59:45 +00:00
devin-ai-integration[bot] d2f3440e5b docs: require changesets only for public surface changes (#1750)
## Summary
Loosen the changeset guidance in CLAUDE.md (and AGENTS.md via symlink):
a changeset is only required when changing the public surface of
packages/cli, packages/js-sdk, or packages/python-sdk — internal
scripts, devtools, and tests no longer need one.

Link to Devin session:
https://app.devin.ai/sessions/3ad4a011b5784998b9acb734f3c08da9
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>
2026-08-21 16:59:04 +02:00
devin-ai-integration[bot] f69a9c00a7 fix(cli): sort sandbox list json output like the table (#1747)
## Summary

`e2b sandbox list --format json` previously printed sandboxes in raw API
order (desc by default), while the pretty table re-sorted client-side
(asc by default, respecting `--order`). This aligns the two: the table's
sort is extracted into `sortSandboxes(sandboxes, order)` (start time
with sandbox-ID tie-break) and the json branch now prints
`sortSandboxes(sandboxes, options.order)`, so both formats show the same
order.

### Usage

```sh
e2b sandbox list --format json               # ascending by start time (same as table)
e2b sandbox list --format json --order desc  # newest first
```

Includes unit tests for `sortSandboxes` and a patch changeset for
`@e2b/cli`.

Link to Devin session:
https://app.devin.ai/sessions/44dcb4b0ca9143b8b023ef6fb8554c72
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>
2026-08-21 13:03:52 +00:00
devin-ai-integration[bot] b17b7262e4 fix(python-sdk): keep streamed request bodies unbuffered across retries (#1718)
## Summary

pyqwest's retry middleware keeps a request replayable by mirroring a
non-`bytes` body into memory as it is sent, so a streamed upload through
the shared retrying transports (`files.write` of a file-like object,
`volume.write_file`) reached the wire in chunks yet accumulated its
whole body in RAM. curioswitch/pyqwest#219 (released in pyqwest 0.10.0)
adds `RetryMode.UNBUFFERED`, which drops that copy: a streamed body is
handed to the next attempt only while nothing has been read from it.

That is exactly what the SDK's connect-only retry policy needs — pyqwest
raises the builtin `ConnectionError` (the only thing
`should_retry_response` retries) only before the request body was
written, so the stream is still untouched on every failure we retry.
`bytes` bodies (unary RPCs, in-memory writes) stay replayable in either
mode.

Both `ConnectionRetryTransport`s (sync + async) now declare it:

```python
def should_retry_request(self, request: Request) -> RetryMode:
    return RetryMode.UNBUFFERED
```

The pyqwest pin moves to `>=0.10.0,<0.11`, the release shipping
`RetryMode`.

Tests (`tests/test_retry_stream_buffering.py`):
- both transports override `should_retry_request` with
`RetryMode.UNBUFFERED` (the inherited hook returns `True` — buffered)
- untouched streams are retried after a simulated connect failure (sync
+ async)
- peak allocation stays far below body size while streaming
(tracemalloc, 16 MiB body), and a stream that failed after its first
chunk is *not* replayed
- the httpx→pyqwest adapters hand the body to the retry middleware as a
stream, not flattened bytes


Link to Devin session:
https://app.devin.ai/sessions/895a6967064e425abfc84b8cfbc32910
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>
2026-08-21 12:46:11 +00:00
github-actions[bot] f6014f17ce [skip ci] Release new versions @e2b/python-sdk@2.45.0 e2b@2.45.0 @e2b/cli@2.17.0 2026-08-21 12:41:38 +00:00
devin-ai-integration[bot] b53deacf2b feat(cli): add sandbox snapshot commands (#1741)
## Summary

Follow-up to #1735: exposes the existing SDK snapshot APIs
(`Sandbox.createSnapshot`, `Sandbox.listSnapshots`,
`Sandbox.deleteSnapshot`) as a new `e2b sandbox snapshot` (alias `snap`)
command group in the CLI. No SDK changes — pure CLI plumbing in a new
`snapshotCommand` registered on `sandboxCommand`.

- `create <sandboxID>` (alias `cr`) — creates a snapshot from the
sandbox; `--name` reuses an existing snapshot template of that name
- `list [sandboxID]` (alias `ls`) — lists snapshots (drains the
`SnapshotPaginator`), optionally filtered by source sandbox ID and
`--name` (tag-qualified names supported); `--format json|pretty` like
`sandbox list`
- `delete <snapshotIDs...>` (alias `dl`) — deletes one or more snapshots
by snapshot ID, reporting not-found ones (mirrors `sandbox kill`
semantics)

### Usage

```sh
e2b sbx snapshot create i8vppvpby8rmiyxdedrwk-4bb04d7b --name my-snapshot
e2b sbx snapshot list                       # all snapshots
e2b sbx snapshot list i8vpp... --format json # only from this sandbox
e2b sbx snapshot delete my-snapshot:latest
```

Includes a minor changeset for `@e2b/cli`.

Link to Devin session:
https://app.devin.ai/sessions/44dcb4b0ca9143b8b023ef6fb8554c72
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>
2026-08-21 14:35:19 +02:00
devin-ai-integration[bot] 8787dfec9b feat(sdk,cli): add sandbox list sorting and filters (#1735)
## Summary

SDK follow-up to the merged API change (e2b-dev/belt#1713) that added
`order`, `startedAfter`, and `template` to `GET /v2/sandboxes`; the
earlier SDK PR for this was closed unfinished. The generated API clients
already had the parameters — this wires them through the public
`Sandbox.list` surface in JS and both Python SDKs (sync + async), plus
the `e2b sandbox list` CLI command, so ordering and filtering happen
server-side across the whole paginated dataset instead of per loaded
page.

New options (mirrored across all three SDK surfaces):
- `order: 'asc' | 'desc'` (default `'desc'`, newest first) — sorts by
sandbox start time; exposed as `SandboxListOrder`
- `query.startedAfter` / `SandboxQuery.started_after` — inclusive lower
bound on start time
- `query.template` / `SandboxQuery.template` — exact template ID or
alias (unknown template ⇒ empty list)

### Usage

JavaScript:
```ts
const paginator = Sandbox.list({
  query: {
    metadata: { env: 'ci' },
    startedAfter: new Date(Date.now() - 60 * 60 * 1000),
    template: 'base',
  },
  order: 'asc',
})
const sandboxes = await paginator.nextItems()
```

Python (sync; async is identical with `AsyncSandbox` / `await`):
```python
paginator = Sandbox.list(
    query=SandboxQuery(
        metadata={"env": "ci"},
        started_after=datetime.now(timezone.utc) - timedelta(hours=1),
        template="base",
    ),
    order="asc",
)
sandboxes = paginator.next_items()
```

CLI:
```sh
e2b sandbox list --template base --started-after 2025-01-01T00:00:00Z --order desc
```
The CLI table respects `--order` when rendering (previously it always
re-sorted ascending by start time; that remains the default).

Includes integration tests for order, `startedAfter`, and template
filtering in JS and both Python test suites, a unit test for CLI table
ordering, plus a minor changeset for `e2b`, `@e2b/python-sdk`, and
`@e2b/cli`.

Link to Devin session:
https://app.devin.ai/sessions/44dcb4b0ca9143b8b023ef6fb8554c72
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>
2026-08-21 14:19:41 +02:00
github-actions[bot] 5995e0ad1c [skip ci] Release new versions e2b@2.44.1 2026-08-20 19:21:37 +00:00
devin-ai-integration[bot] d000bbd2db test: mock all volume tests and remove ENABLE_VOLUME_TESTS skip flag (#1734)
## Summary

Volume tests always run now — the `ENABLE_VOLUME_TESTS` skip flag is
removed and every volume CRUD and file-operation test runs against
deterministic in-process mocks, requiring no live volume infra or
credentials:

- **JS** (`tests/volume/`): `createMockVolumeApi()` returns MSW handlers
with per-instance state — a stateful in-memory filesystem per volume ID
plus the control-plane `POST/DELETE /volumes` used by the `volumeTest`
fixture, which passes the placeholder `TEST_API_KEY` so `file.test.ts`
runs in isolation without ambient credentials.
- **Python** (`tests/mock_volume_content.py` + `conftest.py`):
`MockVolumeContentAPI` implements the same filesystem semantics behind
`httpx.MockTransport`, injected via `attrs.evolve(client,
httpx_args={"transport": ...})` on both the regular and streaming volume
client factories so it survives `with_timeout`. The
`volume`/`async_volume` fixtures go through `Volume.create()` /
`AsyncVolume.create()` with the control-plane calls mocked, matching the
JS fixture entry point.

Both mocks cover write/read (text/bytes/blob/stream/empty),
force-overwrite conflicts, metadata (`uid`/`gid`/`mode`),
nested/recursive `makeDir` (with parent-error propagation), `list`,
`getInfo`, `exists`, `updateMetadata`, and recursive `remove`; entry
types use the `VolumeFileType` enum.

Also fixes a `js-sdk` bug the always-running tests surfaced (introduced
by #1730): `Volume.exists()` caught the deprecated `NotFoundError`, but
`getInfo()` now throws `VolumePathNotFoundError` (a `VolumeError`
subclass), so `exists()` rethrew instead of returning `false` for
missing paths. `exists()` now catches `VolumePathNotFoundError`
(changeset included; Python was already correct since
`VolumePathNotFoundException` subclasses `NotFoundException`).

Link to Devin session:
https://app.devin.ai/sessions/80a50c2aba6441368d775b52a24cd1af
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>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
2026-08-20 21:16:34 +02:00
github-actions[bot] 33195ae163 [skip ci] Release new versions @e2b/python-sdk@2.44.0 e2b@2.44.0 2026-08-20 18:10:17 +00:00
devin-ai-integration[bot] 5759f17e56 feat(sdk): add E2B client for multiple bound connection configs (#1720)
## Summary

Adds an `E2B` client to both SDKs so a process can talk to several API
keys / domains / deployments without going through environment
variables. The client binds a connection config once and exposes the
resource surfaces off it; the named top-level exports are untouched and
keep reading the environment.

Nothing existing changes (changeset is `minor`): the default export is
still `Sandbox`, `Template(...)` keeps working, and `E2B` is a new named
export. Two follow-ups are tracked for v3: making `E2B` the default
export
([SDK-341](https://linear.app/e2b/issue/SDK-341/sdk-v3-js-make-e2b-the-default-export-instead-of-sandbox))
and dropping the `Template` Proxy in favour of `new Template()`
([SDK-342](https://linear.app/e2b/issue/SDK-342/sdk-v3-js-drop-the-template-proxy-require-new-template)).

```ts
import { E2B } from 'e2b'

const { Sandbox, Volume, Template, Secret } = new E2B({
  apiKey: 'e2b_***',
  domain: 'e2b.dev',
})

const sandbox = await Sandbox.create()
const volume = await Volume.create('my-volume')
const exists = await Template.exists('my-template')
await Template.build(Template().fromPythonImage('3'), 'my-env')
await Secret.create('openai-api-key', 'sk-***')

// Per-call options still win over the client's options.
await Sandbox.create({ apiKey: 'e2b_other***' })
```

```python
from e2b import E2B

client = E2B(api_key="e2b_***", domain="e2b.dev")
Sandbox, Volume, Template = client.Sandbox, client.Volume, client.Template
Secret = client.Secret

sandbox = Sandbox.create()
volume = Volume.create("my-volume")
exists = Template.exists("my-template")
secret = Secret.create("openai-api-key", "sk-***")

# Async variants are exposed too.
AsyncSandbox, AsyncTemplate = client.AsyncSandbox, client.AsyncTemplate
async_sandbox = await AsyncSandbox.create()
await AsyncTemplate.exists("my-template")
```

### Mechanism

`client.Sandbox` / `client.Volume` / `client.Template` / `client.Secret`
(plus the `Async*` variants in Python) are per-client subclasses of the
real classes, carrying the bound opts as class-level state. Nothing
process-global is mutated, so clients are isolated from each other and
from the default path, and `cls`/`this` dispatch is preserved (`create`
on a client class returns an instance of that client class).

```ts
// sandboxApi.ts / volume/index.ts / template/index.ts / secret.ts — one hook per class hierarchy
protected static readonly boundOpts?: ConnectionOpts // undefined on the base classes
protected static resolveOpts<T extends ConnectionOpts>(opts?: T) {
  return ConnectionConfig.mergeOpts(this.boundOpts, opts) // { ...bound, ...definedPerCall }
}

// every static method that built a config from raw opts now does
- const config = new ConnectionConfig(opts)
+ const apiOpts = this.resolveOpts(opts)
+ const config = new ConnectionConfig(apiOpts)
```

```py
# sandbox/main.py, volume_sync.py, volume_async.py, template_{sync,async}/main.py, secret/base.py
_bound_api_params: ApiParams = {}  # empty on the base classes

@classmethod
def _resolve_api_params(cls, **opts: Unpack[ApiParams]) -> ApiParams:
    return merge_api_params(cls._bound_api_params, opts)

- config = ConnectionConfig(**opts)
+ config = ConnectionConfig(**cls._resolve_api_params(**opts))
```

`Template` used to be a factory function whose statics were pre-bound to
`TemplateBase`, which left no class for a client to subclass. It is now
the `TemplateBase` class itself, wrapped in a `Proxy` whose only trap
makes it callable without `new`, so `Template(...)` keeps working (no
breaking change) while `client.Template` is a plain subclass like
Sandbox/Volume and `Template.build(...)` resolves `this` naturally:

```ts
export function callableTemplate<T extends typeof TemplateBase>(cls: T) {
  return new Proxy(cls, { apply: (target, _this, args) => new target(...args) })
}
export const Template = callableTemplate(TemplateBase)          // Template() still returns a builder
this.Template = callableTemplate(class extends TemplateBase { boundOpts })  // client.Template
```

Because the trap only intercepts calls, `new Template()`, statics,
`instanceof` and subclassing all go straight to the class, and the
builder's default file context (`getCallerDirectory()`) still resolves
to the user's frame (the trap's frame is inside the SDK and filtered
like the old factory's).

Two side effects of routing everything through the hook:

- Static methods that resolved config off the base class had to move to
`this`/`cls`: `SandboxApi.createSandbox(...)` →
`this.createSandbox(...)` in JS, `new Volume(...)` → `new this(...)`,
and several Python `@staticmethod`s (`SandboxApi.list`,
`_cls_list_snapshots`, `delete_snapshot`, `Volume._class_get_info` /
`_class_list` / `destroy`, and the `Secret` operations) became
`@classmethod`s. Behavior for the top-level classes is unchanged since
their bound opts are empty.
- `DualMethod.__get__` (the descriptor behind `Volume.get_info` /
`Volume.list` working both on the class and on instances) now binds the
class-level function to the accessed class, so `client.Volume.list()`
sees the subclass' bound params instead of `Volume`'s.

Per-call values explicitly set to `undefined` / `None` are dropped when
merging, so they fall back to the client's opts rather than clearing
them into the env-var path.

### Tests

`packages/js-sdk/tests/client.test.ts` (MSW) and
`packages/python-sdk/tests/test_client.py` (local HTTP server, sync +
async) cover: the client's API key/domain being used instead of the env
vars, per-call precedence, rebinding the class (`const S =
client.Sandbox`), rebound `client.Template`, the client template builder
producing the same Dockerfile as the top-level one, two clients staying
isolated, generated-subclass instances, `client.Secret` (sync + async)
using the bound config, the top-level classes still using the env config
with empty bound opts and the default export still being `Sandbox`.



Link to Devin session:
https://app.devin.ai/sessions/772afa048b814ad784b5dde0a599df46
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>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
2026-08-20 18:03:15 +00:00
github-actions[bot] 2e26b825e1 [skip ci] Release new versions @e2b/python-sdk@2.43.0 e2b@2.43.0 2026-08-20 14:55:57 +00:00
devin-ai-integration[bot] f89f8c3f96 Add secrets management to JS and Python SDKs (#1728)
## Summary

Implements Secrets Management in the SDK per the [Secrets Vault SDK
proposal](https://app.notion.com/p/3bab8c29687380b6a8f3e2ecae3f1b50) and
the backend Secrets API. Linear:
[SDK-133](https://linear.app/e2b/issue/SDK-133/sdk-for-managing-secrets).
Docs: [e2b-dev/docs#379](https://github.com/e2b-dev/docs/pull/379).

Spec sync: bumps `spec/infra-ref` to `e19a12b8` (the commit that adds
the Secrets API), adds the `secrets` tag to the `redocly.yaml` filters,
and regenerates via `make codegen` (the regen also pulls in unrelated
upstream spec updates, e.g. the `Error.errorCode` field). The js-sdk
envd schema generation now bundles through a new `envd` redocly api that
filters out operations the upstream spec marks `x-internal: true`
(orchestrator control plane: `/init`, `/freeze`, `/unfreeze`,
`/collapse`, `/fsfreeze`, `/fsthaw`) plus their now-unused component
schemas, so they no longer appear in `src/envd/schema.gen.ts`.

The existing `Secret` class (previously only the `iamToken`/`iam_token`
workload-identity helper) becomes the secrets management surface,
equivalent across JS, sync Python (`Secret`), and async Python
(`AsyncSecret`):

```typescript
Secret.create(name, value, opts?): Promise<SecretInfo>   // POST /secrets
Secret.update(secret, value, opts?): Promise<SecretInfo> // POST /secrets/{secretID} (rotates to a new version)
Secret.getInfo(secret, opts?): Promise<SecretInfo>       // GET /secrets/{secretID}
Secret.list(opts?): SecretPaginator                      // GET /secrets (cursor-paginated)
Secret.exists(secret, opts?): Promise<boolean>           // 200 → true, 404 → false
Secret.destroy(secret, opts?): Promise<boolean>          // 204 → true, 404 → false
Secret.fill(secret): string                              // local marker formatting, no network call
```

Design decisions per the proposal:
- **Values are write-only**: `SecretInfo` carries only metadata
(`secretId`, `name`, `version`, `metadata`, `createdAt`, `updatedAt`);
no read surface or error message includes a value.
- `update`/`getInfo` throw `SecretNotFoundError` /
`SecretNotFoundException` on 404 (subclass of `NotFoundError` /
`NotFoundException`, so generic not-found catches keep working; general
failures throw the new `SecretError` / `SecretException`);
`exists`/`destroy` map 404 to `false` instead.
- `secret` selector accepts either the `sec_` ID or the canonical
lowercase name (backend resolves both).
- `fill` returns the `${e2b.secrets.name}` marker for use in a network
rule's request transform — always the current version, purely local. The
egress proxy replaces the marker with the secret's current value when it
forwards a matching request; unresolvable markers fail open (the request
is forwarded with the affected headers omitted).
- Version-management endpoints from the proposal are marked TBD and not
in the committed backend contract, so they are intentionally not
implemented.

Python moves `e2b/secret.py` to an `e2b/secret/` package (`base.py`
shares `fill`/`iam_token`, `secret_sync.py` / `secret_async.py` mirror
each other); `from e2b import Secret` is unchanged.

Usage:

```typescript
import { Sandbox, Secret } from 'e2b'

const info = await Secret.create('stripe_api_key', 'sk_live_...', { metadata: { env: 'prod' } })
await Secret.update('stripe_api_key', 'sk_live_new...') // rotate → version 2

// Inject into matching outbound requests via a network rule's transform:
const sandbox = await Sandbox.create({
  network: {
    allowOut: ({ rules }) => [...rules.keys()],
    denyOut: ({ allTraffic }) => [allTraffic],
    rules: {
      'api.stripe.com': [
        {
          transform: {
            headers: { Authorization: `Bearer ${Secret.fill('stripe_api_key')}` },
          },
        },
      ],
    },
  },
})

await Secret.destroy('stripe_api_key')
```

```python
from e2b import AsyncSecret

info = await AsyncSecret.create("stripe_api_key", "sk_live_...", metadata={"env": "prod"})
paginator = AsyncSecret.list(limit=100)
while paginator.has_next:
    secrets = await paginator.next_items()
print(AsyncSecret.fill("stripe_api_key"))  # ${e2b.secrets.stripe_api_key}
```

Tests: msw-mocked JS suite (`tests/secret/secret.test.ts`) and
monkeypatched sync/async Python suites covering CRUD, pagination, 404
semantics, and `fill`. `pnpm run format/lint/typecheck` pass; changeset
included (minor for `e2b` and `@e2b/python-sdk`).

Link to Devin session:
https://app.devin.ai/sessions/175095f75cbe42df8710718a1ff2a6a3
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>
2026-08-20 14:49:14 +00:00
devin-ai-integration[bot] d79c6cd973 refactor(js-sdk): drop unused stackTrace params from error constructors (#1732)
## Summary

Removes the `stackTrace` constructor parameter from JS SDK error classes
that never have a caller stack trace attached. Only template/build paths
intentionally capture user frames (`getCallerFrame()`) or pass a trace
along (e.g. `uploadFile`), so the param was dead weight elsewhere.

- Dropped `stackTrace` from: `SandboxError` (base), `TimeoutError`,
`NotEnoughSpaceError`, `NotFoundError`, `FileNotFoundError`,
`SandboxNotFoundError`, `GitUpstreamError`, and the new
`VolumeNotFoundError` / `VolumePathNotFoundError` from #1730.
- Classes that actually receive traces keep them, and now assign
`this.stack` directly instead of forwarding through `super()`:

```ts
export class TemplateError extends SandboxError {
  constructor(message: string, stackTrace?: string) {
    super(message)
    this.name = 'TemplateError'
    if (stackTrace) this.stack = stackTrace
  }
}
// same pattern for InvalidArgumentError; BuildError/FileUploadError unchanged in behavior
```

No behavior change for template/build stack traces; tests unmodified.
Python needs no equivalent change (traceback attachment uses
`.with_traceback()` rather than constructor params).

Link to Devin session:
https://app.devin.ai/sessions/a8493e846f5c424393333236bb8cadc0
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>
2026-08-20 14:48:14 +00:00
devin-ai-integration[bot] 05aa03c35c Add typed not-found errors for volumes (#1730)
## Summary

Volumes threw the plain (JS-deprecated) `NotFoundError` /
`NotFoundException` everywhere. This adds typed subclasses, matching
`SecretNotFoundError` from #1728:

- `VolumeNotFoundError` / `VolumeNotFoundException` — the volume itself
doesn't exist (`Volume.getInfo` / `Volume.get_info`).
- `VolumePathNotFoundError` / `VolumePathNotFoundException` — a
file/directory path inside a volume doesn't exist
(read/write/list/remove/stat content operations).

Both subclass the existing `NotFoundError` / `NotFoundException`, so
existing generic catches keep working. Applied equivalently to the JS
SDK and the sync + async Python SDKs, with tests asserting both the
specific type and the base-class relationship, and a changeset.

```typescript
import { Volume, VolumeNotFoundError, VolumePathNotFoundError } from 'e2b'

try {
  await Volume.getInfo('non-existent-id')
} catch (err) {
  if (err instanceof VolumeNotFoundError) {
    // volume doesn't exist
  }
}

try {
  await vol.readFile('missing.txt')
} catch (err) {
  if (err instanceof VolumePathNotFoundError) {
    // path inside the volume doesn't exist
  }
}
```

```python
from e2b import Volume, VolumeNotFoundException, VolumePathNotFoundException

try:
    Volume.get_info("non-existent-id")
except VolumeNotFoundException:
    ...  # volume doesn't exist

try:
    volume.read_file("missing.txt")
except VolumePathNotFoundException:
    ...  # path inside the volume doesn't exist
```


Link to Devin session:
https://app.devin.ai/sessions/175095f75cbe42df8710718a1ff2a6a3
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>
2026-08-20 16:21:21 +02:00
devin-ai-integration[bot] 61503f75eb fix(js-sdk): guard runtime-probed iam token names in network transforms (#1715)
## Summary

Fixes #1673. In a network `transform` callback, `iam.tokens.toJSON`,
`.then`, `.toString` and `.valueOf` were exempt from the unknown-token
guard, because the runtime reads those names off any object it
serializes, awaits or coerces — without the exemption,
`JSON.stringify(iam.tokens)` inside a callback would throw. So
referencing one as a token name produced `Bearer undefined` or `Bearer
function toString() { [native code] }`. Neither carries a
`${e2b.identity.tokens.…}` placeholder, so the egress proxy forwards it
verbatim and the destination answers 401 on a garbage credential — the
confusing failure the guard exists to prevent. Any other typo already
threw.

The fix separates the probe from a token reference: a probe reads the
name and stops there, a token reference coerces or serializes what it
read.

```ts
// get trap, for a name that is not a registered token
if (RUNTIME_PROBED_PROPS.has(prop)) {
  // `then`/`toJSON`: non-callable, so `await` and `JSON.stringify` treat it as absent.
  // `toString`/`valueOf`: callable, so `String(iam.tokens)` still works — `valueOf`
  // answers with the guarded proxy, not the record behind it.
  const value = prop === 'toString' ? () => Object.prototype.toString.call(proxy)
              : prop === 'valueOf'  ? () => proxy
              : {}

  // Coerced (`Bearer ${…}`) or serialized (`{ 'X-Api-Key': iam.tokens.then }`) → throw.
  Object.defineProperty(value, Symbol.toPrimitive, { value: resolveUnregistered })
  Object.defineProperty(value, 'toJSON', { value: resolveUnregistered, enumerable: true })
  return value
}
```

`toJSON` has to be enumerable: Bun's `JSON.stringify` only finds an own
`toJSON` that is, and the Bun CI leg caught the non-enumerable version.

```ts
await Sandbox.create({
  iam: { tokens: { aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }) } },
  network: {
    rules: {
      'api.example.com': [
        {
          // InvalidArgumentError: Network transform references iam token 'then',
          // which is not registered. Registered tokens: 'aws'.
          transform: ({ iam }) => ({
            headers: { Authorization: `Bearer ${iam.tokens.then}` },
          }),
        },
      ],
    },
  },
})
```

Per review, this is the minimal version: the earlier round also made the
map read-only (`set`/`defineProperty`/`deleteProperty` traps, `Readonly`
typing) and resolved descriptor lookups through the guard. Both were
dropped — they hardened paths nobody hits, and `Object.hasOwn` throwing
was a wart. What is left is the `get`-trap stand-in and its tests.

Unchanged: `JSON.stringify(iam.tokens)`, `await iam.tokens`,
`String(iam.tokens)`, spread, enumeration and `in`; a token actually
named `then`/`toJSON`/`toString`/`valueOf` resolves to its placeholder;
`Sandbox.updateNetwork`, which has no client-side view of the registered
names, still resolves any name to a placeholder. The Python mapping
already raised on every one of these lookups, so no Python change.

Tests: the four names rejected both interpolated and assigned straight
through as a header value, with no create request sent; `valueOf()`
still returning the guarded map; the map's own
serialization/await/coercion; and the `validate: false` path resolving
the same names to placeholders. Verified under Node and Bun.


Link to Devin session:
https://app.devin.ai/sessions/d82670868f7540d387ffadc4587a5ce0
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>
2026-08-20 14:46:40 +02:00
devin-ai-integration[bot] 2be6c12f79 refactor(sdk): resolve template config through a bound-opts class hook (#1721)
## Summary

Preparatory refactor so a per-client `client.Template` can subclass
`TemplateBase` and inject a bound `ConnectionConfig`, the way
`Sandbox`/`Volume` will. No public behavior change: the top-level
`Template()` factory, `Template.build(...)`, `AsyncTemplate.*` etc.
still resolve config from per-call opts + env vars (the bound field is
empty on the base class).

**JS** — terminal statics build their config through a class-level hook
instead of `new ConnectionConfig(opts)` directly:

```ts
class TemplateBase {
  protected static boundConnectionOpts: ConnectionOpts = {}
  protected static resolveConnectionConfig(opts?: ConnectionOpts) {
    return new ConnectionConfig({ ...this.boundConnectionOpts, ...definedEntriesOf(opts) })
  }
}

- const config = new ConnectionConfig(buildOptions)
+ const config = this.resolveConnectionConfig(buildOptions)
```

That only works if `this` is a template class, and the top-level surface
copies the statics off the class (`Template.build =
TemplateBase.build`), where `this` would be the factory function. So the
copies are now bound:

```ts
function boundToBase<T extends (...args: never[]) => unknown>(fn: T): T {
  return fn.bind(TemplateBase) as T  // the cast is only because `bind` collapses overloads
}

- Template.build = TemplateBase.build
+ Template.build = boundToBase(TemplateBase.build)
```

Top-level calls therefore resolve against `TemplateBase` (no bound opts
→ per-call opts + env, unchanged), while `MyTemplate.build(...)` keeps
`this === MyTemplate` and picks up its bound opts. `exists` likewise
dispatches via `this.aliasExists(...)` instead of
`TemplateBase.aliasExists(...)`. `toJSON`/`toDockerfile` untouched.

**Python** — `build`, `build_in_background`, `get_build_status`,
`exists`, `alias_exists`, `assign_tags`, `remove_tags`, `get_tags` went
from `@staticmethod` to `@classmethod` (signatures otherwise identical,
so call sites are unaffected), and the hardcoded lookups now go through
`cls`:

```python
-        config = ConnectionConfig(**opts)
-        data = Template._build(...)                                  # AsyncTemplate._build in the async SDK
-        logs_refresh_frequency=TemplateBase._logs_refresh_frequency,
+        config = cls._resolve_connection_config(**opts)
+        data = cls._build(...)
+        logs_refresh_frequency=cls._logs_refresh_frequency,
```

with the hook on the shared `TemplateBase`:

```python
_bound_api_params: ApiParams = {}

@classmethod
def _resolve_connection_config(cls, **opts: Unpack[ApiParams]) -> ConnectionConfig:
    return ConnectionConfig(**{**cls._bound_api_params, **{k: v for k, v in opts.items() if v is not None}})
```

Precedence is per-call opts > bound opts > env vars; explicitly passed
`undefined`/`None` per-call values are dropped so they don't wipe bound
opts. No `ConnectionConfig` process-global state is touched.

## Usage

```ts
import { TemplateBase } from 'e2b'

class MyTemplate extends TemplateBase {
  protected static boundConnectionOpts = { apiKey: 'e2b_...', domain: 'my.e2b.dev' }
}

await MyTemplate.exists('my-template')                        // bound config
await MyTemplate.exists('my-template', { apiKey: 'e2b_x' })   // per-call wins
```

```python
class MyTemplate(Template):
    _bound_api_params = {"api_key": "e2b_...", "domain": "my.e2b.dev"}

MyTemplate.exists("my-template")
MyTemplate.exists("my-template", api_key="e2b_x")
```

## Tests

New `tests/template/boundConnectionOpts.test.ts` (msw, asserts the
request URL + `X-API-KEY` per operation) and `test_bound_api_params.py`
for sync and async, covering: top-level path unchanged (per-call opts
and env fallback), bound opts as defaults for
`build_in_background`/`exists`/tag ops, per-call override, and
`None`/`undefined` not clearing bound opts.


Link to Devin session:
https://app.devin.ai/sessions/f15b0cecd1fd40e297334ac8ce154af1
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>
2026-08-20 00:50:31 +00:00
devin-ai-integration[bot] 8b49570575 test(js-sdk): tolerate "Script not found" propagation errors in CF deploy suite (#1725)
## Summary

The `cloudflare-deploy` job failed on `main`
([run](https://github.com/e2b-dev/E2B/actions/runs/32312834689/job/96259231560))
with a Cloudflare edge error page that the propagation handling doesn't
cover:

```
Deployed: https://e2b-js-sdk-smoke.spurious-canidae.workers.dev
Worker route not live yet (404), waiting...
Worker is live.
 × sandbox lifecycle inside a deployed Cloudflare Worker 216ms
Error: non-JSON response (500, "Script not found | e2b-js-sdk-smoke.spurious-canidae.workers.dev | Cloudflare")
```

`setup.mts` polled until one colo answered `405`, but the colo that
served the test's POST had the route and not yet the script, so it
returned a `500` "Script not found" page. The test's retry condition
only matched `non-JSON response (404` / `fetch failed`, so this
propagation variant failed on the first attempt (216 ms, no retry)
instead of being absorbed like the 404.

Both propagation checks now recognize it, leaving all assertions on the
worker's JSON response untouched:

```diff
-condition: /non-JSON response \(404|fetch failed/,
+condition: /non-JSON response \(404|Script not found|fetch failed/,
```

and in `waitUntilLive`, a non-404 status whose page `<title>` says
"Script not found" keeps waiting instead of failing fast; any other
error page still throws immediately.

Verified with `pnpm build && pnpm test:cf:deploy` in `packages/js-sdk`
(real `wrangler deploy --temporary`): 1 passed.


Link to Devin session:
https://app.devin.ai/sessions/6b50de812d9c4e5fac07ee8abd810ce0

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-20 01:43:10 +02:00
github-actions[bot] 0b15b3aae6 [skip ci] Release new versions @e2b/python-sdk@2.42.0 e2b@2.42.0 @e2b/cli@2.16.3 2026-08-19 23:26:38 +00:00
michael-e2b 2daced65be chore: tag package homepage URLs with UTM parameters (#1724)
The SDK and CLI homepage fields get utm_source=pypi/npm
(utm_campaign=package_homepage) so traffic from the registry pages
attributes to its real source instead of direct. Takes effect on the
next publish of each package.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-19 23:10:48 +00:00
devin-ai-integration[bot] 42553c22c9 ci: pin @changesets/cli to v2 so releases tag and publish GitHub releases again (#1723)
## Summary
GitHub releases and git tags stopped being created after Aug 13, even
though the Release workflow was green and packages still landed on
npm/PyPI (e.g. `e2b@2.40.0`/`2.41.0` exist on npm but have no tag or
GitHub release).

Root cause: the `changeset` script was `pnpm dlx @changesets/cli`, i.e.
always the latest version. `@changesets/cli` v3.0.0 (published Aug 11)
replaced the `🦋 New tag: ...` output with a clack-style `Created git
tags:` listing. `changesets/action@v1.9.0` parses the publish output for
`New tag:` lines to build `publishedPackages` — with v3 it finds none,
so it skips pushing tags and creating GitHub releases (the workflow's
own "Push new versions" step only runs `git push`, no `--tags`). The Aug
13 run still printed `New tag:`; the Aug 18/19 runs printed `Created git
tags:` and released nothing.

Fix: pin `@changesets/cli` to `2.31.1` as a devDependency and run the
local binary instead of `pnpm dlx`:

```diff
-    "changeset": "pnpm dlx @changesets/cli"
+    "changeset": "changeset"
 devDependencies:
-    "changeset": "^0.2.6",   // unrelated squatter package from 2013
+    "@changesets/cli": "2.31.1",
```

The removed `changeset@0.2.6` devDependency was an unrelated legacy
package that also shadowed the `changeset` bin name.

Moving to changesets v3 later requires bumping `changesets/action` to
v2.1.1 in `publish_packages.yml`; this PR intentionally keeps the
known-good v2 CLI + v1.9.0 action pairing.

Note: tags/releases for `e2b@2.40.0`, `e2b@2.41.0`,
`@e2b/python-sdk@2.40.0`, `@e2b/python-sdk@2.41.0` (and CLI if
applicable) are still missing on GitHub and need a one-off backfill;
this PR only prevents future runs from skipping them.

Link to Devin session:
https://app.devin.ai/sessions/4eeb8679b8d74c848bc46456a035df21
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>
2026-08-20 00:48:45 +02:00
devin-ai-integration[bot] 55b0bd5e6a docs: add TASTE.md pointing to sdk-harness (#1722)
## Summary

Adds a three-line `TASTE.md` that just links to the canonical SDK design
principles at https://github.com/e2b-dev/sdk-harness/blob/main/TASTE.md,
so the doc has a single source of truth instead of a copy that can
drift.

Link to Devin session:
https://app.devin.ai/sessions/d9c1132e485340079d77ced1b1f0dca9
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>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
2026-08-19 20:56:01 +00:00
devin-ai-integration[bot] 7af41e9fab chore: refresh generated MCP server types (#1716)
## Summary

`spec/mcp-server.json` (and the `McpServer` types generated from it for
both SDKs) has been frozen since the MCP beta landed. It is produced by
`mcp-gateway`'s `type-gen` from that repo's `docker-catalog.yaml`; this
refreshes it against a fresh snapshot of Docker's MCP catalog: **222 →
265 servers**.

Regenerated with the existing pipeline only — `packages/js-sdk: pnpm
generate:mcp` (`json2ts`) and `packages/python-sdk: make generate-mcp`
(`datamodel-codegen`). No hand edits.

- **49 new servers**: `n8n`, `neo4j`, `okta`, `temporal`, `proxmox`,
`testkube`, `thingsboard`, `zen`, `zscaler`, `googleFlights`,
`nextDevtools`, `victoriametrics`/`victorialogs`/`victoriatraces`, and
the AWS Labs family (`awslabsCloudwatch`, `awslabsDynamodb`,
`awslabsIam`, `awsPricing`, `amazonNeptune`, ...).
- **6 servers removed** — the catalog no longer ships them: `postgres`,
`root`, `tembo`, `flexprice`, `triplewhale`, `cdataConnectcloud`.
Passing them to `Sandbox.create` no longer type-checks, and since
`McpServerName = keyof McpServer`, `Template().addMcpServer('postgres')`
stops compiling too.
- **4 servers changed their options**: `awsDiagram` and `context7` now
require one (`outputDir`, `apiKey`), so `awsDiagram: {}` / `context7:
{}` no longer type-check; `onlyofficeDocspace` is down to `baseUrl` +
`docspaceApiKey`; `neo4jCypher` renamed keys.
- **71 entries differ in metadata**, but 61 of those are title-only and
10 description-only. Titles feed the generated TS interface names
(`AirtableMCPServer` → `Airtable`), which only matters to a caller who
imported those interface names directly — `mcp.d.ts` types are not
re-exported from the SDK root, only `McpServer` is.

The config is still forwarded to the gateway as written, so a dropped
server can be kept by casting past the type — whether it starts is up to
the gateway.

```ts
import { Sandbox } from 'e2b'

const sandbox = await Sandbox.create({
  mcp: { n8n: { apiKey: process.env.N8N_API_KEY!, apiUrl: 'https://n8n.example.com/api/v1' } },
})
```

The catalog snapshot this was generated from:
https://github.com/e2b-dev/mcp-gateway/pull/3. The `mcp-gateway`
template has to be rebuilt from that snapshot for the new servers to
actually start in a sandbox, so that PR should land (and the template be
rebuilt) before or with this one.

### Known upstream defects, deliberately not hand-patched

Both come from `type-gen`'s naming rules and belong in
`e2b-dev/mcp-gateway`, since editing generated output here is undone by
the next regeneration:

- `vectraAiRux` lists `VECTRABASEURL` as required, but no such property
exists — the catalog maps it from `vectra_url` via the entry's `env`
block, and `type-gen` emits the env-var name verbatim. `type-gen` should
resolve `required` names through `env` and hard-fail on one that matches
no property.
- `VECTRACLIENTID` keeps its env-var spelling because `type-gen` strips
underscores without re-casing.


Link to Devin session:
https://app.devin.ai/sessions/215a9143568a44209fb02e4177143b72

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mish@e2b.dev <mish@e2b.dev>
2026-08-19 20:57:12 +02:00
cursor[bot] d55ddb8b5c test(python-sdk): drop build API path encoding integration tests (#1713)
Supersedes #1709 — claimed via `/sdk claim` by @mishushakov.

This is a clone of #1709: the original commit (`65c96289`) is applied
unmodified, so the tree here is byte-identical to that PR's head and the
original commit authorship and `Co-authored-by` trailer are preserved.
The branch was already current with `main` (1 commit ahead, 0 behind),
so no merge was needed. The contents were not reviewed or changed.
Please close #1709 in favour of this PR.

The original description follows verbatim.

---

## Summary

Removes `tests/shared/template/test_build_api_path_encoding.py`. Path
encoding is already covered by
`tests/shared/api/test_encode_path_param.py`.

## Verification

```bash
cd packages/python-sdk
uv run pytest tests/shared/api/test_encode_path_param.py tests/shared -q
# 175 passed, 1 skipped
```

[Slack
Thread](https://e2b-team.slack.com/archives/D0962B9UKEE/p1786973222264879?thread_ts=1786973222.264879&cid=D0962B9UKEE)

<div><a
href="https://cursor.com/agents/bc-57d27899-5769-437a-a242-7956988cdb1f?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 18:09:31 +00:00
cursor[bot] 43c28b15fb ci(js-sdk): install Playwright Chromium without --with-deps (#1699)
Supersedes #1698 (claimed via `/sdk claim` by @mishushakov). **Please
close #1698 in favour of this PR** — I have no write access to close it
myself.

This is a straight clone: the commit `f65f602` from #1698 is applied
here unmodified (original authorship and the `Co-authored-by: Mish
Ushakov` trailer preserved), with `origin/main` merged in so the branch
is current — `main` had moved one commit ahead (#1693), which touches
none of the two files in this PR. The diff against `main` is identical
to the original: `.github/workflows/js_sdk_tests.yml` and
`packages/js-sdk/package.json`. Per the claim instructions, nothing was
reviewed or changed.

The original description follows, verbatim.

---

Closes
[SDK-339](https://linear.app/e2b/issue/SDK-339/js-sdk-node-ci-legs-spend-most-of-their-time-in-playwright-install).
Related:
[SDK-292](https://linear.app/e2b/issue/SDK-292/run-the-full-js-sdk-unit-test-suite-in-a-browser),
which introduced the `browser` project this install serves.

## Problem

`packages/js-sdk/package.json` had a `pretest` hook running `npx
playwright install --with-deps chromium`. `--with-deps` shells out to
apt on Linux, and to a DISM Media Foundation enable on Windows, on
**every** invocation — regardless of whether the workflow's Playwright
browser cache hit. On one `Test JS SDK` run that hook was 90% of the
Node leg:

| leg | step | time |
| --- | --- | --- |
| node / ubuntu-22.04 | `Run Node tests` total | 23m21s |
| | ↳ `pretest` (`--with-deps`) | **20m57s** |
| | ↳ `vitest run` (101 files, 100 passed) | 2m23s |
| node / windows-latest | `pretest` DISM Media Foundation enable | 4m31s
|

The browser cache worked fine (`Cache hit for: playwright-Linux-1.55.1`,
restored in 3s). The time went to apt: `apt-get update` 1m42s, then 18.4
MB fetched in 18m59s at 16.1 kB/s off a stalling Azure Ubuntu mirror
(`fonts-wqy-zenhei` alone stalled 7m49s).

The mirror stall is transient; being on that path at all is the
structural problem. Every shared library Chromium needs (`libnss3`,
`libgbm1`, `libdrm2`, `libcairo2`, `xvfb`, …) was already `already the
newest version` on the runner image — the only 9 new packages were
CJK/Cyrillic fonts (`fonts-wqy-zenhei`, `fonts-ipafont-gothic`,
`xfonts-*`) that the single headless `browser` test never renders. For
comparison, in the same run the bun (2m44s), deno (2m41s) and cloudflare
(2m1s) legs run the same test code with no Playwright `pretest`.

## Change

- `packages/js-sdk/package.json`: replace the `pretest` hook with an
explicit `playwright:install` script (`playwright install chromium`, no
`--with-deps`).
- `.github/workflows/js_sdk_tests.yml`: run it as its own step gated on
`matrix.runtime == 'node'`, right after the existing browser-cache step,
with a comment recording why `--with-deps` is omitted.

Moving it out of `pretest` also keeps it off every local `pnpm test`,
including for contributors who never touch the browser project.

## Usage

CI installs the browser as a distinct, cache-backed step:

```yaml
      - name: Install Playwright Chromium
        if: matrix.runtime == 'node'
        run: pnpm run playwright:install
```

Locally, the `browser` project needs Chromium once per Playwright
version:

```bash
cd packages/js-sdk
pnpm run playwright:install   # ~7s cold, ~0.8s once installed
pnpm test
```

Without it, the `browser` project fails with Playwright's own
"Executable doesn't exist … run `playwright install`" message; the other
projects (`unit`, `template`, `connectionConfig`) are unaffected.

## Verification

Run on this branch with no prior Playwright deps installed on the
machine:

- `pnpm run playwright:install`: 6.5s cold (Chromium headless shell +
ffmpeg, no apt), 0.78s as a no-op afterwards.
- `pnpm exec vitest run --project browser`: 1 passed. Chromium launches
and drives a real sandbox without any `--with-deps` packages, confirming
the fonts and libs weren't load-bearing.
- `pnpm build` + full `pnpm test`: 101 files, 99 passed / 1 skipped in
2m34s. The one failure is `tests/sandbox/network.test.ts > injected
header is reflected by the httpbin sidecar`, which fails with `404:
template 'httpbin' not found` — it needs a prebuilt `httpbin` template
that this agent's API key doesn't have, unrelated to this change.
- `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` clean for
`packages/js-sdk` (the recursive root scripts fail only in
`packages/python-sdk`, where `uv` isn't installed in this environment).
- `pnpm run check-deps` (knip) reports no new findings; `playwright` is
still resolved as a used devDependency through the new script.

No changeset: this touches only dev tooling and CI, with no change to
published behavior (the `pretest`/`playwright:install` scripts are inert
for consumers of the package). The commit that originally added the
hook, #977, likewise shipped without one.

<div><a
href="https://cursor.com/agents/bc-b8c7df94-5129-496b-ae9f-0c4cb552773a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 19:36:08 +02:00
cursor[bot] 53676931b8 fix(sdk): omit autoResume from the create request when unset (#1694)
## Summary

When a caller does not configure `lifecycle.autoResume` /
`lifecycle["auto_resume"]`, the SDKs resolved the value to their own
local default and always serialized `{"autoResume": {"enabled": false}}`
in `POST /sandboxes`. That made an omitted preference indistinguishable
from an explicit opt-out, so the API could not own or evolve its own
default without SDK clients unintentionally overriding it.

The field is now left out of the request when it is not configured, in
the JavaScript SDK and in both Python paths (sync and async):

| caller | wire |
| --- | --- |
| no `autoResume` configured | field omitted |
| `autoResume: false` / `auto_resume: False` | `{"autoResume":
{"enabled": false}}` |
| `autoResume: true` / `auto_resume: True` | `{"autoResume": {"enabled":
true}}` |

Explicit choices keep exactly their previous wire shape, and the
existing client-side validation is untouched: `autoResume: true` still
requires `onTimeout: 'pause'` and is still rejected together with
`keepMemory: false`. An explicit `null` / `None` from an untyped caller
is treated as "not configured" rather than as an opt-out, matching how
`keepMemory` / `keep_memory` already normalizes `null`.

`autoResume` is absent from the `NewSandbox` `required` list in
`spec/openapi.yml`, so omitting it is spec-legal and needs no codegen
change.

Closes #1677. This is the same request-construction problem as #1669,
which covers the sibling `autoPause` field; that field is deliberately
left alone here so the two changes stay reviewable on their own.

## Usage

No application changes are required — only the request built for callers
who never expressed a preference changes.

```ts
import { Sandbox } from 'e2b'

// autoResume is left out of the request entirely, so the API's default applies
await Sandbox.create({ lifecycle: { onTimeout: 'pause' } })

// an explicit choice is sent exactly as before
await Sandbox.create({ lifecycle: { onTimeout: 'pause', autoResume: true } })
await Sandbox.create({ lifecycle: { onTimeout: 'pause', autoResume: false } })
```

```python
from e2b import Sandbox

# auto_resume is left out of the request entirely, so the API's default applies
Sandbox.create(lifecycle={"on_timeout": "pause"})

# an explicit choice is sent exactly as before
Sandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": True})
Sandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": False})
```

The `AsyncSandbox` surface behaves identically. The CLI already only
passed `autoResume` when `--lifecycle.autoresume` was given, so `e2b
sandbox create --lifecycle.ontimeout pause` now leaves the preference
unset as well.

## Tests

New request-level coverage asserts the body of `POST /sandboxes` for
five cases (nothing configured, only `onTimeout` configured, explicit
`false`, explicit `true`, explicit `null`/`None`) in all three
implementations:

- `packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts` (new, msw) —
5 passed
- `packages/python-sdk/tests/sync/sandbox_sync/test_create.py` — 5 added
- `packages/python-sdk/tests/async/sandbox_async/test_create.py` — 5
added

These need no credentials. Re-running them with the source change
stashed fails exactly the three omission cases per SDK (3 in JS, 6
across sync and async Python) while the explicit `true`/`false` cases
pass both before and after, which is the evidence that existing behavior
is preserved.

Also run, all green:

- `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` from the repo
root
- `packages/python-sdk`: `tests/shared/sandbox`,
`tests/{sync,async}/sandbox_*/test_create.py`,
`tests/{sync,async}/sandbox_*/test_connect.py` — 77 passed. An
`E2B_API_KEY` was available in this environment, so the live lifecycle
tests (auto-pause requiring `connect`, auto-resume waking on HTTP,
filesystem-only snapshot rebooting) really did create sandboxes and
pass.
- `packages/js-sdk`: `tests/sandbox/lifecyclePayload.test.ts` (live, 5
passed) plus the `iam` and `networkTransform` msw suites
- `packages/cli`: full suite, 109 passed — the CLI builds its own
`lifecycle` object, so its tests are relevant here

No integration test pins the API's current default for an unset
`autoResume`: letting the service own that default is the point of the
change, so the new tests assert only that the SDKs omit the field.

## Notes

- A changeset is included (`patch` for `e2b` and `@e2b/python-sdk`) with
both usage examples.
- The `TASTE.md` referenced in the task prompt
(`raw.cursorusercontent.com/e2b/sdk-harness/main/TASTE.md`) returns 404,
and `e2b/sdk-harness` is not reachable via `gh` either, so this follows
the conventions already established in the repo (nested option bags,
normalizing wire `null` to absent, no new client-side validation,
request-level regression tests next to the existing ones).
- No Linear MCP is available in this environment, so no Linear issue is
linked; the GitHub issue is referenced above instead.

<div><a
href="https://cursor.com/agents/bc-32acab7b-7ff0-4e36-8019-ca9901ac3ec0?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/8e94ee92-9b0d-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 19:16:01 +02:00
cursor[bot] 15bd48b73d fix(sdk): omit autoPause when no timeout lifecycle is configured (#1693)
Closes #1669.

## Problem

Both SDKs serialized `autoPause: false` in `POST /sandboxes` whenever
the caller left `lifecycle.onTimeout` / `lifecycle["on_timeout"]` unset,
because the local default (`kill`) was folded into the payload before
the request was built. That collapsed two distinct states at the API
boundary — "no preference expressed" and "explicitly chose `kill`" — so
the service could not own or evolve its own default without SDKs
silently overriding it.

## Change

`autoPause` is now sent only when a timeout action was actually chosen:

| `lifecycle` | wire |
| --- | --- |
| not configured | `autoPause` omitted |
| `onTimeout: 'kill'` | `autoPause: false` |
| `onTimeout: 'pause'` | `autoPause: true` |

An omitted `onTimeout` still resolves to `kill` locally for the existing
`keepMemory` / `autoResume` validation, so no error paths change. A
`null` `onTimeout` from an untyped caller counts as "not configured",
matching how the SDKs already treat nullish option values.

On the Python side the lifecycle normalization was duplicated verbatim
between `sandbox_sync` and `sandbox_async`. It is now a single
`build_lifecycle_config` in `e2b/sandbox/sandbox_api.py`, alongside the
existing `build_iam_config` / `build_network_config` builders, so the
two create paths cannot drift.

## Usage

Nothing changes for callers that configure a lifecycle; the difference
is only visible to callers that do not.

```ts
import { Sandbox } from 'e2b'

// No timeout lifecycle: autoPause is omitted and the API applies its default.
await Sandbox.create()

// Explicit action: autoPause: false / autoPause: true, as before.
await Sandbox.create({ lifecycle: { onTimeout: 'kill' } })
await Sandbox.create({ lifecycle: { onTimeout: 'pause' } })
```

```python
from e2b import Sandbox

# No timeout lifecycle: auto_pause is omitted and the API applies its default.
Sandbox.create()

# Explicit action: autoPause: false / autoPause: true, as before.
Sandbox.create(lifecycle={"on_timeout": "kill"})
Sandbox.create(lifecycle={"on_timeout": "pause"})
```

The async Python SDK behaves identically via `AsyncSandbox.create`.

## Tests

Request-level regression coverage for all three cases, plus the two
"lifecycle present but no action" shapes an untyped caller can produce:

- `packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts` — msw
captures the create body; needs no credentials.
- `packages/python-sdk/tests/shared/sandbox/test_lifecycle_request.py` —
parametrized over the sync and async create paths, asserting the
serialized `NewSandbox` payload.

Both also assert that `autoPauseMemory` still accompanies an explicit
pause, since it is built from the same normalized action.

Run locally: `pnpm run format`, `pnpm run lint` and `pnpm run typecheck`
are clean. `packages/js-sdk` `tests/sandbox` is 250/251 (the one
failure, `network.test.ts > injected header is reflected by the httpbin
sidecar`, fails on `404: template 'httpbin' not found` — the sidecar
template is unavailable in this environment and is unrelated to this
change). `packages/python-sdk` `tests/shared/sandbox`,
`tests/sync/sandbox_sync/test_create.py` and
`tests/async/sandbox_async/test_create.py` pass, including the suites
that create real sandboxes.

Against the live API, creating a sandbox with no lifecycle, with
`on_timeout: "kill"` and with `on_timeout: "pause"` reports `on_timeout`
of `kill`, `kill` and `pause` respectively — so the API's current
default matches the previous client-side default and there is no
observable behavior change today, while the default now lives on the
server.

## Notes for review

- `autoResume` is still always sent (`{ enabled: false }` when unset),
which is the same class of question for that field. I left it alone to
keep this change to what the issue describes — happy to follow up if the
API should own that default too.
- No Linear MCP was available in this environment, so no Linear issue is
linked; the GitHub issue above is the tracking item.

<div><a
href="https://cursor.com/agents/bc-4a366453-8e1e-4a02-97f4-f38f2a302c7b?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/8e94ee92-9b0d-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 18:51:28 +02:00
cursor[bot] 666241d474 refactor(python-sdk): unify the pyqwest connection pools (#1692)
Claimed from #1659 on `/sdk claim` by the PR's own author (@mishushakov,
org member). The original commit is carried over untouched, so
authorship and the `Co-Authored-By` trailer are preserved — only PR
ownership moves. **Please close #1659 in favour of this PR** (`Closes`
does not auto-close pull requests, and this automation has no write
access to do it).

Closes
[SDK-291](https://linear.app/e2b/issue/SDK-291/python-sdk-unify-pyqwest-connection-pools-once-all-http-traffic-is-off).

## What changes

Every persistent HTTP stack in the Python SDK — control-plane REST, the
envd HTTP API, the envd RPC clients, and the volume content API — now
draws its connection pool from `e2b.api.client_sync`/`client_async`
keyed on `(proxy, idle read bound, HTTP version)`, instead of each
caching one of its own; reqwest pools per host internally, so one pool
serves the API host and every per-sandbox host without interference, and
because envd RPC and the envd HTTP API hit the same host an active
sandbox needs a single HTTP/2 connection instead of one per stack. Two
accessors expose it (`get_pyqwest_transport` for connectrpc,
`get_httpx_transport` for the generated httpx clients) while per-layer
concerns stay above the pool, so `PlainHTTPErrorTransport` becomes a
stateless per-client wrapper and Connect-error normalization stays
RPC-only. Streamed downloads keep a pool of their own — the only one
carrying the idle `read_timeout`, since reqwest's read timer runs during
body send and TTFB and would otherwise cut off long uploads.

Sharing puts the sandbox health probe on the connection the failed RPC
was using, so `tests/test_shared_transport_pool.py` pins that at the
frame level with a new multi-connection HTTP/2 server serving both
routes on one pool: an `RST_STREAM` kills only the stream and the probe
reuses the same connection (which is also the proof the pool is
genuinely shared), while a dropped TCP connection makes reqwest redial —
both still answer, so `handle_rpc_exception_with_health` keeps telling a
wedged connection apart from a dead sandbox.

**No user-facing API change**, so there are no usage examples to add —
the public surface, timeouts, retry policy, and proxy handling are all
unchanged, and JS has no counterpart since pyqwest pools are
Python-only.

## Added while claiming

One regression test the original was missing
(`test_{sync,async}_closing_one_client_leaves_the_shared_pool_open` in
`tests/test_api_client_transport.py`). The refactor's docstrings promise
that "closing an httpx client leaves the pool intact for the other
clients on it", and that promise is now load-bearing process-wide rather
than per-stack, but nothing asserted it: pyqwest pools *are* closable
(`SyncHTTPTransport.close`/`HTTPTransport.aclose`) and every httpx
client in the SDK holds the same cached adapter over one. The existing
tests all close their clients inside `finally` and then reset the
caches, so a close that reached the pool would go unnoticed.

The new tests round-trip against the local echo server, then close the
control-plane client and assert that both a sibling client (the envd
HTTP API) and the pool the envd RPC stack executes on directly still
work. Verified in the pinned dependency that
`PyqwestTransport`/`AsyncPyqwestTransport` inherit httpx's no-op
`close`/`aclose` and never touch the wrapped pool, and confirmed the
assertions are not vacuous: forwarding the adapter's `close()` to the
pool makes both of them fail with `RuntimeError: Executing request on
already closed transport`.

## Verification

- `uv run pytest tests/*.py -q` — 264 passed (262 before the added
test).
- `uv run pytest tests/shared -q` — 128 passed, 1 skipped.
- `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` — clean.
- Checked the two claims from the original description that a reader
would have to take on trust: pyqwest's retry middleware does mirror
non-`bytes` request bodies in RAM (`RetryingRequestContent` accumulates
every chunk into a `bytearray` to make the body replayable), which is
why template context uploads deliberately keep their own non-retrying
transport — and why the same buffering applies to volume uploads and
envd `files.write` on the shared retrying pool, a pre-existing issue on
`main` filed as
[SDK-332](https://linear.app/e2b/issue/SDK-332/python-sdk-streamed-uploads-are-mirrored-in-ram-by-the-pyqwest-retry)
rather than something this PR introduces.

## Notes for review

- RPC and envd HTTP now multiplex on one HTTP/2 connection and share its
concurrent-stream budget (Go's default is 250, and hyper dials a second
connection when one saturates) — low risk, but a real behavior change
under heavy per-sandbox concurrency.
- `get_envd_transport` survives only as an alias of `get_transport`
because external consumers (`e2b-code-interpreter`) call it; prefer
`get_transport` inside the SDK.
- The changeset from the original PR is carried over unchanged
(`@e2b/python-sdk` patch); the added test needs none of its own.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<div><a
href="https://cursor.com/agents/bc-bfb51e4b-1116-42f4-8622-ba3bdeacf11a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 18:35:20 +02:00
github-actions[bot] 39fc1d71af [skip ci] Release new versions @e2b/python-sdk@2.41.0 e2b@2.41.0 2026-08-19 16:20:46 +00:00
cursor[bot] 6824cdf313 feat(sdk): route sandbox egress through your own SOCKS5 proxy (BYOP) (#1688)
Drafts the SDK surface for [bring your own
proxy](https://e2b-docs-byop-egress-proxy.mintlify.site/network/byop):
`network.egressProxy` / `network["egress_proxy"]` on sandbox create, on
`updateNetwork` / `update_network`, and in what `getInfo` / `get_info`
reports back. Tunneling happens on the host after the allow and deny
lists are evaluated, so nothing runs inside the sandbox and code running
there can neither see the proxy nor route around it.

## The spec pin comes first

The pinned infra spec marked `egressProxy` `x-not-implemented: true`,
which Redocly's `filter-out` decorator drops from both generated clients
— so the field did not exist in `schema.gen.ts` or in the Python client
models, and no handwritten surface could reach it.
[infra@0716edb9e8](https://github.com/e2b-dev/infra/commit/0716edb9e840f110c5f87c186876c01e61553098)
removes the flag, so the first commit bumps `spec/infra-ref` and re-runs
codegen rather than hand-writing the wire types.

The pin picks up three other spec changes, and all of them are invisible
to the SDKs: `AdminTeamRunningSandboxCounts`, the dead
`NodeDetail.cachedBuilds` field, and `/admin/sandboxes/running-counts`
are admin-tagged, and the envd spec is byte-identical between the two
commits (verified by comparing the `packages/envd/spec` trees at both
refs). `make codegen` could not run here because the VM has no Docker,
so the spec was replaced with the byte-identical upstream file at the
new pin and the two REST generators were run natively with the pinned
`@redocly/cli` and `e2b-openapi-python-client`.

## Usage

Create a sandbox that tunnels its egress:

```ts
import { Sandbox } from 'e2b'

const sandbox = await Sandbox.create({
  network: {
    egressProxy: {
      address: 'proxy.example.com:1080',
      username: 'proxy-user',
      password: 'proxy-password',
    },
  },
})
```

```python
from e2b import Sandbox

sandbox = Sandbox.create(
    network={
        "egress_proxy": {
            "address": "proxy.example.com:1080",
            "username": "proxy-user",
            "password": "proxy-password",
        },
    },
)
```

It composes with the rest of the network configuration — here everything
except `api.example.com` is denied, and what is allowed goes through
your proxy:

```ts
await Sandbox.create({
  network: {
    allowOut: ['api.example.com'],
    denyOut: ({ allTraffic }) => [allTraffic],
    egressProxy: { address: 'proxy.example.com:1080' },
  },
})
```

```python
Sandbox.create(
    network={
        "allow_out": ["api.example.com"],
        "deny_out": lambda ctx: [ctx.all_traffic],
        "egress_proxy": {"address": "proxy.example.com:1080"},
    },
)
```

Set or replace it on a sandbox that is already running, with no restart.
The update replaces the whole configuration instead of merging into it,
so an update that leaves the proxy out stops tunneling:

```ts
await sandbox.updateNetwork({
  allowOut: ['api.example.com'],
  denyOut: ({ allTraffic }) => [allTraffic],
  egressProxy: { address: 'proxy.example.com:1080' },
})

// Stop tunneling: an update without egressProxy clears it
await sandbox.updateNetwork({})
```

```python
sandbox.update_network({
    "allow_out": ["api.example.com"],
    "deny_out": lambda ctx: [ctx.all_traffic],
    "egress_proxy": {"address": "proxy.example.com:1080"},
})

# Stop tunneling: an update without egress_proxy clears it
sandbox.update_network({})
```

Read the active proxy back:

```ts
const info = await sandbox.getInfo()
console.log(info.network?.egressProxy)
// { address: 'proxy.example.com:1080', username: 'proxy-user' }
```

```python
info = sandbox.get_info()
print(info.network["egress_proxy"])
# {'address': 'proxy.example.com:1080', 'username': 'proxy-user'}
```

## Design notes

- **`SandboxEgressProxyOpts` in, `SandboxEgressProxyInfo` out.** The API
never returns the password, so the result type does not have the field —
the same split as `SandboxNetworkRule` / `SandboxNetworkRuleInfo`.
`fromApiEgressProxy` / `_from_client_egress_proxy` map the generated
type at the boundary and drop a password even if a future API version
starts echoing one back, so the type cannot quietly become a lie.
- **The body is rebuilt from known fields**, as `buildIamBody` already
does, so stray keys on the caller's object never reach the wire and a
later mutation of it cannot alter an in-flight request.
- **No client-side validation.** Address form, port range, hostname
resolution, the internal-range rejection and the
password-without-username rule are all the server's — it is the only
side that can check them, and each already comes back as a readable API
error.
- **`null` never reaches a consumer.** The wire field is nullable; both
SDKs normalize it (absent key in Python, `undefined` in JS), and an
explicit `null` / `None` from an untyped caller is treated as "no proxy"
on the way in.
- Both types are exported from the flat entry points (`index.ts`,
`__all__`).

## Testing

Unit-level in both SDKs — msw in JS (12 tests), the shared builders in
Python (11 tests, covering sync and async since they share the
builders). Integration coverage is not included on purpose: tunneling
needs a SOCKS5 proxy reachable from E2B's infrastructure, which CI has
no way to stand up, and the feature is gated behind a private-beta team
flag.

`pnpm run format`, `pnpm run lint` and `pnpm run typecheck` are clean
repo-wide. The remaining test failures in this environment are all
`AuthenticationException` / missing `E2B_API_KEY` in pre-existing
integration suites; no credentials were available on the VM.

## Notes

- BYOP is available on E2B Cloud and in BYOC. A sandbox that names a
proxy on a deployment built from open source `e2b-dev/infra` is rejected
as unsupported by the orchestrator, which is why the field carried
`x-not-implemented` upstream for a while.
- No Linear MCP was available in this run, so no issue is linked.


<div><a
href="https://cursor.com/agents/bc-653eef78-87bb-5c9c-92d8-e573cd7ba5be?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/8e94ee92-9b0d-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 17:54:11 +02:00
cursor[bot] e2eebd570f fix(python-sdk): URL-encode namespaced template IDs and aliases (#1691)
Claimed clone of #1520 (EN-1379), rebuilt on current `main`. Please
close #1520 in favour of this PR.

## Summary

Namespaced template IDs and aliases contain a slash, but the Python SDK
interpolated them into the request path unencoded, so
`Template.exists("namespace/name")` requested
`/templates/aliases/namespace/name` instead of
`/templates/aliases/namespace%2Fname` — the slash split the route rather
than staying inside one path segment.

A new `encode_path_param` helper percent-encodes the `template_id` and
`alias` path params across every template build-API call site, in both
the sync and async implementations. This matches the JS SDK, which
already encodes path params: `openapi-fetch`'s default path serializer
runs each value through `encodeURIComponent`, so no JS change is needed.

## Usage

```python
from e2b import Template

# Namespaced templates now resolve to /templates/aliases/my-team%2Fmy-template
Template.exists("my-team/my-template")

# ... and to /templates/my-team%2Fmy-template/tags
Template.get_tags("my-team/my-template")
```

```python
from e2b import AsyncTemplate

await AsyncTemplate.exists("my-team/my-template")
```

## Changes on top of #1520

- Merged current `main` (the original branch was 26 commits behind), and
confirmed the fix still covers every path-param call site after the
merge.
- Added `tests/shared/template/test_build_api_path_encoding.py`: the
original PR only unit-tested the helper, which would not catch a call
site that forgot to encode, nor httpx decoding `%2F` back into a
separator. The new tests drive the sync and async build APIs through an
`httpx.MockTransport` and assert the raw request path for both a
namespaced alias and a namespaced template ID. Verified they fail when
the encoding is removed.

## Tests

- `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` — all clean.
- `uv run pytest tests/shared` in `packages/python-sdk` — 139 passed, 1
skipped.

A changeset is included (`@e2b/python-sdk` patch); this is a Python-only
change, so the JS SDK is not bumped.

<div><a
href="https://cursor.com/agents/bc-538dde5d-ca2f-4beb-8471-be70e265337d?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tomas Valenta <49156497+ValentaTomas@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 15:44:16 +00:00
cursor[bot] fc34961205 test(python-sdk): drop httpcore-era stream reader tests after the pyqwest migration (#1690)
Claimed from #1656 via `/sdk claim` (requested by @mishushakov, the
original author). Same single commit, original authorship preserved.
**Supersedes #1656, which should be closed in favor of this PR** — I
don't have write access to close it myself.

---

`tests/test_file_stream_reader.py` was written against httpcore and
never migrated with the rest of the pyqwest stack — it builds bare
`httpx.Client()` instances, so it still passes green while exercising a
transport the SDK no longer ships. Both of its load-bearing premises are
dead:

- `_active_connections()` read `client._transport._pool.connections`, an
httpcore-only internal. `PyqwestTransport` has no `_pool` at all.
- `request.extensions["timeout"]["read"]` is no longer a per-chunk idle
bound. The pyqwest adapter collapses read/write into one whole-operation
deadline and exits the timeout scope before the body streams, so it
bounds nothing after the response head.

This deletes the five tests that asserted only httpcore behavior (both
idle-timeout tests, the slow-consumer test, both abandoned-reader tests)
plus the helper, and re-anchors the remaining eight on
`response.is_closed` — `FileStreamReader.close()`'s actual contract,
transport-agnostic and stronger than the pool check, since the
context-manager tests now also assert the response stays open
mid-stream.

Also removes `tests/bugs/`, whose sole file was a permanently
`@pytest.mark.skip`'d pyautogui repro against the `desktop` template.

The real streaming-idle coverage against actual pyqwest transports
already lives in `tests/test_volume_client.py`; the SDK stopped sending
per-request timeouts on streamed reads for this same reason in
`e2b/sandbox_sync/filesystem/filesystem.py`.

Test-only, so no changeset — matching the repo convention for
`test(...)` PRs.

## Usage examples

None — this PR touches only `packages/python-sdk/tests/`. There is no
change to any public API, so no user-facing usage differs.

## Verification

Re-ran the original PR's checks on this branch:

```
$ uv run pytest tests/test_file_stream_reader.py -v
8 passed in 0.31s          # was 13

$ uv run pytest tests/*.py -q
245 passed in 15.16s       # full python-sdk unit suite

$ uv run make format       # ruff format . -> 403 files left unchanged
$ uv run make lint         # ruff check . -> All checks passed!
$ uv run make typecheck    # ty check -> All checks passed!
```

Also confirmed nothing else in the repo references the deleted
`tests/bugs/`, `test_envelope_decode`, or `_active_connections`.

Closes SDK-324

<div><a
href="https://cursor.com/agents/bc-11701ccd-9302-40dc-b58b-33e570bed5c4?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 17:30:52 +02:00
cursor[bot] e09b318f8c test: write test fixtures to temp dirs instead of the repo tree (#1689)
Clone of #1665, opened in response to `/sdk claim` on that PR. The
original #1665 (branch `bangui`, by @mishushakov) can be closed in
favour of this one — the tree here is byte-for-byte identical to its
head, and the original commit is carried over unmodified so authorship
and the `Co-Authored-By` trailer are preserved.

## What changed

Eight test files created their fixtures outside a temporary destination,
so running the suite left directories behind in the working tree.

The five CLI template tests called `fs.mkdtemp` with a bare relative
prefix. `mkdtemp` does not imply `os.tmpdir()` — a relative prefix
resolves against `process.cwd()`, so each run created
`packages/cli/e2b-<name>-testXXXXXX/`. They now join the prefix onto
`os.tmpdir()`.

On the JS SDK side, `getAllFilesInPath` and `spoolTarArchive` wrote into
`__dirname` and now `mkdtemp` under `os.tmpdir()`. `build.test.ts` built
its file context at `tests/template/folder`, relying on the implicit
caller-directory context; it now creates the context under `os.tmpdir()`
and passes it explicitly via `Template({ fileContextPath })`, matching
what the Python mirror in `test_build.py` already does with
`tempfile.mkdtemp` and `file_context_path`.

The Python suite needed no changes: every host-side write already goes
through `tmp_path`, `tempfile.mkdtemp`, or `TemporaryDirectory`.

No usage examples apply — this is a test-only change with no user-facing
surface.

## Verification

Re-ran everything on this branch rather than relying on the original
PR's numbers:

- `packages/cli`: full suite green, 17 files / 109 tests passed.
- `packages/js-sdk`: the two util test files, 24 tests passed.
- `git status` is clean after both runs, with no leftover fixture
directories anywhere in the tree — which is the behaviour this change is
about.
- `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` all clean
across the JS and Python packages; `format` produced no diff.

No changeset: test-only changes do not ship in either published package.

Fixes SDK-334

<div><a
href="https://cursor.com/agents/bc-7faf51ae-b169-4787-9d84-a50ec291b656?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 15:16:11 +00:00
cursor[bot] 02ba746e9f fix(deps): patch 4 advisories found by dependency audit (3 high, 1 medium) (#1685)
Daily dependency vulnerability audit. `pnpm audit` reported 8 findings
across 2 packages (3 distinct advisories, all high), and `pip-audit`
reported 1 (medium). All 4 have published patches, and every one is
applied here. Both ecosystems now report clean.

All findings were cross-referenced against the GitHub Advisory Database
via `gh api /advisories/<ghsa>` to confirm severity and first-patched
version before bumping.

## Advisories fixed

| Severity | CVSS | Advisory | Package | Was | Now |
| --- | --- | --- | --- | --- | --- |
| High | 7.5 |
[CVE-2026-14257](https://github.com/advisories/GHSA-mh99-v99m-4gvg) |
`brace-expansion` | 1.1.16 / 2.1.2 / 5.0.7 | 1.1.18 / 2.1.4 / 5.0.9 |
| High | 7.5 |
[CVE-2026-69152](https://github.com/advisories/GHSA-rgw5-rvv9-x895) |
`brace-expansion` | 1.1.16 / 2.1.2 / 5.0.7 | 1.1.18 / 2.1.4 / 5.0.9 |
| High | 7.5 |
[GHSA-5p4m-2wfm-xmqj](https://github.com/advisories/GHSA-5p4m-2wfm-xmqj)
(no CVE assigned) | `js-yaml` | 3.15.0 / 4.3.0 | 3.15.1 / 4.3.1 |
| Medium | 5.3 |
[CVE-2026-71554](https://github.com/advisories/GHSA-6hr6-w5qg-qmwg) |
`h2` | 4.3.0 | 4.4.1 |

The two `brace-expansion` CVEs are handled together because the second
one bypasses the mitigation added for the first, so only the 1.1.18 /
2.1.4 / 5.0.9 line is safe against both. Note that the existing
overrides already covered earlier rounds of these same advisories — they
were pinning 1.1.13 / 2.1.2 / 5.0.6 and js-yaml 3.15.0 / 4.2.0, which
have since been superseded.

## Why each one matters here

**`brace-expansion` (high, DoS).** Reachable through `glob > minimatch >
brace-expansion`, and `glob` is a *production* dependency of the
published `e2b` JS SDK — so this is the one finding that was not
dev-only. Worth noting for reviewers: `glob@13.0.6` requires
`minimatch@^10.2.2`, which in turn requires `brace-expansion@^5.0.8`, so
a fresh `npm install e2b` already resolves the patched 5.0.9 on its own.
No `js-sdk` manifest change is needed and end users were not exposed;
the override bump is what keeps this repo's own lockfile and CI off the
vulnerable versions.

**`js-yaml` (high, quadratic CPU in `!!omap`).** Dev-tooling only, via
`@changesets/read > ... > read-yaml-file` and `knip`.

**`h2` (medium, duplicate `Host` header / request smuggling).** A
production dependency of the Python SDK. Bumping `uv.lock` alone would
only fix this repo's dev environment, since `uv.lock` does not constrain
downstream installs — so the floor in `pyproject.toml` is raised too,
which is what actually prevents a consumer from resolving the vulnerable
4.3.0 or 4.4.0. `h2` 4.4.1 declares `requires_python >=3.10`, matching
the SDK's own `requires-python`, so no supported Python version is
dropped. This is the only user-facing change in the PR and it carries a
`patch` changeset.

This one is below the high/critical bar the audit normally acts on, and
is included because the remediation is a single in-range floor bump on a
dependency that ships to users.

## Changes

- `package.json` — retarget the `brace-expansion` and `js-yaml` pnpm
overrides at the new patched versions.
- `pnpm-lock.yaml`, `packages/python-sdk/uv.lock` — regenerated.
- `packages/python-sdk/pyproject.toml` — `h2>=4,<5` becomes
`h2>=4.4.1,<5`.
- `.changeset/bump-h2-4-4-1.md` — `patch` for `@e2b/python-sdk`.

No source code changed; this is dependency metadata only.

## Verification

All three audits are clean after the change:

```bash
pnpm audit                 # No known vulnerabilities found
pnpm audit --prod          # No known vulnerabilities found
cd packages/python-sdk && uv run --with pip-audit pip-audit
                           # No known vulnerabilities found
```

`pnpm run format`, `pnpm run lint`, and `pnpm run typecheck` all pass
with no diff.

Tests: 256 Python unit tests, 101 CLI tests, and 345 JS SDK tests pass.
The remaining suites could not run in this environment because no
`E2B_API_KEY` was available — every one of those failures is an
`AuthenticationError: API key is required` / `E2B_API_KEY must be set`
from a live-sandbox integration test, and none is related to this diff.
**The credential-gated integration suites should be confirmed green in
CI before merge.**

```bash
cd packages/python-sdk && uv run pytest tests --ignore=tests/async --ignore=tests/sync --ignore=tests/bugs --ignore=tests/shared -q
# 256 passed

cd packages/cli && npx vitest run
# 101 passed | 8 skipped

cd packages/js-sdk && npx vitest run --project unit --project connectionConfig --project template
# 345 passed; 267 failures, all missing-API-key
```

## Note on PR structure

The audit task asks for one PR per vulnerability. This run was scoped to
a single branch, so all 4 advisories are grouped here. That grouping is
also the correct shape for the two `brace-expansion` CVEs, which share
one fix and cannot be split. If separate PRs are preferred, the three
commits on this branch are already split by advisory group and can be
cherry-picked apart.

<div><a
href="https://cursor.com/agents/bc-c4b46d1f-a426-45b9-9c03-46e5decd398d?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/automations/979f8043-9b01-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
2026-08-19 14:36:24 +02:00
github-actions[bot] b71439b079 Merge branch 'main' of https://github.com/e2b-dev/E2B 2026-08-18 12:56:34 +00:00
github-actions[bot] 5951f14e81 [skip ci] Release new versions @e2b/python-sdk@2.40.0 e2b@2.40.0 2026-08-18 12:56:06 +00:00
Mish Ushakov e130ba7f3b chore(ci): remove the Dependabot changeset workflow (#1683)
Dependabot is being turned off for this repo, so
`.github/workflows/dependabot_changeset.yml` — which committed a `patch`
changeset to every Dependabot PR that touched a released package's
direct production dependencies — has nothing left to run on, and it goes
away along with the paragraph describing it in `.changeset/README.md`.
No other file referenced the workflow, and nothing about hand-written
changesets changes: `npx changeset` is still the way to add one.

Two follow-ups live outside this diff. The repo has no
`.github/dependabot.yml` (it never did), so the bumps we've been getting
came from GitHub's **Dependabot security updates** toggle — that has to
be switched off in *Settings → Advanced Security* for the PRs to
actually stop. And if "Dependabot Changeset" is listed as a required
check in branch protection, it needs removing there or PRs will wait on
a check that no longer runs; the `VERSION_BUMPER_APPID` /
`VERSION_BUMPER_SECRET` credentials this workflow used are worth
double-checking against the other workflows before revoking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:55:14 +02:00
Mish Ushakov 65cd85d321 refactor(cli): remove the E2B_ACCESS_TOKEN auth path (#1679)
Stacked on #1680.

Auth moved to Hydra OAuth in #1481, which left `ensureAccessToken()`
with no callers and the module-level API client attaching a stale
`Authorization: Bearer <access token>` header to every request. This
drops `ensureAccessToken()`, the `accessToken` export, the
`E2B_ACCESS_TOKEN` arm of the auth error box, and the `apiHeaders`
wiring on `connectionConfig` — the only consumers of that header were
`template list`/`create`, and `/templates` is scoped by the API key
alone, while `auth login` / `auth configure` build their own clients
with Hydra JWTs. `requireApiKey: false` stays on the shared client, but
its justification is now that `e2b auth login` runs before any API key
exists and the client is built at import time.

User-facing effect: combined with #1680, the CLI ignores
`E2B_ACCESS_TOKEN` entirely, so CI setups can drop it and keep only the
API key.

```bash
# Before: both were commonly set in CI
export E2B_ACCESS_TOKEN=sk_e2b_...
export E2B_API_KEY=e2b_...

# Now: the API key alone authorizes everything the CLI calls
export E2B_API_KEY=e2b_...
e2b template list
e2b template create my-template
```

Part of
[SDK-6](https://linear.app/e2b/issue/SDK-6/mark-e2b-access-token-as-deprecated-inside-all-code-references).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:01:52 +02:00
Mish Ushakov 6248b12a5e feat(sdk): remove the deprecated accessToken option (#1680)
Removes the deprecated `accessToken` / `access_token` option from both
SDKs, along with its `E2B_ACCESS_TOKEN` environment fallback and the
`Authorization: Bearer` header it produced. The option was already
deprecated in both SDKs — `connectionConfig.ts` and
`connection_config.py` both pointed at `apiHeaders` / `api_headers` as
the replacement — and E2B access tokens are no longer accepted for API
authentication, so resolving one and putting it on the wire was dead
weight. Requests now authenticate with the API key alone.

Callers who need a bearer token for a custom deployment pass it
explicitly, which is what the deprecation notice already told them to
do:

```ts
// Before
const sandbox = await Sandbox.create({ accessToken: token })

// After
const sandbox = await Sandbox.create({
  apiHeaders: { Authorization: `Bearer ${token}` },
})
```

```python
# Before
config = ConnectionConfig(access_token=token)

# After
config = ConnectionConfig(api_headers={"Authorization": f"Bearer {token}"})
```

`Sandbox.envd_access_token` / `traffic_access_token` are unrelated
per-sandbox tokens and are unaffected, as is the volume client's `token`
(which never read the env var — there's a test asserting exactly that).

Part of
[SDK-6](https://linear.app/e2b/issue/SDK-6/mark-e2b-access-token-as-deprecated-inside-all-code-references).
The CLI half is stacked on top in #1679.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:01:51 +02:00
dependabot[bot] 07e35bcffc chore(deps): bump nanoid from 3.3.17 to 3.3.18 in the npm_and_yarn group across 1 directory (#1672)
Bumps the npm_and_yarn group with 1 update in the / directory:
[nanoid](https://github.com/ai/nanoid).

Updates `nanoid` from 3.3.17 to 3.3.18
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ai/nanoid/releases">nanoid's
releases</a>.</em></p>
<blockquote>
<h2>3.3.18</h2>
<ul>
<li>Fixed infinite loop on async for React Native (by <a
href="https://github.com/OvergrowthBeards-JB"><code>@​OvergrowthBeards-JB</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ai/nanoid/blob/3.3.18/CHANGELOG.md">nanoid's
changelog</a>.</em></p>
<blockquote>
<h2>3.3.18</h2>
<ul>
<li>Fixed infinite loop on async for React Native (by <a
href="https://github.com/OvergrowthBeards-JB"><code>@​OvergrowthBeards-JB</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ai/nanoid/commit/9ad98052b316c5e707f8098ace509d2ae165e54d"><code>9ad9805</code></a>
Release 3.3.18 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/55e50a0621ec084b4bb4000ea4e86e1191bd3da8"><code>55e50a0</code></a>
Update CI action</li>
<li><a
href="https://github.com/ai/nanoid/commit/e10f8d40ce9d1ab47f66d65a16b48086432730d0"><code>e10f8d4</code></a>
Update index.native.js (<a
href="https://redirect.github.com/ai/nanoid/issues/606">#606</a>)</li>
<li>See full diff in <a
href="https://github.com/ai/nanoid/compare/3.3.17...3.3.18">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nanoid&package-manager=npm_and_yarn&previous-version=3.3.17&new-version=3.3.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/e2b-dev/E2B/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-18 11:18:43 +02:00
github-actions[bot] f5d702a520 [skip ci] Release new versions @e2b/python-sdk@2.39.1 2026-08-13 16:56:04 +00:00
Mish Ushakov 0d507cd53d fix(python-sdk): restore the http2 parameter on the transport factories (#1671)
The pyqwest migration in 2.38.0 dropped the `http2` parameter from
`get_transport` and `get_envd_transport` (added deliberately in #1347,
2.32.0) and collapsed the transport cache key to the proxy alone, so
`e2b-code-interpreter`'s Jupyter requests —
`get_transport(self.connection_config, http2=False)` — now raise
`TypeError: get_transport() got an unexpected keyword argument 'http2'`;
that is already live, since `e2b = "^2.26.0"` resolves to 2.38.x, and it
blocks the Python half of code-interpreter
[#328](https://github.com/e2b-dev/code-interpreter/pull/328). pyqwest
supports the capability, it just was not threaded through: this restores
the pre-2.38.0 signature (so no consumer code changes, only an `e2b`
floor bump) by passing `http_version=None if http2 else
HTTPVersion.HTTP1` into the pyqwest transports, and puts the HTTP
version back into both cache keys — without that, whichever caller asks
second is handed a transport of the wrong version. The default is
unchanged: `None` leaves TLS connections to ALPN (HTTP/2 against the E2B
API) and uses HTTP/1 for plaintext, exactly as today. HTTP/1.1 is not
cosmetic for the consumer — with HTTP/2 multiplexing, abandoning a
request only resets its stream, so the code-interpreter server never
sees the `http.disconnect` it needs to interrupt the kernel, while
HTTP/1.1's one connection per request closes the connection and the
server observes it.

## Usage

Both factories are internal (nothing is exported from
`e2b/__init__.py`), so there is no public API change; consumers reaching
into them get the 2.32.0 call back:

```python
from e2b.api.client_sync import get_transport, get_envd_transport

# Unchanged: ALPN negotiates the version (HTTP/2 against the E2B API).
transport = get_transport(config)

# Its own pool, pinned to HTTP/1.1, so a cancelled request closes the
# connection and the server observes the disconnect.
http1 = get_transport(config, http2=False)
envd_http1 = get_envd_transport(config, http2=False)
```

The async mirror (`e2b.api.client_async`) is identical.

## Tests

Six new cases in
`packages/python-sdk/tests/test_api_client_transport.py`, sync and
async: cache separation and identity across `http2` / proxy /
`for_streaming`, the `http_version` value actually reaching the pyqwest
transport (`[None, HTTP1, HTTP1]`), and a round trip proving the pinned
transport works. The negotiated version can't be observed locally — the
test echo server is plaintext, where both settings speak HTTP/1 — so it
is asserted at the constructor, with the reason in a comment; it was
verified by hand against `https://api.e2b.app/health` via the
`pyqwest.access` logger, which shows `"HTTP/2 200 OK"` on the default
and `"HTTP/1.1 200 OK"` with `http2=False` on both factories (and
confirms `httpx.Response.http_version` is unreliable through the adapter
— it reports HTTP/1.1 either way). 256 unit tests pass, plus `make
lint`, `make typecheck` and `make format`. No JS change: its transport
is an undici-dispatcher `fetch` with no HTTP-version knob, and the JS
half of code-interpreter #328 is a clean bump.

Closes
[SDK-335](https://linear.app/e2b/issue/SDK-335/python-sdk-get-transport-lost-its-http2-parameter-in-2380-breaking-e2b)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:56:26 +02:00
github-actions[bot] ce634ab5f2 [skip ci] Release new versions @e2b/python-sdk@2.39.0 e2b@2.39.0 2026-08-13 15:07:47 +00:00
Mish Ushakov 07eb9be196 feat(sdk): resolve iam token placeholders in network transform callbacks (#1616)
Stacked on #1606 (`iam-sdk-feature`) — merge that one first. This is the
second half of SDK-245: it makes the workload tokens registered by
`Sandbox.create`'s `iam` option usable, by letting a network rule's
`transform` be a **callback** that receives placeholder strings the
egress proxy resolves per request.

`iam.tokens.aws` is the literal string `${e2b.identity.tokens.aws}` (the
frozen backend spelling — a placeholder can only select a persisted
named token, never an inline audience or claim). The SDK never resolves
it: the wire payload carries the placeholder and the proxy substitutes a
freshly minted JWT-SVID when it forwards the request, so the token value
never reaches SDK-side code or the sandbox.

Referencing a name that isn't registered in `iam.tokens` fails with
`InvalidArgumentError` / `InvalidArgumentException` listing the names
that are — the proxy never turns an unregistered name into a token, so a
typo would otherwise surface as a confusing auth failure at the
destination. `updateNetwork` / `update_network` accepts the same
callbacks, but its payload carries no `iam` config, so token names can't
be validated client-side there and any name resolves to its placeholder.

Static `transform: { headers }` objects keep working unchanged
(including hand-written `${e2b.identity.tokens.<name>}` strings, which
stay the escape hatch for tokens the SDK doesn't know about).

Only `{ iam }` is exposed on the context for now — `${e2b.sandboxId}` /
`${e2b.teamId}` / `${e2b.executionId}` from the older prototype are not
part of the current backend design, so `sandbox` can be added later when
there is something to resolve.

## Usage

```ts
import { Sandbox, Secret } from 'e2b'

const sandbox = await Sandbox.create({
  iam: {
    tokens: {
      aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }),
    },
  },
  network: {
    // Only allow egress to hosts that have rules registered.
    allowOut: ({ rules }) => [...rules.keys()],
    rules: {
      'api.internal.example.com': [
        {
          transform: ({ iam }) => ({
            headers: { Authorization: `Bearer ${iam.tokens.aws}` },
          }),
        },
      ],
    },
  },
})
```

```python
from e2b import Sandbox, Secret

sandbox = Sandbox.create(
    iam={
        "tokens": {
            "aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID"),
        },
    },
    network={
        "allow_out": lambda ctx: list(ctx.rules.keys()),
        "rules": {
            "api.internal.example.com": [
                {
                    "transform": lambda ctx: {
                        "headers": {"Authorization": f"Bearer {ctx.iam.tokens['aws']}"},
                    },
                },
            ],
        },
    },
)
```

Both send:

```json
{
  "iam": { "tokens": { "aws": { "audience": "sts.amazonaws.com", "tokenType": "JWT-SVID" } } },
  "network": {
    "allowOut": ["api.internal.example.com"],
    "rules": {
      "api.internal.example.com": [
        { "transform": { "headers": { "Authorization": "Bearer ${e2b.identity.tokens.aws}" } } }
      ]
    }
  }
}
```

## Notes

- `allowOut` / `deny_out` selectors run **before** transforms are
resolved, so `ctx.rules` still hands back the rules you passed — a
rule's `transform` there is the union (object or callback), not the
materialized object. The `getInfo` view keeps its own narrowed
`SandboxNetworkRuleInfo` type.
- Token names are validated where they are registered and again before
interpolation: a name cannot be empty or contain `{`, `}` or control
characters. The proxy reads a placeholder up to its first `}`, so `a}b`
would mint the unrelated token `a` and leave `b}` as literal text, and a
`{` in a name can open a second placeholder. The interpolation check is
what covers `updateNetwork`, where any name the callback looks up
becomes a placeholder without passing through the `iam` config.
- Every lookup form on `iam.tokens` is guarded, not just `[name]`:
Python's map is a `Mapping` whose `__getitem__` owns resolution (so
`.get('typo')` raises instead of returning `None`), and membership
(`'aws' in ctx.iam.tokens` / `'aws' in iam.tokens`) answers "is it
registered?" without raising so a callback can branch on it. Lookup
checks own keys only, so an unregistered name colliding with an object
member (`constructor`, `__proto__`) reports as unregistered instead of
resolving to a built-in; the four properties the runtime itself reads
(`toJSON`, `then`, `toString`, `valueOf`) still resolve normally, so
serializing, awaiting or coercing the map does not trip the guard.
- A callback must be synchronous and return a plain transform object; a
promise (from an `async` callback), an array, a `Map`/`Date`/class
instance, or a missing return value is rejected with an actionable error
rather than silently creating a rule with no headers. The awaitable is
closed/caught so you don't also get an unawaited-coroutine warning or an
unhandled rejection.

## Tests

New payload-level tests: JS `tests/sandbox/networkTransform.test.ts`
(msw), Python `tests/shared/sandbox/test_network_transform.py` — shared
rather than mirrored into the sync and async suites, since they only
exercise the shared builders. They cover placeholder resolution,
enumerating and membership-testing registered tokens, `JSON.stringify`
of the context not tripping the guard, static transforms staying
byte-identical, `transform: null`, the unregistered-name rejection
through both `[name]` and `.get()`, the no-`iam` rejection,
non-transform and `async` return values, unusable token names (both
braces, a smuggled placeholder, a newline, empty) at registration and on
the update path, and the permissive `updateNetwork` path.

Verified against production on all three surfaces (JS, sync Python,
async Python), where:

1. a static transform carrying `Bearer ${e2b.identity.tokens.aws}` is
accepted by `validateNetworkRules` and round-trips through `getInfo` /
`get_info` unchanged;
2. the callback-resolved payload reaches the API and is answered with
the expected team-gating error (`400: Sandbox IAM workload tokens are
not available for your team.`), since `iam` is still feature-flagged;
3. a misspelled or unusable token name is rejected client-side before
any request is made.

Proxy-side substitution of the placeholder ships separately in belt
(EN-1864); until then the header value is forwarded verbatim, which is
why there is no end-to-end injection test here.

Part of SDK-245.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-13 16:58:44 +02:00
Mish Ushakov 64b25bb37b feat(sdk): add iam workload identity option and Secret.iamToken helper (#1606)
Implements the sandbox workload identity (IAM) feature from the [infra
spec](https://github.com/e2b-dev/belt/blob/main/spec/openapi-infra.yml)
(`SandboxIam` / `SandboxIamTokens` / `SandboxIamToken`, already present
in the pinned spec and generated clients) across the JS SDK and the sync
and async Python SDKs. `Sandbox.create` gains an `iam` option whose
non-empty `tokens` map enables workload identity, and a new `Secret`
class (exported from both main packages) provides `iamToken` /
`iam_token` to define the token values, per the SDK design. The design
doc's `filePath` field is deliberately omitted until it lands in the
OpenAPI spec, and plain `{ audience, tokenType }` objects are accepted
alongside `Secret.iamToken` results. The SDK builds the request body
from only the known token fields (stray properties never reach the wire,
undefined-valued map entries count as empty) and rejects tokens missing
`audience`/`tokenType` (`token_type` in Python) with
`InvalidArgumentError` / `InvalidArgumentException`. Covered by
request-body tests (msw in JS, `NewSandbox` payload tests in Python)
since the backend feature is team-gated; all three surfaces were also
smoke-tested end-to-end against production, where the payload is parsed
and answered with the expected team-gating error.

Fixes SDK-245.

## Usage

```ts
import { Sandbox, Secret } from 'e2b'

const sandbox = await Sandbox.create({
  iam: {
    tokens: {
      aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }),
    },
  },
})
```

```python
from e2b import Sandbox, Secret

sandbox = Sandbox.create(
    iam={
        "tokens": {
            "aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID"),
        },
    },
)
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 16:58:43 +02:00
Mish Ushakov 034c503f1f ci: guard production releases to main, harden the itinerary step (#1662)
Three fixes found while porting this workflow to
`e2b-dev/code-interpreter`
([#327](https://github.com/e2b-dev/code-interpreter/pull/327)).

**`release.yml` can be dispatched from any branch.** It is dispatch-only
and `workflow_dispatch` offers every branch in the picker, so a feature
branch carrying changesets would publish real packages to npm and PyPI
and push the version bump to itself. `preflight` now fails fast unless
the run is on `main`; candidates cut from a branch already go through
`release-candidate.yml`.

**The itinerary step can block a release.** It only feeds the Slack
messages, but a `changeset status` hiccup — or a typo in a future edit
to that inline `node -e` block, which no YAML validation catches — fails
`preflight` and stops the release. It is now `continue-on-error` with a
placeholder fallback in both messages, and the transform moved to
`.github/scripts/build_release_itinerary.cjs` next to `is_release.sh`,
where it can be run against fixture JSON. A package missing from the
label map now shows under its workspace name instead of being dropped by
`order.filter`, so a fourth publishable package would not silently
vanish from the notification.

**`report-failure` did not list `preflight`**, so whether a broken
preflight pings `#monitoring-releases` rested on `failure()` looking
past the job's direct dependencies — not documented either way, so the
job now depends on it explicitly.

Verified: the extracted script reproduces the current output exactly for
`e2b` / `@e2b/python-sdk` / `@e2b/cli`, in the same order, and handles
the empty and unlabeled-package cases; workflow validated against the
Actions schema; the script matches the repo's prettier config.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:15:14 +02:00
Mish Ushakov 11912ffa04 refactor(python-sdk): share one envd HTTP client across the sync sandbox modules (#1655)
The sync flavor built four envd HTTP clients per sandbox — `Filesystem`,
`Commands` and `Pty` each constructed their own — while the async flavor
built one in `Sandbox.__init__` and threaded it down; this builds it
once on the sync side too and passes it into the three modules. No
functional change: `get_envd_transport` already caches the pyqwest
transport per `(proxy, for_streaming)` process-wide, so those four
clients already shared one connection pool — the cost was a few
`httpx.Client` wrappers per sandbox, plus a sync/async divergence that
CLAUDE.md and TASTE.md both ask us to avoid. It also clears the last
cosmetic differences between the two flavors: async `Commands`/`Pty`
swap their `_check_health` lambda closure for the sync side's attribute
+ method, sync `Commands`/`Pty` drop a write-only `_envd_api_url`, and
async `Filesystem` builds its RPC client first to match sync — the three
constructor pairs now differ only in the sync/async client and RPC class
names. All constructors touched are internal, so there is no public API
change and nothing to show as a usage example.

Verified with 336 unit tests, 92 sync and 89 async integration tests
against prod (`commands`, `pty`, `files`), plus `make lint` and `make
typecheck`.

Closes
[SDK-322](https://linear.app/e2b/issue/SDK-322/python-sdk-share-one-envd-http-client-across-the-sync-sandbox-modules)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 18:40:42 +00:00
github-actions[bot] cfd4bedd90 Merge branch 'main' of https://github.com/e2b-dev/E2B 2026-08-10 17:57:16 +00:00