Commit Graph

1821 Commits

Author SHA1 Message Date
github-actions[bot] 5951f14e81 [skip ci] Release new versions 2026-08-18 12:56:06 +00: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
github-actions[bot] f5d702a520 [skip ci] Release new versions 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 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 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] 6acbeb39ee [skip ci] Release new versions 2026-08-10 17:56:55 +00:00
Mish Ushakov b048369307 feat(python-sdk): move the envd HTTP API client onto pyqwest (#1623)
## What

Tracked in [SDK-265](https://linear.app/e2b/issue/SDK-265) (part of the
[SDK-268](https://linear.app/e2b/issue/SDK-268) stack). Stacked on
#1603, at the top of the pyqwest stack (#1601#1602#1603 → this).
Migrate the envd HTTP API client — sandbox file transfers
(`files.read`/`write`), health checks — from httpx-native transports to
pyqwest via the httpx adapter, and dedupe the transport plumbing that
#1558 (envd RPC) and #1601 (REST) each carried a copy of. With this, all
Python SDK traffic runs on pyqwest: REST control plane (#1601), envd RPC
(#1558, connectrpc), envd HTTP API (this PR); the volume content client
(#1602) and template build uploads (#1603) sit below this one in the
stack.

## How

**Shared plumbing** (first commit): `e2b.api` becomes the canonical home
for the proxy narrowing (`proxy_to_config`, with stack-neutral error
messages), the pool tuning, and the flavor `ConnectionRetryTransport` +
a new `retrying_http_transport(proxy, read_timeout=None)` factory;
`e2b.envd.client_sync/client_async` import them instead of defining
their own (envd RPC behavior unchanged, pools stay separate —
unification is SDK-291).

**envd HTTP API** (second commit):

- `get_envd_transport(config, for_streaming=False)` returns
pyqwest-adapter transports cached per `(proxy, streaming)`;
`get_envd_api(config, base_url, for_streaming=False)` builds the httpx
client with sandbox headers + logging hooks. The per-thread (sync) /
per-loop (async) client caching in
`Filesystem`/`Commands`/`Pty`/`AsyncSandbox` is gone — one shared client
per module, same rationale as the `ApiClient` simplification in #1601.
- **Streamed downloads**: the streaming transport carries a 60s
`read_timeout` — an idle bound that resets on every read, capping stalls
without limiting total transfer time. It gets a dedicated pool because
reqwest's read timer keeps ticking while a request body is sent and
while waiting for the response head, so on the shared transport it would
cut off uploads and slow unary responses. An explicit `request_timeout`
becomes the whole-transfer deadline (adapter semantics) and is sent only
when the caller set one; `stream_idle_timeout` stays honored on the
async client via `wait_for` per read (so values above 60s work and `0`
disables), and is documented as ignored on the sync client, which cannot
interrupt a blocking read. Mirrors #1602's volume design.
- **Uploads**: buffered uploads keep `request_timeout` as a
whole-request deadline; streamed (file-like) uploads carry no
client-side timeout and are bounded server-side (envd's idle read
timeout) — both exactly the JS SDK's behavior (`getSignal` for buffered,
no signal for streams).
- **Multipart**: `files=` uploads go out as httpx's `MultipartStream`,
which implements *both* `SyncByteStream` and `AsyncByteStream`. The
pyqwest 0.7 adapter's sync content conversion matched `AsyncByteStream`
first and raised `TypeError("unreachable")` from inside the body
iterator, surfacing as a `WriteError` mid-request ("http2 error: stream
error sent by user"). Fixed upstream in
[pyqwest#196](https://github.com/curioswitch/pyqwest/pull/196), which
matches the sync case first — so this PR carries no workaround (the
stack requires **pyqwest 0.9**, set in #1601). The regression test
stays, now covering the upstream fix.
- The stream readers map the transport's idle timeout (builtin
`TimeoutError` under pyqwest) to the documented `httpx.ReadTimeout`;
`handle_envd_api_transport_exception`'s health-probe path keeps working
because the adapter maps HTTP/2 stream resets to
`httpx.RemoteProtocolError`.

- **RPC logging**: the `LoggingInterceptor` docstring no longer promises
its own removal. pyqwest does log requests
([pyqwest#197](https://github.com/curioswitch/pyqwest/pull/197)), but on
process-wide `pyqwest`/`pyqwest.access` loggers that can't carry the
per-sandbox `logger` and don't see streamed messages or the Connect
error code of a stream that fails inside a `200 OK` — so the interceptor
stays, with those loggers below it.
[pyqwest#192](https://github.com/curioswitch/pyqwest/pull/192), the
middleware it referenced, was closed in favor of #197.

- **Transports**: rebased onto #1603 on pyqwest 0.9, so the envd HTTP
API transports are the stock `PyqwestTransport`/`AsyncPyqwestTransport`
(the SDK's adapter subclasses are gone as of #1601 — 0.9 strips the
`Host` header and maps timeouts itself) with `follow_redirects=False`
and, for the streaming pool, the transport-wide `read_timeout`.

## Testing

- Unit: envd transport keying (streaming vs regular vs REST pools),
`get_envd_api` wiring (headers, transports), multipart regression
through a local server, stream-reader timeout mapping + per-read idle
bound (`tests/test_file_stream_reader.py`), rewritten client-lifecycle
tests (shared across threads). 236 unit tests green; lint + typecheck
green.
- Integration against production sandboxes: full `files` suites
sync+async (123 tests — these caught the multipart bug), `commands` +
`pty` suites both flavors (57 tests). All green.

## Usage example

No API changes:

```python
sbx = Sandbox.create()
sbx.files.write("hello.txt", "hi")            # multipart/octet-stream over pyqwest
with sbx.files.read("hello.txt", format="stream") as stream:
    for chunk in stream:                       # stalls bounded by 60s idle read timeout
        ...
```

Only visible behavior shift: on the **sync** client, `files.read(...,
format="stream", stream_idle_timeout=...)` is now a documented no-op
(the transport-wide 60s idle bound applies); the async client honors it
as before.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:46:38 +02:00
Mish Ushakov b3a7c9f44a feat(python-sdk): move template build-context uploads onto pyqwest (#1603)
## What

Stacked on #1602 (which is stacked on #1601). Migrates the **template
build-context uploads** (streaming the build archive to S3 presigned
URLs in `build_api.upload_file`) onto
[pyqwest](https://github.com/curioswitch/pyqwest) via its
httpx-compatible transport adapter.

Originally deferred from #1601 because S3 presigned URLs reject chunked
transfer encoding and Content-Length framing through reqwest was
unverified. Verified at the wire level (raw-socket capture server):
httpx's Content-Length — derived from the spooled archive (sync) or set
explicitly on the async-iterator body (async) — is forwarded by the
adapter and reqwest keeps Content-Length framing for streamed bodies, no
chunked fallback.


> [!NOTE]
> Rebased onto #1601, which locks **pyqwest 0.9.0**. Two knock-on
changes here: the upload client uses the stock
`PyqwestTransport`/`AsyncPyqwestTransport` (0.9.0's adapter subsumes
what the SDK's transport subclasses did, so #1601 deleted them), and it
builds its proxy from `proxy_to_config(...)` following #1601's rename.

## How

- `e2b/template_sync/build_api.py` / `template_async/build_api.py`:
`upload_file` uses a one-off pyqwest transport instead of the generated
client's httpx transport.
- **Redirects stay with the httpx client.** pyqwest 0.9.0 makes
reqwest's internal redirect following configurable, so it's turned off
on the upload transport: otherwise reqwest would replay the entire
archive body against a new location without httpx knowing. The httpx
client inherits the API client's `follow_redirects` (off), matching the
httpx transport this replaced — so an unexpected hop surfaces as a
failed upload rather than a silent re-upload.
- `verify_ssl=False` on the generated client is no longer honored for
uploads (pyqwest has no insecure-TLS option), and `http2=False` is gone
(S3 negotiates HTTP/1.1 via ALPN anyway).
- The 1-hour upload timeout now bounds the entire upload rather than
each socket write — arguably the intended meaning for that endpoint.

## Testing

- `tests/{sync,async}/*/test_upload_file.py` (the #1243 regression tests
— Content-Length present and equal to the body, no chunked encoding)
pass through pyqwest; the capture handlers now compare header names
case-insensitively since hyper lowercases them where httpcore
title-cased.
- New in both mirrors: `test_upload_file_leaves_redirects_to_httpx` — a
307 on the upload URL surfaces as `FileUploadException` and the capture
server sees exactly one PUT, guarding against reqwest silently following
the hop and replaying the archive.
- Lint (`ruff`), typecheck (`ty`), upload-file suites: green (10/10).

## Usage example

No API changes — template builds upload their context exactly as before:

```python
from e2b import Template

template = Template().from_image("ubuntu:22.04").copy("data/", "/data")
Template.build(template, alias="my-template")   # archive upload now goes through pyqwest
```

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:46:38 +02:00
Mish Ushakov 458c2c4362 feat(python-sdk): move the volume content client onto pyqwest (#1602)
## What

Stacked on #1601. Migrates the **volume content client**
(`Volume`/`AsyncVolume` file operations) onto
[pyqwest](https://github.com/curioswitch/pyqwest) via its
httpx-compatible transport adapter — the same stock httpx transport
adapter + connection-retry stack the REST API client uses after #1601.

Originally deferred from #1601 because
`Volume.read_file(format="stream")` relied on httpx's per-read `read`
timeout as an *idle* timeout, which the adapter can't express per
request (it converts the httpx timeout dict into a whole-request
deadline, and the sync adapter doesn't bound body reads at all).
Unblocked by pyqwest's transport-constructor `read_timeout`, which maps
to reqwest's `ClientBuilder::read_timeout` — verified behaviorally
(local slow-chunk server, sync + async) to be a true per-read idle
timeout: it resets after each successful read, covers body reads, and a
healthy stream longer than the timeout completes untouched.


> [!NOTE]
> Rebased onto #1601, which maps `httpx.Proxy` onto pyqwest's `Proxy`
object and locks pyqwest 0.9.0. Following that: this PR builds its
transport from `proxy_to_config(...)` instead of `proxy_to_url(...)`,
uses the stock `PyqwestTransport`/`AsyncPyqwestTransport` (0.9.0's
adapter drops the redundant `Host` header and maps pyqwest's timeouts
and connection, network, and protocol failures to their httpx
counterparts, so the SDK's transport subclasses are gone), and turns
reqwest's internal redirects off so httpx owns them, as the generated
volume client expects.

## How

- `e2b/volume/client_sync/__init__.py` / `client_async/__init__.py` move
to the same stock adapter + connection-retry stack as the API client.
Caches become process-global, keyed by (proxy, streaming) — previously
one pool per thread (sync) / per event loop (async).
- Streamed downloads go through a **dedicated streaming transport** with
`read_timeout=60s`. It can't live on the shared transport: reqwest's
read timer keeps running while a request body is sent and while waiting
for the response head (verified empirically — a 2.4 s upload against a
0.5 s `read_timeout` dies mid-send), so a shared `read_timeout` would
cut off `write_file` uploads and slow unary responses longer than the
idle bound. Uploads and unary calls stay on a transport without it,
bounded by their whole-request deadlines as before.
- The 60 s default matches the JS SDK exactly: JS bounds stream start by
`requestTimeoutMs` (60 s default) and idle gaps by `streamIdleTimeoutMs
?? requestTimeoutMs`; the Python streaming transport's `read_timeout`
bounds the response head and each idle gap at 60 s, resetting on every
chunk, wire-only (a slow consumer doesn't trip it — verified).
- `AsyncVolume.read_file` keeps honoring an explicit
`stream_idle_timeout` **per call**, the same way JS honors
`streamIdleTimeoutMs` and #1558 bounds stream setup: `asyncio.wait_for`
around each read (response head and every chunk). Explicit values run on
the *regular* transport, so a value above the 60 s transport bound isn't
capped by it and `0` disables idle bounding entirely, restoring the
previous contract. The sync client keeps the parameter but **ignores**
it — it has no way to interrupt a blocking read into the Rust transport,
so its bound must live in the transport.
- Streamed reads are sent without a per-request timeout so the adapter
imposes no whole-request deadline on long downloads; an explicitly
passed `request_timeout` becomes the total-transfer deadline.
- A stalled read surfaces as `httpx.ReadTimeout`, keeping the
established contract: the 0.9.0 adapter maps its own timeouts, and the
async flavor remaps the per-read `stream_idle_timeout` (an
`asyncio.wait_for` expiry) to match.
- Proxy narrowing follows #1601: `str`, `httpx.URL`, and reducible
`httpx.Proxy` values work; inexpressible extras raise
`InvalidArgumentException`.

## Testing

- `tests/test_volume_client.py` rewritten: process-global transport
caching (shared across threads and event loops), streaming vs regular
transport separation, plus end-to-end streamed reads through
`Volume.read_file`/`AsyncVolume.read_file` against a local chunked
server — a healthy stream longer than the idle timeout completes (proves
the timeout resets per read), a mid-body stall raises
`httpx.ReadTimeout`, a slow response head on a *non-streamed* read is
not cut off by the idle bound, and a slow response head on a streamed
read is (JS handshake-timeout parity). Async `stream_idle_timeout`: an
explicit value aborts a stall, a value above the transport bound isn't
capped by it, and `0` disables idle bounding.
- Volume content integration tests couldn't run end-to-end (the test
team's key gets `403: use of volumes is not enabled`); the
mock-transport volume content tests and the local-server stream tests
cover that path.
- Lint (`ruff`), typecheck (`ty`), unit suite: green.

## Usage example

No API changes for the common path:

```python
volume = Volume.connect(volume_id, token=token)
stream = volume.read_file("big.bin", format="stream")     # stalls bounded by the
for chunk in stream:                                      # transport-wide idle read
    ...                                                   # timeout (httpx.ReadTimeout)

volume.read_file("big.bin", format="stream", stream_idle_timeout=5)  # sync: accepted, ignored

async_volume = await AsyncVolume.connect(volume_id, token=token)
stream = await async_volume.read_file(
    "big.bin", format="stream", stream_idle_timeout=5     # async: honored per read,
)                                                         # 0 disables idle bounding
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:46:37 +02:00
Mish Ushakov a874ced97a feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter (#1601)
## What

Migrate all httpx REST API client traffic in the Python SDK — the E2B
control plane (sandbox lifecycle, listing, templates, volumes control
plane) — to [pyqwest](https://github.com/curioswitch/pyqwest) (Rust
reqwest/hyper), using its httpx-compatible transport adapter
(`pyqwest.httpx.PyqwestTransport` / `AsyncPyqwestTransport`). The
generated openapi client and `ApiClient`/`AsyncApiClient` keep their
httpx surface — logging event hooks, per-request timeouts, headers, and
redirects behave as before — only the transport underneath is swapped.

envd RPC already runs on pyqwest via connectrpc (#1558). This PR touches
only the control-plane client; the rest of the stack builds on it: #1623
(envd HTTP API client), #1602 (volume content client), #1603 (template
uploads).

Requires **pyqwest 0.9** — pinned in `pyproject.toml` (`>=0.9.0,<0.10`)
with `uv.lock` refreshed. 0.8 brought the `Proxy` object
([pyqwest#194](https://github.com/curioswitch/pyqwest/pull/194)) and
request loggers
([pyqwest#197](https://github.com/curioswitch/pyqwest/pull/197)); 0.9
([release
notes](https://github.com/curioswitch/pyqwest/discussions/214)) folds
the two adapter workarounds this PR used to carry into the adapter
itself and makes redirect handling configurable, so the SDK no longer
subclasses the adapter at all.

## How

- `e2b/api/client_sync/__init__.py` / `client_async/__init__.py`:
`get_transport` now returns a pyqwest-backed httpx transport — a
`SyncHTTPTransport`/`HTTPTransport` (`tls_include_system_certs=True`,
proxy, pool tuning mapped from
`E2B_KEEPALIVE_EXPIRY`/`E2B_MAX_KEEPALIVE_CONNECTIONS`), wrapped in a
`ConnectionRetryTransport` for connect-only retries honoring
`E2B_CONNECTION_RETRIES`, wrapped in the stock
`PyqwestTransport`/`AsyncPyqwestTransport` httpx adapter.
- pyqwest transports are thread-safe and loop-independent (I/O runs on a
Rust tokio runtime), so the caches are process-global keyed by proxy —
previously one pool per thread (sync) / per event loop (async).
- **`ApiClient` sheds its threading machinery**: the
`transport_factory`/`async_transport_factory` plumbing, the thread-local
`httpx.Client` cache, and the per-loop `WeakKeyDictionary` of
`AsyncClient`s are gone. A single lazily-created httpx client (the
generated base behavior, the same shape the volume client already uses)
serves all threads and event loops; `httpx.Client` is documented
thread-safe and nothing below it is loop-bound. Closing that client
can't tear down the shared pool — the adapter transports don't override
`close()`/`aclose()`.
- **Host header** (upstream in 0.9): sending the `Host` header httpx
auto-adds on an HTTP/2 connection makes the E2B API edge reset the
stream with `PROTOCOL_ERROR` (reproduced with plain pyqwest against
`api.e2b.app`); hyper derives `Host`/`:authority` from the URL. The
adapter now skips a `host` header matching the URL, so the SDK-side
strip is gone — and unlike that strip, a genuinely custom `Host`
override is still forwarded.
- **Timeout exceptions** (upstream in 0.9): pyqwest raises the builtin
`TimeoutError`; the adapter maps it to `httpx.ReadTimeout` both while
awaiting the response head and while reading the body, preserving the
`httpx.TimeoutException` contract for callers. Connection, network, and
protocol failures likewise arrive as
`httpx.ConnectError`/`ConnectTimeout`, `httpx.ReadError`/`WriteError`,
and `httpx.RemoteProtocolError` instead of leaking pyqwest/builtin
types.
- **Redirects**: the pyqwest transports are built with
`follow_redirects=False` (0.9 made it configurable; reqwest's default is
to follow). Otherwise redirects are followed inside the transport,
hiding 3xx responses from httpx and leaving `response.history` empty —
even though the generated clients ask for no redirect following. httpx
owns them again, as with the transports this replaced.
- **Proxy**: `proxy=` accepts a URL string, `httpx.URL`, or an
`httpx.Proxy` — including its credentials (sent as
`Proxy-Authorization`) and any headers configured for the proxy, via
pyqwest's `Proxy` object. `proxy_to_config` normalizes all three into a
`ProxyConfig` tuple that both keys the transport cache and builds the
`pyqwest.Proxy`, so the same proxy URL with different credentials or
headers gets its own pool. A per-proxy `ssl_context` has no counterpart
and raises `InvalidArgumentException` rather than being silently
dropped. (`ProxyConfig` is a `NamedTuple`, not a frozen dataclass:
`tests/test_env_var_parsing.py` reloads `e2b.api`, and a dataclass
`__eq__` compares class identity, so keys built before and after a
reload would silently stop matching.)
- **`ProxyTypes` is ours now**: the public type of the `proxy` option
(already exported from `e2b`) used to be imported at runtime from
httpx's private `_types` module in eleven modules. It is defined there
as `Union[str, URL, Proxy]` — exactly the three forms the SDK's two
narrowers accept — so it's spelled out once in `e2b.connection_config`
and imported from there. Same public name, same type to a type checker,
no private-module dependency, and a place for a pyqwest proxy type to
land as the remaining transports move off httpx.
`e2b.envd.client_shared.proxy_to_url` took a bare `object` while
`e2b.api.proxy_to_config` took `Optional[ProxyTypes]`; both now say the
same thing. `isinstance` narrowing stays rather than duck-typing
`.url`/`.auth` — httpx is a required dependency here (the generated REST
client *is* an httpx client, and envd file transfers use httpx
directly), so probing attributes would trade a clear
`InvalidArgumentException` on a mistyped argument for no dependency
savings.
- **Request logs**: pyqwest logs one line per request on the
`pyqwest.access` logger and lifecycle records on `pyqwest`, both at
`DEBUG` — the transport-level diagnostics httpcore used to provide, now
that httpcore is out of the path. Noted on `get_transport`; the SDK's
own `logger` option is unchanged and sits above it on the httpx client.
- **HTTP/2**: negotiated via ALPN for TLS connections (reqwest default),
equivalent to the `http2=True` transports this replaces.

## What stays behind (handled by the stacked PRs)

- **envd HTTP API client** (file transfers, health checks): #1623, which
also dedupes the transport plumbing this PR and #1558 each carry a copy
of (the proxy narrowing, pool tuning, retry transport — envd keeps
byte-identical duplicates until then).
- **Volume content client**: its streaming download relies on httpx's
per-read `read` timeout as an *idle* timeout, which the adapter can't
express per request — #1602.
- **Template build context upload**: one-off httpx client PUTing to S3
presigned URLs — #1603.

## Timeout semantics note

`request_timeout` was previously httpx's per-phase timeout
(connect/read/write each bounded separately, so a slow multi-phase
request could exceed it in total). Through the adapter it becomes an
overall deadline per API call (async: headers + body; sync: up to
response headers). For the SDK's REST calls — all unary with small JSON
bodies — this is a tightening, arguably closer to what `request_timeout`
promises.

## Testing

- `tests/test_api_client_transport.py` rewritten for the new semantics:
global per-proxy transport caching, a single httpx client shared across
threads/loops (including 32-way concurrent request tests against a local
server), timeout → `httpx.ReadTimeout` mapping for both the response
head and a stalled body (slow/stalling local server), redirects
surfacing to httpx (302 returned as-is, `response.history` populated
when the caller opts in), the connection-only retry policy,
`proxy_to_config` conversion, and sync+async round-trips through a real
local HTTP server exercising pyqwest end to end. The two host-header
unit tests are gone with the subclasses they tested — that behavior is
the adapter's now.
- Two tests cover the pyqwest proxy/logging surface: an echo server
standing in for a proxy asserts that the absolute-form request target,
`Proxy-Authorization`, and the extra proxy header actually arrive, and
the `pyqwest.access` record is asserted for an API call.
- On pyqwest 0.9.0 from PyPI: `uv sync --locked`, unit suite
(`tests/*.py`, 238 passed), `ruff check`, `ty check` — all green.
- Integration against the production API (real key) was run on 0.8.0:
`tests/sync/api_sync`, `tests/async/api_async`,
create/kill/timeout/connect — all green. (These initially failed with
`RemoteProtocolError: StreamReset` until the host header stopped being
forwarded, so they genuinely exercise the new stack; that fix now comes
from the adapter.)

## Usage example

No API changes for the common path:

```python
from e2b import Sandbox

sbx = Sandbox.create()          # control-plane calls now go through pyqwest
Sandbox.list()
sbx.kill()
```

Proxy handling — URL strings and `httpx.Proxy` objects work, credentials
and proxy headers included:

```python
Sandbox.create(proxy="http://user:pass@localhost:8030")            # ok (unchanged)
Sandbox.create(proxy=httpx.Proxy("http://localhost:8030",
                                 auth=("user", "pass")))           # sent as Proxy-Authorization
Sandbox.create(proxy=httpx.Proxy("http://localhost:8030",
                                 headers={"X-Auth": "t"}))         # sent to the proxy
Sandbox.create(proxy=httpx.Proxy("https://localhost:8030",
                                 ssl_context=ctx))                 # raises InvalidArgumentException
```

`ProxyTypes` — already exported from `e2b` — is now defined by the SDK
rather than re-exported from `httpx._types`, with the same three
members:

```python
from e2b import ProxyTypes   # Union[str, httpx.URL, httpx.Proxy]
```

Transport-level HTTP logs, replacing the httpcore records this migration
removes:

```python
import logging

logging.basicConfig()
logging.getLogger("pyqwest.access").setLevel(logging.DEBUG)

Sandbox.create()
# DEBUG pyqwest.access - HTTP Request: POST https://api.e2b.app/sandboxes "HTTP/2 201 Created"
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:46:37 +02:00
Mish Ushakov cab27aa6fa fix(sdk): clean up sandbox when MCP gateway startup fails (#1548)
## Problem

Fixes #1498.

`Sandbox.create` allocates a remote sandbox before starting
`mcp-gateway`. If gateway startup fails, creation throws before the
sandbox object is returned. As a result, the caller has no sandbox ID to
clean up, and the orphaned sandbox continues consuming resources until
it times out.

This state transition exists in synchronous Python, asynchronous Python,
and JavaScript/TypeScript.

## Changes

- Add a rollback boundary around MCP gateway startup in all three SDK
implementations: on failure, best-effort kill the newly allocated
sandbox, then re-raise.
- Surface gateway startup failure as `SandboxError` (JS) /
`SandboxException` (Python) with a `Failed to start MCP gateway:
<stderr>` message. Previously the intended message was unreachable dead
code — foreground `commands.run` already throws on non-zero exit — so
callers got a bare `CommandExitError`/`CommandExitException`.
- In async Python, re-raise `asyncio.CancelledError` from the
best-effort `kill()` so caller cancellation (e.g. `asyncio.timeout`) is
honored; only ordinary cleanup failures are suppressed and never mask
the original error.
- Add integration coverage for synchronous Python, asynchronous Python,
and TypeScript. The tests pin the sandbox to the base template (which
has no `mcp-gateway` binary) so gateway startup genuinely fails after
allocation.
- Add a patch changeset for `e2b` and `@e2b/python-sdk`.

## Usage Behavior

No API changes. A failed creation no longer leaves a sandbox behind, and
the error is now descriptive:

```ts
try {
  const sandbox = await Sandbox.create({ mcp: { ... } })
} catch (err) {
  // err is SandboxError: "Failed to start MCP gateway: <stderr>"
  // the allocated sandbox has already been killed — no orphan is left running
}
```

## Validation

All three integration tests verified against real infra: creation
rejects with the documented error and no sandbox remains.

## Notes

Supersedes #1547 by @hxaxd (squash-merged into this branch to preserve
attribution).

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

---------

Co-authored-by: 苏紫辰 <155808914+hxaxd@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 17:26:08 +02:00
github-actions[bot] e6111419b5 [skip ci] Release new versions 2026-08-07 14:48:17 +00:00
Mish Ushakov d5a382ed67 chore(js-sdk): bump undici to ^7.29.0 and optional undici8 to 8.10.0 (#1645)
Bumps both undici dependencies in the js-sdk past the 2026-07-24
security advisories: the required `undici` from `^7.28.0` to `^7.29.0`,
and the optional `undici8` (`npm:undici@…`) from 8.8.0 to 8.10.0. Both
releases patch one High
([GHSA-4cwx-7wf7-3272](https://github.com/nodejs/undici/security/advisories/GHSA-4cwx-7wf7-3272),
cache-control parsing / cross-user disclosure) and four Medium
advisories, clearing the open Dependabot alerts for undici; 8.10.0
additionally fixes HTTP/2 request settling, refused-stream retries and
GOAWAY handling, which we exercise because every dispatcher the SDK
builds sets `allowH2: true`.

A root `pnpm.overrides` entry (`undici@>=7.0.0 <7.29.0`) is included
because miniflare pins undici at exactly 7.28.0, which would otherwise
keep a vulnerable copy in the lockfile; with it, the lockfile carries
only 7.29.0 and 8.10.0. No code change was needed and there is no
user-facing API change — 7.29.0 still requires Node `>=20.18.1` and
8.10.0 still requires `>=22.19.0`, matching the `UNDICI_8_MIN_NODE` gate
in `packages/js-sdk/src/undici.ts`, so `getUndiciPackageCandidates()`
picks the same package on the same Node versions.

`format`, `lint` and `typecheck` pass, `tests/undici.test.ts` is 9/9,
and `test:cf` was run to confirm miniflare still boots on the overridden
undici. The remaining vitest projects need `E2B_API_KEY`, which isn't
available locally, so they're left to CI. A patch changeset for `e2b` is
included.

Linear:
[SDK-317](https://linear.app/e2b/issue/SDK-317/js-sdk-bump-optional-undici8-dependency-to-8100)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:29:06 +00:00
github-actions[bot] 2d2823c94a [skip ci] Release new versions 2026-08-07 14:15:10 +00:00
Mish Ushakov 88f41f3927 fix(python-sdk): port current JS stripAnsi regex to strip_ansi_escape_codes (#1545)
## Summary

The Python SDK's `strip_ansi_escape_codes` (used to clean template build
log messages) still used the old ansi-regex pattern, while the JS SDK's
`stripAnsi` was rewritten in #895. This ports the current JS regex to
Python so both SDKs clean logs identically: OSC sequences (hyperlinks,
window titles) are matched non-greedily up to the first string
terminator — including content spanning newlines — and CSI sequences are
stripped without requiring a terminator.

Following review feedback, both implementations now also strip the
remaining ECMA-48 string controls — DCS (Sixel, tmux passthrough), SOS,
PM, and APC — through their string terminator, so control payloads don't
leak into cleaned logs. This goes beyond upstream `chalk/ansi-regex`,
click, and Rich, none of which fully strip DCS payloads, and restores
what the old Python pattern handled.

Also mirrors the Python test suite into the JS SDK (which previously had
no `stripAnsi` tests) — 20 identical cases per side — and verified
byte-for-byte identical output between the two implementations on all of
them. Includes a patch changeset for `e2b` and `@e2b/python-sdk`.

## Example

Log messages that previously leaked OSC or DCS sequences into template
build output are now cleaned:

```python
from e2b.template.utils import strip_ansi_escape_codes

strip_ansi_escape_codes("\x1b]8;;https://e2b.dev\x07E2B\x1b]8;;\x07")  # "E2B"
strip_ansi_escape_codes("\x1b]0;title\nstill title\x07done")           # "done"
strip_ansi_escape_codes("\x1b[38:2::255:0:0mRED\x1b[0m")               # "RED"
strip_ansi_escape_codes("\x1bPq#0;2;0;0;0~~@@\x1b\\image")             # "image" (Sixel DCS)
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 15:04:40 -07:00
Mish Ushakov 998e560a1a fix(python-sdk): relax wcmatch constraint to >=10.1,<12 (#1638) 2026-08-05 19:08:11 +02:00
Mish Ushakov 86f7b8e2f8 fix(js-sdk): export the Git argument and status types (#1642)
Carries the change from #1635 (by @karpovantonme) into `main` as a
single squash commit — #1635 was retargeted at
`fix/export-git-argument-types`, merged there, and this PR promotes that
branch.

`Git.reset()`, `Git.restore()` and `Git.status()` are public, but the
types naming their arguments and results were not reachable from the
package entry point.

`src/index.ts` re-exported fifteen `Git*` types and omitted
`GitResetMode`, `GitResetOpts` and `GitRestoreOpts`. `GitStatusLabel`
was worse off — `src/sandbox/git/index.ts` re-exported `GitBranches`,
`GitConfigScope`, `GitFileStatus` and `GitStatus` from `./utils` but not
`GitStatusLabel`, so it was unreachable from anywhere in the package,
even though it is the type of `GitFileStatus.status`.

The practical effect: you could call the methods, but you could not name
what you pass them, so you could not write a typed wrapper.

```ts
// before — all four fail
import type {
  GitResetMode,
  GitResetOpts,
  GitRestoreOpts,
  GitStatusLabel,
} from 'e2b'

// the workaround people end up with
type ResetMode = Parameters<Git['reset']>[0] extends { mode?: infer M } ? M : never
```

```ts
// after
import { Sandbox } from 'e2b'
import type { GitResetMode, GitResetOpts, GitStatusLabel } from 'e2b'

async function hardResetTo(sbx: Sandbox, repo: string, target: string) {
  const mode: GitResetMode = 'hard'
  const opts: GitResetOpts = { mode, target, cwd: repo }
  return sbx.git.reset(opts)
}

function isBlocking(status: GitStatusLabel) {
  return status === 'conflict' || status === 'deleted'
}
```

## On SDK parity

Python already exports `GitResetMode` (`e2b.GitResetMode`), so this
brings JS up to it. The other three have no Python counterpart by
design: the sync and async implementations take keyword arguments rather
than option objects, so there is nothing shaped like
`GitResetOpts`/`GitRestoreOpts`, and `GitFileStatus.status` is typed as
a plain `str` there, so there is no `GitStatusLabel` either. Nothing to
mirror on the Python side.

## Notes

- Type-only re-exports, no runtime change. Changeset included (`e2b`:
patch).
- No test: a missing re-export is invisible to `tsc --noEmit` because
`packages/js-sdk/tsconfig.json` includes only `src`, so an `import type
… from '../src'` test passes either way. A declaration-reading test was
dropped from #1635 during review.

## Not touched

The same gap exists for a few non-Git types — `FilesystemListOpts`,
`WatchOpts`, `PtyCreateOpts` and `PtyConnectOpts` are exported from
their own modules but not from `src/index.ts`. Scope kept to the Git
surface, as in #1635.

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

Co-authored-by: Anton Karpov <30812217+karpovantonme@users.noreply.github.com>
Co-authored-by: Anton Karpov <karpovantonme@gmail.com>
2026-08-05 15:49:02 +02:00
github-actions[bot] 7a1fe4528c [skip ci] Release new versions 2026-08-03 19:45:46 +00:00
Joe Lombrozo 2821fb0b69 feat(sdk): route volume content to BYOC cluster domain (#1634)
When a team is connected to a custom (BYOC) cluster, the volume API now
returns that cluster's domain in the create and get responses. The JS
and Python (sync + async) SDKs use this domain as the destination for
volume content requests instead of the default api.<E2B_DOMAIN> host,
falling back to the configured domain when none is returned.

The domain field is read defensively from the response until
spec/infra-ref is bumped to the infra commit that adds it and `make
codegen` regenerates the typed schema.


Claude-Session: https://claude.ai/code/session_01212WCmNz1prPKrjhTv2PDj

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Matt Brockman <matt.brockman@e2b.dev>
2026-08-03 10:24:15 -07:00
github-actions[bot] 9ef3f1dbbe [skip ci] Release new versions 2026-07-31 19:39:47 +00:00
Mish Ushakov 1ebe925ee0 fix(js-sdk): detect web platform objects by shape, not by class (#1618)
SDK-299

## Symptom

Two shapes of failure, one cause.

Every control-plane call crashing in an app that embeds the SDK next to
a server shim:

```
TypeError: Failed to parse URL from [object Request]
    at fetch (…/undici/index.js:157:10)
    at wrapped (…/e2b/src/undici.ts:126)
```

…and, quietly, uploads that arrive at the sandbox containing the eight
bytes `[object Blob]` instead of the file.

## Root cause

`value instanceof Blob` does not answer *"is this a Blob"*, it answers
*"was this minted by the `Blob` class this module happens to see"*. In a
Node process those are different questions: libraries replace the web
globals exactly the way they replace `globalThis.fetch` —
`@hono/node-server` installs its own `Request`, remix's
`installGlobals()` swaps `Request`/`Blob`/`File`, `web-streams-polyfill`
swaps `ReadableStream`, jsdom-style test environments bring their own
copies of all of them — and values also cross realms (`node:vm`,
`worker_threads`). `src/undici.ts` already late-binds the global `fetch`
for this reason; the brand checks never got the same treatment.

It is reachable with a **single** shim install, no exotic dependency
duplication:

1. `openapi-fetch` captures `Request: CustomRequest =
globalThis.Request` when the client is created (`dist/index.mjs:11`) and
mints every request from it,
2. `EnvdApiClient` is built once in the `Sandbox` constructor and stored
(`src/sandbox/index.ts:201`), so it outlives anything that swaps the
global afterwards,
3. from then on the SDK checks each request against a class that did not
mint it. Which copy "wins" the global is import-order dependent, so the
crash appears and disappears with unrelated dependency changes.

Verified locally, frame for frame: real `undici`/`undici8` throw
`TypeError: Failed to parse URL from [object Request]` for **any**
`Request` they did not mint — including Node's native one — so the
destructure in `toUndiciRequestInput` is load-bearing and a missed brand
check is fatal rather than merely slower.

## The whole family

Every brand check on the data path had the same defect, and each one
failed differently:

| Site | Misfires on | User-visible effect |
| --- | --- | --- |
| `undici.ts` `toUndiciRequestInput` | foreign `Request` | **every API
call throws** `Failed to parse URL from [object Request]` |
| `api/inflight.ts` `limitConcurrency` | foreign `Request` | abort
signal ignored while the request waits for a slot |
| `utils.ts` `toBlob` | foreign `Blob` | upload body is the text
`"[object Blob]"` |
| `utils.ts` `toBlob` | foreign `ReadableStream` | upload body is the
text `"[object ReadableStream]"` |
| `utils.ts` `toUploadBody` (gzip) | foreign `ReadableStream` |
`pipeThrough(new CompressionStream())` never settles — the upload hangs
|
| `utils.ts` `toUploadBody` | foreign `ReadableStream` | file buffered
into memory instead of streamed (OOM on large files) |
| `filesystem/index.ts` `hasStreamableData` | foreign `ReadableStream` |
same, plus the multipart path is chosen for a stream |
| `volume/index.ts` `readFile` | foreign `Blob`/`ArrayBuffer` |
**returns an empty file** |
| `undici.ts` `toUndiciRequestInput` (body) | foreign `Request`'s stream
body | body sent as the text `"[object ReadableStream]"` |

The `Blob`/stream rows are silent data corruption, confirmed against
real undici:

```js
await new Response(foreignBlob).text() // → "[object Blob]"
await new Response(foreignStream).text() // → "[object ReadableStream]"
```

## Fix

New internal `src/is.ts` asks what a value *is*: `instanceof` stays the
fast path, then it falls back to the members and `Symbol.toStringTag`
the platform guarantees (`isRequestLike`, `isBlobLike`,
`isReadableStreamLike`, `isArrayBufferLike`). Nothing is added to the
public surface.

Detection alone is not enough where the SDK hands data back to the
platform — the platform brand-checks too, and a detected-but-not-adopted
foreign stream would be stringified instead of buffered, i.e. worse than
before. So the conversions adopt what they detect:

- `toBlob` copies a foreign `Blob`'s bytes (`new Blob([await
data.arrayBuffer()], { type: data.type })`) and pumps a foreign stream
through a native one via its reader;
- the adoption itself is not class-dependent, which took two rounds to
get right (see *Adoption* below);
- `toUploadBody` returns `{ body, streamed }` instead of leaving
`filesystem`/`volume` to re-derive "did it stream?" with another brand
check on the result — only that function knows the decision it made.

### What used to break

```ts
import { serve } from '@hono/node-server' // installs its own globalThis.Request
import { Sandbox } from 'e2b'

const sandbox = await Sandbox.create()
await sandbox.files.write('/tmp/a.txt', 'hi') // TypeError: Failed to parse URL from [object Request]
```

```ts
import { ReadableStream } from 'web-streams-polyfill' // not the native class

// Used to upload the literal text "[object ReadableStream]"; with gzip it hung.
await sandbox.files.write('/tmp/big.bin', bigPolyfillStream, { gzip: true })
```

### Adoption

The platform accepts exactly two kinds of stream body: **its own
class**, and **any async iterable** (verified against `undici@8`/Node —
everything else is stringified). Async iterability is the half that
survives a replaced global, since a native stream stays async-iterable
even when `globalThis.ReadableStream` is a polyfill. Hence two helpers,
each named for the contract it satisfies:

- `toDispatchableStream` — for request bodies: passes through the
platform's own class *or* an async iterable, adopts the rest. Adopting
on `!(stream instanceof ReadableStream)` alone would have been
class-dependent in the same way as the bug: with a polyfilled global, a
perfectly good native stream fails the check and gets re-wrapped into a
polyfill instance the platform likes *less*.
- `toNativeStream` — for `pipeThrough(new CompressionStream(…))`, the
stricter consumer: it insists on its own class, so async iterability is
not enough there and every foreign stream is adopted.

Everything that isn't already a stream reaches the gzip path through
`toBlob`, whose result is always a native `Blob`, so the gzip path needs
no blob branch of its own. That in turn means nothing ever calls
`stream()` on a `Blob` the SDK didn't make, so `isBlobLike` only
requires `arrayBuffer` beyond the tag — one less requirement is one less
implementation whose upload would silently be the text `"[object
Blob]"`.

The same reasoning applies one level down: a `Request` from another
fetch implementation exposes *that* implementation's stream as its
`body`, so accepting the Request without adopting its body would only
move the stringification, not remove it.

## Tests

`tests/foreignPlatformObjects.ts` provides the fixtures: `ForeignBlob`
and `foreignReadableStream` are separate implementations rather than
subclasses (a subclass still passes `instanceof`, so it would prove
nothing), and `foreignRequestClasses()` returns two sibling subclasses
of the native `Request` — instances of one are fully functional Requests
that the other disowns, which is what the shims actually produce.

- `tests/is.test.ts` — the four predicates, including the negatives that
keep them honest (a `URL`, a string, an `IncomingMessage`-shaped `{ url,
method, headers }`).
- `tests/utils.test.ts` — content round-trips for
`toBlob`/`toUploadBody` across native/foreign `Blob`s and streams, plus
a gzip round-trip through `DecompressionStream` for all four input
kinds.
- `tests/undici.test.ts` — a disowned `Request` reaches undici
destructured as `(url, init)`; and, on Node, the same request goes
through the **real** undici `fetch` (via `MockAgent`, so no network) and
is matched on method, path and headers.
- `tests/api/inflight.test.ts` — an already-aborted disowned `Request`
rejects with `AbortError` without consuming a slot.

Each adoption rule has a test that fails without it: dropping the
async-iterable clause fails the "does not re-wrap a native stream when
the global class was replaced" case, using `toDispatchableStream` in the
gzip path fails the async-iterable gzip case, and dropping the body
adoption fails with `expected '[object ReadableStream]' to be 'hello'`.

Red/green: with `src/` reverted, the three Request tests fail (the
undici one with the production error, `ERR_INVALID_URL { input: '[object
Request]' }`) and the data-path tests fail with `expected '[object
Blob]' to be 'hello'` / `expected '[object ReadableStream]' to be
'hello'`.

Verification run: unit + `connectionConfig` (129), `tests/sandbox/files`
+ `tests/volume` against prod (92 passed, real streamed/gzipped
uploads), all four changed files green on Node, Bun, Deno and workerd,
plus `tsc`, `lint`, `prettier` and `build`.

## Out of scope

`network.rules instanceof Map` (`sandboxApi.ts`) and the `instanceof
Error` checks are left alone: nothing replaces `globalThis.Map`, and the
errors are ours. Python needs no counterpart — its REST stack takes
bytes/iterables and has no equivalent brand checks.

Supersedes #1610 (@himself65), which fixed the `Request` half of this
and diagnosed the crash; the reproduction there is what led to auditing
the rest.


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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-31 12:28:51 -07:00
Mish Ushakov 2df7651ee6 test(sdk): run firewall transform tests against an httpbin sidecar sandbox (#1631)
Follow-up to #1632, which added the template this depends on. Now
rebased onto `main`, so this is just the test change.

## Problem

The firewall transform tests asserted header injection by curling
`httpbin.e2b.team`, an externally hosted service the suite had to keep
alive.

## Fix

Starts a sidecar sandbox from the `httpbin` template instead: the rule
is keyed on the sidecar's `getHost(8080)` and the assertion reads the
injected header back from `/headers`, in the JS, sync Python, and async
Python suites. The sidecar's ready command has already passed by the
time `create` resolves, so the server is serving and no readiness
polling is needed. The template name lives in one fixture per SDK —
`httpbinTemplate` in `tests/template.ts` and the `httpbin_template`
fixture in `conftest.py`.

Also drops two comments merged in #1632 that claimed the tests spawn
`e2b/httpbin`. The bare alias is what resolves, same as `base` — the
team slug only appears in the display name.

⚠️ Do not merge before **Build and push prepared templates** has been
dispatched with `template: httpbin` — the tests resolve the template by
name and fail until it exists on the E2B team.

Verified against production: all three tests pass with the injected
header reflected by the sidecar, spawning the template by its bare alias
with a key that owns it — the same situation as CI.

SDK-304

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-30 23:37:14 +02:00
Mish Ushakov 4a2571d321 test(js-sdk): wait for workers.dev propagation in the CF deploy suite (#1592)
## Problem

The `cloudflare-deploy` CI job intermittently fails with `non-JSON
response (404): <!DOCTYPE html>...` ([example
run](https://github.com/e2b-dev/E2B/actions/runs/30009788154/job/89214641280)).
The truncated HTML boilerplate is easy to mistake for a Cloudflare
captcha/challenge page, but it's the standard Cloudflare **404** page:
each `wrangler deploy --temporary` lands on a brand-new account
subdomain (`e2b-js-sdk-smoke.<random>.workers.dev`), and Cloudflare
serves "nothing is here yet" until the route propagates to the edge. The
in-test retry window (initial attempt + 10 retries × 3s ≈ 35s) wasn't
always enough.

## Fix

- `setup.mts`: after the deploy, poll the worker URL until the worker
itself answers (405 to GET — the worker is POST-only), with a 240s
deadline. Tests only start once the route is live. Only the propagation
404 and thrown fetch errors (transient DNS/connect) keep the poll
waiting — any other status (403 challenge, 500 from a broken worker,
...) is a real failure and fails the setup immediately, with the error
page's `<title>` in the message.
- `run.test.ts`: retry only on the propagation 404 / `fetch failed`, so
other Cloudflare error pages propagate on the first attempt; include the
page `<title>` in the `non-JSON response` error, since the truncated
body is boilerplate shared by every Cloudflare error page.

Test-only change, no changeset.

## Verification

Ran `pnpm test:cf:deploy` against real Cloudflare (both revisions):

```
Deployed: https://e2b-js-sdk-smoke.quick-bike.workers.dev
Worker route not live yet (404), waiting...
Worker route not live yet (404), waiting...
Worker is live.

 Test Files  1 passed (1)
      Tests  1 passed (1)
```

The fresh subdomain served 404 for ~6s post-deploy — exactly the failure
mode from CI — then the suite passed on the first test attempt. `pnpm
run format`, `lint`, and `typecheck` pass.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 09:30:43 -07:00
dependabot[bot] 45d26792f1 chore(deps-dev): bump datamodel-code-generator from 0.34.0 to 0.64.0 in /packages/python-sdk in the uv group across 1 directory (#1621)
> [!NOTE]
> Manual follow-up commit on top of Dependabot's bump (addressing review
feedback): the codegen image pin was out of sync, so this PR also bumps
it and carries the regenerated output.

### Manual changes on top of the bump

- `codegen.Dockerfile` bumped from `datamodel-code-generator==0.34.0` to
`0.64.0`. `pyproject.toml`'s `codegen` group and the Dockerfile must
stay in sync (the comment above the group says so) — CI's `Generated
files` check regenerates from the image, so leaving the image at
`0.34.0` would make `make init` produce output CI rejects.
- `packages/python-sdk/e2b/sandbox/mcp.py` regenerated with `0.64.0`.
Two output changes:
- builtin generics (`list[str]`, `dict[str, Any]` instead of
`List`/`Dict`), fine on the SDK's `>=3.10` floor;
- `additionalProperties: false` in `spec/mcp-server.json` is now honored
as PEP 728 `closed=True` (`0.34.0` silently dropped it), and `TypedDict`
is imported from `typing_extensions` accordingly.
- `typing-extensions>=4.1.0` → `>=4.10.0`. `closed=True` is evaluated at
class-creation time, i.e. on `import e2b`, and 4.10.0 is the first
release whose `TypedDict` accepts the keyword (4.9.0 raises `TypeError:
_TypedDictMeta.__new__() got an unexpected keyword argument 'closed'`).
- Changeset added (`patch` for `@e2b/python-sdk`), since the regenerated
file and the dependency floor ship to users.

Nothing changes for callers at runtime — `McpServer` is still a plain
dict at the call site:

```python
from e2b import Sandbox

sbx = Sandbox.create(mcp={"duckduckgo": {}, "brave": {"braveApiKey": "..."}})
```

The `closed` types are also inert for type checkers in practice, because
the public `McpServer` is `Union[BaseMcpServer, GitHubMcpServer]` and
the second arm is a `Dict[str, ...]`. Verified: `pyright` and `mypy`
both clean against the snippet above, `ruff`/`ty`/`pnpm typecheck`
clean, 283 offline Python unit tests pass, and regenerating with the
full pinned toolchain (`python:3.10` + `black==26.3.1` + the other
Dockerfile pins) reproduces the committed file byte-for-byte.

---

Bumps the uv group with 1 update in the /packages/python-sdk directory:
[datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator).

Updates `datamodel-code-generator` from 0.34.0 to 0.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/koxudaxi/datamodel-code-generator/releases">datamodel-code-generator's
releases</a>.</em></p>
<blockquote>
<h2>0.64.0</h2>
<h2>Breaking Changes</h2>
<h3>Code Generation Changes</h3>
<ul>
<li>Self-referencing fields are now quoted with
<code>--disable-future-imports</code> - When
<code>--disable-future-imports</code> is set (no <code>from __future__
import annotations</code> and no native PEP 649 deferred evaluation on
Python &lt; 3.14), self-referencing and forward-referencing field
annotations in regular <code>BaseModel</code> classes are now emitted as
quoted forward references instead of bare names. Previously such
annotations were left unquoted, producing invalid code that raised
<code>NameError</code> (Ruff F821) at class-evaluation time. Output for
the common case (with <code>from __future__ import annotations</code> or
Python 3.14 native deferred annotations) is unchanged. Users who
snapshot/golden-file generated output for the
<code>--disable-future-imports</code> configuration with
self-referencing models will see the annotation change from unquoted to
quoted, e.g. <code>children: Optional[List[Node]]</code> →
<code>children: Optional[List[&quot;Node&quot;]]</code>. (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3387">#3387</a>)</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>Update CHANGELOG for 0.63.0 by <a
href="https://github.com/dcg-generated-docs"><code>@​dcg-generated-docs</code></a>[bot]
in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3345">koxudaxi/datamodel-code-generator#3345</a></li>
<li>Deduplicate module content builder by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3346">koxudaxi/datamodel-code-generator#3346</a></li>
<li>Deduplicate import reference helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3348">koxudaxi/datamodel-code-generator#3348</a></li>
<li>Refactor jsonschema root model registration by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3352">koxudaxi/datamodel-code-generator#3352</a></li>
<li>Refactor XML Schema literal helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3349">koxudaxi/datamodel-code-generator#3349</a></li>
<li>Move builtin formatter helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3351">koxudaxi/datamodel-code-generator#3351</a></li>
<li>Deduplicate Pydantic v2 config helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3350">koxudaxi/datamodel-code-generator#3350</a></li>
<li>Deduplicate DataType type hint rendering by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3354">koxudaxi/datamodel-code-generator#3354</a></li>
<li>Fix <code>constr()</code> for string fields carrying
minItems/maxItems by <a
href="https://github.com/DarkaMaul"><code>@​DarkaMaul</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3353">koxudaxi/datamodel-code-generator#3353</a></li>
<li>Cover non-finite import idempotence by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3367">koxudaxi/datamodel-code-generator#3367</a></li>
<li>Deduplicate input text detection by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3357">koxudaxi/datamodel-code-generator#3357</a></li>
<li>Remove stale protobuf coverage pragma by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3358">koxudaxi/datamodel-code-generator#3358</a></li>
<li>Cover explicit null OpenAPI media schemas by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3360">koxudaxi/datamodel-code-generator#3360</a></li>
<li>Simplify Python version feature checks by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3361">koxudaxi/datamodel-code-generator#3361</a></li>
<li>Speed up CI checks by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3378">koxudaxi/datamodel-code-generator#3378</a></li>
<li>Add maintainer link to docs footer and README by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3379">koxudaxi/datamodel-code-generator#3379</a></li>
<li>Use builtin formatter in CI by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3380">koxudaxi/datamodel-code-generator#3380</a></li>
<li>Split coverage by OS by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3381">koxudaxi/datamodel-code-generator#3381</a></li>
<li>Simplify import removal cleanup by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3362">koxudaxi/datamodel-code-generator#3362</a></li>
<li>Pin deprecation warning stacklevel by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3363">koxudaxi/datamodel-code-generator#3363</a></li>
<li>Pin public module exports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3364">koxudaxi/datamodel-code-generator#3364</a></li>
<li>Cover to_hashable branch cases by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3366">koxudaxi/datamodel-code-generator#3366</a></li>
<li>Cover stable toposort behavior by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3369">koxudaxi/datamodel-code-generator#3369</a></li>
<li>Extract registry render helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3371">koxudaxi/datamodel-code-generator#3371</a></li>
<li>Fix minItems for arrays of URI strings by <a
href="https://github.com/sjh9714"><code>@​sjh9714</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3377">koxudaxi/datamodel-code-generator#3377</a></li>
<li>Deduplicate config value validators by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3372">koxudaxi/datamodel-code-generator#3372</a></li>
<li>Cover CLI option metadata helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3374">koxudaxi/datamodel-code-generator#3374</a></li>
<li>Cover Pydantic v2 version fallback by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3368">koxudaxi/datamodel-code-generator#3368</a></li>
<li>Fix nullable JSON Schema const enums by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3355">koxudaxi/datamodel-code-generator#3355</a></li>
<li>Pin patchable generation seams by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3365">koxudaxi/datamodel-code-generator#3365</a></li>
<li>Cover utility helper behavior by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3375">koxudaxi/datamodel-code-generator#3375</a></li>
<li>Cover DefaultPutDict behavior by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3376">koxudaxi/datamodel-code-generator#3376</a></li>
<li>Cover validator config normalization by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3373">koxudaxi/datamodel-code-generator#3373</a></li>
<li>Avoid expensive runtime type checks by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3382">koxudaxi/datamodel-code-generator#3382</a></li>
<li>Avoid eager builtin formatter import by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3383">koxudaxi/datamodel-code-generator#3383</a></li>
<li>Avoid eager TOML parser import by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3384">koxudaxi/datamodel-code-generator#3384</a></li>
<li>Stabilize msgspec payload tests by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3385">koxudaxi/datamodel-code-generator#3385</a></li>
<li>Avoid eager input parser imports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3386">koxudaxi/datamodel-code-generator#3386</a></li>
<li>Avoid eager parser model imports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3388">koxudaxi/datamodel-code-generator#3388</a></li>
<li>Avoid eager AsyncAPI converter imports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3389">koxudaxi/datamodel-code-generator#3389</a></li>
<li>Dispose parser on parse errors by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3390">koxudaxi/datamodel-code-generator#3390</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/koxudaxi/datamodel-code-generator/blob/main/CHANGELOG.md">datamodel-code-generator's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/koxudaxi/datamodel-code-generator/releases/tag/0.64.0">0.64.0</a>
- 2026-06-14</h2>
<h2>Breaking Changes</h2>
<h3>Code Generation Changes</h3>
<ul>
<li>Self-referencing fields are now quoted with
<code>--disable-future-imports</code> - When
<code>--disable-future-imports</code> is set (no <code>from __future__
import annotations</code> and no native PEP 649 deferred evaluation on
Python &lt; 3.14), self-referencing and forward-referencing field
annotations in regular <code>BaseModel</code> classes are now emitted as
quoted forward references instead of bare names. Previously such
annotations were left unquoted, producing invalid code that raised
<code>NameError</code> (Ruff F821) at class-evaluation time. Output for
the common case (with <code>from __future__ import annotations</code> or
Python 3.14 native deferred annotations) is unchanged. Users who
snapshot/golden-file generated output for the
<code>--disable-future-imports</code> configuration with
self-referencing models will see the annotation change from unquoted to
quoted, e.g. <code>children: Optional[List[Node]]</code> →
<code>children: Optional[List[&quot;Node&quot;]]</code>. (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3387">#3387</a>)</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>Update CHANGELOG for 0.63.0 by <a
href="https://github.com/dcg-generated-docs"><code>@​dcg-generated-docs</code></a>[bot]
in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3345">koxudaxi/datamodel-code-generator#3345</a></li>
<li>Deduplicate module content builder by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3346">koxudaxi/datamodel-code-generator#3346</a></li>
<li>Deduplicate import reference helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3348">koxudaxi/datamodel-code-generator#3348</a></li>
<li>Refactor jsonschema root model registration by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3352">koxudaxi/datamodel-code-generator#3352</a></li>
<li>Refactor XML Schema literal helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3349">koxudaxi/datamodel-code-generator#3349</a></li>
<li>Move builtin formatter helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3351">koxudaxi/datamodel-code-generator#3351</a></li>
<li>Deduplicate Pydantic v2 config helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3350">koxudaxi/datamodel-code-generator#3350</a></li>
<li>Deduplicate DataType type hint rendering by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3354">koxudaxi/datamodel-code-generator#3354</a></li>
<li>Fix <code>constr()</code> for string fields carrying
minItems/maxItems by <a
href="https://github.com/DarkaMaul"><code>@​DarkaMaul</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3353">koxudaxi/datamodel-code-generator#3353</a></li>
<li>Cover non-finite import idempotence by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3367">koxudaxi/datamodel-code-generator#3367</a></li>
<li>Deduplicate input text detection by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3357">koxudaxi/datamodel-code-generator#3357</a></li>
<li>Remove stale protobuf coverage pragma by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3358">koxudaxi/datamodel-code-generator#3358</a></li>
<li>Cover explicit null OpenAPI media schemas by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3360">koxudaxi/datamodel-code-generator#3360</a></li>
<li>Simplify Python version feature checks by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3361">koxudaxi/datamodel-code-generator#3361</a></li>
<li>Speed up CI checks by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3378">koxudaxi/datamodel-code-generator#3378</a></li>
<li>Add maintainer link to docs footer and README by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3379">koxudaxi/datamodel-code-generator#3379</a></li>
<li>Use builtin formatter in CI by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3380">koxudaxi/datamodel-code-generator#3380</a></li>
<li>Split coverage by OS by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3381">koxudaxi/datamodel-code-generator#3381</a></li>
<li>Simplify import removal cleanup by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3362">koxudaxi/datamodel-code-generator#3362</a></li>
<li>Pin deprecation warning stacklevel by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3363">koxudaxi/datamodel-code-generator#3363</a></li>
<li>Pin public module exports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3364">koxudaxi/datamodel-code-generator#3364</a></li>
<li>Cover to_hashable branch cases by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3366">koxudaxi/datamodel-code-generator#3366</a></li>
<li>Cover stable toposort behavior by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3369">koxudaxi/datamodel-code-generator#3369</a></li>
<li>Extract registry render helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3371">koxudaxi/datamodel-code-generator#3371</a></li>
<li>Fix minItems for arrays of URI strings by <a
href="https://github.com/sjh9714"><code>@​sjh9714</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3377">koxudaxi/datamodel-code-generator#3377</a></li>
<li>Deduplicate config value validators by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3372">koxudaxi/datamodel-code-generator#3372</a></li>
<li>Cover CLI option metadata helpers by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3374">koxudaxi/datamodel-code-generator#3374</a></li>
<li>Cover Pydantic v2 version fallback by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3368">koxudaxi/datamodel-code-generator#3368</a></li>
<li>Fix nullable JSON Schema const enums by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3355">koxudaxi/datamodel-code-generator#3355</a></li>
<li>Pin patchable generation seams by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3365">koxudaxi/datamodel-code-generator#3365</a></li>
<li>Cover utility helper behavior by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3375">koxudaxi/datamodel-code-generator#3375</a></li>
<li>Cover DefaultPutDict behavior by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3376">koxudaxi/datamodel-code-generator#3376</a></li>
<li>Cover validator config normalization by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3373">koxudaxi/datamodel-code-generator#3373</a></li>
<li>Avoid expensive runtime type checks by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3382">koxudaxi/datamodel-code-generator#3382</a></li>
<li>Avoid eager builtin formatter import by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3383">koxudaxi/datamodel-code-generator#3383</a></li>
<li>Avoid eager TOML parser import by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3384">koxudaxi/datamodel-code-generator#3384</a></li>
<li>Stabilize msgspec payload tests by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3385">koxudaxi/datamodel-code-generator#3385</a></li>
<li>Avoid eager input parser imports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3386">koxudaxi/datamodel-code-generator#3386</a></li>
<li>Avoid eager parser model imports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3388">koxudaxi/datamodel-code-generator#3388</a></li>
<li>Avoid eager AsyncAPI converter imports by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3389">koxudaxi/datamodel-code-generator#3389</a></li>
<li>Dispose parser on parse errors by <a
href="https://github.com/koxudaxi"><code>@​koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3390">koxudaxi/datamodel-code-generator#3390</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/53a25ab8ddb132ac68a2795247fc855b8f445d84"><code>53a25ab</code></a>
Fast path schema output (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3410">#3410</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/ee2087f32e6100f5c3642e7ea8506aa38e9df26c"><code>ee2087f</code></a>
Skip discriminator import scan (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3411">#3411</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/bdf5ddfc27f94a06ba8d289759193bb09daadd34"><code>bdf5ddf</code></a>
fix: quote self-referencing fields when --disable-future-imports is set
(<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3387">#3387</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/ad4ec877fa6708baebdaaf820171d24bfe5bf0cb"><code>ad4ec87</code></a>
Cache payload validation strategies (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3409">#3409</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/b191d52a0a1d83edeac9553119f70b2f5c131126"><code>b191d52</code></a>
Shard Python tests (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3408">#3408</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/29dd6d74c95dd7799d51f2c707db24862809eb23"><code>29dd6d7</code></a>
Cache parsed sources (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3407">#3407</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/93e2fe3cf5774d5e4d2083fac365e5bcbf0a647a"><code>93e2fe3</code></a>
Defer generation refresh (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3406">#3406</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/bb01d9c628f9077cc5dd72320ae60a18abd5b790"><code>bb01d9c</code></a>
Lazy root format exports (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3405">#3405</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/48237ed8c3af3bb58b5e6b274925ebb646412d3a"><code>48237ed</code></a>
Fast path JSON schemas (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3404">#3404</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/b21d106c88ac22f137cd4562389ad95a50c2e912"><code>b21d106</code></a>
Slot generation facts (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3403">#3403</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/koxudaxi/datamodel-code-generator/compare/0.34.0...0.64.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=datamodel-code-generator&package-manager=uv&previous-version=0.34.0&new-version=0.64.0)](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>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-30 18:15:52 +02:00
Tomas Srnka 6733f36755 fix(sdk): align Python Fedora/Alpine image defaults with JS (#1625)
Python `from_fedora_image` defaulted to `fedora:42` (end-of-life) and
`from_alpine_image` to `alpine:3.22`, while JS already pinned
`fedora:44`/`alpine:3.24` — the same call produced a different base
image per SDK. Aligns Python; also fixes the JS type docs, which still
named the old defaults.

```python
Template().from_fedora_image()  # fedora:44 (was fedora:42)
Template().from_alpine_image()  # alpine:3.24 (was alpine:3.22)
```

Follow-up to #1612; both defaults are still unreleased.
2026-07-30 16:04:39 +00:00
Tomas Srnka 1504fbc843 SDK: fromFedoraImage/fromAlpineImage/fromArchImage helpers (#1612)
## What
Adds the missing non-Debian base-image convenience helpers to **both
SDKs**, mirroring the existing
`fromUbuntuImage`/`fromDebianImage`/`fromPythonImage`/`fromNodeImage`/`fromBunImage`:

- **JS/TS** (`packages/js-sdk`): `fromFedoraImage(variant?)`,
`fromAlpineImage(variant?)`, `fromArchImage(variant?)` + unit tests
- **Python** (`packages/python-sdk`): `from_fedora_image(variant)`,
`from_alpine_image(variant)`, `from_arch_image(variant)` + sync/async
unit tests

## Why
This is the **customer-facing half** of infra **#3381** (distro-aware
template provisioning). The engine now builds + boots
Ubuntu/Debian/Fedora/RHEL-family/Arch/Alpine on real KVM; before this PR
the SDK exposed distro helpers for the Debian family only, so
Fedora/Alpine/Arch were reachable only via the generic `fromImage()`.
These give them first-class parity.

## Verification (honest)
- **New helper unit tests pass locally** — JS `fromDistroImages.test.ts`
→ 6/6 green (`vitest`, no auth). Python `test_from_distro_images.py`
(sync + async) committed.
- **Full integration suite**: requires E2B API keys — fails locally with
`AuthenticationError` **identically on `main`** (215/187/29), i.e.
**zero regression** from this change; CI runs it with secrets.
- Lint scoped to the touched files.

## Not in this PR
The public **docs** still state *"only Debian-based images …
Alpine/RedHat not supported"* — but that text lives in
**`e2b-dev/docs`**, not this monorepo, so it's a **separate docs PR**
(being opened against `e2b-dev/docs`). Flagging so this + that land
together.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-30 12:17:02 +02:00
Mish Ushakov 9e3e52b4fb Add --user/--cwd/--env terminal flags to sandbox create & connect (#1501)
Exposes `--user`, `--cwd`, and repeatable `--env KEY=VALUE` flags on
`e2b sandbox create` (and the deprecated `spawn` alias) and `e2b sandbox
connect`, forwarding them to the underlying PTY session so the connected
terminal starts as the given user, in the given working directory, and
with the given environment variables. The SDKs already supported these
PTY options — this just wires them through the CLI. The `--env` arg
parser is extracted into a shared `src/utils/env.ts` and reused across
`create`, `connect`, and `exec`. Added unit tests for the parser and CLI
tests covering the new flags; a changeset is included for `@e2b/cli`.

## Usage

```bash
# Start the terminal as root, in /app, with custom env vars
e2b sandbox create base --user root --cwd /app --env FOO=bar --env TOKEN=abc123

# Same flags when attaching to an already-running sandbox
e2b sandbox connect <sandboxID> --user root --cwd /app --env FOO=bar
```

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 17:33:43 -07:00
Mish Ushakov 05b7a792ff fix(ci): depend on the SDK via workspace:^ so releases tag the version bump (#1619)
Closes
[SDK-298](https://linear.app/e2b/issue/SDK-298/release-tags-point-at-the-commit-before-the-version-bump).
Replaces #1615, which moved the tags after the fact instead of removing
the reason they were misplaced.

## The bug

Every published release tag pointed at the commit *preceding* its own
version bump:

```console
$ git show '@e2b/python-sdk@2.35.0:packages/python-sdk/pyproject.toml' | head -3
[project]
name = "e2b"
version = "2.34.0"      # ← tagged 2.35.0
```

Anything that builds from a git tag rather than a registry got the
previous release: distro packagers, `pip install git+…@tag`, any bisect
over a release regression. `python3Packages.e2b` in nixpkgs shipped
1.5.0 as 1.5.1 from June 2025.

## Root cause: a dependency cycle

`changeset publish` tags whatever commit it publishes from, so the fix
is to commit the version bump first. That was impossible:

```
tag              must point at →  release commit
release commit   must contain   →  pnpm-lock.yaml
pnpm-lock.yaml   contains       →  integrity hash of a tarball this release uploads
```

`packages/cli` depended on `e2b` by registry range, so `changeset
version` rewrote that range and the lockfile had to be re-resolved
against a tarball that did not exist yet. The lockfile could only be
refreshed *after* publishing, which forced the commit — and therefore
the tags — after it too.

## The fix

`packages/cli`: `"e2b": "^2.36.1"` → `"e2b": "workspace:^"`.

The lockfile now records `link:../js-sdk` and stops changing at release
time, so the release commit is complete before anything is uploaded:

| | before | after |
|---|---|---|
| 1 | `pnpm run version` | `pnpm run version` |
| 2 | publish **+ tag** ← wrong commit | **commit** (local) |
| 3 | refresh `pnpm-lock.yaml` (retry ≤6×) | publish **+ tag** ← right
commit |
| 4 | commit + push | push |

That deletes the lockfile-refresh step and its whole
registry-propagation retry loop (#1589), and `createGithubReleases:
true` keeps doing the tagging and GitHub releases — no custom tagging
code. Keeping the commit local also improves recovery: a publish that
uploads *nothing* leaves the branch untouched with the changesets
intact, so re-dispatching retries cleanly.

### Landing that commit is now mandatory, so the push is resilient

Once the tags point at a local commit, getting it onto the branch stops
being bookkeeping. `changesets/action` pushes each tag as soon as
`changeset publish` reports it (`runPublish` → `git.pushTag`), *before*
it propagates a non-zero exit — so three things changed:

- **The push is gated on the tags themselves** — `git tag --points-at
HEAD` — not on whether the publish step succeeded. The tags are the
thing that has to end up reachable, so they are the right thing to ask.
A partial failure (npm succeeds, then python-sdk's `postPublish` fails
on PyPI) used to skip the push and strand tags on a commit that reached
no branch while `main` kept the old versions.

I first wrote this as `!cancelled() && (success() ||
steps.release.outputs.published == 'true')`, which was wrong in both
directions: `success()` fires in exactly the case that must be skipped
(publish exits 0 having uploaded nothing → pushes a bump with no tags,
cementing a version that can never be published), and `published` is
left unset when the action *throws* after tagging (`core.setOutput` runs
only on a normal return from `runPublish`, but `git.pushTag` happens
inside it) — so it was skipped in the very case it existed for. The tag
gate also covers `@e2b/python-sdk`, which the npm-derived output never
did, since `privatePackages.tag` is on.

- **A partial publish is reported, not swallowed.** It still has to land
— otherwise the pushed tags hang off no branch — but the bump is then on
the branch with the changesets consumed, so re-dispatching will not
retry what failed. The step now names the tags that did land and points
out that `postPublish`'s PyPI upload was skipped (the root script is
`changeset publish && ... postPublish`, so a non-zero npm exit
short-circuits it).
- **A non-fast-forward is reconciled with a merge,** not a rebase (which
would orphan the tags) and not a hard failure. Hard-failing left an
already-published release needing manual git surgery, and a naive
re-dispatch would publish nothing (versions already on the registry),
tag nothing, and report **success** — quietly recreating SDK-298.
- **`git add -A` replaces `commit -am`,** which cannot stage new files.
`changeset version` writes each `CHANGELOG.md` fresh, so no release
commit has ever contained one:

  ```console
  $ git show --stat cf8296cf8 | tail -4
   .changeset/lucky-pandas-wave.md    |  5 ---
   packages/cli/package.json          |  4 +-
   packages/js-sdk/package.json       |  2 +-
   pnpm-lock.yaml                     | 77 +-----------------
  ```

## The published packages do not change

`pnpm publish` (which `changeset publish` uses in a pnpm workspace)
rewrites the protocol. Verified on the real CLI package with the
workspace SDK at 9.9.9:

```
e2b dependency -> ^9.9.9
PASS: no workspace: in published manifest
```

## Verification

| check | result |
|---|---|
| `pnpm install --frozen-lockfile` on a clean clone | consistent |
| `pnpm run version` touches the lockfile? | **no** — `git diff
pnpm-lock.yaml` empty after bumping sdk 2.36.1→2.36.2, cli→2.16.1 |
| tags land on the release commit | `PASS e2b@2.36.2`, `PASS
@e2b/python-sdk@2.36.0`; tagged trees contain `"version": "2.36.2"` /
`version = "2.36.0"` |
| CLI still bumped when the SDK is | yes, `updateInternalDependencies`
still sees the internal dep |
| CLI typecheck / tests | clean / 102 passed (1 pre-existing failure
needs `E2B_API_KEY` + a built `dist`) |
| CLI bundle | builds, contains the workspace SDK, zero external
`require("e2b")` |
| `pnpm publish` git checks | `changeset publish` passes
`--no-git-checks` for pnpm ≥5 (repo pins 9.15.5); added explicitly to
the RC flows, which publish from a feature branch with an uncommitted
bump |
| `prepack` guard | `npm pack` fails and produces no tarball; `pnpm
pack` passes and rewrites to `^2.36.1` |
| `pnpm publish` lifecycle | runs `prepublishOnly` + `prepack` +
`prepare`, so the RC still builds; `pnpm pack` runs only `prepack` +
`prepare` (0.37 s, no rebuild) |
| `pnpm publish --provenance` | flag accepted (pnpm forwards to the npm
publish it spawns) |
| `pnpm link --global` | links the workspace CLI (2.16.0) and resolves
`workspace:^`; tested against an isolated `PNPM_HOME` |
| `pnpm version` / `pnpm pkg` | pnpm forwards both to npm verbatim, so
these are the same code path as before — neither resolves dependencies,
so `workspace:` is inert there |
| lockfile vs `exclude-links-from-lockfile=true` | `--frozen-lockfile`
green with the new `link:../js-sdk` entry, including after a
release-style version bump |

## Everything packs and publishes with pnpm

Only pnpm rewrites `workspace:`. `npm pack` copies the protocol into the
tarball verbatim and `npm install` then refuses it. Rather than hand-pin
the range back before each npm call, every flow that produces or
installs a CLI tarball now uses pnpm:

| flow | before | after |
|---|---|---|
| `pkg_artifacts.yml` | `npm pack` | `pnpm pack` |
| `publish_candidates.yml` | `npm publish --provenance` | `pnpm publish
--provenance --no-git-checks` |
| `.github/actions/build-cli` | `npm install -g .` | `pnpm link
--global` |

The `build-cli` action was a **third** npm consumer of the manifest,
missed on the first pass. It only worked because npm symlinks a local
directory for `-g` without resolving its dependencies at all — verified:
a control package depending on `chalk` installed with exit 0 and chalk
was never fetched. Force packing (`install_links=true`) and it dies with
`EUNSUPPORTEDPROTOCOL`.

Moving the rewrite to pack time changes *when* it resolves, which
matters in `pkg_artifacts.yml`: `pnpm pack` uses whatever version the
workspace SDK has at that moment, and that job renames the SDK to an
unpublished prerelease. Packing the CLI first was required —

```console
# SDK renamed first (wrong order)
CLI packed with e2b -> ^2.36.2-fake-branch.0   # never published → ETARGET

# CLI packed first (as merged)
e2b-cli-2.16.1-fake-branch.0.tgz -> e2b: ^2.36.1   # published, resolvable
```

`publish_candidates.yml` needs the opposite order and already had it:
the SDK RC *is* published first, so the CLI correctly pins that RC.
`--no-git-checks` is new there — candidates are cut from a feature
branch with the version bump uncommitted, so `pnpm publish` would
otherwise refuse.

### Not enforced, deliberately

I went down a path here and backed out of it, so it is worth recording.
I first added a
`prepack` guard on `packages/cli` that refused to build a tarball for
any packer but
pnpm. It had three bypasses: `npm_config_user_agent` is inherited, so
npm spawned from
pnpm still reports `pnpm/…` and sailed through it; and
`--ignore-scripts` and
`npm install -g <dir>` never run lifecycle scripts at all. I then
replaced it with a
step that installed the packed tarball with npm on every PR, which did
cover all of
those (verified: it rejects an `npm pack` tarball with
`EUNSUPPORTEDPROTOCOL` while
`pnpm pack` resolves the range to `^2.36.1`).

Both are now gone, in favour of keeping this PR to its actual subject.
So the rewrite is
unverified: the existing flows all use `pnpm pack`/`pnpm publish`, and
`changeset publish` picks pnpm by detecting the workspace, so it happens
— but nothing
catches it if a future flow reaches for npm instead. The tarball-install
step is a cheap
seven lines if we later decide we want it.

## Behavior change worth knowing

CLI tests previously resolved `e2b` from `node_modules`, i.e. the
*previously released* SDK, while `tsconfig.json` and the tsdown bundle
already used `../js-sdk/src`. `vitest.config.ts` now has a matching
alias, so all three agree and tests exercise the SDK that ships. The
alias is load-bearing — without it the workspace package's `main`
(`dist/index.js`) doesn't exist until the SDK is built:

```
Error: Failed to resolve entry for package "e2b".
⎯⎯⎯⎯⎯⎯ Failed Tests 11 ⎯⎯⎯⎯⎯⎯⎯
```

A broken SDK in the tree now fails CLI tests. `cli_tests.yml` and
`pkg_artifacts.yml` already built the SDK before the CLI, so no CI
ordering changed.

## Not retagging the past

Tags up to `e2b@2.36.1` / `@e2b/cli@2.16.0` / `@e2b/python-sdk@2.35.0`
stay off by one — moving published tags breaks anyone who pinned them.
**Build those versions from the npm tarball or the PyPI sdist, not from
the git tag.** That matters for distro packagers: `python3Packages.e2b`
in nixpkgs shipped 1.5.0 as 1.5.1 for exactly this reason. This caveat
is recorded here and in [SDK-298](https://linear.app/e2b/issue/SDK-298)
rather than in the repo.

## Follow-up

SDK-298 also notes `packages/python-sdk/pyproject.toml` pins
`uv_build>=0.10.0,<0.11.0`, so packagers on uv 0.11.x must patch it to
build at all. And `e2b-dev/code-interpreter` has the same tag bug in its
own publish workflow.

> **Corrections to earlier versions of this description:**
>
> 1. It suggested demoting `e2b` to a `devDependency` since the bundle
inlines it. That breaks the CLI — but *not* for the reason given next.
> 2. It then claimed `tsdown.config.ts` derives `alwaysBundle` from
`dependencies`, so removing `e2b` makes it *external* and the CLI ships
a bare `require("e2b")`. **That is backwards.** tsdown externalizes
exactly the production dependencies (`getProductionDeps` = `dependencies
∪ peerDependencies ∪ optionalDependencies`), so listing `e2b` there is
what would externalize it; `alwaysBundle` exists to cancel that. A
devDependency is *also* inlined. Verified with a control: moving `e2b`
to `devDependencies` still emits zero `require("e2b")`, while adding it
to `excludedPackages` is what produces the bare require and drops the
bundle from 2.14 MB to 1.84 MB.
>
> The real reason it must stay a dependency is runtime resolution: the
SDK reaches `undici`, `glob` and `tar` through `dynamicImport`, which is
deliberately opaque to bundlers, so they resolve from `node_modules` at
run time. The CLI declares none of them and gets all three via `e2b`:
>
> ```
> undici: present   undici8: present   glob: present   tar: present
> ```
>
> Without them `e2b template build` loses `glob`/`tar` and
`loadUndici()` returns `undefined`, silently downgrading every request
to the global `fetch` and giving up H2 and proxy support. So: **do not
demote `e2b` to a devDependency.** This warning lives only here — there
is no in-repo note for it.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 16:59:33 +00:00
Mish Ushakov 2c061eb8cf chore(cli): migrate dashboard links to project-era tab entrypoints (#1605)
## Summary

Following the dashboard's teams→projects rename (e2b-dev/dashboard#521),
this PR points the `--project` flag help at the dashboard's
`?tab=general` entrypoint (General settings, where the project ID lives)
instead of the old `?tab=team`, and adds a `@e2b/cli` patch changeset.
It also rewrites the CLI README's headless-auth note to use
`E2B_API_KEY` (the supported browserless path) instead of
`E2B_ACCESS_TOKEN`, pointing at the dashboard's API Keys tab, and drops
the now-redundant `E2B_ACCESS_TOKEN` vs `E2B_API_KEY` callout. The SDKs'
`?tab=keys` and the CLI's `?tab=personal` links stay unchanged — those
tabs remain valid entrypoints.

## Example

```
$ e2b template create --help
  -t, --project <project-id>  specify the project ID that the operation will be associated with.
                              You can find project ID in the project settings in the E2B dashboard
                              (https://e2b.dev/dashboard?tab=general).
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:21:35 +02:00
Mish Ushakov ee0ad25117 docs(sdk): rename team to project in snapshot docstrings (#1562)
Renames team → project terminology in the JS and Python SDK snapshot
docstrings: the `list_snapshots`/snapshot list `name` filter example now
reads `"my-project/my-snapshot"`, and `SnapshotInfo.names` is documented
as "including project slug and tag (e.g. project-slug/my-snapshot:v2)".

Documentation-only — no exported names, runtime behavior, or wire
protocol change; generated API clients and `spec/openapi.yml` are
intentionally untouched until the backend exposes project-named
endpoints. Includes a patch changeset for `e2b` and `@e2b/python-sdk`.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 16:15:24 +02:00
github-actions[bot] cf8296cf89 [skip ci] Release new versions 2026-07-27 13:36:02 +00:00
Mish Ushakov 48e9249270 fix(cli): bump @npmcli/package-json to ^7, clearing deprecated glob@10 (#1614)
Follow-up to #1613, which fixed the `glob@11` deprecation warning in
`e2b` but left `@e2b/cli` warning via `@npmcli/package-json@5 →
glob@10`. That PR proposed `@npmcli/package-json@7`, but `^7` alone
isn't enough — 7.0.0–7.0.2 still depend on the equally deprecated
`glob@^11`, and the move to `glob@13` only landed in **7.0.4**, so this
pins `^7.0.5`. Since `@npmcli/package-json@7` requires Node `^20.17.0 ||
>=22.9.0`, the CLI's Node 22 floor moves from `>=22` to `>=22.9.0` —
matching the dependency exactly rather than excluding anyone it still
supports. Node 20 support is unchanged, since `^20.17.0` covers the
existing `>=20.18.1 <21`.

## Before / after

```console
$ npm install @e2b/cli          # before
npm warn deprecated glob@11.1.0: Old versions of glob are not supported...
npm warn deprecated glob@10.5.0: Old versions of glob are not supported...
added 183 packages in 4s

$ npm install @e2b/cli          # after (both tarballs packed locally)
added 145 packages in 1s
```

No API change. `e2b template init` is the only consumer, and the
`PackageJson.load`/`create`/`update`/`save` surface it uses is unchanged
across the bump.

## Verification

- Packed `e2b` + `@e2b/cli` and installed into a scratch project with
`overrides` pointing `e2b` at the local tarball (the post-release
state): zero deprecation warnings, `npm ls glob --all` reports only
`glob@13.0.6`.
- Ran `e2b template init -n my-tmpl -l typescript` from that packed
install against a real host `package.json` — scripts added, pre-existing
scripts preserved.
- `packages/cli` suite: 102 passed / 1 skipped, including all 14
`template init` tests, which assert on the written `package.json` in
both the `load` (existing file) and `create` (no file) branches.
`template/create.test.ts` fails identically on a clean tree in this
environment — it requires `E2B_API_KEY`.
- `pnpm run format` / `lint` / `typecheck` clean.
`@types/npmcli__package-json` stays at `^4.0.4`; v7 ships no types.
- `.tool-versions` is untouched: the pinned `nodejs 22.18.0` already
satisfies `>=22.9.0`, so CI (which derives `node-version` from that
file) needs no change.

Closes SDK-297. Follow-up to #1613 (SDK-296).

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 06:18:41 -07:00
Mish Ushakov 178e267ba2 fix(js-sdk): bump deprecated glob@^11 to ^13 (#1613)
Closes #1611.

`e2b` declared `"glob": "^11.1.0"`, and glob 11 is deprecated on npm, so
**every** `npm install` of any project that depends on `e2b` — directly
or transitively — printed a deprecation warning. Downstream packages
can't silence it themselves: npm `overrides` and `npm-shrinkwrap.json`
only apply to the top-level project being installed, not to a transitive
dependency's own range. It can only be fixed here.

Thanks @clayboby for the report and the verification work.

## Before / after

```console
$ npm install e2b@2.36.0        # before
npm warn deprecated glob@11.1.0: Old versions of glob are not supported, and contain
widely publicized security vulnerabilities, which have been fixed in the current version.
added 37 packages in 1s

$ npm install e2b               # after (this branch, packed locally)
added 26 packages in 1s
```

No API change — this is a dependency bump. The 37 → 26 package drop
comes from glob 13 moving its CLI (and
`jackspeak`/`@isaacs/cliui`/`string-width`/… ) out to a separate
`glob-bin` package.

## Why ^13 is safe

glob 12 and 13 only made **CLI-only** breaking changes, per [glob's
changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md):

- **v12** — "Remove the unsafe `--shell` option."
- **v13** — "Move the CLI program out to a separate package,
`glob-bin`."

The SDK's only use of glob is `getAllFilesInPath` in the template build
path (`src/template/utils.ts`, loaded via `dynamicImport('glob')`),
which touches the named async export `glob(pattern, opts)`, the options
`ignore` / `withFileTypes` / `dot` / `cwd`, and `Path#isDirectory()` /
`#fullpath()` / `#relative()`. All unchanged in 13.

glob 13.0.6's `engines` (`18 || 20 || >=22`) satisfy the SDK's
(`>=20.18.1 <21 || >=22`), and it's still dual CJS/ESM, so both build
outputs resolve it.

## Also in this PR: `"types": ["node"]` in the js-sdk tsconfig

glob 13 pulls `minipass@^7.1.3`, which removed the `/// <reference
types="node" />` that TypeScript 7's native `tsc` was (accidentally)
relying on to see Node globals — it doesn't auto-include
`node_modules/@types`. Without this, the bump fails `tsc --noEmit` with
~25 `TS2591 Cannot find name 'process'/'Buffer'` errors. Requesting
`node` explicitly is the right fix and makes the typecheck independent
of a transitive dependency's d.ts.

## Verification

- `tsc --noEmit` clean for js-sdk and cli; `pnpm run lint` / `format`
clean; `tsdown` build clean and `glob` still emitted as an external
`dynamicImport("glob")`, not inlined.
- `getAllFilesInPath` unit suite (17 tests: ignore patterns,
dotfiles/dotdirs, recursive dirs, deterministic sort, `.` pattern) green
against the real glob 13.0.6 on Node, **Bun 1.3.14, and Deno 2.8.1**.
- Full `unit` + `connectionConfig` projects: 401 passed / 30 skipped
against prod.
- Full `template` project: 133 passed / 3 skipped, including real
end-to-end template builds that exercise `COPY` (the glob path).
- Packed the tarball and installed it into a scratch project to confirm
the warning is actually gone (output above), plus CJS `require('e2b')`
and ESM `import 'e2b'` both load.

## Not fixed here

`@e2b/cli` installs still warn, via `@npmcli/package-json@5.2.1 →
glob@10.5.0`. Clearing that needs `@npmcli/package-json@7`, whose
`engines` (`^20.17.0 || >=22.9.0`) are narrower than the CLI's own
(`>=20.18.1 <21 || >=22`, so Node 22.0–22.8 would drop out) — separate
change, separate decision.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 14:39:35 +02:00
Mish Ushakov b5119539ca fix(js-sdk): share lazy fetcher loading and drop the new Function import trick (#1607)
Extracts the duplicated api/envd fetcher-loading logic into shared
`createRuntimeFetch` + `buildDispatchedFetch` helpers in `undici.ts`,
fixing two behaviors along the way: a failed fetcher build is no longer
cached forever (the next request retries, with a guarded
compare-and-clear so a stale awaiter can't clobber a newer in-flight
build — covered by a regression test reproducing the microtask
interleaving), and the no-undici fallback now late-binds
`globalThis.fetch` so fetch replacements installed after the first
request (msw, instrumentation) are picked up.

`loadUndici` now uses the shared `dynamicImport` helper, whose import is
kept opaque to downstream bundlers via `webpackIgnore`/`@vite-ignore`
annotations instead of the `new Function('return import(...)')` trick —
so environments that disallow code generation from strings (CSP,
`--disallow-code-generation-from-strings`) now load undici normally
instead of silently degrading to the global fetch. The now-internal
`toUndiciRequestInput`/`UndiciRequestInit` are no longer exported. No
user-facing API changes; verified with the unit suites (lint/typecheck
clean) plus real API integration tests through the new dispatcher path.

### Test-suite fallout from the import fix

Dropping the `new Function` trick exposed a hidden test dependency: that
trick throws under vitest's vm evaluation, so every vitest run had
silently fallen back to the msw-patchable global fetch. With module
loading un-broken, the Node test runs dispatched through real undici,
bypassing msw's `globalThis.fetch` patch — mocked requests escaped to
the real API (real 404s in the tags suite, 3-minute hangs in the
abortSignal suites waiting for msw's `request:start`, and 28 real
template builds per run from the stacktrace suite).

Fixed centrally: `tests/globalFetchFallback.setup.ts`, registered via
`setupFiles` for the unit and template projects, mocks
`buildDispatchedFetch` to run the SDK's real undici-unavailable fallback
(late-bound `globalThis.fetch`), so msw suites need no per-file mock and
future msw suites are covered automatically. Suites that inject their
own `loadUndici` (the api/envd transport tests) keep it, so the
dispatcher wiring itself stays covered. Verified under Node, Bun, and
Cloudflare workerd.

Closes SDK-290

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:37:14 -07:00
github-actions[bot] 59c6996a1e [skip ci] Release new versions 2026-07-24 14:45:57 +00:00
Mish Ushakov 4fcf7cb150 feat: sync API specs from infra and belt with Copybara (#1564)
The specs in `spec/` were copied from their source repos by hand and had
drifted ~2,400 lines behind infra, so they are now imported with
Copybara (`copy.bara.sky`, run in a pinned Docker image by
`scripts/fetch-spec.sh`): `make codegen` re-fetches them at the commits
pinned in `spec/infra-ref` and `spec/belt-ref` before generating, and
the generated-files CI check fails if the tracked copies don't match the
pins. Regenerating from the current pins picks up the accumulated spec
changes in the generated JS/Python clients (renamed request schemas,
`SandboxNetworkConfig`, `SandboxIam` workload identity,
`FILE_TYPE_SYMLINK`, access-token auth deprecation, volume path-metadata
tweaks). The one handwritten SDK change follows from that: the public
`FileType` enums gain a `SYMLINK` member (JS and both Python surfaces)
so entries envd reports as symlinks show up in `files.list()` and
`getInfo()`/`get_info()` instead of being silently skipped as unknown
types. The custom `spec/remove_extra_tags.py` tag-filtering script is
replaced by Redocly CLI's `filter-in` decorator (`redocly.yaml`), which
produces identical generated JS output; a `filter-out` decorator
additionally drops any operation or component schema the upstream specs
mark `x-not-implemented: true` (currently the SOCKS5
`SandboxEgressProxyConfig`/`egressProxy` surface, which infra flagged as
spec-only); each SDK's bundle now goes to its own gitignored
`spec/openapi_generated.<api>.yml` instead of both pipelines overwriting
one shared file; Python client models now list fields in spec order
instead of alphabetical (mechanical reordering only — construct models
with keyword args). Spec fetches try whatever GitHub token is available
and fall back to the tracked copies with a warning (the public infra
specs also fetch anonymously); in CI a short-lived belt-scoped token is
minted from the org-wide Autofixer GitHub App (no new secrets), so fork
PRs simply fall back for the belt spec; the CI workflows also cache the
Copybara image alongside the codegen image, and the previously ignored
`CODEGEN_IMAGE` env is honored by the Makefile.

## Usage

```sh
# update the specs: bump a pin, then regenerate
echo <infra-commit-sha> > spec/infra-ref
make codegen

# fetch a single spec without regenerating
pnpm fetch:api-spec     # spec/openapi.yml from infra
pnpm fetch:envd-spec    # spec/envd/ from infra
pnpm fetch:volume-spec  # spec/openapi-volumecontent.yml from belt

# try the latest spec without touching the pin
E2B_INFRA_REF=main pnpm fetch:api-spec

# change which endpoint tags an SDK exposes
$EDITOR redocly.yaml && make codegen
```

```ts
// symlinks are now visible in the filesystem API (JS; same shape in Python)
const entries = await sandbox.files.list('/home/user')
const link = entries.find((e) => e.type === FileType.SYMLINK)
console.log(link?.symlinkTarget)
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:37:02 +02:00
Mish Ushakov ada1744cf1 test(js-sdk): run the template test suite on Bun (#1600)
## Description

Adds `--project template` to `test:bun` so the Bun CI leg runs the
template suite, matching the Deno leg (#1595).

No code changes are needed: the template suite previously failed under
Bun because Bun's JavaScriptCore elides tail-call frames and the
fixed-depth stack walk attributed build errors one frame past the user's
call site (the workaround attempt in #1596 was closed in favor of
#1599). With #1599's boundary-based frame selection (now merged), the
suite passes under Bun as-is.

The CI workflow already passes `E2B_API_KEY`/`E2B_DOMAIN` to the Bun
leg, and the matrix comment (updated in #1595) already covers Bun
re-running API-backed suites, so `package.json` is the only change.

## Testing

Full `test:bun` (unit + connectionConfig + template) green locally on
Bun 1.3.14 against the real API: 530 passed, 35 skipped, 0 failed —
including all 34 stack-trace/caller-directory tests that pin exact user
call-site line/columns, the frames Bun used to elide.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:51:24 +00:00
Mish Ushakov 1ae3f92090 feat(js-sdk): run the full unit test suite in Cloudflare workerd (#1593)
## What

Promotes `test:cf` from a single dist smoke test to the **full unit +
connectionConfig suite running inside Cloudflare's workerd**
(`@cloudflare/vitest-pool-workers`) — the same coverage `test:bun` and
`test:deno` get. Locally: **74 files / 393 tests green** against prod
sandboxes. The real-deploy suite (`test:cf:deploy`) is unchanged and
keeps covering the built bundle on actual Cloudflare infrastructure (the
pool can't reproduce bundling bugs like #1579).

## SDK fixes the suite surfaced

1. **Dropped-connection mapping for Workers** (`src/envd/rpc.ts`):
workerd surfaces a sandbox connection drop as `Network connection lost`,
which fell through to a cryptic `SandboxError`. It's now matched like
the Node/Bun/Deno variants, so killing a sandbox mid-request surfaces as
the health-checked `TimeoutError`:

   ```ts
const cmd = await sandbox.commands.run('sleep 60', { background: true })
   await sandbox.kill()
await cmd.wait() // now rejects with TimeoutError('…sandbox was killed
or reached its end of life…') on Workers too
   ```

2. **Double connection release on stream cancel**
(`src/connectionConfig.ts`): `wrapStreamWithConnectionCleanup` claimed
its `release` was idempotent but had no guard — cancelling a streamed
download while a read was in flight ran `cleanup()` twice (both the
`cancel` callback and the pending `pull` resolving `done` fire).
workerd's stream scheduling hits this deterministically; the pooled
connection was double-released.

Both are runtime-behavior fixes specific to the JS fetch/streams stack —
no Python SDK equivalent applies.

## Test adjustments

- **boot_id reads** in the two "filesystem-only pause" tests now use
`commands.run('cat …')` instead of `files.read`: envd's non-gzip
download path serves procfs files as an empty 200 (filed as
e2b-dev/infra#3363 — Go `ServeContent` sizes them by stat, which is 0).
Only clients that don't negotiate gzip (workerd's fetch) observe it; the
command path sidesteps the bug while keeping the reboot assertion on all
runtimes.
- **runtime.test.ts** Node-host detection scenarios skip under workerd
via the existing host guard (same treatment as Bun/Deno).
- **Pool config filters expected unhandled-rejection shapes** via
vitest's `onUnhandledError` (not the blanket
`dangerouslyIgnoreUnhandledErrors`): workerd reports a rejection as
unhandled unless a handler attaches within the same microtask drain —
even inline `await expect(op()).rejects` trips it — and vitest never
processes the `rejectionhandled` retraction on any runtime, so the
suite's deliberate rejections false-positive ~60× per run. A diagnostic
pairing `unhandledrejection` with `rejectionhandled` confirmed all of
them are handled-late false positives (zero genuine leaks). The filter
drops only the shapes the tests provoke (SDK error classes,
`ConnectError`, `AbortError`, workerd's `Network connection lost.`, one
test stub); unknown rejection shapes and uncaught exceptions still fail
the run — verified with a planted never-handled `TypeError` (exit 1).

## CI

Rebased onto #1588's per-runtime matrix: the `cloudflare` leg (already
ubuntu-only there) now runs the full suite; no extra jobs added. The
stale `tests/integration` exclude was dropped after #1591 removed that
suite.

## Notes

- Suite config needs `nodejs_compat_populate_process_env` +
`E2B_API_KEY`/`E2B_DOMAIN` miniflare bindings so the SDK and tests read
env like on Node.
- The deleted `tests/runtimes/cloudflare/run.test.ts` (dist smoke) is
fully subsumed: lifecycle coverage by the suite, bundle coverage by
`test:cf:deploy` + `tests/bundle/edgeCompat.test.ts`.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:42:51 +02:00
Mish Ushakov 3f46d56026 fix(sdk): select stack-trace frames by SDK boundary instead of fixed depth (#1599)
## Description

Template build stack traces were captured by walking a fixed number of
frames (`STACK_TRACE_DEPTH` plus `±1` arithmetic at ~15 call sites),
which broke whenever the frame count between `new Error()` and user code
shifted — TS class-field initializer frames (#1539) and Bun's tail-call
frame elision were both this bug. This PR makes two related changes:

1. **Boundary-based frame selection.** The caller's frame is now the
first one whose file lies outside the SDK package, making extra
transpiler frames and elided delegating frames irrelevant. In the JS
SDK, frame parsing is delegated to `error-stack-parser-es` (ESM-only, so
it's a devDependency inlined into both dist formats via tsdown
`noExternal` — the engines range includes Node versions without
`require(esm)`); the Python SDK equivalently walks `f_back` until
`co_filename` leaves the `e2b` package root, in the shared builder used
by both sync and async. If no user frame is identifiable (e.g. the SDK
is bundled into the caller's own file), capture degrades to no trace
rather than a wrong frame.
2. **Dead machinery removed.** Because boundary capture resolves through
SDK-internal delegation (`remove()` → `runCmd()`, `fromDockerfile()` →
parser) to the user's call site on its own, the suppress/override
collection machinery (`runInNewStackTraceContext`,
`runInStackTraceOverrideContext`, the enabled/override flags, and their
Python equivalents) became redundant and is removed — superseding the
approach in #1596.

Error `.stack` synthesis (keeping the `Name: message` header and the
throw site on `cause`) was prototyped here and backed out — it will come
as a follow-up PR.

## Usage

No API changes — build errors now point at the user's call site
regardless of runtime or transpiler:

```ts
const template = Template()
  .fromBaseImage()
  .runCmd('./does-not-exist') // ← build failures point exactly here

await Template.build(template, 'my-template')
```

## Testing

- JS: `unit` + `template` vitest projects green against the real API
(incl. 27 per-method stacktrace tests pinning exact call-site
line/columns, `bunInstall` now covered); edge-compat bundle test and CLI
build verified; built CJS/ESM dists smoke-tested with
`require()`/`import()`.
- Python: all 184 template tests green (shared + sync + async, incl.
both `test_stacktrace.py` suites, `bun_install` now covered); `ruff` and
`ty` clean.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:42:40 +02:00
Mish Ushakov 00253c39cc feat(python-sdk): migrate envd RPC to the official connectrpc client (#1558)
Replaces the vendored `e2b_connect` client and the custom Go
`protoc-gen-connect-python` plugin with the official Connect RPC client
for Python ([`connectrpc`](https://github.com/connectrpc/connect-py),
transport: `pyqwest`/Rust hyper), and switches the envd messages from
Google's `protobuf` runtime to Buf's
[`protobuf-py`](https://github.com/bufbuild/protobuf-py) (which
`connectrpc` already requires) — the SDK no longer depends on the
conflict-prone `protobuf` package at all, and the protoc binary drops
out of the codegen image. The wire format (same protos, same JSON) is
unchanged. Closing a command or watch stream early now sends
`RST_STREAM`, fixing abandoned streams leaking on the shared HTTP/2
connection, and peer resets surface as typed `ConnectError`s. The
plumbing mirrors the `e2b.api` layout: shared pieces (a JSON codec that
ignores unknown response fields, proxy narrowing, pool tuning) live in
`e2b/envd/client_shared.py`, the flavor-specific pyqwest transports
(wrapped in pyqwest's retry middleware, see the retry note below) and
`create_rpc_client` factories in `e2b/envd/client_sync/` and
`e2b/envd/client_async/`, and the default-header/logging interceptors in
`e2b/envd/interceptors.py`; `e2b/envd/rpc.py` maps `connectrpc` error
codes onto the existing SDK exceptions, so the public API is unchanged
(`sandbox.commands.run(...)`, `files.watch_dir(...)`, etc. work exactly
as before). The REST API and file upload/download keep using `httpx`.

The `proxy` connection option now applies to sandbox RPC calls too —
[pyqwest
0.7.0](https://github.com/curioswitch/pyqwest/releases/tag/v0.7.0) added
an httpx-style `proxy` parameter to its transports, so commands, PTY,
and filesystem watch traffic follow the same proxy as the REST API and
file transfers (an earlier revision of this PR could only fall back to
`http_proxy`/`https_proxy` env vars for RPC):

```python
sandbox = Sandbox.create(proxy="http://user:pass@localhost:8030")
# REST *and* RPC (commands, PTY, watch) traffic goes through the proxy
result = sandbox.commands.run("echo through-the-proxy")
```

Notes:
- `e2b_connect` is no longer shipped in the wheel; code importing it
directly should switch to `connectrpc` (`ConnectError`, `Code`) — SDK
exception types are unchanged.
- The generated `e2b.envd.*.*_pb2` modules are replaced by `protobuf-py`
equivalents (`process_pb`, `filesystem_pb`) with a different message API
(`Oneof` objects, `has_field`); these are internal modules —
`e2b-code-interpreter` and `e2b-desktop` were verified not to import
them.
- RPC transports are cached per proxy URL. `httpx.URL` and `httpx.Proxy`
proxies keep working for RPC calls when they reduce to a proxy URL
(`httpx.Proxy` auth is folded back into the URL userinfo); `httpx.Proxy`
extras that pyqwest can't express — custom headers, an `ssl_context` —
raise `InvalidArgumentException` rather than being silently dropped.
- Plain (non-Connect-encoded) HTTP error responses — an edge proxy or
gateway answering for envd — keep the vendored client's status mapping
even when they carry a JSON body that isn't a valid Connect error (e.g.
a gateway's `{"code": 429}` raises `RateLimitException`, not a
misleading sandbox-timeout); only JSON bodies with a valid Connect
`code` string are left to connectrpc to parse. An envd response that
fails to decode surfaces as a `SandboxException` with a clear message —
the SDK's JSON codec raises a typed `ConnectError(INTERNAL)` at the
source (connectrpc re-raises codec-raised `ConnectError`s unchanged),
rather than the error being reconstructed from `__cause__` heuristics in
the exception mapper.
- pyqwest 0.7.0 explicit transports default to an **empty TLS root
store** (0.6.2 used reqwest's defaults), so the envd transports pass
`tls_include_system_certs=True`; the dependency floor is
`pyqwest>=0.7.0` accordingly.
- Connection retries (`E2B_CONNECTION_RETRIES`, default 3) use pyqwest's
transport-level retry middleware (`pyqwest.middleware.retry`), narrowed
to retry only the builtin `ConnectionError` — raised solely while
establishing the connection, before the request could have reached envd
— with exponential backoff. A retry can therefore never replay a
delivered request, for unary and streaming RPCs alike; the previous
stack's replay of unary calls whose connection dropped mid-request is
dropped deliberately, since it could re-execute a delivered call (e.g.
`SendInput`). Pinned by unit tests plus end-to-end tests driving the
generated stubs through the middleware
(`tests/test_envd_retry_transport.py`).
- For async streaming calls (`commands.run`/`connect`, PTY,
`watch_dir`), `request_timeout` bounds opening the stream — the wait
until envd confirms with a start event, matching the JS SDK's
`requestTimeoutMs` — raising `TimeoutException` and cancelling the
HTTP/2 stream when exceeded (pinned frame-level in
`tests/test_envd_stream_reset.py`). The running stream stays bounded by
the command/watch `timeout`. The sync SDK cannot interrupt its blocking
wait, so `request_timeout` is not applied to sync stream setup — both
setup and the running stream are bounded by `timeout` (unlimited when
`0`).
- The RPC logging interceptor was upstreamed to pyqwest as a logging
middleware
([curioswitch/pyqwest#192](https://github.com/curioswitch/pyqwest/pull/192));
the SDK keeps its own `LoggingInterceptor` until that merges and ships
in a release the SDK can depend on.
- `pyqwest` ships binary wheels for manylinux/musllinux (x86_64,
aarch64), macOS arm64 + x86_64 (Intel wheels landed in 0.7.0), Windows
x64, and PyPy.
- The `RST_STREAM`-on-early-close behavior is pinned by frame-level
regression tests (`tests/test_envd_stream_reset.py`): a plaintext HTTP/2
server records the frames the real generated clients (with the SDK's
codec and interceptors) send — early close via `disconnect()`, close
through the logging interceptor, and abandoning the stream must all send
`RST_STREAM(CANCEL)`; normal completion must send none (sync + async).
- `E2B_MAX_CONNECTIONS` no longer applies to sandbox RPC traffic:
reqwest's pool bounds only idle connections per host
(`E2B_KEEPALIVE_EXPIRY`, `E2B_MAX_KEEPALIVE_CONNECTIONS`), not the total
number of open connections. It still applies to the REST API and file
transfers.
- The sync sandbox modules build one RPC client each and share it across
threads — the connectrpc sync client is stateless per call over the
process-global transport (verified with a 16-thread frame-level test);
only the httpx envd API clients stay per-thread with their transports.
- Also fixes numeric env-var parsing (`E2B_KEEPALIVE_EXPIRY`,
`E2B_MAX_KEEPALIVE_CONNECTIONS`, `E2B_MAX_CONNECTIONS`,
`E2B_CONNECTION_RETRIES`): an empty-string value now falls back to the
default instead of raising `ValueError` at import time.
2026-07-24 05:41:04 -07:00
Mish Ushakov 5e141a765f fix(js-sdk): use commands.run in Sandbox.getHost() example (#1531) (#1550)
The JSDoc `@example` on the public `Sandbox.getHost()` method calls
`sandbox.commands.exec(...)`, but the `Commands` class has no `exec`
method. It
exposes `run`. Copy-pasting the documented snippet therefore throws:

```
TypeError: sandbox.commands.exec is not a function
```

### Where

`packages/js-sdk/src/sandbox/index.ts`, in the `getHost()` doc comment:

```ts
/**
 * ...
 * @example
 * ```ts
 * const sandbox = await Sandbox.create()
 * // Start an HTTP server
 * await sandbox.commands.exec('python3 -m http.server 3000')  // <- no such method
 * // Get the hostname of the HTTP server
 * const serverURL = sandbox.getHost(3000)
 * ```
 */
```

The `Commands` class (`packages/js-sdk/src/sandbox/commands/index.ts`)
exposes
`list`, `sendStdin`, `closeStdin`, `kill`, `connect`, and `run` (four
`run`
overloads), plus a private `start`. There is no `exec`. The correct
method here
is `run`, which is what every other example already uses, including the
sibling
`@example` in this same file (the `commands.run(...)` snippet a few
methods up)
and both Python SDK mirrors (`get_host` in `sandbox_sync/main.py` and
`sandbox_async/main.py` already use `commands.run`).

### Fix

One token, `exec` -> `run`:

```ts
- await sandbox.commands.exec('python3 -m http.server 3000')
+ await sandbox.commands.run('python3 -m http.server 3000')
```

Documentation only. No behavior or type change.

### Parity with the Python SDK

The repo guidelines ask that SDK changes be mirrored across the JS and
Python
SDKs. Here the Python `get_host` examples already use `commands.run`
correctly,
so this defect exists only in the JS SDK doc comment and no Python
change is
needed to reach parity.

### Tests

This is a JSDoc `@example` correction with no runtime code path to
exercise, so
it adds no test, matching the repo's existing precedent for
documentation-only
fixes (e.g. `.changeset/sandbox-list-docstring.md`, and merged doc-fix
PRs such
as #1511 / #1500 / #1260, none of which added a regression test).
Correctness is
that the example now names the real public API: after the change,
`commands.exec`
no longer appears anywhere in the SDK source, and `commands.run` matches
the
`Commands` class and the sibling examples.

Offline gates run locally (Node 20, pnpm 9.15.5):

```
pnpm --filter e2b run lint        # oxlint, clean
pnpm --filter e2b run typecheck   # tsc --noEmit, clean
pnpm --filter e2b run build       # tsc + tsup, ESM/CJS/DTS built
prettier --check src/sandbox/index.ts  # clean
```

A changeset (`e2b`, patch) is included.

---

## Linked issues

- None. There is no existing GitHub issue for this; it is a self-evident
public
doc-example defect (the documented snippet throws at runtime). Not
filing a
  separate issue for a one-token doc fix.

## Pre-flight checklist (repo AGENTS.md / CLAUDE.md gates)

- [x] `pnpm run format` - `prettier --check` clean on the changed file
- [x] `pnpm run lint` - oxlint clean (exit 0)
- [x] `pnpm run typecheck` - tsc --noEmit clean (exit 0)
- [x] `pnpm run build` - tsc + tsup clean
- [x] Changeset generated - `.changeset/fix-gethost-example-command.md`
(`e2b`: patch)
- [x] Conventional Commit message (`fix(js-sdk): ...`, reuses `js-sdk`
scope)
- [ ] Test added - not applicable (doc-only `@example`; see Tests
section for precedent)
- [ ] DCO / CLA - no sign-off required by this repo; CLA is signed via
`@cla-bot`
      on the PR after opening (as on prior PRs #1518 / #1519 / #1507)

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Co-authored-by: Anas Khan <anxkhn28@gmail.com>
2026-07-24 05:37:19 -07:00
Mish Ushakov 9ee4414e6d feat(js-sdk): run the template test suite on Deno (#1595)
## What

Extends the Deno vitest run (#1585) with the `template` project and
fixes the real runtime bug the suite surfaced. Split out of #1594 (Bun
counterpart: #1596).

```jsonc
// packages/js-sdk/package.json
"test:deno": "deno run -A npm:vitest run --project unit --project connectionConfig --project template",
```

## Bug — Deno: template uploads used chunked transfer encoding

Deno's native `fetch` ignores an explicit `Content-Length` header on
stream bodies and falls back to `Transfer-Encoding: chunked` — exactly
the failure #1243 fixed for Node, since S3-compatible presigned PUT URLs
reject chunked uploads with 501.

`uploadFile` now streams the spooled archive through **undici's
`fetch`** (via the existing `loadUndici()` helper — undici 8 where it
imports, undici 7 on Bun, global `fetch` where undici isn't resolvable,
e.g. bundled apps), which honors the `Content-Length` header on stream
bodies on every runtime. One upload path, no runtime sniffing.

Approaches rejected along the way, all verified empirically with 1GB
uploads + RSS sampling:

- **File-backed `Blob` body (`fs.openAsBlob`)** — lazy on Node/Bun, but
Deno's shim reads the whole file into memory eagerly
(denoland/deno#32316), and Bun infers an unstrippable MIME type from the
extension whose `Content-Type` breaks presigned signatures (403 against
production storage).
- **`node:http(s)` on Deno** — works (and is memory-bounded), but can't
be unified: Bun's `node:http` ignores abort signals, and it's a second
code path.

Known caveat: Deno's `Readable.toWeb` shim has no backpressure, so the
archive is buffered in memory during upload on Deno (Node and Bun stream
in lockstep with the socket). Filed upstream as denoland/deno#36275 —
accepted as Deno's to fix rather than worked around here.

As part of this, `tarFileStream` became `spoolTarArchive`, returning `{
path, size, cleanup }` with caller-owned cleanup instead of a
self-deleting read stream. `tests/template/uploadFile.test.ts` also
asserts no `Content-Type` header is sent.

## Python SDK parity

Intentionally none: `upload_file` already sends a sized file body via
httpx.

## Testing

- Real template builds (`tests/template/build.test.ts`, against prod S3
presigned URLs) green under **Node, Deno, and Bun**
- `uploadFile` + `spoolTarArchive` suites green under Node, Deno, and
Bun
- `tests/template/abortSignal.test.ts` green under Deno
- `pnpm build`, `lint`, `typecheck`, `prettier --check` clean

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:11:24 +00:00
Mish Ushakov 5417dd4f9f fix(deps): resolve all open Dependabot alerts (#1598)
## Summary

Fixes all 8 open [Dependabot
alerts](https://github.com/e2b-dev/E2B/security/dependabot), all in
`pnpm-lock.yaml`:

| Package | Severity | Alerts | Before | After | How |
|---|---|---|---|---|---|
| `@vitest/browser` | critical | #328 | 4.1.8 | 4.1.10 | updated the
vitest family in js-sdk and cli devDeps (4.1.10 peer-requires
`vitest@4.1.10` exactly) |
| `tar` | critical/high/medium ×4 | #324–#327 | 7.5.16 | 7.5.21 | bumped
the js-sdk runtime dep floor to `^7.5.19` + repo-wide override |
| `sharp` | high | #329 | 0.34.5 | 0.35.3 | new override (pinned exactly
by miniflare, dev-only) |
| `shell-quote` | high | #323 | 1.8.4 | 1.10.0 | widened existing
override (dev-only, via npm-run-all) |
| `brace-expansion` | high | #322 | 2.1.0 | 2.1.2 | widened existing
override |

The only runtime-dependency change is `tar` in the js-sdk (used for
template build contexts), so a patch changeset for `e2b` is included.
The CLI bundles the SDK and its dependencies into `dist/index.js`, so
the published CLI also ships the vulnerable `tar` — a patch changeset
for `@e2b/cli` is included to rebundle it. Everything else is dev
tooling or lockfile-only.

## Verification

- `pnpm run lint` and `pnpm run typecheck` pass (the 7 python-sdk ty
diagnostics pre-exist on main)
- js-sdk: unit + connectionConfig (393 passed) and template projects
(132 passed, exercises the new `tar` end-to-end against the real API) on
vitest 4.1.10; `pnpm run build` clean
- js-sdk `test:cf` passes — miniflare/workerd boots with sharp 0.35.3
- cli: full suite green (103 passed) on vitest 4.1.10

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:56:01 +02:00
Mish Ushakov 67bf112efc test(js-sdk): rename deprecated test.scoped() to test.override() (#1597)
vitest 4.1 deprecates `test.scoped()` in favor of `test.override()`,
emitting 15 warnings during test collection in CI. This renames all
`sandboxTest.scoped()` fixture overrides to `sandboxTest.override()`
across the six affected test files (network, snapshot, internetAccess,
secure, files/signing, commands/envVars). It's a pure rename — the
vitest 4.1.8 types confirm an identical signature — and `vitest list` on
all six files now collects with zero deprecation warnings. Test-only
change, so no changeset.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:13:25 +00:00
Mish Ushakov e00503b090 fix(ci): recover release 30006966441 and retry lockfile update with backoff (#1589)
## What happened

Release run
[30006966441](https://github.com/e2b-dev/E2B/actions/runs/30006966441)
successfully published **e2b@2.35.3** and **@e2b/cli@2.15.0** to npm and
pushed both tags, but then failed on the **Update lock file** step:
`pnpm i` ran ~6 seconds after `npm publish` and the registry had not
propagated the new version yet (`ERR_PNPM_NO_MATCHING_VERSION: No
matching version found for e2b@^2.35.3 — the latest release of e2b is
"2.35.2"`). Because that step failed, the **Commit new versions** step
was skipped, leaving main with stale versions and unconsumed changesets.

Auditing the rest of the publish path for similar races also turned up a
long-dead step: the `@e2b/sdk` alias republish.

## Changes

**Commit 1 — replay the missing release commit.** Reproduces exactly
what the bot would have committed: `pnpm run version` (consumes the
three changesets, bumps js-sdk 2.35.2 → 2.35.3 and cli 2.14.0 → 2.15.0)
followed by `pnpm i --no-link --no-frozen-lockfile` (now succeeds — the
registry has long since propagated). The only commit that landed on main
after the release was dispatched
([e334c87](https://github.com/e2b-dev/E2B/commit/e334c87f8fc60be56cc5970d6f6399331242bace))
touches only `.github/`, so per the workflow's own safety rule the
version bump is safe to apply on top: the published artifacts match the
source.

**Commit 2 — prevent recurrence.** The `Update lock file` step in
`publish_packages.yml` now retries with exponential backoff
(10/20/40/80/160s, up to ~5 min total) before failing, since the npm
registry is eventually consistent and this race will recur on any
release where propagation takes more than a few seconds.

**Commit 3 — remove the dead `@e2b/sdk` alias republish.**
`packages/js-sdk/scripts/post-publish.sh` republished each release under
the deprecated `@e2b/sdk` name and immediately re-deprecated it. It has
silently failed on every release since 2.5.0 (2025-10-28): the CI npm
token lacks publish rights to `@e2b/sdk` (`E404` on `PUT
https://registry.npmjs.org/@e2b%2fsdk`, npm's masking of 403) and the
`|| true` swallowed the error — visible in this run's log right before
the lockfile failure. All published `@e2b/sdk` versions already carry
the "renamed to e2b" deprecation notice, which is the coherent end
state; resuming alias publishes would only reward not migrating. The
script and its `postPublish` hook are deleted (the root `pnpm run -r
postPublish` stays — python-sdk still uses its hook for PyPI). No
changeset: nothing in the published artifact's runtime changes, and the
alias hasn't published in 9 months so user-visible behavior is
unchanged.

## Notes

- Please merge before the next release: until then main still claims
2.35.2/2.14.0, and a future `changeset version` run would compute wrong
bumps from the stale base.
- The version-bump commit intentionally consumes the existing three
changesets.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 07:34:25 -07:00
Mish Ushakov aa3c2593b9 test(js-sdk): remove unused integration test suite (#1591)
## Summary

Removes `packages/js-sdk/tests/integration/` — the suite was never wired
into CI: no workflow references `test:integration` or sets the
`E2B_INTEGRATION_TEST` env var that gated every test. The tests were
also stale, referencing hardcoded template IDs (`en716jw99aj63v1k8ugh`,
`integration-test-v1`) that likely no longer exist and passing
`timeoutMs: 120` (120 ms). Also removes the `test:integration` script,
the `integration` vitest project, and the unused `isIntegrationTest`
helper from `tests/setup.ts`. Test-only change, no changeset needed.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:02:57 +00:00