Commit Graph

508 Commits

Author SHA1 Message Date
Mish Ushakov c1415f3ec7 Stream volume file uploads and downloads instead of buffering in memory (#1453)
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>
2026-06-22 12:11:03 -07:00
Mish Ushakov 60feee3cf6 Stream SDK file uploads and downloads instead of buffering in memory (#1433)
## 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>
2026-06-22 19:45:32 +02:00
Mish Ushakov 8171a03765 fix(python-sdk): let api_headers Authorization win over deprecated access_token (#1464)
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>
2026-06-20 14:53:37 +02:00
Mish Ushakov f3e7f33973 refactor(sdks): tidy SDK auth and deprecate ConnectionConfig access token (#1452)
## 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>
2026-06-18 14:20:21 +02:00
Matt Brockman 432c0913c8 Add integration user agent composibility (#1454)
user agent is now composable, improving attribution
2026-06-17 14:21:50 -07:00
Mish Ushakov 706c553295 fix(sdks): fix template build bugs and consolidate shell quoting (#1442)
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>
2026-06-17 10:57:21 +02:00
Mish Ushakov 7cec36dcb5 fix(python-sdk): drop stream read timeout, enforce command timeout server-side (#1448)
## 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>
2026-06-16 20:23:54 +02:00
Mish Ushakov 5de9bc2354 fix(python-sdk): map httpcore timeouts to TimeoutException to fix flaky test (#1444)
## 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>
2026-06-16 12:56:09 +02:00
Mish Ushakov 78c200afc8 feat(sdk): allow disabling client-side API key validation (#1360)
## 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>
2026-06-16 11:58:55 +02:00
Mish Ushakov e88ae338e8 fix(sdks): handle signed URL expiration edge cases in upload/download URLs (#1429)
## 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>
2026-06-16 11:27:34 +02:00
Mish Ushakov 5ea287b11e fix(sdk): WriteInfo.type enum, tz-aware times, gzip on default upload path (#1437)
## 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>
2026-06-16 11:26:42 +02:00
Mish Ushakov cb061d269b fix(sdk): correct command/PTY stream handling in Python and JS SDKs (#1441)
## 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>
2026-06-15 23:03:27 +02:00
Mish Ushakov b52eb3c76d fix: debug-mode Sandbox.connect() and Unset token handling in Python connect() (#1428)
## 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>
2026-06-15 21:15:38 +02:00
Mish Ushakov e0ed071443 fix(python-sdk): anchor file-metadata validation regexes with \A/\Z (#1438)
## 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>
2026-06-15 12:11:41 -07:00
Mish Ushakov 82add5b4ea fix(sdks): raise typed, actionable errors when sandbox dies mid-request (#1419)
## 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>
2026-06-15 19:01:06 +00:00
Mish Ushakov e873ee94b6 feat(sdk): add allowNetworkMounts option to filesystem watch (#1420)
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>
2026-06-15 15:34:02 +02:00
Mish Ushakov 32880d6e96 fix(python-sdk): key async transport caches by loop object, not id(loop) (#1434)
## 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>
2026-06-12 22:18:20 +02:00
Mish Ushakov 0b0c728fe8 Fix volume SDK issues: transports, timeouts, empty files, eager stream errors (#1431)
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>
2026-06-12 19:17:11 +00:00
Weilu JIa 1328d9fe44 fix(python): Retry connections on HTTP/2 API transports (#1425) 2026-06-12 11:17:27 +00:00
Matt Brockman 82d6e323fc Use dedicated clients for Python template uploads (#1424) 2026-06-11 17:10:58 -07:00
Matt Brockman 554dc88bd1 Deduplicate sync thread-local client setup (#1423) 2026-06-11 16:05:38 -07:00
Matt Brockman 44c1e9f575 Fix/python sync thread local envd (#1422)
safer threading handling
2026-06-11 19:39:33 +00:00
Mish Ushakov 1d5259ca1d fix(sdks): transport proxy caching, request timeout, and option handling (#1421)
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>
2026-06-11 19:12:47 +00:00
Mish Ushakov da85b1e33c feat(sdk): add includeEntry option to filesystem watch (#1385)
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>
2026-06-10 19:02:09 +02:00
Mish Ushakov 961ffbae84 feat(sdks): expose user-defined file metadata on sandbox.files (#1383)
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>
2026-06-10 16:05:34 +00:00
Mish Ushakov 7dc861f899 fix: align behavior and API surface between the JS and Python SDKs (#1411)
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>
2026-06-10 13:40:22 +00:00
Mish Ushakov 4e16cffc2f feat(js-sdk): add proxy connection param (#1386)
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>
2026-06-09 18:29:16 +02:00
Mish Ushakov d86368a11e fix(python-sdk): align sync and async implementations (#1403)
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>
2026-06-09 14:15:15 +02:00
Mish Ushakov 7296b2c55d fix(python-sdk): drop envd headers from control-plane connect request (#1402)
`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>
2026-06-09 12:43:54 +02:00
Mish Ushakov f2550fa999 chore(python-sdk): mark packages as typed (PEP 561) (#1363)
## 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>
2026-06-09 12:36:06 +02:00
Matt Brockman 6c04e31f68 fix(python-sdk): use thread-local API transports (#1399)
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.
2026-06-08 12:36:11 +02:00
Mish Ushakov 08012eeb4a feat: add sendStdin/closeStdin to CommandHandle (#1397)
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>
2026-06-08 10:51:39 +02:00
Tomas Valenta 073661a8b5 feat(sdk): add API-only header options (#1395)
## Summary
- Adds `apiHeaders` / `api_headers` for API-only custom headers.
- Deprecates the existing `headers` option in favor of the explicit API
header option.

## Usage
```ts
await Sandbox.create({ apiHeaders: { Authorization: 'Bearer ...' } })
```

```python
sandbox = Sandbox.create(api_headers={"Authorization": "Bearer ..."})
```

## Tests
- Python syntax checks passed for touched files.
- IDE lints and `git diff --check` passed.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 23:25:21 -07:00
Mish Ushakov e7a82ea9eb chore: remove unused dead code across SDK packages (#1388)
Removes internal symbols with zero references, found via knip
(js-sdk/cli) and vulture (python-sdk) and verified with repo-wide greps:
`wait` (js-sdk),
`asSandboxTemplate`/`asHeadline`/`selectOption`/`basicDockerfile` (cli),
and `format_execution_timeout_error` (python-sdk). No public API changes
— only dead, unexported-from-index or unreferenced code is dropped.
`format`, `lint`, and `typecheck` pass for all touched packages, and a
patch changeset is included for the three published packages.

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

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:50:13 +02:00
Mish Ushakov 5b2bb941de fix(sdk): consistent rate limit error for envd 429 responses (#1387)
## Summary

The main API client already raised `RateLimitError` (JS) /
`RateLimitException` (Python) for HTTP 429, but the lower-level **envd**
HTTP and RPC layers fell through to a generic sandbox error, so the same
rate-limit condition surfaced as a different type depending on which
request path hit it. This maps envd 429 (and the equivalent gRPC
`ResourceExhausted` code) to the dedicated rate-limit error consistently
across the JS SDK and the Python sync/async SDKs. The JS RPC layer was
also missing the `ResourceExhausted` mapping entirely, which is now
added for parity with Python.

## Changes
- `js-sdk/src/envd/api.ts`, `python-sdk/e2b/envd/api.py` — envd HTTP 429
→ `RateLimitError`/`RateLimitException`
- `js-sdk/src/envd/rpc.ts` — added gRPC `Code.ResourceExhausted` →
`RateLimitError`
- Added unit tests for both the envd HTTP and RPC error mappers in JS
and Python
- Changeset (`e2b`: patch)

## Usage example
```ts
import { Sandbox, RateLimitError } from 'e2b'

try {
  await sandbox.files.write('/tmp/file.txt', 'data')
} catch (err) {
  if (err instanceof RateLimitError) {
    // now reliably caught regardless of which request path was rate limited
  }
}
```

```python
from e2b import RateLimitException

try:
    sandbox.files.write("/tmp/file.txt", "data")
except RateLimitException:
    ...
```

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

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:44:27 -07:00
Matt Brockman 4b9cc043dc Fix/python envd stable transport (#1368) 2026-05-29 16:39:30 -07:00
Mish Ushakov 4a4bb36839 feat(sdk): validate E2B API key format client-side (#1356)
## Summary

- Both JS and Python SDKs now validate that the configured E2B API key
matches `e2b_` followed by 40 hex characters (mirroring the server-side
check in
[`infra/.../keys/key.go`](https://github.com/e2b-dev/infra/blob/main/packages/shared/pkg/keys/key.go#L66))
and throw `AuthenticationError` / `AuthenticationException` with an
example token (`e2b_0000…`) and a link to the API Keys dashboard tab.
- Validation runs inside `ApiClient` / `ApiClient.__init__` whenever an
API key is present, so callers get immediate, actionable feedback
instead of a generic 401 from the server.
- Added unit tests (`validateApiKey.test.ts`,
`test_validate_api_key.py`) and updated existing fixtures that used
placeholder keys like `'test-key'` / `'base-api-key'` to use the valid
format.

## Test plan

- [x] `pnpm run format`, `pnpm run lint`, `pnpm run typecheck`
- [x] `pnpm exec vitest run tests/api/validateApiKey.test.ts
tests/api/handleApiError.test.ts tests/sandbox/abortSignal.test.ts
tests/template/abortSignal.test.ts
tests/sandbox/configPropagation.test.ts tests/connectionConfig.test.ts`
- [x] `poetry run pytest tests/test_validate_api_key.py
tests/test_api_client_transport.py
tests/sync/sandbox_sync/test_config_propagation.py
tests/async/sandbox_async/test_config_propagation.py
tests/test_connection_config.py`

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 21:21:19 +00:00
Mish Ushakov 2691c73d1c chore(sdk): sync OpenAPI spec from infra, regenerate clients (#1357)
## Summary
- Sync `spec/openapi.yml` from
[e2b-dev/infra@main](https://github.com/e2b-dev/infra/blob/main/spec/openapi.yml)
and re-run `make codegen`.
- Schema changes surfaced in the generated clients:
`SandboxMetric.memCache` (new required int64 — also exposed on the
public `SandboxMetrics` wrapper in both SDKs), `NodeStatus` gains
`standby`, `TeamUser.email` becomes nullable + deprecated, and `POST
/v3/templates` gains a `403` response.
- Upstream-only spec changes (not generated because the client filters
by tag): new `AuthProviderBearerAuth`/`AuthProviderTeamAuth` security
schemes, new admin endpoints for team API keys, and a `clusterID` query
param on `GET /nodes`.

## Test plan
- [x] \`pnpm run format\`, \`pnpm run lint\`, \`pnpm run typecheck\`
- [x] JS \`tests/sandbox/metrics.test.ts\` against the live API
- [x] Python sync + async \`test_metrics.py\` against the live API

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:57:28 +02:00
Mish Ushakov a6bf71a083 fix(sdks): handle multi-source COPY/ADD in fromDockerfile (#1355)
## Summary

- Fixes #1349: `Template.fromDockerfile` (JS) and
`Template.from_dockerfile` (Python) silently dropped intermediate
sources from multi-source `COPY`/`ADD`, keeping only the first one and
producing broken images without warning.
- Both parsers now emit one `copy()` call per source to the same
destination (matching Docker semantics), preserving `--chown` across all
calls.
- Added tests in both SDKs (multi-source COPY, and multi-source COPY
with `--chown`), plus changesets for `e2b` and `@e2b/python-sdk`.

## Test plan

- [x] `pnpm run test tests/template/methods/fromDockerfile.test.ts` (JS)
- [x] `poetry run pytest
tests/{async,sync}/template_*/methods/test_from_dockerfile.py` (Python)
- [x] `pnpm run format` / `pnpm run lint`

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:57:03 +02:00
Mish Ushakov c485bf5476 feat(sdk): add Sandbox.updateNetwork / update_network (#1337) 2026-05-26 17:12:29 -07:00
Mish Ushakov 3786f34336 feat(sdk): support structured network rules with per-host transforms (#1286) 2026-05-26 16:35:21 -07:00
Jakub Novák 8640378c17 feat(python-sdk): allow opting out of HTTP/2 in get_transport (#1347) 2026-05-22 12:40:00 -07:00
Mish Ushakov a9bb287fc1 fix(python-sdk): close gRPC streams on watcher/command teardown (#1346)
## Summary
- `AsyncWatchHandle.stop()` and `AsyncCommandHandle.disconnect()`
previously only cancelled the consumer task and left the underlying
server-streaming gRPC call open — the `await self._events.aclose()` was
commented out as a Python 3.8 `RuntimeError` workaround. On long-lived
sandboxes this leaks one stream per call and eventually produces
`Code.internal: error creating watcher: too many open files`.
- The SDK now pins `python = "^3.10"`, so the workaround is removed.
`stop()`/`disconnect()` cancel the consumer task, await it, then
`aclose()` the async generator. The JS SDK already aborts the underlying
request via `AbortController`, so no JS change is needed.

## Test plan
- [ ] CI: `pnpm run format`, `pnpm run lint`, `pnpm run typecheck`
(passed locally)
- [ ] CI: `tests/async/sandbox_async/files/test_watch.py` and async
command tests still pass
- [ ] Reproduce the leak: in a long-lived async sandbox, repeatedly
create+stop a watcher and confirm fd count no longer climbs

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-22 20:51:21 +02:00
Jakub Novák 2680c89c3e Remove Sandbox.betaCreate / beta_create (#1344)
It didn't have any extra functionality
2026-05-22 16:36:29 +02:00
Tomas Valenta d21b936bf7 fix(sdks): make lifecycle.on_timeout and auto_pause precedence consistent (#1343)
## Summary

When `lifecycle.on_timeout` is set it wins; otherwise we fall back to
the `auto_pause` argument.

Previously the Python SDKs subscripted `lifecycle["on_timeout"]`, which
raised `KeyError` if a caller passed a `lifecycle` dict missing that key
(TypedDict is not enforced at runtime). The JS SDK silently used the
whole `lifecycle` object even when `onTimeout` was undefined. In both
cases, mixing `lifecycle` and `auto_pause` had inconsistent and
surprising behavior across the public surfaces (`create` vs
`beta_create`).

Now both SDKs use `.get`/optional chaining on `on_timeout` and only
treat `lifecycle` as authoritative when that field is actually present.

Touched files:
- `packages/python-sdk/e2b/sandbox_async/sandbox_api.py`
- `packages/python-sdk/e2b/sandbox_sync/sandbox_api.py`
- `packages/js-sdk/src/sandbox/sandboxApi.ts`

---------

Co-authored-by: Jakub Novak <jakub@e2b.dev>
2026-05-22 02:28:18 -07:00
Mish Ushakov 2ac5de2edf feat(js-sdk): support AbortSignal for request cancellation (#1328) 2026-05-15 23:24:16 +02:00
Mish Ushakov eaf452a82b feat: add optional name to createSnapshot and return snapshot names (#1327)
## Summary
- Add optional `name` parameter to `createSnapshot` / `create_snapshot`
in the JS and Python SDKs so callers can name the resulting snapshot
template.
- Return the `names` field from the snapshot API on `SnapshotInfo` (both
in `createSnapshot` responses and in `listSnapshots` paginator results)
so callers can discover the namespaced snapshot names.
- Includes a changeset (`patch` for `e2b` and `@e2b/python-sdk`).

## Test plan
- [ ] `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` all pass
locally
- [ ] Integration tests on a sandbox with valid credentials:
`sandbox.createSnapshot({ name: 'my-snap' })` returns non-empty `names`

Resolves https://github.com/e2b-dev/E2B/issues/1249

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 17:22:31 +00:00
Matt Brockman 20ea715252 Enable HTTP/2 for Python SDK transports for sandbox/main api calls (#1310)
switches python sdk to use http2 for calls to main api + sandboxes

doesn't add it to volumes yet - need to test those separately
2026-05-05 17:23:46 -07:00
Mish Ushakov 2c995d4494 refactor(sdk): make octet-stream file upload opt-in via useOctetStream (#1296)
## Summary

- Adds an opt-in `useOctetStream` / `use_octet_stream` flag to sandbox
file write — JS on `FilesystemWriteOpts`, Python keyword on `write` /
`write_files` (async + sync).
- Changes the default upload path to `multipart/form-data` regardless of
envd version. Callers must opt in to `application/octet-stream`
(requires envd 0.5.7 or later).

## Example

JS:

```ts
// Default — multipart/form-data
await sandbox.files.write('hello.txt', 'world')

// Opt in to application/octet-stream (envd >= 0.5.7)
await sandbox.files.writeFiles(
  [{ path: 'a.txt', data: 'a' }, { path: 'b.txt', data: 'b' }],
  { useOctetStream: true },
)
```

Python:

```python
# Default — multipart/form-data
sandbox.files.write('hello.txt', 'world')

# Opt in to application/octet-stream (envd >= 0.5.7)
await sandbox.files.write_files(
    [{'path': 'a.txt', 'data': 'a'}, {'path': 'b.txt', 'data': 'b'}],
    use_octet_stream=True,
)
```

## Test plan

- [ ] JS: `pnpm --filter e2b run lint && pnpm --filter e2b run
typecheck`
- [ ] Python: `cd packages/python-sdk && poetry run make lint && poetry
run make typecheck`
- [ ] Manual write with and without `useOctetStream` /
`use_octet_stream` against envd 0.5.7+.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:16:33 +00:00
Kagura 2f0ff5f0f7 fix(sdk): prevent shell injection in MCP config via proper escaping (#1276)
## Summary

Fixes #1154

When creating a sandbox with an `mcp` config, the JSON-serialized config
is interpolated directly into a shell command wrapped in single quotes.
Since `json.dumps()` / `JSON.stringify()` do not escape single quotes,
any MCP config value containing a single quote (e.g., API keys, tokens,
URLs) breaks out of shell quoting and allows arbitrary command execution
inside the sandbox.

## Changes

### Python SDK (`sandbox_async/main.py`, `sandbox_sync/main.py`)
- Use `shlex.quote()` to properly escape the JSON config string (4
locations)
- `shlex.quote()` is a stdlib function designed exactly for this purpose

### JS/TS SDK (`sandbox/index.ts`)
- Add a `shellQuote()` helper that escapes single quotes using the
standard `'\'''` pattern (equivalent to Python's `shlex.quote()`)
- Apply it to both MCP config interpolation sites (2 locations)

## Before / After

**Before** (vulnerable):
```
mcp-gateway --config '{"servers": {"test": {"envs": {"KEY": "it's a value"}}}}'
#                                                            ^^ breaks out
```

**After** (safe):
```
mcp-gateway --config '{"servers": {"test": {"envs": {"KEY": "it'\''s a value"}}}}'
#                                                            ^^^^ properly escaped
```

## Testing

Verified escaping behavior for both Python (`shlex.quote`) and JS
(`shellQuote`) with the PoC from the issue — single quotes in config
values are properly escaped and no longer allow shell breakout.

---------

Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 07:30:14 -07:00