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>
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>
## 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>
## Summary
The CLI stores credentials (E2B access token and team API key) in
plaintext at `~/.e2b/config.json`. Today the file is created with the
process default umask, which on most Linux distributions and macOS
results in mode `0644` — readable by every other local user and by any
process running as a different UID on the same machine.
This PR routes all three write sites through a single
`writeUserConfig()` helper that creates `~/.e2b` as `0700` and
`config.json` as `0600`, matching the convention used by the AWS CLI
(`~/.aws/credentials`), `kubectl` (`~/.kube/config`), and `gh`
(`~/.config/gh/hosts.yml`).
- **CWE:** CWE-312 (Cleartext Storage of Sensitive Information) —
partial mitigation. The file remains plaintext on disk (the existing `//
TODO` in `user.ts` already acknowledges that keychain storage is the
proper long-term fix); this change reduces exposure to other local users
/ less-privileged processes, which is the standard industry mitigation
while plaintext storage remains.
- **Affected file:** `packages/cli/src/user.ts` and the three writers in
`packages/cli/src/commands/`.
- **Severity:** Moderate on shared / multi-user machines (CI runners,
dev VMs, jump boxes); low on single-user workstations.
## What's in `~/.e2b/config.json`
```ts
{
email, accessToken, // user access token
teamName, teamId, teamApiKey // team API key
}
```
`accessToken` authenticates the user against the E2B control plane;
`teamApiKey` authorizes sandbox creation against the team. Either is
sufficient to impersonate the user / spend on the team's account.
## Fix
A new helper in `packages/cli/src/user.ts`:
```ts
export function writeUserConfig(configPath: string, config: UserConfig): void {
const dir = path.dirname(configPath)
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
fs.chmodSync(dir, 0o700)
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 })
fs.chmodSync(configPath, 0o600)
}
```
The explicit `chmodSync` calls are intentional: `mkdirSync({ mode })`
and `writeFileSync({ mode })` only set permissions when the path is
created. If the directory or file already exists with looser permissions
(the common case for users upgrading), `chmodSync` corrects them on the
next write.
Call sites updated:
- `packages/cli/src/commands/auth/login.ts`
- `packages/cli/src/commands/auth/configure.ts`
- `packages/cli/src/commands/template/buildWithProxy.ts`
`logout` uses `unlinkSync` and is unaffected. I grep'd the package for
any other writers to `USER_CONFIG_PATH` — these three are the complete
set.
Behavior on Windows: `chmodSync` only manipulates the read-only bit on
Windows, which is consistent with how the AWS/kubectl/gh CLIs behave.
ACL hardening on Windows is out of scope for this change.
## Tests
Added `packages/cli/tests/user_config_permissions.test.ts`, which writes
a config to a temporary path and asserts the resulting directory is
`0700` and file is `0600`, plus that the JSON round-trips correctly.
Manually verified before/after on Linux:
```
# before this PR
$ ls -l ~/.e2b/config.json
-rw-r--r-- 1 user user 234 ... config.json
# after
$ ls -l ~/.e2b/config.json
-rw------- 1 user user 234 ... config.json
```
## Why this is worth fixing
The exploitable scenario is a multi-tenant or shared-account host:
another local user (or a process running as `nobody`, a CI worker UID, a
sandboxed app, etc.) can `cat ~/<victim>/.e2b/config.json` and lift live
credentials. No privilege escalation, no race, no special tooling — the
file is simply world-readable today.
Before submitting, I tried to disprove the finding: I checked whether
E2B sets a restrictive umask anywhere in the CLI bootstrap (it doesn't),
whether the tokens are short-lived enough to make disclosure low-impact
(the access token isn't visibly rotated and the team API key is
long-lived), and whether the directory itself was being created
restrictively elsewhere (it wasn't — `mkdirSync` was called with default
mode). None of those mitigations are in place, so the permission
tightening is doing real work.
This doesn't close out CWE-312 — that requires moving the secrets out of
plaintext entirely, which the existing TODO acknowledges. It does close
the "any local user can read it" gap, which is the cheap, high-value
half of the mitigation.
_Submitted by Sebastion — autonomous open-source security research from
[Foundation Machines](https://foundationmachines.ai). Free for public
repos via the [Sebastion AI GitHub
App](https://github.com/marketplace/sebastion-ai)._
---------
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Fixes#1259
## Problem
The `CommandHandle.wait()` method fires `onStdout`, `onStderr`, and
`onPty` callbacks without `await`, so async callbacks run as
fire-and-forget microtasks. If a callback performs I/O (e.g. writing to
a file, sending over network), `wait()` can resolve before the callback
finishes, leading to lost data or race conditions.
## Fix
Add `await` before each optional-chain callback invocation:
```diff
-this.onStdout?.(stdout)
+await this.onStdout?.(stdout)
```
This is fully backwards compatible — `await`-ing a sync function's
return value is a no-op.
## Tests
3 parameterized test cases (`stdout`, `stderr`, `pty`) verify that
`wait()` does not resolve until an async callback's promise settles.
_This fix was developed with AI assistance and reviewed by a human._
## 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>
## 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>
## 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>
Set limit for max concurrent inflight connection, with burst traffic it
could happen that the number of connection overwhelms the underlaying
infrastructure or at least saturate it to the point each request is too
slow to succeed and blocking the rest
## Summary
- Extends AbortSignal support (introduced for Sandbox in #1328) to
`Template.build`, `buildInBackground`, `getBuildStatus`, `exists`,
`assignTags`, `removeTags`, and `getTags`. Aborting the signal cancels
in-flight requests and, for `Template.build`, the status-polling loop.
- Refactors signal+timeout plumbing: `ConnectionConfig` now stores
`signal`, and `ApiClient` / `VolumeApiClient` auto-apply it (plus
`requestTimeoutMs`) to every request via a custom fetch wrapper. The 22
explicit \`signal: config.getSignal(...)\` lines across sandboxApi.ts
and volume/index.ts are dropped.
- Volume's \`FILE_TIMEOUT_MS\` overrides move one level up into the
\`VolumeConnectionConfig\` constructor. Dead
\`VolumeConnectionConfig.getSignal()\` removed.
## Test plan
- [x] \`pnpm run typecheck\` / \`lint\` / \`format\`
- [x] New \`tests/template/abortSignal.test.ts\` covering all 8 Template
entry points (MSW-based)
- [x] Existing \`tests/sandbox/abortSignal.test.ts\`,
\`tests/template/uploadFile.test.ts\`,
\`tests/connectionConfig.test.ts\` still pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## 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)
## Summary
- `e2b auth login` previously crashed on headless machines (no
`xdg-open`) with an unhandled `error` event from the spawned browser
process.
- Attach an `error` listener (and `.catch`) to the `open` call; on
failure, print the login URL so the user can open it manually.
- Added a changeset for `@e2b/cli` (patch).
## Test plan
- [ ] On a headless Linux box without `xdg-open`, run `e2b auth login`
and confirm the CLI prints the manual URL instead of crashing.
- [ ] On macOS/Linux with a desktop, confirm the browser still opens
automatically and login completes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
## 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>
## 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>
Enables HTTP/2 for JS SDK sandbox envd traffic in Node by routing envd
RPC/API requests through undici with an HTTP/2-enabled dispatcher.
Non-Node runtimes continue to use global fetch. Management API and
volume clients are unchanged.
Requires bumping node from >=20 to >= 20.18.1 for undici