Follow-up to #1433. Builds on the shared streaming infrastructure
introduced there (`FILE_TIMEOUT_MS`, request-controller/stream-cleanup
helpers in `connectionConfig`, `io_utils` chunk iterators, the `runtime`
guard) and applies the same streaming model to volumes.
> [!NOTE]
> Based on `mishushakov/stream-write-file-upload` (#1433). Merge that PR
first; this PR's diff will then retarget to `main` automatically.
## What changed
- **`Volume.writeFile()` / `Volume.write_file()`** — stream the request
body instead of buffering it in memory.
- JS: `ReadableStream` data is streamed outside the browser
(half-duplex); browsers still buffer since they can't stream request
bodies.
- Python: file-like objects are streamed in chunks (async wraps them in
an async iterator; sync passes them to httpx directly, text-mode IO is
encoded chunk-by-chunk).
- **`Volume.readFile(format="stream")` / `read_file(format="stream")`**
— the request timeout now bounds only the initial handshake, not the
body read, matching the sandbox `files.read` stream path. A dropped
connection during the handshake surfaces the same typed, health-checked
error; JS supports `signal` to cancel an in-flight stream and cancels
unconsumed bodies on error so the pooled connection is released.
## Usage
JS — stream a file straight to a volume without buffering:
```ts
import { createReadStream } from 'node:fs'
import { Readable } from 'node:stream'
const stream = Readable.toWeb(createReadStream('large-input.bin'))
await volume.writeFile('/data/large-input.bin', stream)
// read back as a stream; the body lives until consumed/cancelled
const out = await volume.readFile('/data/large-input.bin', { format: 'stream' })
for await (const chunk of out) {
// process chunk
}
```
Python — stream a file-like object:
```python
with open("large-input.bin", "rb") as f:
volume.write_file("/data/large-input.bin", f) # streamed, not read() into memory
for chunk in volume.read_file("/data/large-input.bin", format="stream"):
... # process chunk
```
## Testing
- `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` pass.
- Added volume streaming tests (JS `tests/volume/file.test.ts`; Python
sync/async `test_file.py` text-stream cases).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
Removes full in-memory buffering from the SDK **sandbox** file-transfer
paths, in both JS and Python (sync + async).
**Streamed uploads** — `Sandbox.files.write` / `write_files` streams
`ReadableStream` (JS, outside the browser) and file-like (Python) input
to the sandbox with chunk-by-chunk gzip compression, instead of
buffering the whole body in memory. `useOctetStream`/`use_octet_stream`
now defaults to auto-detect — octet-stream when any entry is streamable
(so streamed uploads aren't silently buffered), `multipart/form-data`
otherwise; browsers always use `multipart/form-data` since streaming
request bodies aren't supported there. A streamed upload is bounded by a
per-chunk timeout on the wire (Python's per-write `httpx` timeout,
default the request timeout); a stalled upload the wire can't observe is
bounded server-side. On Python's `AsyncSandbox`, the blocking file reads
and gzip compression of a streamed upload now run in a worker thread so
a large upload doesn't stall the event loop.
**Streamed downloads** — `Sandbox.files.read(format="stream")` now
streams the response body from the sandbox instead of downloading it
into memory before iterating (Python sync + async), and the 60s request
timeout no longer kills the stream while it's being consumed:
- The request timeout now bounds only the initial handshake.
- The body is bounded by a per-chunk **idle-read timeout** on the wire —
a per-`read()` option (`streamIdleTimeoutMs` in JS,
`stream_idle_timeout` in Python; default the request timeout — 60s —
`0`/`None` to disable). It's armed only while waiting on a network read
and cleared the moment a chunk arrives, so it aborts only when the
server stops sending mid-stream; a slow or paused consumer never trips
it (a held-but-unread stream is reclaimed server-side, not by this
timer).
- A dropped connection during the handshake surfaces the same typed,
health-checked error as non-stream reads. In JS, `signal` can still
cancel an in-flight stream.
- The stream holds its pooled connection until it is consumed to the
end, cancelled/closed, errors, or the idle timeout fires — consume it
fully, use the context manager, or close it. (This replaces the earlier
GC-finalizer net.) Python returns a
`FileStreamReader`/`AsyncFileStreamReader` supporting deterministic
cleanup via `close()`/`aclose()` and (async) context-manager use; both
still satisfy `Iterator[bytes]`/`AsyncIterator[bytes]`, so existing
iteration is unchanged.
**Empty files** — JS `Sandbox.files.read()` with `blob` or `stream`
format now returns a format-correct empty value (empty `Blob` / empty
`ReadableStream`) for empty files instead of `""`.
> [!NOTE]
> The equivalent **volume** streaming changes
(`Volume.writeFile`/`write_file`, `Volume.readFile`/`read_file` streams)
live in a follow-up PR, #1453, which is based on this branch.
## Usage
```ts
// JS: upload a large file without holding it in memory
const file = createReadStream('large.bin')
await sandbox.files.write('large.bin', Readable.toWeb(file), { gzip: true })
// JS: consume a download for longer than 60s without it being killed
const stream = await sandbox.files.read('large.bin', { format: 'stream' })
for await (const chunk of stream) { /* ... */ }
// JS: tune (or disable) the per-chunk idle-read timeout for a read
const stream = await sandbox.files.read('large.bin', {
format: 'stream',
streamIdleTimeoutMs: 120_000, // 0 to disable
})
// JS: empty files now return format-correct empty values
const blob = await sandbox.files.read('empty.txt', { format: 'blob' }) // Blob (size 0), not ''
```
```python
# Python: streamed upload and download
with open("large.bin", "rb") as f:
sandbox.files.write("large.bin", f, gzip=True)
for chunk in sandbox.files.read("large.bin", format="stream"):
...
# Python: deterministic cleanup when not reading the stream to the end
with sandbox.files.read("large.bin", format="stream") as stream:
first_chunk = next(iter(stream)) # connection released on block exit
# Python: tune (or disable) the per-chunk idle-read timeout for a read
for chunk in sandbox.files.read(
"large.bin", format="stream", stream_idle_timeout=120.0 # None to disable
):
...
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Why
The Release run
[27844631141](https://github.com/e2b-dev/E2B/actions/runs/27844631141/job/82412481993)
**published all packages successfully** but then failed at the final
*"Commit new versions"* step — the version-bump commit-back to \`main\`
was rejected as non-fast-forward (another PR landed on \`main\` during
the release window).
As a result the registries are ahead of the repo:
| Package | Published | Repo (main) before this PR |
|---|---|---|
| \`e2b\` (JS) | 2.30.4 (npm) | 2.30.3 |
| \`@e2b/cli\` | 2.12.2 (npm) | 2.12.1 |
| \`e2b\` (Python) | 2.29.4 (PyPI) | 2.29.3 |
The changeset \`fix-logo-pypi-npm.md\` was also never consumed and is
still on \`main\`.
## What this PR does
Replays exactly what the failed *"Commit new versions"* step would have
committed — i.e. \`pnpm run version\` (changeset version +
\`postVersion\` poetry sync) + lockfile update:
- Bumps \`e2b\` → 2.30.4, \`@e2b/cli\` → 2.12.2, \`@e2b/python-sdk\` →
2.29.4 (matching what's already published)
- Deletes the consumed changeset \`fix-logo-pypi-npm.md\`
- Updates \`pnpm-lock.yaml\` (CLI's \`e2b\` dep → 2.30.4)
No new packages are published by merging this — it only syncs the repo
to the registries. **Do not re-run the Release workflow** for this
changeset; the versions already exist on npm/PyPI.
The Python SDK's `ApiClient` applied the deprecated `access_token`
*after* merging `config.headers` (which includes `api_headers`), so a
custom `Authorization` passed via `api_headers` was silently overwritten
— the opposite of the JS SDK, where a custom `Authorization` wins. This
moves the deprecated access token before `config.headers` so
`api_headers` now takes precedence, matching JS. No change when only
`access_token` is set.
```python
# Custom Authorization via api_headers now wins over the deprecated access_token
ConnectionConfig(api_key="e2b_...", access_token="old", api_headers={"Authorization": "Bearer custom"})
# -> Authorization: Bearer custom
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Fixes the duplicate logo issue on NPM and PyPI caused by #1462. The
`#gh-light-mode-only` / `#gh-dark-mode-only` URL fragments are
GitHub-specific — NPM and PyPI ignore them and render both `<img>` tags.
Switches all three package READMEs (CLI, JS SDK, Python SDK) to
`<picture>` elements:
```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".../logo-white.png">
<source media="(prefers-color-scheme: light)" srcset=".../logo-black.png">
<img alt="E2B Logo" src=".../logo-black.png" width="200">
</picture>
```
- **GitHub**: `<picture>` + `prefers-color-scheme` handles theme
switching
- **NPM/PyPI**: `<picture>` not supported, falls back to the single
`<img>` (black logo)
Link to Devin session:
https://app.devin.ai/sessions/4983f23d23934d2c9a51733f5f9920f3
Requested by: @mlejva
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: vasek <vasek.mlejnsky@gmail.com>
## Summary
Replace the old `logo-circle.png` in the CLI, JS SDK, and Python SDK
READMEs with the new E2B wordmark logos that adapt to GitHub's theme
setting.
Each package README now uses a `<picture>` element:
```html
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".../logo-white.png">
<source media="(prefers-color-scheme: light)" srcset=".../logo-black.png">
<img alt="E2B Logo" src=".../logo-black.png" width="200">
</picture>
```
- **Light theme** → black logo (`logo-black.png`)
- **Dark theme** → white logo (`logo-white.png`)
- **NPM/PyPI** (no `<picture>` support) → falls back to the black logo
via the `<img>` tag
New logo assets added to `readme-assets/`: `logo-black.png`,
`logo-white.png`.
Includes a patch changeset for `@e2b/cli`, `e2b` (JS SDK), and
`@e2b/python-sdk`.
Link to Devin session:
https://app.devin.ai/sessions/4983f23d23934d2c9a51733f5f9920f3
Requested by: @mlejva
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: vasek <vasek.mlejnsky@gmail.com>
## Summary
The access token was only ever used by the CLI, never by any SDK
operation — sandbox, template, and volume calls all authenticate with
the API key. This cleans up the auth plumbing and **deprecates** (rather
than removes) the access token on `ConnectionConfig`, so there's no
breaking change for direct SDK consumers.
## Changes
- **Deprecated** the `accessToken` (JS) / `access_token` (Python) option
on `ConnectionConfig`. It still works exactly as before — when set (or
via `E2B_ACCESS_TOKEN`) the `Authorization: Bearer` header is still sent
— but `apiHeaders` is now the recommended way to pass custom auth.
- **Clear error when the API key is missing**, pointing to the API Keys
tab (`https://e2b.dev/dashboard?tab=keys`). In JS this is gated by a
`requireApiKey` option (default `true`) so callers that authenticate
differently — like the CLI hitting `/teams` with an access token — can
opt out; in Python the API key is always required.
- Removed the unused access-token toggle from the API clients:
`requireAccessToken` (JS) / `require_access_token` (Python). No caller
ever set it to a non-default value, so behavior is unchanged.
- The CLI now passes the access token to the `/teams` endpoint via
`apiHeaders` instead of the deprecated option, and opts out of the
API-key requirement on its own clients.
- Decoupled the sandbox-scoped envd access token from
`ConnectionConfig`: `EnvdApiClient` now owns its own `envdAccessToken`
field and sets the `X-Access-Token` header itself, removing a redundant
manually-set header.
## Recommended usage
```ts
// Deprecated
new ConnectionConfig({ accessToken: 'my-token' })
// Preferred
new ConnectionConfig({ apiHeaders: { Authorization: 'Bearer my-token' } })
```
```python
# Deprecated
ConnectionConfig(access_token="my-token")
# Preferred
ConnectionConfig(api_headers={"Authorization": "Bearer my-token"})
```
## Verification
`pnpm run typecheck`, `pnpm run lint`, Python `make typecheck`, and the
unit tests all pass — including new tests for the API-key requirement
(and its opt-out) in both SDKs. Confirmed the `Authorization: Bearer`
header is still sent for both the deprecated option and
`E2B_ACCESS_TOKEN`.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the connection to a sandbox is dropped mid-request, each JS runtime
surfaces it with different wording, but the health check only recognized
Node/undici's `terminated` — so on Bun and Deno a sandbox killed
mid-request was reported as a raw transport error instead of a
`TimeoutError`. This matches every known variant (Node: `terminated`,
Bun: `The socket connection was closed unexpectedly`, Deno: `error
reading a body from connection`) via a shared list used by both the RPC
and fetch error handlers, and broadens the fetch-error type guard from
`TypeError` to `Error` since Bun raises a plain `Error`. Added
parametrized tests covering all three runtimes; format, lint, and
typecheck pass.
No Python SDK change is needed — it detects dropped connections by
exception type (`RemoteProtocolError`), which is already
runtime-agnostic, and Bun/Deno are JS-only runtimes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes seven template-build correctness bugs across the JS and Python
(sync + async) SDKs, plus a small shell-quoting cleanup. Each fix has
unit/regression coverage, and real end-to-end builds were run against
the API in all three SDK variants.
**Template fixes**
- `getAllFilesInPath` now sorts by full path so the files hash no longer
depends on filesystem traversal order (the JS `sort()` was a no-op on
glob `Path` objects).
- `waitForPort` anchors the port match so port 80 no longer matches
8080.
- The readycmd helpers (`waitForURL`/`waitForFile`/`waitForProcess`) and
the file-op helpers (`remove`/`rename`/`makeDir`/`makeSymlink`) now
shell-quote interpolated values/paths.
- `waitForBuildFinish` keeps fetching logs after a terminal status so
the tail of the build logs (beyond the API's 100-entries-per-call limit)
is no longer dropped.
- COPY instructions now collect one stack trace each, so failed-step
traces stay aligned after `copy()` with multiple sources or
`copyItems()`.
- JS `LogEntry` strips ANSI escape codes in its constructor, matching
the Python SDK.
**Cleanup:** consolidated three duplicate single-quote shell helpers
(`shellQuote`, the new `quoteShellArg`, and git's `shellEscape`) into
one faithful `shlex.quote` port in `utils.ts` — safe values stay
unquoted, keeping generated commands and layer-cache hashes stable.
**Behavior note:** templates with paths/URLs containing spaces or shell
metacharacters now build correctly; plain paths are unchanged, so
existing layer caches are preserved.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Follow-up to #1444 (the `httpcore` → `TimeoutException` mapping, now
merged). That mapping made the flaky timeout deterministic, but the
underlying cause remained: the streaming HTTP `read` timeout was set to
the command `timeout`, so it raced the server's own `deadline_exceeded`
response. This PR removes the read timeout on streaming calls entirely
and relies on the server-side `connect-timeout-ms` header to enforce the
command timeout — matching the JS SDK, which has no per-chunk read
timeout. The race is now structurally impossible rather than mapped
over.
## Usage
```python
cmd = sandbox.commands.run("sleep 10", timeout=2, background=True)
try:
for _ in cmd:
pass
except TimeoutException:
print("command timed out") # server-side deadline_exceeded, no transport race
```
## Tradeoff
A silently dropped connection (no RST) on a command with `timeout=0`
(disabled) now has no client-side read backstop and relies on keepalive
pings — the same posture as the JS SDK.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
The flaky test `test_run_with_too_short_timeout_iterating` failed
intermittently because, when iterating a background command's output,
the Python SDK sets the HTTP stream `read` timeout to the command
`timeout` — so it races the server's own `deadline_exceeded` response.
When the client read timeout won, a raw `httpcore.ReadTimeout` leaked
out instead of a `TimeoutException`. This PR maps
`httpcore.TimeoutException` to `TimeoutException` in
`handle_rpc_exception`, so callers get a consistent timeout error
regardless of which side fires first, plus unit tests for the mapping.
It also adds a JS parity test (JS was never affected — connect-es always
normalizes timeouts into an already-mapped `ConnectError`).
## Usage
```python
cmd = sandbox.commands.run("sleep 10", timeout=2, background=True)
try:
for _ in cmd:
pass
except TimeoutException:
print("command timed out") # now raised reliably, no raw httpcore.ReadTimeout
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Allow disabling client-side API key **format** validation. Previously
the SDKs hard-required keys to match the `e2b_<hex>` pattern, which
blocked deployments that issue API keys with a different format. Instead
of a custom-prefix override, this adds a simple on/off toggle.
The default behaviour is unchanged (validation stays **on**).
## Configuration
| Form | JS | Python |
| --- | --- | --- |
| Env var | `E2B_VALIDATE_API_KEY=false` | `E2B_VALIDATE_API_KEY=false`
|
| Connection option | `validateApiKey: false` | `validate_api_key=False`
|
The connection option takes priority over the environment variable.
## Usage
**JavaScript / TypeScript**
```ts
import { Sandbox } from 'e2b'
// Via connection option
const sandbox = await Sandbox.create({
apiKey: 'custom_key_format',
validateApiKey: false,
})
// Or via env var: E2B_VALIDATE_API_KEY=false
```
**Python**
```python
from e2b import Sandbox
# Via connection option
sandbox = Sandbox(
api_key="custom_key_format",
validate_api_key=False,
)
# Or via env var: E2B_VALIDATE_API_KEY=false
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
- Python `upload_url`/`download_url` now raise
`InvalidArgumentException` when `use_signature_expiration` is passed for
an unsecured sandbox, matching the JS SDK (which now throws
`InvalidArgumentError` instead of a plain `Error`).
- A signature expiration of `0` was treated as falsy and silently
produced a never-expiring signed URL; it now produces an immediately
expiring URL in both SDKs.
- Adds unit tests mirrored across both SDKs
(`tests/sandbox/urls.test.ts` ↔ `tests/test_sandbox_urls.py`) plus a
changeset.
## Usage
```python
sbx = Sandbox() # not secure=True
sbx.download_url("a.txt", use_signature_expiration=120) # now raises InvalidArgumentException instead of silently ignoring the expiration
```
```ts
const sbx = await Sandbox.create({ secure: true })
await sbx.downloadUrl('a.txt', { useSignatureExpiration: 0 }) // URL now expires immediately instead of never
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
- Python `write()` / `write_files()` now return `WriteInfo.type` as the
`FileType` enum instead of the raw API string (sync and async; the JS
string union was already correct, and the volumes client already
converts to `VolumeEntryStatType`).
- `EntryInfo.modified_time` is now timezone-aware UTC (protobuf
`ToDatetime()` returns naive datetimes by default), and naive volume
`atime`/`mtime`/`ctime` timestamps are normalized to UTC.
- `gzip=true` uploads now imply the `application/octet-stream` path in
both JS and Python instead of being silently ignored on the default
`multipart/form-data` path; on envd < 0.5.7 the upload falls back to
uncompressed multipart, matching the existing `use_octet_stream`
fallback.
- Adds sandbox-free unit tests for the model conversions, strengthens
write/info integration test assertions, and includes changesets for
`@e2b/python-sdk` and `e2b`.
## Usage examples
```python
info = sandbox.files.write("hello.txt", "hi")
info.type == FileType.FILE # was the raw string "file"
entry = sandbox.files.get_info("hello.txt")
entry.modified_time.tzinfo # datetime.timezone.utc (was None)
sandbox.files.write("big.bin", data, gzip=True) # now actually gzip-compressed
```
## Test plan
- [x] `pytest tests/test_filesystem_models.py` (new unit tests, 5
passed)
- [x] Python sync + async integration tests for `write`, `info`,
`content_encoding` (16 each, passed against live sandboxes)
- [x] JS `write.test.ts` + `contentEncoding.test.ts` (14 passed)
- [x] `pnpm run format`, `pnpm run lint`, `pnpm run typecheck`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
`E2B_ACCESS_TOKEN` is deprecated, so CLI commands whose endpoints accept
either credential now authenticate with `E2B_API_KEY` instead of
requiring an access token.
- `e2b template list` now uses `ensureAPIKey()`. The underlying `GET
/templates` endpoint accepts both `ApiKeyAuth` and `AccessTokenAuth`,
and since the access token is deprecated we standardize on the API key.
- `e2b template create` no longer calls `ensureAccessToken()`. Its only
API calls — `POST /v3/templates` and `POST
/v2/templates/{id}/builds/{bid}` (via the SDK's `Template.build`) —
accept only `ApiKeyAuth`, so requiring an access token locked out
API-key-only environments for no reason.
- `e2b template build` is intentionally left as-is: its v1 endpoints and
docker-registry login are access-token-only at the API level.
- Removed the now-unused `ensureAccessTokenOrAPIKey()` helper and the
`'BOTH'` variant of the auth-error box that an earlier iteration of this
PR introduced.
- Adds a real backend-integration test in
`tests/commands/template/create.test.ts` that mirrors the existing
`backend_integration.test.ts` pattern: use the real `E2B_DOMAIN` and
assert end-to-end that `template create` succeeds with only
`E2B_API_KEY` set (no `E2B_ACCESS_TOKEN`). Uses a unique template name
per run and cleans up the created template in `afterAll`.
## Test plan
- [x] `pnpm --filter @e2b/cli run typecheck`
- [x] `pnpm --filter @e2b/cli run lint`
- [x] `pnpm --filter @e2b/cli run format`
- [x] `pnpm --filter @e2b/cli run test` (local, with real `E2B_API_KEY`
— new test passes; create succeeds without `E2B_ACCESS_TOKEN`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
Fixes three command/PTY streaming issues in the Python and JS SDKs:
- **Multibyte UTF-8 corruption (JS + Python sync/async):** stdout/stderr
were decoded per-chunk, so a UTF-8 character split across two stream
chunks turned into replacement characters. Each handle now keeps a
persistent incremental decoder per stream
(`codecs.getincrementaldecoder` in Python, a shared `TextDecoder` with
`{ stream: true }` in JS) and flushes any incomplete trailing bytes to
`�` on the end event, preserving the existing broken-UTF-8 behavior.
- **`commands.list()` optionals (Python):** now returns `None` instead
of `""` for unset proto3-optional `tag` and `cwd` fields, matching the
declared `Optional[str]` types and the JS SDK.
- **Leaked connections (Python):** command/PTY/watch streams are now
closed when stream setup fails, instead of abandoning the generator (and
its pooled HTTP connection) until GC.
## Usage example
```python
# Split multibyte output is now decoded correctly instead of returning "ð\x9f\x98\x80"-style garbage
result = sandbox.commands.run("printf '😀'")
assert result.stdout == "😀"
# Unset fields are None rather than ""
proc = sandbox.commands.list()[0]
assert proc.tag is None # previously ""
```
## Testing
- New unit tests for incremental/trailing UTF-8 decoding (Python sync +
async, JS).
- Live command/PTY/watch integration suites pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
`Sandbox.connect()` now short-circuits in debug mode instead of calling
the control plane, matching `Sandbox.create()` — fixed in the JS SDK and
both sync/async Python SDKs (static and instance variants). The Python
SDK's `connect()` also previously passed the generated client's `Unset`
sentinel through as the envd/traffic access tokens when they were absent
(non-secure sandboxes), which made `download_url()`/`upload_url()` emit
broken signed URLs; `_cls_connect` now normalizes the response into
`SandboxCreateResponse` with proper `None` values, the same pattern
`_create_sandbox` already uses. Dead `Unset` checks and the now-unused
generated `Sandbox` model import were cleaned up, and unit tests cover
both behaviors in all three implementations. Debug mode is resolved
through `ConnectionConfig`, so the `E2B_DEBUG` env var triggers the
short-circuit in both `connect()` and `create()`, not just an explicit
`debug=True`.
## Usage
```ts
// JS: works fully offline with E2B_DEBUG / debug: true (no control plane call)
const sbx = await Sandbox.connect(sandboxId, { debug: true })
```
```python
# Python: non-secure sandboxes get unsigned URLs again instead of broken signatures
sbx = Sandbox.connect(sandbox_id)
print(sbx.download_url("file.txt")) # no garbage signature when envd token is absent
# Debug mode skips the control plane, like Sandbox.create()
sbx = Sandbox.connect(sandbox_id, debug=True)
```
## Testing
New unit tests in `tests/sandbox/connect.test.ts`,
`tests/sync/sandbox_sync/test_connect.py`, and
`tests/async/sandbox_async/test_connect.py`; existing connect
integration suites pass against the live API (8/8 sync Python, 8/8 async
Python, 6/6 JS).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Python's `$` regex anchor also matches just before a trailing newline,
so file-metadata keys/values ending in `\n` passed client-side
validation (unlike the JS SDK, where `$` matches only the true end of
string) and then failed deep in the HTTP stack with an opaque "illegal
header value" error. This re-anchors both validation regexes with
`\A`/`\Z` so such inputs are rejected upfront with
`InvalidArgumentException`, matching JS behavior and the existing
convention used for the API-key pattern.
Also adds trailing-newline rejection test cases to the sync/async Python
suites and, for parity of coverage, to the JS SDK suite (JS already
rejected them — no behavior change there).
## Usage example
```python
sandbox.files.write("file.txt", "x", metadata={"author": "mish\n"})
# before: passed validation, then httpcore raised 'Illegal header value'
# after: raises InvalidArgumentException with a clear message
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Problem
When a sandbox is killed (or reaches its end of life) while a request is
in flight, both SDKs surfaced unusable errors:
- **JS**: `SandboxError: 2: [unknown] terminated` — typed, but cryptic
and says nothing about the sandbox being killed.
- **Python**: leaked a completely raw `httpcore.RemoteProtocolError:
<StreamReset stream_id:1, error_code:2, remote_reset:True>`.
This affected the whole envd streaming family (`commands.run`, PTY
sessions, `files.watchDir`/`watch_dir`) and the `files.read`/`write`
HTTP transfers.
The stream-reset signature alone can't distinguish the sandbox dying
from an intermediary (load balancer, network) dropping the connection —
so the SDKs now actively check, and only transform the error when the
sandbox is confirmed gone.
## Fix
**Health-check disambiguation.** When the connection-terminated
signature appears (JS: `ConnectError` `Code.Unknown` + `terminated` or
Undici `TypeError: terminated`; Python: `httpcore`/`httpx`
`RemoteProtocolError`), the SDK probes envd's `/health` endpoint:
- **502 (sandbox confirmed gone)** → `TimeoutError` (JS) /
`TimeoutException` (Python): "The sandbox was killed or reached its end
of life while the request was in flight." This matches how requests to
an *already-dead* sandbox surface today (the 502 / `Code.Unavailable`
mappings raise the timeout error type), so the exception type no longer
depends on whether the sandbox died just before or just during the
request.
- **Anything else** (still running, or probe inconclusive) → the
original error propagates unchanged, exactly as before this PR.
The probe (5s timeout) runs only on the termination signature, never on
the happy path or for other errors. A health-check closure is plumbed
into `Commands`/`Pty`/`Filesystem` and the command/watch handles in JS
and sync/async Python; `Commands`/`Pty` now receive the envd API client
in their constructors (internal signature change).
**Cleanup** (`e2b_connect/client.py`): removed the
`@_retry(RemoteProtocolError, 3)` decorators from
`call_server_stream`/`acall_server_stream`. They never executed —
`inspect.iscoroutinefunction` is false for (async) generator functions,
and calling a generator function doesn't run its body, so the wrapper's
`try/except` could never fire. A *working* mid-stream retry would be
wrong anyway (it would replay already-delivered events). Unary retries
are unchanged.
## Before / after
```ts
const sandbox = await Sandbox.create()
const cmd = await sandbox.commands.run('sleep 60', { background: true })
await sandbox.kill() // e.g. from another process
await cmd.wait()
// before: SandboxError: 2: [unknown] terminated
// after: TimeoutError: [unknown] terminated: The sandbox was killed or reached
// its end of life while the request was in flight.
```
```python
sandbox = Sandbox.create()
cmd = sandbox.commands.run("sleep 60", background=True)
sandbox.kill()
cmd.wait()
# before: httpcore.RemoteProtocolError: <StreamReset stream_id:1, error_code:2, remote_reset:True> (not an e2b type!)
# after: e2b.exceptions.TimeoutException: <StreamReset ...>: The sandbox was killed
# or reached its end of life while the request was in flight.
```
If the health probe does not confirm the sandbox is gone (e.g. a load
balancer dropped the connection, or local envd in debug mode), the
original error propagates unchanged — the SDK only makes a claim when it
has verified it.
## Notes
- Not covered: errors raised while consuming a `format: 'stream'` body
**after** `files.read` returns (JS `ReadableStream` consumption happens
in user code). Python is fully covered since httpx buffers non-streaming
responses inside the request call.
## Tests
- Unit: confirmed-kill → `TimeoutError`/`TimeoutException`, raw-error
passthrough for running/unknown/probe-failure, health check skipped for
unrelated errors — 28 Python + 25 JS assertions pass.
- Integration (run against live sandboxes, all passing): start `sleep
60`, kill the sandbox, assert `wait()` raises
`TimeoutError`/`TimeoutException` with the *confirmed* kill message —
JS, sync Python, and async Python.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Client-side counterpart to
[e2b-dev/infra#2982](https://github.com/e2b-dev/infra/pull/2982): adds
an `allowNetworkMounts`/`allow_network_mounts` option to filesystem
directory watching across the JS and Python (sync + async) SDKs, so
clients can explicitly opt into watching paths on network filesystem
mounts (NFS, CIFS, SMB, FUSE), which envd rejects by default. Events on
network mounts may be unreliable or not delivered at all, hence the
explicit opt-in.
This regenerates the filesystem proto code from the updated spec and
threads the flag through `watchDir`/`watch_dir` (streaming `WatchDir`
and polling `CreateWatcher`). The option requires envd 0.6.4 (shipped by
the infra PR); using it against an older sandbox throws a
`TemplateError`/`TemplateException`. Default behavior is unchanged.
Includes new watch tests for all three SDKs and a minor-bump changeset
for `e2b` and `@e2b/python-sdk`.
> Note: the new tests exercise the flag on a regular directory (a
network mount can't be set up from SDK tests) and require envd 0.6.4, so
this should land with/after the infra deploy. All pre-existing watch
tests pass; the new ones currently fail with the expected
`TemplateError` against the deployed envd.
### Usage
**JavaScript**
```ts
const handle = await sandbox.files.watchDir(
'/mnt/nfs-share/my-dir',
(event) => console.log(event.type, event.name),
{ allowNetworkMounts: true }
)
```
**Python (async)**
```python
handle = await sandbox.files.watch_dir(
"/mnt/nfs-share/my-dir",
on_event=lambda e: print(e.type, e.name),
allow_network_mounts=True,
)
```
**Python (sync)**
```python
handle = sandbox.files.watch_dir("/mnt/nfs-share/my-dir", allow_network_mounts=True)
for e in handle.get_new_events():
print(e.type, e.name)
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
`Sandbox.getMetrics()` in the JS SDK passed the `start` and `end`
options as path parameters instead of query parameters, so openapi-fetch
never serialized them and the requested time range was silently ignored.
They are now sent under `query`, matching the OpenAPI spec; the Python
SDKs already passed them correctly. The metrics tests across JS and
Python (sync/async) now assert that returned metrics fall within the
requested window (with slack for 5s metric-bucket alignment) and that a
window from before the sandbox existed returns no metrics, which would
have caught this regression.
## Usage
```ts
const start = new Date(Date.now() - 60_000)
const metrics = await sandbox.getMetrics({ start, end: new Date() })
// metrics are now actually limited to the requested time range
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
The per-event-loop caches for `AsyncHTTPTransport`s and
`httpx.AsyncClient`s (Sandbox API, envd, and volume clients) were keyed
by `id(asyncio.get_running_loop())`, but CPython reuses object ids of
dead loops almost immediately — so sequential loops could inherit a
transport bound to a previous, closed loop and fail. The caches are now
`weakref.WeakKeyDictionary`s keyed by the loop object itself, which
makes stale id collisions impossible and releases entries when their
loop is garbage collected (fixing a leak where dead-loop entries
accumulated forever). Added regression tests covering the
sequential-loop scenario for all three caches.
This pattern no longer breaks:
```python
# e.g. a worker or test harness running repeated event loops
for job in jobs:
asyncio.run(process_with_sandbox(job)) # each run previously risked
# inheriting a closed loop's transport
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes a batch of review findings in the JS and Python volume SDKs,
aligning behavior between the two. Python now caches `AsyncVolume` HTTP
transports per event loop and proxy (sync per thread) instead of a
process-wide singleton, applies the 60s default `request_timeout` to
metadata operations that previously ran with httpx timeouts disabled, no
longer falls back to `E2B_ACCESS_TOKEN` for volume content auth, and no
longer mutates the caller's `headers` dict. JS `Volume.readFile` now
returns empty values instead of `undefined` for empty files, and volume
content requests get the documented 60s default request timeout. All
changes are covered by new mock-based unit tests (no live API needed)
plus changesets for both SDKs.
## Usage examples
```python
volume = await AsyncVolume.connect(volume_id)
# Times out after 60s by default (previously could hang indefinitely)
entries = await volume.list("/")
```
```ts
const volume = await Volume.connect(volumeId)
// Returns an empty Blob / ReadableStream for empty files (previously undefined)
const blob = await volume.readFile('empty.txt', { format: 'blob' })
// Times out after the documented 60s by default; pass 0 to disable
await volume.getInfo('file.txt', { requestTimeoutMs: 0 })
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
`handleApiError` and `handleEnvdApiError` used the presence of an error
body as the failure signal, but openapi-fetch returns `error: undefined`
for non-2xx responses with `Content-Length: 0` (and `error: ''` for
empty bodies without that header), so such failures were silently
treated as success — e.g. `sandbox.isRunning()` returned `true` for a
failing `/health` endpoint. Both handlers now gate on `response.ok` and
fall back to the status text when no error message is available. Volumes
go through the same shared handler, so they inherit the fix. Regression
tests cover both handlers and the volume path, including a
500-with-empty-body case that fails against the old code.
## Example
```ts
// Before: a 500 response with Content-Length: 0 was treated as success
await Volume.create('my-volume') // resolved with 'Response data is missing'
// After: the failure surfaces as the proper SDK error
await Volume.create('my-volume') // throws VolumeError('500: Internal Server Error')
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
The `Sandbox.kill()` instance method discarded the boolean result of the
static `SandboxApi.kill()`, returning `Promise<void>` — inconsistent
with the static method and with both Python SDKs (sync and async), which
return `bool` from both variants. It now returns `Promise<boolean>`
(`true` if the sandbox was killed, `false` if it was not found),
including `true` in debug mode to match the other implementations.
Non-breaking: `void` → `boolean` is additive for existing callers. The
kill test now asserts the return value.
## Usage
```ts
const sandbox = await Sandbox.create()
const killed = await sandbox.kill()
console.log(killed) // true if killed, false if the sandbox was not found
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes a batch of connection-handling bugs found in review, with unit
tests for each and a changeset for both SDKs.
- **Python**: cached HTTP transports are now keyed on the configured
proxy, so clients with different (or no) proxy settings no longer
silently reuse the transport built for the first proxy seen.
- **Python**: `request_timeout` now applies to control-plane API
requests — the underlying httpx client was previously built with no
timeout at all (`request_timeout=0` still disables it).
- **Python**: the server-stream parser no longer stalls or drops the
final envelope when the remaining payload is shorter than the 5-byte
envelope header; also removed the no-op `@_retry` decorators from the
streaming RPC methods to avoid confusion.
- **JS + Python**: an explicit `debug=False` / `debug: false` now
overrides `E2B_DEBUG=true` instead of being ignored.
- **JS**: the RPC logger no longer crashes requests with a `TypeError`
when a response contains protobuf int64 (`bigint`) fields (e.g.
`EntryInfo.size` returned by `files.list()`/`stat()` and `includeEntry`
watch events); they are logged as strings.
## Usage examples
```ts
// JS: logging RPCs whose responses carry int64 fields no longer throws
const sbx = await Sandbox.create({ logger: console })
await sbx.files.list('/')
// JS: force-disable debug mode even when E2B_DEBUG=true is set
const sbx2 = await Sandbox.create({ debug: false })
```
```python
# Python: per-call API timeout is now actually enforced
Sandbox.list(request_timeout=10)
# Python: clients with different proxies get their own transports
Sandbox.create(proxy="http://127.0.0.1:8080")
Sandbox.create() # no longer routed through the proxy above
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Client-side counterpart to
[e2b-dev/infra#2930](https://github.com/e2b-dev/infra/pull/2930): adds
an `includeEntry`/`include_entry` option to filesystem directory
watching across the JS and Python (sync + async) SDKs, so each
`FilesystemEvent` can carry the affected entry's `EntryInfo`
(best-effort — unset for remove/rename-away events where the path no
longer exists). This regenerates the filesystem proto code from the
updated spec, threads the flag through `watchDir`/`watch_dir` (streaming
`WatchDir` and polling `CreateWatcher`), maps the new `entry` field onto
the event, and extracts a shared entry-mapping helper reused by
`list`/`getInfo`/`rename`. The option degrades gracefully: older
sandboxes (< envd 0.6.2) ignore it and leave `entry` unset, so there's
no hard version gate. Includes new watch tests for all three SDKs and a
minor-bump changeset for `e2b` and `@e2b/python-sdk`.
> Note: the entry-info tests require envd 0.6.2 (shipped by the infra
PR), so this should land with/after that deploy.
### Usage
**JavaScript**
```ts
const handle = await sandbox.files.watchDir(
'my-dir',
(event) => {
console.log(event.type, event.name, event.entry?.path, event.entry?.type)
},
{ includeEntry: true }
)
```
**Python (async)**
```python
def on_event(e):
print(e.type, e.name, e.entry.path if e.entry else None)
handle = await sandbox.files.watch_dir("my-dir", on_event=on_event, include_entry=True)
```
**Python (sync)**
```python
handle = sandbox.files.watch_dir("my-dir", include_entry=True)
for e in handle.get_new_events():
print(e.type, e.name, e.entry.path if e.entry else None)
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a `metadata` option to file uploads and surfaces persisted metadata
on every `EntryInfo` / `WriteInfo` returned by `getInfo`, `list`,
`rename`, and write responses, across the JS and Python (sync + async)
SDKs.
Metadata is sent as `X-Metadata-<key>: <value>` request headers and
persisted by envd as `user.e2b.*` extended attributes; the same map is
applied to every file in a multi-file upload. Keys and values must be
printable US-ASCII and keys are lowercased by the sandbox, so they may
differ in case when read back. Requires **envd 0.6.2 or later**.
This syncs the envd OpenAPI spec and filesystem proto with
[infra#2732](https://github.com/e2b-dev/infra/pull/2732) and regenerates
the JS/Python clients.
## Usage
**JavaScript / TypeScript**
```ts
// Single file
const info = await sandbox.files.write('report.txt', 'hello', {
metadata: { author: 'mish', purpose: 'demo' },
})
console.log(info.metadata) // { author: 'mish', purpose: 'demo' }
// Multiple files (same metadata applied to each)
await sandbox.files.writeFiles(
[
{ path: 'a.txt', data: 'A' },
{ path: 'b.txt', data: 'B' },
],
{ metadata: { source: 'import' } }
)
// Read it back
const stat = await sandbox.files.getInfo('report.txt')
console.log(stat.metadata) // { author: 'mish', purpose: 'demo' }
```
**Python**
```python
# Single file
info = sandbox.files.write("report.txt", "hello", metadata={"author": "mish"})
print(info.metadata) # {"author": "mish"}
# Multiple files (same metadata applied to each)
sandbox.files.write_files(
[
WriteEntry(path="a.txt", data="A"),
WriteEntry(path="b.txt", data="B"),
],
metadata={"source": "import"},
)
# Read it back
stat = sandbox.files.get_info("report.txt")
print(stat.metadata) # {"author": "mish"}
```
The async Python API is identical with `await`.
## Tests
Integration tests cover the round-trip across `write` / `getInfo` /
`list` / `rename`, octet-stream uploads, multi-file uploads,
overwrite-clears-stale-metadata, and metadata written directly as
`user.e2b.*` xattrs via `sandbox.commands.run` surfacing in `getInfo`.
They require a sandbox running envd 0.6.2+.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Aligns several behavioral and API-surface discrepancies between the JS
and Python SDKs found during a cross-SDK audit. **Python:**
`commands.send_stdin`/`CommandHandle.send_stdin` now accept `bytes`
(plus `request_timeout` on the handle), `git.reset` gets a typed
`GitResetMode` with JS-matching validation, `sandbox_url` is threaded
through `get_api_params` (and the dead `SandboxOpts` key removed), and
`from_image` requires both `username` and `password` when credentials
are given. **JS:** `getFullInfo` was removed in favor of a single
`getInfo` that now includes `sandboxDomain` (matching Python's
`get_info`), `fromImage` requires both credentials, `getBuildStatus`
defaults `logsOffset` to `0`, `getMetrics`/`kill` short-circuit
consistently in debug mode (instance + static), and `requestTimeoutMs:
0` explicitly disables the request timeout. Tests were added on both
sides (git-arg validation, stdin bytes, credential validation,
timeout-0, connection config) and the CLI's `sandbox info` now uses
`getInfo`. See the changeset for the full per-SDK list.
## Usage examples
```ts
// JS: registry credentials now require both fields
Template().fromImage('registry.example.com/img:latest', { username: 'u', password: 'p' })
// JS: getInfo now exposes sandboxDomain (getFullInfo removed)
const info = await Sandbox.getInfo(sandboxId)
console.log(info.sandboxDomain)
// JS: disable the request timeout
await Sandbox.create({ requestTimeoutMs: 0 })
```
```python
# Python: send raw bytes to stdin
sandbox.commands.send_stdin(cmd.pid, b"hello")
# Python: typed git reset mode (validated)
sandbox.git.reset(repo, mode="hard")
# Python: registry credentials require both fields
Template().from_image("registry.example.com/img:latest", username="u", password="p")
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a `proxy` connection parameter to the JS SDK, mirroring the Python
SDK. When set, requests are routed through the given HTTP proxy via an
undici `ProxyAgent` dispatcher (fetchers are cached per-proxy so
non-proxy traffic is unaffected). It applies to control-plane API
requests, all requests made to the returned sandbox (REST plus
filesystem/commands/pty RPC), and volume requests. Behavior is unchanged
when no proxy is provided, and unit tests cover both the API and envd
fetch paths.
## Usage
```ts
import { Sandbox } from 'e2b'
// Routes API + all sandbox requests through the proxy
const sandbox = await Sandbox.create({
proxy: 'http://user:pass@127.0.0.1:8080',
})
await sandbox.files.write('/hello.txt', 'world')
// Also works when connecting to an existing sandbox
const sbx = await Sandbox.connect(sandboxId, { proxy: 'http://127.0.0.1:8080' })
```
> Proxying relies on the optional `undici` package and the Node runtime;
in browser/edge runtimes requests use global `fetch`, which has no proxy
support (same as the existing HTTP/2 dispatcher).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Reconciles divergences found while auditing the sync and async Python
SDK trees, keeping behavior equivalent across both.
- **Parameter ordering:** aligned `_create` and `Commands._start`
signatures to the public API and to each other, and reordered the
`Commands.connect` rpc args (`headers` before `timeout`) to match the
`_start` convention.
- **`pause` return:** the public `pause()` / `beta_pause()` are now
annotated `-> str` and actually return the sandbox ID (matching
`_cls_pause` and the class-method form, which already returned it)
instead of `-> None`; the `:return:` docstrings are restored.
- **Exceptions:** the internal "Body of the request is None" guard in
`sandbox_api` now consistently raises a bare `Exception` (matching the
volume client) instead of mixing `Exception`/`SandboxException` between
sync and async.
- **Misc:** async `Filesystem.write` now passes keyword args; the async
constructor reuses the cached `envd_api_url` property instead of
recomputing the sandbox URL; async pty `resize` gains a `-> None`
annotation.
- **Docstrings:** aligned the deprecation marker and
`get_metrics`/`write_files`/`kill` wording across sync/async, and fixed
a `**seconds**s` typo.
These are alignment/consistency fixes only; the deeper architectural
async-vs-sync splits (streaming-vs-polling `watch_dir`, pty `on_data`,
command output callbacks) are intentional and left untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Sandbox.connect` was attaching the data-plane envd headers
(`E2b-Sandbox-Id`, `E2b-Sandbox-Port`) to the control-plane `POST
/sandboxes/{id}/connect` call in both the sync and async SDKs. These
headers belong only on data-plane (filesystem/commands/pty) requests, so
this aligns the Python SDK with the JS SDK, which never sends them on
the connect call.
## Usage
No API change — `Sandbox.connect(sandbox_id)` (and the async equivalent)
behaves the same, just without the spurious headers on the control-plane
request.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Add empty `py.typed` markers to the `e2b` and `e2b_connect` packages so
mypy/Pyright honor the inline annotations on `Sandbox`, `AsyncSandbox`,
and other public APIs instead of treating imports as `Any`. Includes a
patch changeset for `@e2b/python-sdk`.
## Test plan
- [ ] `pip install` the built wheel in a fresh env and confirm `mypy` no
longer reports `e2b` as untyped without `--follow-untyped-imports`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## Summary
- Replace verbose
`paths['/route']['method']['responses'|'requestBody'][...]` traversal
with direct `components['schemas'][...]` references in
`packages/js-sdk/src/template/buildApi.ts` and
`packages/cli/src/commands/template/build.ts`.
- Matches the existing convention used throughout `sandboxApi.ts` and
reads at a glance.
## Test plan
- [x] `pnpm run format` / `lint` / `typecheck` pass for `e2b` and
`@e2b/cli`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>