## 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>
Adds a `buildTemplate` CI job that builds and publishes the `base`
template through the e2b CLI, running alongside the existing DockerHub
image push (renamed to `buildAndPushImage`). For security, the CLI is
built from source in this repo rather than installing the published
`@e2b/cli` package; this build-and-global-install logic lives in a
reusable composite action at `.github/actions/build-cli` so it can be
shared across workflows. Removes the static `templates/base/e2b.toml`
since template config is now driven by the CLI invocation, and switches
the Dockerfile's `node` user/group creation to system accounts (`-r`).
## Usage
Any workflow can build and install the CLI globally with a single step:
```yaml
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/build-cli
- run: e2b template create base --memory-mb 512
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a `dorny/paths-filter` change-detection job to the PR-triggered
workflows so jobs only run when relevant paths change: Lint/Typecheck
run only when package code, spec, or lint configs change; Generated
files runs only when codegen inputs/outputs change; and the
JS/Python/CLI SDK tests run only when the respective SDK changes (CLI
also runs on JS SDK changes since it builds against it). The SDK test
jobs are gated *inside* the reusable workflows via a new `run` input
rather than by skipping the caller, so the required matrix status checks
still report (skipped jobs report success) and branch protection stays
satisfied. Shared paths (spec, lockfiles, `package.json`,
`.tool-versions`) and `workflow_dispatch` runs still trigger everything.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <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>
## Summary
Fixes Dependabot alert
[#304](https://github.com/e2b-dev/E2B/security/dependabot/304)
(critical,
[CVE-2026-9277](https://nvd.nist.gov/vuln/detail/CVE-2026-9277) /
[GHSA-w7jw-789q-3m8p](https://github.com/ljharb/shell-quote/security/advisories/GHSA-w7jw-789q-3m8p)).
`shell-quote` is a transitive dependency (pulled in via `npm-run-all`).
Versions `< 1.8.4` are vulnerable to a command-injection issue:
`quote()` did not escape line terminators (`\n`, `\r`, U+2028, U+2029)
in object `.op` values, allowing content after a newline to execute as a
separate shell command.
This adds a pnpm override forcing `shell-quote` to `^1.8.4`, consistent
with the existing security-override pattern in the root `package.json`.
The lockfile now resolves `shell-quote@1.8.4`.
## Changes
- `package.json`: add `"shell-quote@<1.8.4": "^1.8.4"` to
`pnpm.overrides`
- `pnpm-lock.yaml`: regenerated — `shell-quote` 1.8.3 → 1.8.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <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 the previously-missing test coverage for `pty.kill()`, which was
untested across all three SDK implementations despite the rest of the
PTY API (create/connect/sendInput/resize) being well covered.
Each suite gets two tests covering both return paths of `kill()`:
- **Kill a live PTY** — asserts `kill()` returns `true`, then confirms
the process is gone via `kill -0 <pid>` (throws
`ProcessExitError`/`CommandExitException`).
- **Kill a non-existent PID** — asserts `kill()` returns `false`,
matching the documented not-found behavior.
The tests mirror the style of the existing `commands.kill` tests and
were verified to lint cleanly and be discovered by vitest/pytest (full
runs require a live sandbox).
## Files
- `packages/js-sdk/tests/sandbox/pty/kill.test.ts`
- `packages/python-sdk/tests/sync/sandbox_sync/pty/test_pty_kill.py`
- `packages/python-sdk/tests/async/sandbox_async/pty/test_pty_kill.py`
🤖 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>
## Summary
Removes the `ensureAccessToken()` call (and its now-unused import) from
`e2b template create`. The command authenticates solely via the API key
(`ensureAPIKey()`), so the access-token check was redundant.
## Changes
- Drop `ensureAccessToken` import and call in
`packages/cli/src/commands/template/create.ts`.
- Add a patch changeset for `@e2b/cli`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
makes the sync python python API http transport cache thread-local to
handle unsafe usage of the shared transport under pressure (e.g.
concurrent template builds). uses the same logic that we were using for
envd.
test
`test_sync_api_transport_cache_reuses_within_thread_and_isolates_across_threads`
fails on main, passes on branch.
Adds `sendStdin`/`send_stdin` and `closeStdin`/`close_stdin` directly on
the command handle (JS, Python sync, and Python async) so background
commands can be fed stdin and signalled EOF without reaching back to
`sandbox.commands` with the PID. The handle delegates to the existing
`Commands` methods via closures, mirroring how `kill` is wired, and also
adds the previously-missing `close_stdin`/`aclose_stdin` to the Python
`Commands` class (version-gated on `ENVD_ENVD_CLOSE`, matching JS).
PTY-created handles don't support these and raise a clear error, and the
existing PID-based `Commands.sendStdin` methods are untouched, so the
change is fully backward-compatible. Includes handle-based tests across
all three SDKs and a changeset bumping `e2b` and `@e2b/python-sdk` at
patch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
- Strips all v1 build logic from \`e2b template build\` (\`bd\`): Docker
build/push, API calls, config-loading, and retry/proxy handling are
removed
- The command now only displays the existing yellow deprecation warning
(pointing to the v2 migration guide) and exits with code 1
- Deletes \`buildWithProxy.ts\` which is no longer referenced anywhere
- Moves \`getDockerfile\` helper (used by \`template create\` and
\`template migrate\`) to a new shared \`dockerfile.ts\` module, leaving
\`build.ts\` as a clean stub
## Test plan
- [ ] Run \`e2b template build\` — confirm deprecation warning is shown
and the command exits immediately
- [ ] Run \`e2b template create\` and \`e2b template migrate\` — confirm
they still work (both use the moved \`getDockerfile\` helper)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>