Commit Graph

760 Commits

Author SHA1 Message Date
Mish Ushakov 2defe39bd7 feat(cli): rename team to project in ~/.e2b/config.json (#1570)
Bumps `~/.e2b/config.json` to `version: 2` and renames
`teamName`/`teamId`/`teamApiKey` to
`projectName`/`projectId`/`projectApiKey`. The rename is internal to the
config file format — all user-facing CLI output, flags (`--team`), and
env vars (`E2B_TEAM_ID`) still say "team", and API `teamID` parameters
are unchanged.

Existing v1 configs keep working: they are converted to the new format
in memory on read, and the file on disk is left untouched — the v2
format is only persisted through paths that write the config anyway
(login, `e2b auth configure`, token refresh), so older CLI versions can
still read the file in the meantime. Unrecognized configs are no longer
deleted either; the CLI treats them as signed out and `e2b auth login`
overwrites them. Tools that read the config file directly must handle
the new field names once the file is written in the v2 format.

## Usage

```jsonc
// ~/.e2b/config.json (fresh login, or any config write after upgrading)
{
  "version": 2,
  "projectName": "default",
  "projectId": "team-id",
  "projectApiKey": "e2b_...",
  // identity, oauth, tokens, last_refresh unchanged
}
```

CLI output is unchanged:

```bash
$ e2b auth info
You are logged in as user@example.com,
Selected team: default (team-id)
```

## Testing

`user_config_migration.test.ts` covers in-memory v1→v2 migration, v2
pass-through, and unrecognized configs being treated as signed out
without deleting the file; existing config-permissions and backend
integration tests updated to the new fields. `format`, `lint`,
`typecheck`, `build`, and `pnpm run test` pass (backend integration
suites are environment-gated on credentials).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:22:31 +02:00
Mish Ushakov d417e9c4e6 test(js-sdk): Cloudflare Workers smoke tests (workerd pool + real deploy) (#1586)
Adds two Cloudflare Workers smoke suites for the JS SDK, both exercising
the built `dist/index.mjs`: `pnpm test:cf` runs the sandbox lifecycle
inside workerd via `@cloudflare/vitest-pool-workers`, and `pnpm
test:cf:deploy` deploys a worker to an ephemeral Cloudflare preview
account (`wrangler deploy --temporary` in the suite's global setup — no
Cloudflare credentials needed) and asserts the same lifecycle against
the live `workers.dev` URL, deleting the worker in teardown. The pool
suite immediately caught a runtime-detection bug: Node-compat shims
populate `process.release.name` inside Workers, so `getRuntime()`
misdetected Workers as Node and loaded `undici`; explicit runtime
markers now take precedence over the generic Node check (unit-tested,
changeset included). Both suites run in CI after the build step,
alongside the Bun and Deno suites (deploy suite on ubuntu only).

> [!IMPORTANT]
> Merge #1583 first: the deploy suite reproduces the exact #1579 startup
crash (Cloudflare rejects the upload with validation error 10021,
`createRequire` receiving undefined `import.meta.url`) and stays red
until that fix lands. Verified green end-to-end with #1583 applied.

Usage:

```bash
cd packages/js-sdk && pnpm build

# sandbox lifecycle inside local workerd (vitest-pool-workers)
pnpm test:cf

# deploy to a temporary Cloudflare preview account, test the live worker, delete it
E2B_API_KEY=... pnpm test:cf:deploy
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 13:57:18 +02:00
Mish Ushakov a406f78658 feat(js-sdk): run the full test suite under Bun (#1584)
## What

Runs the JS SDK's full vitest suite (the `unit` and `connectionConfig`
projects — 419 tests) under the Bun runtime, replacing the previous
single `bun:test` smoke test (superseded — the suite covers the SDK
under Bun far more thoroughly).

- `pnpm test:bun` → `bunx --bun vitest run --project unit --project
connectionConfig`
- CI step in `js_sdk_tests.yml` (ubuntu only for now); the old smoke
test and its Windows Bun install are removed

## SDK fixes surfaced by running the suite under Bun

1. **Late-bind `globalThis.fetch` on non-Node runtimes**
(`src/api/http2.ts`, `src/envd/http2.ts`). The factories previously
returned the bare global `fetch` reference, so:
   - every per-proxy cache entry was the identical function, and
- a `fetch` swapped in *after* client creation (msw, instrumentation,
test stubs) was either ignored or — worse — a temporary stub was
captured permanently in the module-level fetcher cache.

   They now return a closure that reads `globalThis.fetch` at call time.

2. **Pin abort reasons to their `AbortController`**
(`src/connectionConfig.ts`). Bun (observed on 1.3.14) holds
`AbortSignal.reason` weakly: a timeout `DOMException` constructed inside
a `setTimeout` callback gets garbage-collected, so consumers saw
`signal.reason === undefined` instead of a `TimeoutError`. Reasons are
now also stored on the controller, keeping them alive, and a losing
(post-abort) call never overwrites the pin. No behavior change on other
runtimes.

   ```ts
// Before (on Bun): sandbox operations that timed out aborted with
reason undefined
// After: they abort with DOMException('Request handshake timed out
after 30000ms', 'TimeoutError')
   const sbx = await Sandbox.create({ requestTimeoutMs: 30_000 })
   ```

## Test changes

- `tests/envd/http2.test.ts`: the "uses global fetch outside Node" test
now asserts late-binding behavior (a fetch stubbed after fetcher
creation is picked up) instead of reference identity.
- `tests/volume/volume.test.ts`: the msw-mocked `format: 'stream'` read
is split into its own test and skipped on Bun — reading `response.body`
of an msw-intercepted fetch via a reader yields an immediately-done
stream there (msw/Bun incompatibility; `.text()`/`.blob()` work). Real
network streams on Bun work and are covered by the sandbox `files.read`
tests that now run under Bun.

## Verification

Locally on Bun 1.3.14 (macOS arm64) and Node 22:

- `pnpm test:bun`: 73 files passed, 389 tests passed / 30 skipped, 0
failed
- `npx vitest run --project unit --project connectionConfig` (Node): 389
passed / 29 skipped, 0 failed
- browser project (chromium via playwright): passed
- `pnpm run format` / `lint` / `typecheck`: clean

Python SDK parity: not applicable — the changes are JS-runtime-specific
(Bun/global-fetch handling).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 11:33:09 +00:00
github-actions[bot] ab5f7666c9 [skip ci] Release new versions 2026-07-23 11:03:38 +00:00
Mish Ushakov a16dcdfc0c feat(cli): rename --team flag to --project and add E2B_PROJECT_ID env var (#1580)
Renames the `--team` flag to `-t, --project` on `template list`,
`template publish`, `template unpublish`, and `template delete`.
`--team` keeps working as a hidden alias that prints a deprecation
warning to stderr. The project ID can now also be set via the new
`E2B_PROJECT_ID` environment variable, with `E2B_TEAM_ID` still
supported as a fallback. Resolution precedence: `--project` > `--team` >
`E2B_PROJECT_ID` > `E2B_TEAM_ID` > `~/.e2b/config.json`.

## Usage

```sh
e2b template list --project <project-id>   # new flag (also -t)
e2b template list --team <project-id>      # still works, warns: "The --team flag is deprecated, use --project instead."

E2B_PROJECT_ID=<project-id> e2b template list   # new env var
E2B_TEAM_ID=<project-id> e2b template list      # still supported
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 10:51:25 +00:00
Mish Ushakov f10989813c fix(js-sdk): drop bare require calls that crash edge runtimes at import (#1583)
Fixes #1579. Bare `require` references in the SDK's ESM source made
tsdown emit an eager `createRequire(import.meta.url)` shim at module
scope in `dist/index.mjs`, which throws in Cloudflare Workers (workerd)
where `import.meta.url` is undefined in bundled code — so `import 'e2b'`
crashed before any API call.

`sha256` now uses WebCrypto directly (the `node:crypto` fallback was
dead code, since package engines require Node ≥ 20.18.1 and
`globalThis.crypto` exists on all supported runtimes), and
`getCallerDirectory` loads `fileURLToPath` via a static top-level
`import url from 'node:url'`, matching the existing sibling
`node:fs`/`node:os`/`node:path` imports in the same file.
`dynamicRequire` is removed entirely (no remaining callers), and a new
bundle test (`tests/bundle/edgeCompat.test.ts`) fails the suite if a
`require` shim ever reappears in `dist/index.mjs` — it skips locally
when `dist/` hasn't been built and throws in CI, where the workflow
always builds first.

Verified against the issue's repro in real workerd via wrangler:
`e2b@2.35.1` reproduces the crash, while this build imports cleanly and
runs a full sandbox lifecycle from inside a Worker. Also verified:
chromium browser test, Bun runtime test, signing/secure tests (WebCrypto
signatures accepted end-to-end), and the full template suite (134 tests)
against live infra.

### Usage

No API changes — importing the SDK in a Cloudflare Worker (with
`nodejs_compat`) now works again:

```ts
import { Sandbox } from 'e2b'

export default {
  async fetch(request: Request, env: Env) {
    const sandbox = await Sandbox.create({ apiKey: env.E2B_API_KEY })
    const result = await sandbox.commands.run('echo hello from workerd')
    await sandbox.kill()
    return Response.json({ stdout: result.stdout })
  },
}
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 10:42:58 +00:00
github-actions[bot] 43db96a0ef [skip ci] Release new versions 2026-07-22 18:40:32 +00:00
Matt Brockman e5a4bd655d Use undici8.8 when on node >= 22.19 (#1575) 2026-07-22 10:36:04 -07:00
Mish Ushakov 04827ab163 chore(cli): remove dead e2b.toml write path (#1569)
## Description

The CLI no longer writes `e2b.toml` anywhere, so this removes the dead
code around it:

- `saveConfig` and its `getConfigHeader` helper in
`packages/cli/src/config/index.ts` had zero callers — removed along with
now-unused imports.
- The `team_id` field is dropped from the config schema and the unused
`localConfigTeamId` parameter from `resolveTeamId` — nothing consumed it
since the legacy `template build` command was removed. Team resolution
is now: `--team` flag → `E2B_TEAM_ID` env → `~/.e2b/config.json` (the
last only when `E2B_API_KEY` isn't set). yup ignores unknown keys, so
legacy tomls containing `team_id` still parse.

Parsing (`loadConfig`, `deleteConfig`, `getConfigPath`) is intentionally
kept as the backward-compatibility read path for legacy projects:
`template migrate` (its whole purpose), `template publish`, `template
delete`, and `sandbox create`. No user-facing behavior changes; includes
a `@e2b/cli` patch changeset.

## Test

Format, lint, and typecheck pass; CLI tests: 88 passed, 8 skipped (one
pre-existing backend integration suite fails only due to missing
`E2B_API_KEY` in the environment).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:06:07 +02:00
Jakub Kracina c0fe6081bd feat(cli): rename user-visible "team" wording to "project" in terminal output (#1577)
## Summary

Copy-only rename of the remaining user-visible "team" strings to
"project" in the CLI (EN-1891) — part of the Teams → Projects rename,
following the dashboard copy pass. 12 string literals across `auth
login`, `auth info`, `auth configure`, and `template publish`; no flag,
env var, config key, API call, or exit-code behavior changes.

Explicitly untouched (owned by other PRs): `--team` flag + help text
(#1571), `~/.e2b/config.json` keys (#1570), `e2b.toml` `team_id`
(#1569), internal identifiers and API `Team` types. Best merged after
#1569–#1571 to keep their rebases trivial.

## Usage examples

```
$ e2b auth login
Logged in as you@e2b.dev with selected project Your Project

$ e2b auth info
You are logged in as you@e2b.dev,
Selected project: Your Project (a1b2c3d4)

$ e2b auth configure
? Select project
  Your Project (a1b2c3d4) (currently selected project)
Project Your Project (a1b2c3d4) selected.

$ e2b template publish
⚠️ This will make the template public to everyone outside your project
```

## Testing

- No new tests — strings only, not functionality (per review). Existing
suite passes except the pre-existing backend-integration suites that
need live sandbox access (fail identically on main).
- Patch changeset included.
2026-07-22 16:33:08 +02:00
Mish Ushakov 4990471484 fix(cli): sort sandbox list by timestamp instead of locale date string (#1573)
Fixes #1572

## Problem

`e2b sandbox list` sorted rows *after* converting `startedAt` to a
locale string, so ordering was lexicographic over strings like
`"9/1/2026, 10:00:00 AM"`. In en-US, `"9/..."` sorts after `"10/..."`,
so September sandboxes appeared after October ones — chronological order
broke at any single-digit/double-digit month or day boundary.

## Fix

Sort by the raw `startedAt` timestamp (with the existing sandbox-ID
tiebreak) before formatting for display. The row-building logic is
extracted into an exported `buildTableRows` helper and covered by unit
tests, including the September/October regression case. The input array
is no longer mutated in place.

## Example

```
$ e2b sandbox list --state paused

Paused sandboxes
Sandbox ID   ...  Started at
sbx-sep      ...  9/1/2026, 12:00:00 PM    ← previously listed after October
sbx-oct      ...  10/1/2026, 11:00:00 AM
```

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

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/e2b-dev/codesmith/E2B/pr/1573"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787240223&installation_model_id=14389&pr_number=1573&repository=e2b-dev%2FE2B&return_to=https%3A%2F%2Fgithub.com%2Fe2b-dev%2FE2B%2Fpull%2F1573&signature=85af1311c0e7aa33bbe8ea331f8bb86f82e89ab6e8ec39b29ab776c3a1466cc1"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:17:13 +02:00
Mish Ushakov be1ffa19f6 chore(deps): remove dead pnpm overrides and add CLI changeset (#1561)
Follow-up to #1559 with two changes. First, it removes four
`pnpm.overrides` entries whose targets are no longer in the dependency
graph at all — `@next/eslint-plugin-next>glob` (the parent package is
gone), `yaml@2.x`, `@tootallnate/once`, and `flatted`; the lockfile
change is header-only and no resolved package versions change, verified
with a clean `pnpm audit`. The remaining overrides are kept because no
parent's declared range excludes the vulnerable versions, so they are
the only enforcement of the patched floors. Second, it adds a patch
changeset for `@e2b/cli`: the CLI bundles all runtime dependencies into
`dist/index.js` at build time (tsdown `alwaysBundle`), so the patched
transitive deps from #1559 (e.g. brace-expansion 5.0.7 via the
glob/minimatch chains) only reach users through a new release. No
changeset is needed for the `e2b` SDK or Python SDK since they publish
dependency ranges that resolve fresh at user install time. Supersedes
#1560.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:20:05 +00:00
Mish Ushakov 36639f5321 feat(cli): remove per-command tag from integration attribution (#1557)
## Description

Removes the `e2b-cli-command/<command>` token (added in #1544) from the
CLI's User-Agent integration attribution, so CLI traffic is attributed
only by tool and version. This also lets `connectionConfig` and `client`
in `packages/cli/src/api.ts` go back to plain `const` exports, deleting
the per-command config/client rebuild machinery and the `preAction` hook
that drove it. The attribution test now only checks the SDK and CLI
tags, and a patch changeset for `@e2b/cli` is included.

User-Agent sent by `e2b sandbox list`, before and after:

```
before: e2b-js-sdk/2.9.0 (Node.js/22.11.0) e2b-cli/2.13.3 e2b-cli-command/sandbox.list
after:  e2b-js-sdk/2.9.0 (Node.js/22.11.0) e2b-cli/2.13.3
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:57:04 +00:00
github-actions[bot] 50de0af442 [skip ci] Release new versions 2026-07-17 09:59:36 +00:00
Mish Ushakov 95e4dc2832 feat(sdk): add sandbox fork to JS and Python SDKs (#1554)
## Summary

Adds SDK support for the new `POST /sandboxes/{sandboxID}/fork` endpoint
(e2b-dev/infra#3202): checkpoint a running sandbox in place (briefly
paused, snapshotted with full memory state, and resumed — its ID and
expiration stay untouched) and boot `count` new sandboxes from that
snapshot.

- **spec**: adds `SandboxForkRequest` / `SandboxForkResult` schemas and
the `/sandboxes/{sandboxID}/fork` path (mirroring the infra spec); JS
and Python API clients regenerated via `make codegen`.
- **js-sdk**: `sandbox.fork(opts)` instance method and
`Sandbox.fork(sandboxId, opts)` static method. Returns
`Promise<Array<Sandbox | Error>>` — one entry per requested fork, each
either a connected `Sandbox` instance or an `Error` describing why that
fork failed to start (`Promise.allSettled`-style, matching the per-fork
results of the API). Per-fork error codes go through the same code→class
mapping as other API errors (extracted from `handleApiError` into
`apiErrorFromCode`), so e.g. a per-fork 429 (sandbox limit) surfaces as
`RateLimitError`. `SandboxForkOpts` extends the full `ConnectionOpts`
(like `SandboxConnectOpts`), so `proxy`, `logger`, `apiUrl`, etc. work
with fork-by-ID. `timeoutMs` defaults to 5 minutes like
`create`/`connect`; `count` defaults to 1 and is validated client-side
(`InvalidArgumentError` for `count < 1`); a whole-request 404 maps to
`SandboxNotFoundError` (the source sandbox is the missing resource —
same semantics as `pause`/`connect`/`setTimeout`), carrying the API
error message when present; per-fork 404 error codes map to generic
`NotFoundError` (the missing resource is fork-internal, e.g. the
snapshot).
- **python-sdk**: `sandbox.fork(timeout=..., count=...)` /
`Sandbox.fork(sandbox_id, ...)` and the `AsyncSandbox` equivalents (same
`@class_method_variant` instance/static pattern as `connect`/`pause`),
returning `List[Union[Sandbox, Exception]]`. Per-fork errors map through
the shared `api_exception_from_code` (extracted from
`handle_api_exception`). `timeout` is in seconds per Python SDK
convention; an explicit `timeout=0` is preserved. Whole-request 404
raises `SandboxNotFoundException`; per-fork 404 codes map to generic
`NotFoundException`.
- **changesets**: minor bumps for `e2b` and `@e2b/python-sdk`.

## Usage

JS:

```ts
const sandbox = await Sandbox.create()

const [fork1, fork2] = await sandbox.fork({ count: 2, timeoutMs: 60_000 })
if (fork1 instanceof Sandbox) {
  await fork1.commands.run('echo "hello from fork"')
}

// or by ID
const forks = await Sandbox.fork(sandbox.sandboxId, { count: 2 })
```

Python (sync / async):

```python
sandbox = Sandbox.create()

fork1, fork2 = sandbox.fork(count=2, timeout=60)
if isinstance(fork1, Sandbox):
    fork1.commands.run('echo "hello from fork"')

# or by ID
forks = Sandbox.fork(sandbox.sandbox_id, count=2)
```

```python
sandbox = await AsyncSandbox.create()
fork1, fork2 = await sandbox.fork(count=2)
```

## Notes

- The JS option is named `timeoutMs` (milliseconds) to match
`SandboxOpts.timeoutMs` / `SandboxConnectOpts.timeoutMs`; the API
receives seconds via `timeoutToSeconds` as elsewhere.
- Failed forks are returned as error **values** in the array rather than
rejected promises, so a partial failure doesn't throw away the
successful forks and there are no unhandled-rejection hazards. A
per-fork error message includes the API error code only when the API
returned one.

## Test plan

- [x] `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` pass at
the repo root (`ty` diagnostics identical to baseline)
- [x] Offline tests pass: `count < 1` → `InvalidArgumentError` /
`InvalidArgumentException` in JS, Python sync, and Python async;
`handleApiError` suite passes after the `apiErrorFromCode` extraction
(plus a behavior-parity check of the Python `handle_api_exception`
refactor)
- [ ] Integration tests (single fork with FS state inheritance +
independence, multi-fork with unique IDs, fork-by-ID, fork of killed
sandbox → `SandboxNotFoundError`) are written but currently fail against
prod with 404 because the fork endpoint (e2b-dev/infra#3202) is not
deployed yet — they should pass once it lands.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:50:34 +00:00
github-actions[bot] 8c87016a57 [skip ci] Release new versions 2026-07-16 09:24:56 +00:00
Mish Ushakov 2c77fc00bb feat(sdk): add name filter to snapshot list (#1523)
Adds an optional `name` filter to `Sandbox.listSnapshots()` /
`Sandbox.list_snapshots()`, mirroring the infra snapshots list endpoint
([e2b-dev/infra#3184](https://github.com/e2b-dev/infra/pull/3184)). The
filter accepts a snapshot name or ID, optionally tag-qualified (e.g.
`"my-snapshot"`, `"my-team/my-snapshot"` or `"my-snapshot:v1"`); unknown
names return an empty list. It's a flat top-level option alongside the
existing `sandboxId` filter (non-breaking) and can be combined with it —
the backend applies both with AND, matching the `metadata`+`state`
behavior of `Sandbox.list()`. Applied equivalently across the OpenAPI
spec, generated clients, and the JS + Python sync/async SDKs, with tests
and a changeset.

## Usage

```ts
// JS/TS
const paginator = Sandbox.listSnapshots({ name: 'my-snapshot' })
const snapshots = await paginator.nextItems()

// combine filters (snapshots from a sandbox matching a name)
Sandbox.listSnapshots({ sandboxId: 'sandbox-id', name: 'my-snapshot' })
```

```python
# Python (sync)
paginator = Sandbox.list_snapshots(name="my-snapshot")
snapshots = paginator.next_items()

# Python (async)
paginator = AsyncSandbox.list_snapshots(name="my-snapshot")
snapshots = await paginator.next_items()
```

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:06:37 +02:00
github-actions[bot] 78a91ab72f [skip ci] Release new versions 2026-07-15 09:27:07 +00:00
Mish Ushakov 7474d904a2 fix(python-sdk): correct inverted no_install_recommends docstring (#1533)
Promotes the merged #1532 (by @anxkhn) from the staging branch
`fix/no-install-recommends-docstring` into `main`.

`TemplateBuilder.apt_install()` documents its `no_install_recommends`
parameter as
"Whether to install recommended packages", but the generated command
does the
opposite. In `packages/python-sdk/e2b/template/main.py` the command adds
apt-get's
`--no-install-recommends` flag when the argument is `True`:

```python
f"... apt-get install -y {'--no-install-recommends ' if no_install_recommends else ''}..."
```

`--no-install-recommends` tells apt to *skip* recommended packages, so
`no_install_recommends=True` skips them rather than installing them. A
user who
follows the docstring gets the inverse of the documented behavior. The
parameter
name and apt-get's own semantics confirm the code is correct and the
docstring was
wrong; this rewords the docstring line to match the real behavior.

The `--no-install-recommends` flag was introduced in #983; the docstring
has been
inverted since then.

This is Python-only. The JS twin `aptInstall` applies the same flag but
has no
per-parameter JSDoc for `noInstallRecommends` (it appears only inside an
`@example`), so there is nothing contradictory to fix on the JS side.
There is a
single Python definition (no sync/async mirror for the template
builder).

No behavior change; documentation-only, plus a `@e2b/python-sdk: patch`
changeset.

### Usage

```python
from e2b import Template

template = Template().from_image("ubuntu:22.04")

# Install recommended packages as well (apt-get default):
template.apt_install("vim")

# Skip recommended packages (adds apt-get's --no-install-recommends):
template.apt_install("vim", no_install_recommends=True)
```

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

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Co-authored-by: Anas Khan <anxkhn28@gmail.com>
2026-07-14 16:14:45 +02:00
Mish Ushakov 99e536f6eb fix(python-sdk): stop leaking per-call proxy pools in volume content clients (#1534)
The Python volume content client factories passed both `proxy` and the
shared cached `transport` to httpx, so with a proxy configured (e.g.
`Volume.connect(volume_id, proxy="http://user:pass@127.0.0.1:8080")`),
every volume operation mounted a fresh, never-closed proxy transport
that bypassed the cached connection pool. The client-level `proxy`
argument is now dropped — the proxy is already baked into the cached
transport, so proxied requests keep working but reuse one pooled
transport per thread/event loop.

The volume transports also gained connect-level retries
(`E2B_CONNECTION_RETRIES`, default 3), matching the core API and envd
transports. Includes a changeset for a `@e2b/python-sdk` patch release.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:14:26 +02:00
Mish Ushakov a4e07a6ab2 feat(cli): attribute CLI traffic with e2b-cli and per-command tags (#1544)
Follow-up promised in #1524 (original attempt #1525, closed while
blocked on the backend User-Agent parser, since fixed by
e2b-dev/infra#3149, which now iterates User-Agent tokens and ignores
unrecognized ones — so the extra tags are safe on template builds).

Sets `ConnectionConfig.setIntegration('e2b-cli/<version>')` at the top
of `src/api.ts` before the shared connection config is built at import
time, and a commander `preAction` hook extends the tag with the
canonical invoked command (alias `ls` reports as `list`), rebuilding the
shared config and client since they capture the User-Agent at
construction. Every CLI request then carries:

```
User-Agent: e2b-js-sdk/2.32.0 e2b-cli/2.13.1 e2b-cli-command/sandbox.list
```

Tests drive the built CLI (`sandbox list` and the `ls` alias) against a
local stub API server and assert the received User-Agent, which also
guards that the bundle keeps shipping the workspace SDK where
`setIntegration` exists. Includes a patch changeset for `@e2b/cli`.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:14:03 +02:00
Mish Ushakov 347ebe8ad8 fix(cli): correct memory-mb help default and use non-deprecated pause (#1510)
Fixes two small CLI issues. The `e2b template create --memory-mb` help
text claimed a default of 512 MB, but the real default is 1024 MB — the
help now reflects that. The `e2b sandbox pause` command was calling the
deprecated `Sandbox.betaPause()` alias and now calls `Sandbox.pause()`
directly.

## Usage

```sh
e2b template create --help   # --memory-mb now shows "The default value is 1024."
e2b sandbox pause <sandboxID>  # behaves the same, no longer uses the deprecated method
```

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 05:03:42 -07:00
Mish Ushakov 64e9bc02b6 fix(js-sdk): unpin useDefineForClassFields — make caller-directory resolution emit-invariant (#1539)
Follow-up to #1536, which pinned `useDefineForClassFields: false` in the
js-sdk tsconfig because raising `target` to `es2022` flips the default
to `true`, and that broke the template builder. This PR fixes the root
cause and removes the pin, so the SDK now compiles with the standard
es2022 `[[Define]]` class-field semantics.

## Root cause

`TemplateBase` resolved its default `fileContextPath` in a **class field
initializer**:

```ts
private fileContextPath: PathLike =
  runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.')
```

With native class fields (define semantics), V8 evaluates field
initializers in an extra `<instance_members_initializer>` stack frame:

```
at getCallerDirectory (utils.ts)
at <instance_members_initializer> (index.ts)   ← extra frame under define semantics
at new TemplateBase (index.ts)
at Template (index.ts)
at user code                                    ← fixed-depth walk lands one frame short
```

`getCallerDirectory` walks the stack at a fixed depth, so it landed on
the SDK's own `src/template` directory instead of the caller's —
`.copy('folder/*', …)` then globbed against the wrong base dir (`Error:
No files found in .../src/template/...`), and the resulting client-side
failure mis-attributed build-step stack traces (the two
`stacktrace.test.ts` failures were cascades of this one bug).

## Fix

Move the default resolution into the constructor body, where the stack
shape is identical under both emits:

```ts
constructor(options?: TemplateOptions) {
  this.fileContextPath =
    options?.fileContextPath ??
    (runtime === 'browser' ? '.' : (getCallerDirectory(STACK_TRACE_DEPTH) ?? '.'))
```

The call is now emit-invariant (same `STACK_TRACE_DEPTH`), so the
tsconfig pin is removed. The method-level `getCallerFrame` call sites
were never affected — method bodies don't change shape with class-field
semantics.

Only the js-sdk is touched: the Python SDKs resolve the caller via
`inspect` and don't have this failure mode, and the CLI bundle doesn't
include `TemplateBase`.

## Usage example

Fixes relative-path resolution for SDK consumers whose toolchain emits
native class fields (e.g. esbuild/vitest with `target: es2022+`):

```ts
// user-project/scripts/template.ts
const template = Template()
  .fromBaseImage()
  .copy('assets/*', '/app/assets') // now resolves against user-project/scripts/,
                                   // not the SDK's own directory
```

## Verification

- `tests/template/stacktrace.test.ts` — 30/30 pass with the flag
defaulted (`true`), and still 30/30 when explicitly set back to `false`
(emit-invariance)
- `tests/template/build.test.ts` — 4/4 pass against the real backend
(real `.copy` glob + build)
- Smoke-tested built `dist/index.mjs` and `dist/index.js` from an
external directory: `fileContextPath` resolves to the importing script's
directory in both
- Unit project A/B: identical results with and without this change
(remaining failures are pre-existing `E2B_API_KEY`-gated live tests)
- `pnpm run typecheck`, `lint`, `format` 

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 05:01:53 -07:00
github-actions[bot] dbc6bfa161 [skip ci] Release new versions 2026-07-13 15:42:35 +00:00
Mish Ushakov 09e12b3f65 feat(sdk): set-once integration attribution via ConnectionConfig.setIntegration (#1524)
Replaces the per-call `integration` connection option with a set-once,
process-wide setter — `ConnectionConfig.setIntegration()` in JS and
`ConnectionConfig.set_integration()` in Python — so integrations
wrapping the SDK tag themselves once at startup and every request
carries the identifier in the `User-Agent` header, with no threading
through individual SDK calls. The setter is internal and hidden from
generated docs; the `integration` option is removed from
`ConnectionConfigOpts` (kept as a deprecated alias of `ConnectionOpts`)
and from the Python constructor, and the round-trip machinery from #1459
is no longer needed since rebuilt configs read the process-wide value.
User-Agent handling now follows a single rule in both SDKs via one
shared helper per SDK: an explicitly provided `User-Agent` always wins,
otherwise the SDK sends its own tagged with the current integration —
and SDK-built values are recomputed whenever a config is rebuilt, so
clearing or changing the integration propagates. Tests cover
attribution, clearing, config rebuilds, and custom User-Agent precedence
in both SDKs, with changesets for `e2b` and `@e2b/python-sdk` (minor).
CLI attribution using this setter will follow in a separate PR.

Usage (internal integrations only):

```ts
import { ConnectionConfig } from 'e2b'
ConnectionConfig.setIntegration('e2b-code-interpreter/0.1.0') // once at startup
```

```python
from e2b import ConnectionConfig
ConnectionConfig.set_integration("e2b-code-interpreter/0.1.0")  # once at startup
```

A caller-supplied `User-Agent` (via `headers`/`apiHeaders`) is preserved
in both SDKs:

```ts
const sbx = await Sandbox.create({ apiHeaders: { 'User-Agent': 'my-app/1.0' } })
// requests carry: my-app/1.0
```

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:52:09 +02:00
Mish Ushakov 49367c8491 build: switch from tsup to tsdown (#1515)
Switches the build tooling for `packages/js-sdk` and `packages/cli` from
`tsup` (esbuild) to `tsdown` (rolldown), replacing each `tsup.config.js`
with a `tsdown.config.ts` and updating the `build`/`dev` scripts and
devDependencies. The published artifact layout is intentionally
unchanged — the SDK still ships `dist/index.js` (CJS), `dist/index.mjs`
(ESM) and `dist/index.d.ts`/`.d.mts`, and the CLI still ships an
executable `dist/index.js` plus `dist/templates` — kept identical via
`fixedExtension: false`. CLI dependency bundling is preserved by mapping
the old `noExternal` to tsdown's `deps.alwaysBundle` (still excluding
the ESM-only, dynamically-imported `inquirer`), and template copying
moves from an `onSuccess` shell step to tsdown's `copy` option.

Also aligns Node versions: `engines.node` for both packages is set to
`20 || >=22`, the CLI build targets `node20`, and the pinned `nodejs` in
`.tool-versions` is bumped to `22.11.0`. The large `pnpm-lock.yaml` diff
is expected — it swaps the tsup/esbuild dependency tree for tsdown's
rolldown tree (no lockfile format change).

## Verification
- Both packages build cleanly with output filenames identical to the
previous tsup builds.
- `typecheck`, `lint` (oxlint) and `build` pass for both packages; the
built CLI runs (`--version`).
- Built js-sdk imports correctly in both CJS (`require`) and ESM
(`import`), exposing the default `Sandbox` export and all named exports.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:04:03 +02:00
Mish Ushakov e6c4e7e9d5 chore(js): modernize Connect/Protobuf and React test deps (#1512)
## What

Modernizes the JS SDK's dependencies while remaining fully compatible
with the current supported Node range (`>=20.18.1`) — no engine changes
and no breaking impact for consumers.

- **`@connectrpc/connect` / `@connectrpc/connect-web`:** `2.0.0-rc.3` →
`^2.1.2` (off the pre-release pin onto the stable line, and switched to
a `^` range).
- **`@bufbuild/protobuf`:** `^2.6.2` → `^2.12.1`.
- **React test deps:** `react` / `@types/react` → `^19.2.0`, and
`react-dom` / `@types/react-dom` added at `^19.2.0` (previously
auto-installed as v18 peers). Dev/test-only — no runtime impact.
- **CI:** standardized `actions/setup-node` (mixed v3/v4/v6) to `v6`
across all workflows; the three `@v3` uses were on the deprecated Node16
action runtime.

No public SDK API changes — the sandbox filesystem and command RPCs use
the same Connect transport configuration.

## Why undici / Node floor were dropped from this PR

An earlier revision also bumped `undici` 7 → 8 and raised the Node floor
to `>=22.19.0`. Usage data shows **Node 20 is still the single largest
SDK runtime (~39% of sandbox creations)**, so dropping it would break
the largest consumer segment via `engine-strict` install failures.
undici 8 was the *only* change forcing Node 22, and undici `7.28.0`
(already the latest 7.x) supports Node 20 — so undici stays at `^7.28.0`
and the engine floor is unchanged. undici 8 is a good candidate for a
future major once Node 20 usage declines.

## Verification

- typecheck, lint (oxlint), and build pass
- 22 mocked Connect/undici transport unit tests pass
- 106 live filesystem/command tests pass over connectrpc `2.1.2` +
undici `7.28.0`

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 16:37:50 +00:00
github-actions[bot] 0feb926937 [skip ci] Release new versions 2026-07-08 13:37:26 +00:00
Mish Ushakov a6b1cf4bcf fix(python-sdk): strip colon-separated SGR escape codes in build logs (#1522)
### What

Cherry-picks the fix from #1519.

`strip_ansi_escape_codes` in the Python SDK only matched
semicolon-separated CSI
parameters, so colon-separated SGR sequences leaked literal escape
garbage into
template build-log messages. This widens the parameter class from `;` to
`[;:]`
so colon-separated sequences are stripped too, matching the JS SDK's
`stripAnsi`.

Modern terminals emit colon-separated SGR sequences:

- 256-color: `\x1b[38:5:82m`
- truecolor: `\x1b[38:2::255:0:0m`
- curly underline: `\x1b[4:3m`

The two SDKs share one source (chalk/ansi-regex) and the JS twin was
already
updated to support colons (`packages/js-sdk/src/utils.ts:95`, comment:
"supports
; and :"); the Python port lagged behind. `strip_ansi_escape_codes` is
consumed
by `LogEntry.__post_init__`
(`packages/python-sdk/e2b/template/logger.py`), so
the leftover escape bytes showed up in Python build logs only.

### The one-line fix

```python
# packages/python-sdk/e2b/template/utils.py:319
- r"(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))",
+ r"(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))",
```

### Usage example (before / after)

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

# 256-color, colon-separated
strip_ansi_escape_codes("\x1b[38:5:82mX\x1b[0m")
# before: ":5:82mX"   after: "X"

# truecolor, colon-separated
strip_ansi_escape_codes("\x1b[38:2::255:0:0mRED\x1b[0m")
# before: ":2::255:0:0mRED"   after: "RED"

# semicolon variants already worked and still do
strip_ansi_escape_codes("\x1b[38;5;82mX\x1b[0m")  # "X"  (unchanged)
```

### Tests

Unit tests at

`packages/python-sdk/tests/shared/template/utils/test_strip_ansi_escape_codes.py`
(no API key / sandbox): colon-256, colon-truecolor, curly-underline,
plus
basic/semicolon regressions. All 7 pass locally.

### Changeset

`.changeset/python-strip-ansi-colon.md` (patch on `@e2b/python-sdk`).

### Notes

Original PR: #1519 (by @anxkhn). Opened against a fresh branch off
`main` per
request, rather than merging #1519 directly.

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

Co-authored-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
2026-07-02 10:45:49 -07:00
Mish Ushakov 2b7dd17f10 feat(sdk): add gzip option to template copy layer (#1482)
Adds a `gzip` option to the template `.copy()` / `copyItems` layer that
controls whether copied files are gzipped before upload, threaded from
the copy call through the build-time tar stream in both the JS SDK and
the sync/async Python SDKs. It is enabled by default to preserve
existing behavior, so passing `gzip: false` (`gzip=False`) uploads an
uncompressed tar — useful for already-compressed payloads where gzip
adds CPU cost without shrinking the upload. The option name matches
node-tar's own `gzip` option and the existing sandbox filesystem `gzip`
kwarg. Gzip is deliberately excluded from the file cache hash, so
toggling it does not bust the build cache. Tests in both SDKs were
updated for the new argument and extended with `gzip: false` cases
asserting the archive is not gzipped yet still extracts, and a changeset
(`minor` for both packages) is included.

> [!NOTE]
> The server that extracts these uploaded archives lives in another repo
and must auto-detect compression (peek the gzip `0x1f 0x8b` magic)
rather than assuming gzip; confirm it handles plain tars before release.

## Usage

```ts
// JS/TS
template.copy('model.bin', '/app/', { gzip: false })
template.copyItems([{ src: 'a.bin', dest: '/app/', gzip: false }])
```

```python
# Python (sync & async)
template.copy('model.bin', '/app/', gzip=False)
template.copy_items([{ 'src': 'a.bin', 'dest': '/app/', 'gzip': False }])
```

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:50:21 +02:00
Mish Ushakov 9b4a74388d chore(cli): remove unused dockerfile-ast dependency (#1509)
The CLI declared `dockerfile-ast` as a dependency but never imported it
— all Dockerfile parsing in the CLI goes through the `e2b` SDK, which
keeps its own (newer) `dockerfile-ast` dependency. This drops the
redundant copy from `packages/cli/package.json`, removing
`dockerfile-ast@0.6.1` and its sub-deps from the lockfile while
`dockerfile-ast@0.7.1` (used by the js-sdk) stays. No behavior change;
CLI typecheck and lint pass, and a `@e2b/cli` patch changeset is
included.

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 05:15:00 -07:00
Mish Ushakov c385566c29 fix(python-sdk): correct Sandbox.list() docstring (also lists paused) (#1511)
Integration branch PR for #1500. Merges the docstring fix into `main`.

Once #1500 is merged into `python-sdk-list-docstring-base`, this PR will
carry those changes into `main`.

---------

Co-authored-by: Leinux <tristone13th@outlook.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:10:33 +00:00
mattteufel-e2b d071bb78c8 fix(cli): use absolute import in generated Python build scripts (#1505)
Fixes the generated Python build scripts to use an absolute import (from
template import template) instead of a relative one, which broke python
build_dev.py with ImportError: attempted relative import with no known
parent package since the files are emitted as flat siblings with no
package. This reverts an unintended change from #954 that was flagged by
Cursor Bugbot at the time but not addressed. Fixes #1477.
2026-06-29 22:46:31 +02:00
Mish Ushakov 2869febdee feat(cli): add config override flags to template migrate (#1494)
Adds override flags to `e2b template migrate` so the generated SDK files
don't have to inherit everything from `e2b.toml`: `--name`/`-n`
(template name), `--cmd`/`-c` (start command), `--ready-cmd` (ready
command), `--cpu-count`, and `--memory-mb`. Each flag falls back to the
corresponding config value when omitted, and `--memory-mb` is validated
to be even. Includes tests covering the overrides and the odd-memory
rejection, plus a changeset for `@e2b/cli`.

## Usage

```bash
e2b template migrate \
  --language typescript \
  --name my-custom-name \
  --cmd "node server.js" \
  --ready-cmd "curl localhost:3000" \
  --cpu-count 4 \
  --memory-mb 2048
```

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:36:46 +02:00
Matt Brockman f160f08c7b Keep integration attribution on connection config (#1459)
moves integration attirbution to more private thing to avoid confusing people with first class kwargs
2026-06-26 18:30:35 -07:00
Lukáš Huvar bb45f185f1 Introduce generic paginator base class for JS and Python SDKs (#1491)
Extracts the cursor-based pagination state machine into a reusable base
class — `Paginator` in the JS SDK's `utils`, `PaginatorBase` in
`e2b/utils.py` — that owns `hasNext`/`nextToken` and the `x-next-token`
header handling, and migrates the sandbox and snapshot paginators onto
it. Each concrete paginator now just implements `nextItems`/`next_items`
to fetch its own page, so future list endpoints (templates, builds,
etc.) can add pagination by subclassing without reimplementing the
bookkeeping. Applied equivalently to the JS SDK and both Python sync and
async implementations, with unit tests covering the shared base. There
are no public API changes — `Sandbox.list()` / `listSnapshots()` and the
existing paginator types behave identically.

## Usage (unchanged)

```ts
const paginator = Sandbox.list()
while (paginator.hasNext) {
  const sandboxes = await paginator.nextItems()
  console.log(sandboxes)
}
```

```python
paginator = Sandbox.list()
while paginator.has_next:
    sandboxes = paginator.next_items()
    print(sandboxes)
```
2026-06-26 14:53:56 +02:00
Mish Ushakov bb1696871b Stream template build-context upload from disk instead of buffering in memory (#1435)
## Summary

Template builds previously buffered the entire gzipped build-context tar
archive in memory before uploading it. This PR spools the archive to a
temporary file and streams it from disk during upload — in the JS SDK
and both sync and async Python SDKs — so memory usage no longer scales
with the size of the build context.

The upload keeps an explicit `Content-Length` header (taken from the
spooled file's size), which S3 presigned PUT URLs require — they reject
`Transfer-Encoding: chunked` with `501 NotImplemented` (#1243).

## Changes

- **JS** (`packages/js-sdk/src/template/`):
`tarFileStream`/`tarFileStreamUpload` are replaced by `tarFileToStream`,
which writes the archive to a temp file and returns a self-cleaning read
stream plus its `size`. The spooled temp file deletes itself once the
stream is closed (consumed, errored, or destroyed) via the stream's
`close` event — mirroring the Python SDK's `tar_file_stream`. `buildApi`
streams this body with `duplex: 'half'` and an explicit `Content-Length`
from `size`; if `fetch` throws before consuming the body, it destroys
the stream to trigger the same cleanup. There is no separate cleanup
callback, so a cleanup failure can no longer mask the upload result.
- **Python** (`packages/python-sdk/e2b/template/utils.py`,
`template_async/build_api.py`, `template_sync/build_api.py`):
`tar_file_stream` now writes to a `tempfile.TemporaryFile` instead of
`io.BytesIO` and returns the file object positioned at the start; the
upload streams from it with an explicit `Content-Length` and closes it
(deleting the temp file) when done.
- Tests updated for the new return shapes (JS `tarFileToStream.test.ts`,
`uploadFile.test.ts`; Python upload/tar tests), including assertions
that the spooled archive is removed on both the consume and destroy
paths.

## Usage

No API changes — `Template.build()` / template builds behave the same,
just without holding the build context in memory:

```ts
await Template.build(template, { alias: 'my-template' })
```

```python
Template.build(template, alias="my-template")
```

Split out of #1433, which covers streaming for sandbox/volume file
uploads and downloads.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-25 20:56:10 +02:00
Mish Ushakov 8b8a224f8b feat(python-sdk): add logger option for request/debug logging (#1409)
Adds a `logger` option (a standard library `logging.Logger`) to
`Sandbox.create`/`AsyncSandbox.create` and the static
`Sandbox.connect(sandbox_id, ...)`, wired into the API client, the envd
client, the volume content client, and the RPC (ConnectRPC) path. The
logger is stored on the sandbox and propagates to all of its later
operations — including control-plane calls like
`kill`/`pause`/`set_timeout`/`get_info` (via `get_api_params`) — so
logging keeps working after construction; mirroring the JS SDK, `logger`
is a construction-time option and not a public per-request parameter
those methods accept from the caller, and nothing is logged unless a
logger is supplied. The stdlib `logging.Logger` is used directly as the
adapter (no ported JS `Logger` interface), and log levels match JS:
requests at `INFO`, successful API and unary RPC responses at `INFO`,
streamed RPC messages at `DEBUG`, failed API responses (status >= 400)
at `ERROR`. The always-on module-level (`e2b.*`) request logging at the
transport layer was removed in favor of this opt-in client-layer
logging, and volume content operations continue to accept `logger` per
call via `VolumeApiParams` to match the JS Volume API. Includes a
changeset and unit tests in `tests/test_logging_option.py`.

## Usage

```python
import logging
from e2b import Sandbox

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("my-app.e2b")

sbx = Sandbox.create(logger=logger)
sbx.commands.run("echo hello")   # RPC logged via `logger`
sbx.set_timeout(60)              # control-plane call also logged via `logger`
```

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Matt Brockman <matt.brockman@e2b.dev>
2026-06-25 20:43:04 +02:00
github-actions[bot] ec260376dc [skip ci] Release new versions 2026-06-25 18:04:08 +00:00
Mish Ushakov de0c401626 fix(sdk): correct filesystem watch handle callback and timeout behavior (#1480) 2026-06-25 19:51:53 +02:00
Babis Chalios 7e7e9514df feat(sdk): filesystem-only auto-pause via lifecycle.onTimeout object form (#1471)
## Filesystem-only auto-pause (`onTimeout` object form)

Adds an object form to the sandbox **lifecycle** `onTimeout`
(`on_timeout` in Python) that controls the snapshot kind taken when a
sandbox auto-pauses on timeout, via `keepMemory` (`keep_memory`).

`onTimeout` now accepts either the existing bare action (`'pause'` /
`'kill'`) or the object form `{ action, keepMemory }`. When `keepMemory`
is `false` (with `action: 'pause'`), a timeout auto-pause takes a
**filesystem-only** snapshot (no memory) instead of a full memory one,
so the sandbox cold-boots (reboots) from disk on resume — losing running
processes and open connections. Defaults to `true` (full memory
snapshot), so existing callers are unaffected. **The bare string form is
unchanged.**

It's the create-time / auto-pause counterpart to the explicit
`pause(keepMemory=false)` from #1465: same `keepMemory` naming, mapped
onto the `autoPauseMemory` create field.

### Type safety
The object form is a **discriminated union** on `action`: `keepMemory`
is only valid with `action: 'pause'`. Pairing it with `action: 'kill'`
is a **compile-time type error** (TS) / static error (`ty`), and is
additionally rejected at runtime (`InvalidArgumentError` /
`InvalidArgumentException`) for untyped callers.

### Behavior & validation
- `keepMemory` only applies to a `pause` action.
- **Incompatible with auto-resume** — auto-resume wakes a paused sandbox
on inbound traffic by restoring its memory snapshot in place; a
filesystem-only snapshot has no memory to restore (resuming cold-boots
it), so it must be resumed explicitly via `connect()`. Combining
`keepMemory: false` with `autoResume` is rejected client-side.

### Usage
```ts
// JS/TS — filesystem-only auto-pause on timeout
const sbx = await Sandbox.create({
  lifecycle: { onTimeout: { action: 'pause', keepMemory: false } },
})

// bare string form still works (full memory snapshot)
const sbx2 = await Sandbox.create({ lifecycle: { onTimeout: 'pause' } })
```
```python
# Python
sbx = Sandbox.create(
    lifecycle={"on_timeout": {"action": "pause", "keep_memory": False}}
)
```

### Changes
- `spec/openapi.yml`: `autoPauseMemory` on the create body (+
regenerated JS/Python clients).
- JS `SandboxOnTimeout` discriminated union (`'pause' | 'kill' | {
action: 'pause'; keepMemory? } | { action: 'kill' }`) and the Python
`SandboxOnTimeoutPause` / `SandboxOnTimeoutKill` TypedDicts, wired
through `createSandbox` / `_create_sandbox` (sync + async) to
`autoPauseMemory`, with the client-side guards.
- Tests: payload serialization + validation (offline, incl. the `action:
'kill'` type/runtime guard) and live cold-boot e2e in both SDKs;
changeset (`e2b` + `@e2b/python-sdk`, minor).

### Backend dependency
The live e2e tests exercise the real auto-pause→cold-boot path and
require the infra-side `autoPauseMemory` support (e2b-dev/infra#3055),
now merged and deployed.

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

---------

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:16:56 +00:00
Babis Chalios cb5a3870b6 feat(sdk): filesystem-only snapshots (pause memory:false) (#1465)
## Summary

Adds an optional **`memory`** flag to `pause` in both the JS and Python
SDKs. When `memory` is `false`, the pause captures **only the
filesystem** (no memory snapshot); resuming such a snapshot **cold-boots
(reboots)** the sandbox from disk — losing in-memory state, running
processes, and open connections. Defaults to `true` (full memory
snapshot), so existing callers are unaffected.

This is the SDK surface for the filesystem-only snapshot feature on the
infra side.

## Usage

```ts
// JS / TS
const sbx = await Sandbox.create()
await sbx.pause({ memory: false })   // filesystem-only snapshot
const resumed = await sbx.connect()  // resumes by cold-booting from disk
```

```python
# Python (sync)
sbx = Sandbox()
sbx.pause(memory=False)              # filesystem-only snapshot
resumed = sbx.connect()              # resumes by cold-booting from disk

# Python (async)
sbx = await AsyncSandbox.create()
await sbx.pause(memory=False)
resumed = await sbx.connect()
```

`memory` defaults to `true` — `pause()` / `pause({})` behave exactly as
before.

## What changed

- **spec**: optional `memory: boolean` (default `true`) on `POST
/sandboxes/{sandboxID}/pause` (`SandboxPauseRequest`); both API clients
regenerated via `make codegen`.
- **JS**: `Sandbox.pause` / `betaPause` accept `{ memory }` →
`SandboxApi.pause` sends the request body.
- **Python**: `pause(memory=...)` / `beta_pause` → `_cls_pause` (sync +
async) sends `SandboxPauseRequest(memory=...)`.
- **Tests**: filesystem-only pause+resume reboots the guest while the
filesystem survives — JS (`tests/sandbox/snapshot.test.ts`) and Python
sync + async. All pass against a local stack; `format` / `lint` /
`typecheck` clean.
- **Changeset**: `minor` for `e2b` and `@e2b/python-sdk`.

## Note (related infra observation, not addressed here)

While testing, a filesystem-only **resume cold-boots into a different
default exec context** (`root` / `/root`) than a memory resume (`user` /
`/home/user`). The filesystem itself is fully intact; tests use absolute
paths to be robust to this. Worth confirming on the infra reboot path
whether the template's default user should be restored after a cold
boot.

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

---------

Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 10:41:57 +00:00
github-actions[bot] 31a93bed0c [skip ci] Release new versions 2026-06-25 06:10:56 +00:00
Ben Fornefeld 5370d54bfa feat(cli): replace access token auth with Hydra OAuth flow (#1481)
## Summary

Restructures the CLI config schema to v1 with nested , , and sections.
Replaces the legacy e2b access token auth with a Hydra OAuth flow using
refresh tokens. Token expiry is decoded from the JWT claim at runtime
instead of being stored.

## Changes

- **New config schema (v1)**: , , , , ,  (ISO timestamp)
- **Token refresh**: decodes from the JWT access token, refreshes via
Hydra when expired, writes only (not )
- **Deprecated config handling**: Old flat configs without are deleted
with a re-login prompt. No migration path — users re-authenticate.
- ****: Set on  and , not on token refresh
- ****: New helper for direct error throwing without type narrowing;
auth commands use it instead of
- **Type-safe team responses**: Removed  casts, use  type extraction
- **Logout**: Revokes refresh token via Hydra before deleting config;
fixed crash when deprecated config already deleted by
- **Removed**: Token expiry display from ,  from 

## Config example

```json
{
  "version": 1,
  "identity": { "email": "user@example.com" },
  "oauth": { "token_endpoint": "https://hydra.../oauth2/token", "client_id": "..." },
  "tokens": { "access_token": "...", "refresh_token": "..." },
  "last_refresh": "2024-06-24T12:00:00.000Z",
  "teamName": "...", "teamId": "...", "teamApiKey": "..."
}
```

## Test plan

- [x] `pnpm run typecheck` passes
- [x] `pnpm exec eslint` passes on changed files
- [x] `pnpm exec prettier --check` passes
- [x] `pnpm exec vitest run tests/user_config_permissions.test.ts`
passes
- [ ] Manual: `e2b auth login` writes v1 config
- [ ] Manual: token refresh via `e2b auth configure` with expired JWT
- [ ] Manual: old flat config triggers deprecation and re-login

Depends on: dashboard PR adding the Hydra OAuth CLI flow

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-06-24 18:03:55 -07:00
Mish Ushakov 2a98cce8c7 fix(js-sdk): stop CommandHandle.disconnect() leaking the output subscription (#1474)
## Description

This PR fixes two related issues in the command handle's event handling.

### 1. JS `CommandHandle.disconnect()` leaked the output subscription

`disconnect()` was fire-and-forget — it only triggered the transport
abort and relied entirely on HTTP/2 abort propagation to stop events,
which is unreliable under keepalive: `onStdout`/`onStderr`/`onPty` could
keep firing for output produced after `disconnect()` returned.

`disconnect()` now sets a cooperative `disconnected` flag and aborts the
transport. The flag is checked before every callback dispatch in the
event loop, so once `disconnect()` returns no callback fires for output
that arrives (or was buffered) after the call — even if the underlying
abort hasn't torn the stream down yet. It does **not** wait for the
event handler to drain, so it returns promptly even for an idle command
(e.g. `sleep`) whose stream produces no further output, never blocks on
an in-flight callback, and does not deadlock when awaited from inside a
callback.

The async Python SDK was already correct here (`disconnect()` cancels
the event-handling task), and the sync Python SDK has no background
subscription (events are consumed only while the caller iterates). The
added Python tests confirm both.

### 2. Exit code was lost when a disconnected consumer stopped on a
flushed `end`-event chunk

When the `end` event flushes trailing decoder bytes (an incomplete
multibyte sequence → replacement character) and the consumer stops
iterating on the first flushed chunk, the generator was aborted before
the result was assigned, so `wait()` failed as if the process never
produced a result. The `end` handler now records the result **before**
yielding the flushed chunks, across the JS, async Python, and sync
Python SDKs.

## Usage

```js
const handle = await sandbox.commands.run(daemon, { background: true, stdin: true, onStdout })
await sandbox.commands.sendStdin(handle.pid, 'turn1\n')
await handle.disconnect() // resolves promptly; onStdout will not fire again
await sandbox.commands.sendStdin(handle.pid, 'turn2\n') // turn2 output never reaches onStdout
```

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:03:56 +00:00
Matt Brockman 21af1f8a15 fix(python): avoid quadratic stdout/stderr accumulation in command ha… (#1472)
merged from https://github.com/e2b-dev/E2B/pull/1457

## Problem

`AsyncCommandHandle` / `CommandHandle` accumulate streamed output with
`self._stdout += out` per chunk. Because `self._stdout` is an instance
attribute (`STORE_ATTR`), CPython's in-place string-concatenation
optimization — which only applies to local `STORE_FAST` targets —
doesn't apply, so each append re-copies the entire buffer. For commands
that emit large volumes of output this becomes O(n²) in total bytes, and
in async contexts it stalls the event loop for hundreds of ms per chunk
near the tail.

## Fix

Buffer decoded chunks in a `list[str]` and `"".join()` them on read.
This restores linear-time accumulation and keeps streaming responsive,
with no change to the resulting `stdout`/`stderr` values or the public
API.

## Notes

- Applies the same change to both the sync and async command handles.
- Pure internal change; the incremental UTF-8 decoding behavior is
preserved.
- Changeset included (`@e2b/python-sdk`, patch).

Co-authored-by: davidzeng-pplx <david.zeng@perplexity.ai>
2026-06-23 13:08:43 +02:00
github-actions[bot] 7d4d620fa8 [skip ci] Release new versions 2026-06-22 19:39:46 +00:00
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 3cb6ca5f92 [skip ci] Release new versions (sync repo with published packages) (#1468)
## Why

The Release run
[27844631141](https://github.com/e2b-dev/E2B/actions/runs/27844631141/job/82412481993)
**published all packages successfully** but then failed at the final
*"Commit new versions"* step — the version-bump commit-back to \`main\`
was rejected as non-fast-forward (another PR landed on \`main\` during
the release window).

As a result the registries are ahead of the repo:

| Package | Published | Repo (main) before this PR |
|---|---|---|
| \`e2b\` (JS) | 2.30.4 (npm) | 2.30.3 |
| \`@e2b/cli\` | 2.12.2 (npm) | 2.12.1 |
| \`e2b\` (Python) | 2.29.4 (PyPI) | 2.29.3 |

The changeset \`fix-logo-pypi-npm.md\` was also never consumed and is
still on \`main\`.

## What this PR does

Replays exactly what the failed *"Commit new versions"* step would have
committed — i.e. \`pnpm run version\` (changeset version +
\`postVersion\` poetry sync) + lockfile update:

- Bumps \`e2b\` → 2.30.4, \`@e2b/cli\` → 2.12.2, \`@e2b/python-sdk\` →
2.29.4 (matching what's already published)
- Deletes the consumed changeset \`fix-logo-pypi-npm.md\`
- Updates \`pnpm-lock.yaml\` (CLI's \`e2b\` dep → 2.30.4)

No new packages are published by merging this — it only syncs the repo
to the registries. **Do not re-run the Release workflow** for this
changeset; the versions already exist on npm/PyPI.
2026-06-22 10:15:33 -07:00