@e2b/python-sdk@2.45.1
1105 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
29794d0d56 |
Route volume-api generation through redocly filtering (#1731)
## Summary Follow-up to #1728 (per review: https://github.com/e2b-dev/E2B/pull/1728#discussion_r3822169921): the volume-content client generation now bundles through redocly first, like the envd pipeline, so future `x-internal: true` operations in the synced spec are dropped before SDK schemas are generated. - `redocly.yaml`: new `volume` API rooted at `spec/openapi-volumecontent.yml` with the same `filter-out: x-internal` + `remove-unused-components` decorators as `envd`. - js-sdk `generate:volume-api` and python-sdk `generate-volume-api` now run `redocly bundle volume -o spec/openapi_generated.volume.yml` before their generators (the bundle output is gitignored like the other `openapi_generated.*.yml` files). Regenerated output: the Python volume client is byte-identical (the spec has no `x-internal` operations today); the JS `schema.gen.ts` only loses four response components (400/401/403/409) that no operation referenced, dropped by `remove-unused-components`. Now that #1728 is merged, this is rebased onto `main` (single commit, codegen plumbing only) and ready for review. Link to Devin session: https://app.devin.ai/sessions/175095f75cbe42df8710718a1ff2a6a3 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> |
||
|
|
f6014f17ce | [skip ci] Release new versions | ||
|
|
8787dfec9b |
feat(sdk,cli): add sandbox list sorting and filters (#1735)
## Summary SDK follow-up to the merged API change (e2b-dev/belt#1713) that added `order`, `startedAfter`, and `template` to `GET /v2/sandboxes`; the earlier SDK PR for this was closed unfinished. The generated API clients already had the parameters — this wires them through the public `Sandbox.list` surface in JS and both Python SDKs (sync + async), plus the `e2b sandbox list` CLI command, so ordering and filtering happen server-side across the whole paginated dataset instead of per loaded page. New options (mirrored across all three SDK surfaces): - `order: 'asc' | 'desc'` (default `'desc'`, newest first) — sorts by sandbox start time; exposed as `SandboxListOrder` - `query.startedAfter` / `SandboxQuery.started_after` — inclusive lower bound on start time - `query.template` / `SandboxQuery.template` — exact template ID or alias (unknown template ⇒ empty list) ### Usage JavaScript: ```ts const paginator = Sandbox.list({ query: { metadata: { env: 'ci' }, startedAfter: new Date(Date.now() - 60 * 60 * 1000), template: 'base', }, order: 'asc', }) const sandboxes = await paginator.nextItems() ``` Python (sync; async is identical with `AsyncSandbox` / `await`): ```python paginator = Sandbox.list( query=SandboxQuery( metadata={"env": "ci"}, started_after=datetime.now(timezone.utc) - timedelta(hours=1), template="base", ), order="asc", ) sandboxes = paginator.next_items() ``` CLI: ```sh e2b sandbox list --template base --started-after 2025-01-01T00:00:00Z --order desc ``` The CLI table respects `--order` when rendering (previously it always re-sorted ascending by start time; that remains the default). Includes integration tests for order, `startedAfter`, and template filtering in JS and both Python test suites, a unit test for CLI table ordering, plus a minor changeset for `e2b`, `@e2b/python-sdk`, and `@e2b/cli`. Link to Devin session: https://app.devin.ai/sessions/44dcb4b0ca9143b8b023ef6fb8554c72 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> |
||
|
|
5995e0ad1c | [skip ci] Release new versions | ||
|
|
d000bbd2db |
test: mock all volume tests and remove ENABLE_VOLUME_TESTS skip flag (#1734)
## Summary
Volume tests always run now — the `ENABLE_VOLUME_TESTS` skip flag is
removed and every volume CRUD and file-operation test runs against
deterministic in-process mocks, requiring no live volume infra or
credentials:
- **JS** (`tests/volume/`): `createMockVolumeApi()` returns MSW handlers
with per-instance state — a stateful in-memory filesystem per volume ID
plus the control-plane `POST/DELETE /volumes` used by the `volumeTest`
fixture, which passes the placeholder `TEST_API_KEY` so `file.test.ts`
runs in isolation without ambient credentials.
- **Python** (`tests/mock_volume_content.py` + `conftest.py`):
`MockVolumeContentAPI` implements the same filesystem semantics behind
`httpx.MockTransport`, injected via `attrs.evolve(client,
httpx_args={"transport": ...})` on both the regular and streaming volume
client factories so it survives `with_timeout`. The
`volume`/`async_volume` fixtures go through `Volume.create()` /
`AsyncVolume.create()` with the control-plane calls mocked, matching the
JS fixture entry point.
Both mocks cover write/read (text/bytes/blob/stream/empty),
force-overwrite conflicts, metadata (`uid`/`gid`/`mode`),
nested/recursive `makeDir` (with parent-error propagation), `list`,
`getInfo`, `exists`, `updateMetadata`, and recursive `remove`; entry
types use the `VolumeFileType` enum.
Also fixes a `js-sdk` bug the always-running tests surfaced (introduced
by #1730): `Volume.exists()` caught the deprecated `NotFoundError`, but
`getInfo()` now throws `VolumePathNotFoundError` (a `VolumeError`
subclass), so `exists()` rethrew instead of returning `false` for
missing paths. `exists()` now catches `VolumePathNotFoundError`
(changeset included; Python was already correct since
`VolumePathNotFoundException` subclasses `NotFoundException`).
Link to Devin session:
https://app.devin.ai/sessions/80a50c2aba6441368d775b52a24cd1af
Requested by: @mishushakov
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mish@e2b.dev <mish@e2b.dev>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
|
||
|
|
33195ae163 | [skip ci] Release new versions | ||
|
|
5759f17e56 |
feat(sdk): add E2B client for multiple bound connection configs (#1720)
## Summary Adds an `E2B` client to both SDKs so a process can talk to several API keys / domains / deployments without going through environment variables. The client binds a connection config once and exposes the resource surfaces off it; the named top-level exports are untouched and keep reading the environment. Nothing existing changes (changeset is `minor`): the default export is still `Sandbox`, `Template(...)` keeps working, and `E2B` is a new named export. Two follow-ups are tracked for v3: making `E2B` the default export ([SDK-341](https://linear.app/e2b/issue/SDK-341/sdk-v3-js-make-e2b-the-default-export-instead-of-sandbox)) and dropping the `Template` Proxy in favour of `new Template()` ([SDK-342](https://linear.app/e2b/issue/SDK-342/sdk-v3-js-drop-the-template-proxy-require-new-template)). ```ts import { E2B } from 'e2b' const { Sandbox, Volume, Template, Secret } = new E2B({ apiKey: 'e2b_***', domain: 'e2b.dev', }) const sandbox = await Sandbox.create() const volume = await Volume.create('my-volume') const exists = await Template.exists('my-template') await Template.build(Template().fromPythonImage('3'), 'my-env') await Secret.create('openai-api-key', 'sk-***') // Per-call options still win over the client's options. await Sandbox.create({ apiKey: 'e2b_other***' }) ``` ```python from e2b import E2B client = E2B(api_key="e2b_***", domain="e2b.dev") Sandbox, Volume, Template = client.Sandbox, client.Volume, client.Template Secret = client.Secret sandbox = Sandbox.create() volume = Volume.create("my-volume") exists = Template.exists("my-template") secret = Secret.create("openai-api-key", "sk-***") # Async variants are exposed too. AsyncSandbox, AsyncTemplate = client.AsyncSandbox, client.AsyncTemplate async_sandbox = await AsyncSandbox.create() await AsyncTemplate.exists("my-template") ``` ### Mechanism `client.Sandbox` / `client.Volume` / `client.Template` / `client.Secret` (plus the `Async*` variants in Python) are per-client subclasses of the real classes, carrying the bound opts as class-level state. Nothing process-global is mutated, so clients are isolated from each other and from the default path, and `cls`/`this` dispatch is preserved (`create` on a client class returns an instance of that client class). ```ts // sandboxApi.ts / volume/index.ts / template/index.ts / secret.ts — one hook per class hierarchy protected static readonly boundOpts?: ConnectionOpts // undefined on the base classes protected static resolveOpts<T extends ConnectionOpts>(opts?: T) { return ConnectionConfig.mergeOpts(this.boundOpts, opts) // { ...bound, ...definedPerCall } } // every static method that built a config from raw opts now does - const config = new ConnectionConfig(opts) + const apiOpts = this.resolveOpts(opts) + const config = new ConnectionConfig(apiOpts) ``` ```py # sandbox/main.py, volume_sync.py, volume_async.py, template_{sync,async}/main.py, secret/base.py _bound_api_params: ApiParams = {} # empty on the base classes @classmethod def _resolve_api_params(cls, **opts: Unpack[ApiParams]) -> ApiParams: return merge_api_params(cls._bound_api_params, opts) - config = ConnectionConfig(**opts) + config = ConnectionConfig(**cls._resolve_api_params(**opts)) ``` `Template` used to be a factory function whose statics were pre-bound to `TemplateBase`, which left no class for a client to subclass. It is now the `TemplateBase` class itself, wrapped in a `Proxy` whose only trap makes it callable without `new`, so `Template(...)` keeps working (no breaking change) while `client.Template` is a plain subclass like Sandbox/Volume and `Template.build(...)` resolves `this` naturally: ```ts export function callableTemplate<T extends typeof TemplateBase>(cls: T) { return new Proxy(cls, { apply: (target, _this, args) => new target(...args) }) } export const Template = callableTemplate(TemplateBase) // Template() still returns a builder this.Template = callableTemplate(class extends TemplateBase { boundOpts }) // client.Template ``` Because the trap only intercepts calls, `new Template()`, statics, `instanceof` and subclassing all go straight to the class, and the builder's default file context (`getCallerDirectory()`) still resolves to the user's frame (the trap's frame is inside the SDK and filtered like the old factory's). Two side effects of routing everything through the hook: - Static methods that resolved config off the base class had to move to `this`/`cls`: `SandboxApi.createSandbox(...)` → `this.createSandbox(...)` in JS, `new Volume(...)` → `new this(...)`, and several Python `@staticmethod`s (`SandboxApi.list`, `_cls_list_snapshots`, `delete_snapshot`, `Volume._class_get_info` / `_class_list` / `destroy`, and the `Secret` operations) became `@classmethod`s. Behavior for the top-level classes is unchanged since their bound opts are empty. - `DualMethod.__get__` (the descriptor behind `Volume.get_info` / `Volume.list` working both on the class and on instances) now binds the class-level function to the accessed class, so `client.Volume.list()` sees the subclass' bound params instead of `Volume`'s. Per-call values explicitly set to `undefined` / `None` are dropped when merging, so they fall back to the client's opts rather than clearing them into the env-var path. ### Tests `packages/js-sdk/tests/client.test.ts` (MSW) and `packages/python-sdk/tests/test_client.py` (local HTTP server, sync + async) cover: the client's API key/domain being used instead of the env vars, per-call precedence, rebinding the class (`const S = client.Sandbox`), rebound `client.Template`, the client template builder producing the same Dockerfile as the top-level one, two clients staying isolated, generated-subclass instances, `client.Secret` (sync + async) using the bound config, the top-level classes still using the env config with empty bound opts and the default export still being `Sandbox`. Link to Devin session: https://app.devin.ai/sessions/772afa048b814ad784b5dde0a599df46 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> |
||
|
|
2e26b825e1 | [skip ci] Release new versions | ||
|
|
f89f8c3f96 |
Add secrets management to JS and Python SDKs (#1728)
## Summary Implements Secrets Management in the SDK per the [Secrets Vault SDK proposal](https://app.notion.com/p/3bab8c29687380b6a8f3e2ecae3f1b50) and the backend Secrets API. Linear: [SDK-133](https://linear.app/e2b/issue/SDK-133/sdk-for-managing-secrets). Docs: [e2b-dev/docs#379](https://github.com/e2b-dev/docs/pull/379). Spec sync: bumps `spec/infra-ref` to `e19a12b8` (the commit that adds the Secrets API), adds the `secrets` tag to the `redocly.yaml` filters, and regenerates via `make codegen` (the regen also pulls in unrelated upstream spec updates, e.g. the `Error.errorCode` field). The js-sdk envd schema generation now bundles through a new `envd` redocly api that filters out operations the upstream spec marks `x-internal: true` (orchestrator control plane: `/init`, `/freeze`, `/unfreeze`, `/collapse`, `/fsfreeze`, `/fsthaw`) plus their now-unused component schemas, so they no longer appear in `src/envd/schema.gen.ts`. The existing `Secret` class (previously only the `iamToken`/`iam_token` workload-identity helper) becomes the secrets management surface, equivalent across JS, sync Python (`Secret`), and async Python (`AsyncSecret`): ```typescript Secret.create(name, value, opts?): Promise<SecretInfo> // POST /secrets Secret.update(secret, value, opts?): Promise<SecretInfo> // POST /secrets/{secretID} (rotates to a new version) Secret.getInfo(secret, opts?): Promise<SecretInfo> // GET /secrets/{secretID} Secret.list(opts?): SecretPaginator // GET /secrets (cursor-paginated) Secret.exists(secret, opts?): Promise<boolean> // 200 → true, 404 → false Secret.destroy(secret, opts?): Promise<boolean> // 204 → true, 404 → false Secret.fill(secret): string // local marker formatting, no network call ``` Design decisions per the proposal: - **Values are write-only**: `SecretInfo` carries only metadata (`secretId`, `name`, `version`, `metadata`, `createdAt`, `updatedAt`); no read surface or error message includes a value. - `update`/`getInfo` throw `SecretNotFoundError` / `SecretNotFoundException` on 404 (subclass of `NotFoundError` / `NotFoundException`, so generic not-found catches keep working; general failures throw the new `SecretError` / `SecretException`); `exists`/`destroy` map 404 to `false` instead. - `secret` selector accepts either the `sec_` ID or the canonical lowercase name (backend resolves both). - `fill` returns the `${e2b.secrets.name}` marker for use in a network rule's request transform — always the current version, purely local. The egress proxy replaces the marker with the secret's current value when it forwards a matching request; unresolvable markers fail open (the request is forwarded with the affected headers omitted). - Version-management endpoints from the proposal are marked TBD and not in the committed backend contract, so they are intentionally not implemented. Python moves `e2b/secret.py` to an `e2b/secret/` package (`base.py` shares `fill`/`iam_token`, `secret_sync.py` / `secret_async.py` mirror each other); `from e2b import Secret` is unchanged. Usage: ```typescript import { Sandbox, Secret } from 'e2b' const info = await Secret.create('stripe_api_key', 'sk_live_...', { metadata: { env: 'prod' } }) await Secret.update('stripe_api_key', 'sk_live_new...') // rotate → version 2 // Inject into matching outbound requests via a network rule's transform: const sandbox = await Sandbox.create({ network: { allowOut: ({ rules }) => [...rules.keys()], denyOut: ({ allTraffic }) => [allTraffic], rules: { 'api.stripe.com': [ { transform: { headers: { Authorization: `Bearer ${Secret.fill('stripe_api_key')}` }, }, }, ], }, }, }) await Secret.destroy('stripe_api_key') ``` ```python from e2b import AsyncSecret info = await AsyncSecret.create("stripe_api_key", "sk_live_...", metadata={"env": "prod"}) paginator = AsyncSecret.list(limit=100) while paginator.has_next: secrets = await paginator.next_items() print(AsyncSecret.fill("stripe_api_key")) # ${e2b.secrets.stripe_api_key} ``` Tests: msw-mocked JS suite (`tests/secret/secret.test.ts`) and monkeypatched sync/async Python suites covering CRUD, pagination, 404 semantics, and `fill`. `pnpm run format/lint/typecheck` pass; changeset included (minor for `e2b` and `@e2b/python-sdk`). Link to Devin session: https://app.devin.ai/sessions/175095f75cbe42df8710718a1ff2a6a3 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> |
||
|
|
d79c6cd973 |
refactor(js-sdk): drop unused stackTrace params from error constructors (#1732)
## Summary Removes the `stackTrace` constructor parameter from JS SDK error classes that never have a caller stack trace attached. Only template/build paths intentionally capture user frames (`getCallerFrame()`) or pass a trace along (e.g. `uploadFile`), so the param was dead weight elsewhere. - Dropped `stackTrace` from: `SandboxError` (base), `TimeoutError`, `NotEnoughSpaceError`, `NotFoundError`, `FileNotFoundError`, `SandboxNotFoundError`, `GitUpstreamError`, and the new `VolumeNotFoundError` / `VolumePathNotFoundError` from #1730. - Classes that actually receive traces keep them, and now assign `this.stack` directly instead of forwarding through `super()`: ```ts export class TemplateError extends SandboxError { constructor(message: string, stackTrace?: string) { super(message) this.name = 'TemplateError' if (stackTrace) this.stack = stackTrace } } // same pattern for InvalidArgumentError; BuildError/FileUploadError unchanged in behavior ``` No behavior change for template/build stack traces; tests unmodified. Python needs no equivalent change (traceback attachment uses `.with_traceback()` rather than constructor params). Link to Devin session: https://app.devin.ai/sessions/a8493e846f5c424393333236bb8cadc0 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> |
||
|
|
05aa03c35c |
Add typed not-found errors for volumes (#1730)
## Summary Volumes threw the plain (JS-deprecated) `NotFoundError` / `NotFoundException` everywhere. This adds typed subclasses, matching `SecretNotFoundError` from #1728: - `VolumeNotFoundError` / `VolumeNotFoundException` — the volume itself doesn't exist (`Volume.getInfo` / `Volume.get_info`). - `VolumePathNotFoundError` / `VolumePathNotFoundException` — a file/directory path inside a volume doesn't exist (read/write/list/remove/stat content operations). Both subclass the existing `NotFoundError` / `NotFoundException`, so existing generic catches keep working. Applied equivalently to the JS SDK and the sync + async Python SDKs, with tests asserting both the specific type and the base-class relationship, and a changeset. ```typescript import { Volume, VolumeNotFoundError, VolumePathNotFoundError } from 'e2b' try { await Volume.getInfo('non-existent-id') } catch (err) { if (err instanceof VolumeNotFoundError) { // volume doesn't exist } } try { await vol.readFile('missing.txt') } catch (err) { if (err instanceof VolumePathNotFoundError) { // path inside the volume doesn't exist } } ``` ```python from e2b import Volume, VolumeNotFoundException, VolumePathNotFoundException try: Volume.get_info("non-existent-id") except VolumeNotFoundException: ... # volume doesn't exist try: volume.read_file("missing.txt") except VolumePathNotFoundException: ... # path inside the volume doesn't exist ``` Link to Devin session: https://app.devin.ai/sessions/175095f75cbe42df8710718a1ff2a6a3 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> |
||
|
|
61503f75eb |
fix(js-sdk): guard runtime-probed iam token names in network transforms (#1715)
## Summary Fixes #1673. In a network `transform` callback, `iam.tokens.toJSON`, `.then`, `.toString` and `.valueOf` were exempt from the unknown-token guard, because the runtime reads those names off any object it serializes, awaits or coerces — without the exemption, `JSON.stringify(iam.tokens)` inside a callback would throw. So referencing one as a token name produced `Bearer undefined` or `Bearer function toString() { [native code] }`. Neither carries a `${e2b.identity.tokens.…}` placeholder, so the egress proxy forwards it verbatim and the destination answers 401 on a garbage credential — the confusing failure the guard exists to prevent. Any other typo already threw. The fix separates the probe from a token reference: a probe reads the name and stops there, a token reference coerces or serializes what it read. ```ts // get trap, for a name that is not a registered token if (RUNTIME_PROBED_PROPS.has(prop)) { // `then`/`toJSON`: non-callable, so `await` and `JSON.stringify` treat it as absent. // `toString`/`valueOf`: callable, so `String(iam.tokens)` still works — `valueOf` // answers with the guarded proxy, not the record behind it. const value = prop === 'toString' ? () => Object.prototype.toString.call(proxy) : prop === 'valueOf' ? () => proxy : {} // Coerced (`Bearer ${…}`) or serialized (`{ 'X-Api-Key': iam.tokens.then }`) → throw. Object.defineProperty(value, Symbol.toPrimitive, { value: resolveUnregistered }) Object.defineProperty(value, 'toJSON', { value: resolveUnregistered, enumerable: true }) return value } ``` `toJSON` has to be enumerable: Bun's `JSON.stringify` only finds an own `toJSON` that is, and the Bun CI leg caught the non-enumerable version. ```ts await Sandbox.create({ iam: { tokens: { aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }) } }, network: { rules: { 'api.example.com': [ { // InvalidArgumentError: Network transform references iam token 'then', // which is not registered. Registered tokens: 'aws'. transform: ({ iam }) => ({ headers: { Authorization: `Bearer ${iam.tokens.then}` }, }), }, ], }, }, }) ``` Per review, this is the minimal version: the earlier round also made the map read-only (`set`/`defineProperty`/`deleteProperty` traps, `Readonly` typing) and resolved descriptor lookups through the guard. Both were dropped — they hardened paths nobody hits, and `Object.hasOwn` throwing was a wart. What is left is the `get`-trap stand-in and its tests. Unchanged: `JSON.stringify(iam.tokens)`, `await iam.tokens`, `String(iam.tokens)`, spread, enumeration and `in`; a token actually named `then`/`toJSON`/`toString`/`valueOf` resolves to its placeholder; `Sandbox.updateNetwork`, which has no client-side view of the registered names, still resolves any name to a placeholder. The Python mapping already raised on every one of these lookups, so no Python change. Tests: the four names rejected both interpolated and assigned straight through as a header value, with no create request sent; `valueOf()` still returning the guarded map; the map's own serialization/await/coercion; and the `validate: false` path resolving the same names to placeholders. Verified under Node and Bun. Link to Devin session: https://app.devin.ai/sessions/d82670868f7540d387ffadc4587a5ce0 Requested by: @mishushakov --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mish@e2b.dev <mish@e2b.dev> |
||
|
|
2be6c12f79 |
refactor(sdk): resolve template config through a bound-opts class hook (#1721)
## Summary
Preparatory refactor so a per-client `client.Template` can subclass
`TemplateBase` and inject a bound `ConnectionConfig`, the way
`Sandbox`/`Volume` will. No public behavior change: the top-level
`Template()` factory, `Template.build(...)`, `AsyncTemplate.*` etc.
still resolve config from per-call opts + env vars (the bound field is
empty on the base class).
**JS** — terminal statics build their config through a class-level hook
instead of `new ConnectionConfig(opts)` directly:
```ts
class TemplateBase {
protected static boundConnectionOpts: ConnectionOpts = {}
protected static resolveConnectionConfig(opts?: ConnectionOpts) {
return new ConnectionConfig({ ...this.boundConnectionOpts, ...definedEntriesOf(opts) })
}
}
- const config = new ConnectionConfig(buildOptions)
+ const config = this.resolveConnectionConfig(buildOptions)
```
That only works if `this` is a template class, and the top-level surface
copies the statics off the class (`Template.build =
TemplateBase.build`), where `this` would be the factory function. So the
copies are now bound:
```ts
function boundToBase<T extends (...args: never[]) => unknown>(fn: T): T {
return fn.bind(TemplateBase) as T // the cast is only because `bind` collapses overloads
}
- Template.build = TemplateBase.build
+ Template.build = boundToBase(TemplateBase.build)
```
Top-level calls therefore resolve against `TemplateBase` (no bound opts
→ per-call opts + env, unchanged), while `MyTemplate.build(...)` keeps
`this === MyTemplate` and picks up its bound opts. `exists` likewise
dispatches via `this.aliasExists(...)` instead of
`TemplateBase.aliasExists(...)`. `toJSON`/`toDockerfile` untouched.
**Python** — `build`, `build_in_background`, `get_build_status`,
`exists`, `alias_exists`, `assign_tags`, `remove_tags`, `get_tags` went
from `@staticmethod` to `@classmethod` (signatures otherwise identical,
so call sites are unaffected), and the hardcoded lookups now go through
`cls`:
```python
- config = ConnectionConfig(**opts)
- data = Template._build(...) # AsyncTemplate._build in the async SDK
- logs_refresh_frequency=TemplateBase._logs_refresh_frequency,
+ config = cls._resolve_connection_config(**opts)
+ data = cls._build(...)
+ logs_refresh_frequency=cls._logs_refresh_frequency,
```
with the hook on the shared `TemplateBase`:
```python
_bound_api_params: ApiParams = {}
@classmethod
def _resolve_connection_config(cls, **opts: Unpack[ApiParams]) -> ConnectionConfig:
return ConnectionConfig(**{**cls._bound_api_params, **{k: v for k, v in opts.items() if v is not None}})
```
Precedence is per-call opts > bound opts > env vars; explicitly passed
`undefined`/`None` per-call values are dropped so they don't wipe bound
opts. No `ConnectionConfig` process-global state is touched.
## Usage
```ts
import { TemplateBase } from 'e2b'
class MyTemplate extends TemplateBase {
protected static boundConnectionOpts = { apiKey: 'e2b_...', domain: 'my.e2b.dev' }
}
await MyTemplate.exists('my-template') // bound config
await MyTemplate.exists('my-template', { apiKey: 'e2b_x' }) // per-call wins
```
```python
class MyTemplate(Template):
_bound_api_params = {"api_key": "e2b_...", "domain": "my.e2b.dev"}
MyTemplate.exists("my-template")
MyTemplate.exists("my-template", api_key="e2b_x")
```
## Tests
New `tests/template/boundConnectionOpts.test.ts` (msw, asserts the
request URL + `X-API-KEY` per operation) and `test_bound_api_params.py`
for sync and async, covering: top-level path unchanged (per-call opts
and env fallback), bound opts as defaults for
`build_in_background`/`exists`/tag ops, per-call override, and
`None`/`undefined` not clearing bound opts.
Link to Devin session:
https://app.devin.ai/sessions/f15b0cecd1fd40e297334ac8ce154af1
Requested by: @mishushakov
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mish@e2b.dev <mish@e2b.dev>
|
||
|
|
8b49570575 |
test(js-sdk): tolerate "Script not found" propagation errors in CF deploy suite (#1725)
## Summary The `cloudflare-deploy` job failed on `main` ([run](https://github.com/e2b-dev/E2B/actions/runs/32312834689/job/96259231560)) with a Cloudflare edge error page that the propagation handling doesn't cover: ``` Deployed: https://e2b-js-sdk-smoke.spurious-canidae.workers.dev Worker route not live yet (404), waiting... Worker is live. × sandbox lifecycle inside a deployed Cloudflare Worker 216ms Error: non-JSON response (500, "Script not found | e2b-js-sdk-smoke.spurious-canidae.workers.dev | Cloudflare") ``` `setup.mts` polled until one colo answered `405`, but the colo that served the test's POST had the route and not yet the script, so it returned a `500` "Script not found" page. The test's retry condition only matched `non-JSON response (404` / `fetch failed`, so this propagation variant failed on the first attempt (216 ms, no retry) instead of being absorbed like the 404. Both propagation checks now recognize it, leaving all assertions on the worker's JSON response untouched: ```diff -condition: /non-JSON response \(404|fetch failed/, +condition: /non-JSON response \(404|Script not found|fetch failed/, ``` and in `waitUntilLive`, a non-404 status whose page `<title>` says "Script not found" keeps waiting instead of failing fast; any other error page still throws immediately. Verified with `pnpm build && pnpm test:cf:deploy` in `packages/js-sdk` (real `wrangler deploy --temporary`): 1 passed. Link to Devin session: https://app.devin.ai/sessions/6b50de812d9c4e5fac07ee8abd810ce0 Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
0b15b3aae6 | [skip ci] Release new versions | ||
|
|
2daced65be |
chore: tag package homepage URLs with UTM parameters (#1724)
The SDK and CLI homepage fields get utm_source=pypi/npm (utm_campaign=package_homepage) so traffic from the registry pages attributes to its real source instead of direct. Takes effect on the next publish of each package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
7af41e9fab |
chore: refresh generated MCP server types (#1716)
## Summary
`spec/mcp-server.json` (and the `McpServer` types generated from it for
both SDKs) has been frozen since the MCP beta landed. It is produced by
`mcp-gateway`'s `type-gen` from that repo's `docker-catalog.yaml`; this
refreshes it against a fresh snapshot of Docker's MCP catalog: **222 →
265 servers**.
Regenerated with the existing pipeline only — `packages/js-sdk: pnpm
generate:mcp` (`json2ts`) and `packages/python-sdk: make generate-mcp`
(`datamodel-codegen`). No hand edits.
- **49 new servers**: `n8n`, `neo4j`, `okta`, `temporal`, `proxmox`,
`testkube`, `thingsboard`, `zen`, `zscaler`, `googleFlights`,
`nextDevtools`, `victoriametrics`/`victorialogs`/`victoriatraces`, and
the AWS Labs family (`awslabsCloudwatch`, `awslabsDynamodb`,
`awslabsIam`, `awsPricing`, `amazonNeptune`, ...).
- **6 servers removed** — the catalog no longer ships them: `postgres`,
`root`, `tembo`, `flexprice`, `triplewhale`, `cdataConnectcloud`.
Passing them to `Sandbox.create` no longer type-checks, and since
`McpServerName = keyof McpServer`, `Template().addMcpServer('postgres')`
stops compiling too.
- **4 servers changed their options**: `awsDiagram` and `context7` now
require one (`outputDir`, `apiKey`), so `awsDiagram: {}` / `context7:
{}` no longer type-check; `onlyofficeDocspace` is down to `baseUrl` +
`docspaceApiKey`; `neo4jCypher` renamed keys.
- **71 entries differ in metadata**, but 61 of those are title-only and
10 description-only. Titles feed the generated TS interface names
(`AirtableMCPServer` → `Airtable`), which only matters to a caller who
imported those interface names directly — `mcp.d.ts` types are not
re-exported from the SDK root, only `McpServer` is.
The config is still forwarded to the gateway as written, so a dropped
server can be kept by casting past the type — whether it starts is up to
the gateway.
```ts
import { Sandbox } from 'e2b'
const sandbox = await Sandbox.create({
mcp: { n8n: { apiKey: process.env.N8N_API_KEY!, apiUrl: 'https://n8n.example.com/api/v1' } },
})
```
The catalog snapshot this was generated from:
https://github.com/e2b-dev/mcp-gateway/pull/3. The `mcp-gateway`
template has to be rebuilt from that snapshot for the new servers to
actually start in a sandbox, so that PR should land (and the template be
rebuilt) before or with this one.
### Known upstream defects, deliberately not hand-patched
Both come from `type-gen`'s naming rules and belong in
`e2b-dev/mcp-gateway`, since editing generated output here is undone by
the next regeneration:
- `vectraAiRux` lists `VECTRABASEURL` as required, but no such property
exists — the catalog maps it from `vectra_url` via the entry's `env`
block, and `type-gen` emits the env-var name verbatim. `type-gen` should
resolve `required` names through `env` and hard-fail on one that matches
no property.
- `VECTRACLIENTID` keeps its env-var spelling because `type-gen` strips
underscores without re-casing.
Link to Devin session:
https://app.devin.ai/sessions/215a9143568a44209fb02e4177143b72
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mish@e2b.dev <mish@e2b.dev>
|
||
|
|
43c28b15fb |
ci(js-sdk): install Playwright Chromium without --with-deps (#1699)
Supersedes #1698 (claimed via `/sdk claim` by @mishushakov). **Please close #1698 in favour of this PR** — I have no write access to close it myself. This is a straight clone: the commit `f65f602` from #1698 is applied here unmodified (original authorship and the `Co-authored-by: Mish Ushakov` trailer preserved), with `origin/main` merged in so the branch is current — `main` had moved one commit ahead (#1693), which touches none of the two files in this PR. The diff against `main` is identical to the original: `.github/workflows/js_sdk_tests.yml` and `packages/js-sdk/package.json`. Per the claim instructions, nothing was reviewed or changed. The original description follows, verbatim. --- Closes [SDK-339](https://linear.app/e2b/issue/SDK-339/js-sdk-node-ci-legs-spend-most-of-their-time-in-playwright-install). Related: [SDK-292](https://linear.app/e2b/issue/SDK-292/run-the-full-js-sdk-unit-test-suite-in-a-browser), which introduced the `browser` project this install serves. ## Problem `packages/js-sdk/package.json` had a `pretest` hook running `npx playwright install --with-deps chromium`. `--with-deps` shells out to apt on Linux, and to a DISM Media Foundation enable on Windows, on **every** invocation — regardless of whether the workflow's Playwright browser cache hit. On one `Test JS SDK` run that hook was 90% of the Node leg: | leg | step | time | | --- | --- | --- | | node / ubuntu-22.04 | `Run Node tests` total | 23m21s | | | ↳ `pretest` (`--with-deps`) | **20m57s** | | | ↳ `vitest run` (101 files, 100 passed) | 2m23s | | node / windows-latest | `pretest` DISM Media Foundation enable | 4m31s | The browser cache worked fine (`Cache hit for: playwright-Linux-1.55.1`, restored in 3s). The time went to apt: `apt-get update` 1m42s, then 18.4 MB fetched in 18m59s at 16.1 kB/s off a stalling Azure Ubuntu mirror (`fonts-wqy-zenhei` alone stalled 7m49s). The mirror stall is transient; being on that path at all is the structural problem. Every shared library Chromium needs (`libnss3`, `libgbm1`, `libdrm2`, `libcairo2`, `xvfb`, …) was already `already the newest version` on the runner image — the only 9 new packages were CJK/Cyrillic fonts (`fonts-wqy-zenhei`, `fonts-ipafont-gothic`, `xfonts-*`) that the single headless `browser` test never renders. For comparison, in the same run the bun (2m44s), deno (2m41s) and cloudflare (2m1s) legs run the same test code with no Playwright `pretest`. ## Change - `packages/js-sdk/package.json`: replace the `pretest` hook with an explicit `playwright:install` script (`playwright install chromium`, no `--with-deps`). - `.github/workflows/js_sdk_tests.yml`: run it as its own step gated on `matrix.runtime == 'node'`, right after the existing browser-cache step, with a comment recording why `--with-deps` is omitted. Moving it out of `pretest` also keeps it off every local `pnpm test`, including for contributors who never touch the browser project. ## Usage CI installs the browser as a distinct, cache-backed step: ```yaml - name: Install Playwright Chromium if: matrix.runtime == 'node' run: pnpm run playwright:install ``` Locally, the `browser` project needs Chromium once per Playwright version: ```bash cd packages/js-sdk pnpm run playwright:install # ~7s cold, ~0.8s once installed pnpm test ``` Without it, the `browser` project fails with Playwright's own "Executable doesn't exist … run `playwright install`" message; the other projects (`unit`, `template`, `connectionConfig`) are unaffected. ## Verification Run on this branch with no prior Playwright deps installed on the machine: - `pnpm run playwright:install`: 6.5s cold (Chromium headless shell + ffmpeg, no apt), 0.78s as a no-op afterwards. - `pnpm exec vitest run --project browser`: 1 passed. Chromium launches and drives a real sandbox without any `--with-deps` packages, confirming the fonts and libs weren't load-bearing. - `pnpm build` + full `pnpm test`: 101 files, 99 passed / 1 skipped in 2m34s. The one failure is `tests/sandbox/network.test.ts > injected header is reflected by the httpbin sidecar`, which fails with `404: template 'httpbin' not found` — it needs a prebuilt `httpbin` template that this agent's API key doesn't have, unrelated to this change. - `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` clean for `packages/js-sdk` (the recursive root scripts fail only in `packages/python-sdk`, where `uv` isn't installed in this environment). - `pnpm run check-deps` (knip) reports no new findings; `playwright` is still resolved as a used devDependency through the new script. No changeset: this touches only dev tooling and CI, with no change to published behavior (the `pretest`/`playwright:install` scripts are inert for consumers of the package). The commit that originally added the hook, #977, likewise shipped without one. <div><a href="https://cursor.com/agents/bc-b8c7df94-5129-496b-ae9f-0c4cb552773a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/view-automation-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/view-automation-light.png"><img alt="View Automation" width="141" height="28" src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com> |
||
|
|
53676931b8 |
fix(sdk): omit autoResume from the create request when unset (#1694)
## Summary
When a caller does not configure `lifecycle.autoResume` /
`lifecycle["auto_resume"]`, the SDKs resolved the value to their own
local default and always serialized `{"autoResume": {"enabled": false}}`
in `POST /sandboxes`. That made an omitted preference indistinguishable
from an explicit opt-out, so the API could not own or evolve its own
default without SDK clients unintentionally overriding it.
The field is now left out of the request when it is not configured, in
the JavaScript SDK and in both Python paths (sync and async):
| caller | wire |
| --- | --- |
| no `autoResume` configured | field omitted |
| `autoResume: false` / `auto_resume: False` | `{"autoResume":
{"enabled": false}}` |
| `autoResume: true` / `auto_resume: True` | `{"autoResume": {"enabled":
true}}` |
Explicit choices keep exactly their previous wire shape, and the
existing client-side validation is untouched: `autoResume: true` still
requires `onTimeout: 'pause'` and is still rejected together with
`keepMemory: false`. An explicit `null` / `None` from an untyped caller
is treated as "not configured" rather than as an opt-out, matching how
`keepMemory` / `keep_memory` already normalizes `null`.
`autoResume` is absent from the `NewSandbox` `required` list in
`spec/openapi.yml`, so omitting it is spec-legal and needs no codegen
change.
Closes #1677. This is the same request-construction problem as #1669,
which covers the sibling `autoPause` field; that field is deliberately
left alone here so the two changes stay reviewable on their own.
## Usage
No application changes are required — only the request built for callers
who never expressed a preference changes.
```ts
import { Sandbox } from 'e2b'
// autoResume is left out of the request entirely, so the API's default applies
await Sandbox.create({ lifecycle: { onTimeout: 'pause' } })
// an explicit choice is sent exactly as before
await Sandbox.create({ lifecycle: { onTimeout: 'pause', autoResume: true } })
await Sandbox.create({ lifecycle: { onTimeout: 'pause', autoResume: false } })
```
```python
from e2b import Sandbox
# auto_resume is left out of the request entirely, so the API's default applies
Sandbox.create(lifecycle={"on_timeout": "pause"})
# an explicit choice is sent exactly as before
Sandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": True})
Sandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": False})
```
The `AsyncSandbox` surface behaves identically. The CLI already only
passed `autoResume` when `--lifecycle.autoresume` was given, so `e2b
sandbox create --lifecycle.ontimeout pause` now leaves the preference
unset as well.
## Tests
New request-level coverage asserts the body of `POST /sandboxes` for
five cases (nothing configured, only `onTimeout` configured, explicit
`false`, explicit `true`, explicit `null`/`None`) in all three
implementations:
- `packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts` (new, msw) —
5 passed
- `packages/python-sdk/tests/sync/sandbox_sync/test_create.py` — 5 added
- `packages/python-sdk/tests/async/sandbox_async/test_create.py` — 5
added
These need no credentials. Re-running them with the source change
stashed fails exactly the three omission cases per SDK (3 in JS, 6
across sync and async Python) while the explicit `true`/`false` cases
pass both before and after, which is the evidence that existing behavior
is preserved.
Also run, all green:
- `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` from the repo
root
- `packages/python-sdk`: `tests/shared/sandbox`,
`tests/{sync,async}/sandbox_*/test_create.py`,
`tests/{sync,async}/sandbox_*/test_connect.py` — 77 passed. An
`E2B_API_KEY` was available in this environment, so the live lifecycle
tests (auto-pause requiring `connect`, auto-resume waking on HTTP,
filesystem-only snapshot rebooting) really did create sandboxes and
pass.
- `packages/js-sdk`: `tests/sandbox/lifecyclePayload.test.ts` (live, 5
passed) plus the `iam` and `networkTransform` msw suites
- `packages/cli`: full suite, 109 passed — the CLI builds its own
`lifecycle` object, so its tests are relevant here
No integration test pins the API's current default for an unset
`autoResume`: letting the service own that default is the point of the
change, so the new tests assert only that the SDKs omit the field.
## Notes
- A changeset is included (`patch` for `e2b` and `@e2b/python-sdk`) with
both usage examples.
- The `TASTE.md` referenced in the task prompt
(`raw.cursorusercontent.com/e2b/sdk-harness/main/TASTE.md`) returns 404,
and `e2b/sdk-harness` is not reachable via `gh` either, so this follows
the conventions already established in the repo (nested option bags,
normalizing wire `null` to absent, no new client-side validation,
request-level regression tests next to the existing ones).
- No Linear MCP is available in this environment, so no Linear issue is
linked; the GitHub issue is referenced above instead.
<div><a
href="https://cursor.com/agents/bc-32acab7b-7ff0-4e36-8019-ca9901ac3ec0?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a
href="https://cursor.com/automations/8e94ee92-9b0d-11f1-ba66-0e7d0216e441"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/view-automation-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/view-automation-light.png"><img
alt="View Automation" width="141" height="28"
src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a> </div>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
|
||
|
|
15bd48b73d |
fix(sdk): omit autoPause when no timeout lifecycle is configured (#1693)
Closes #1669. ## Problem Both SDKs serialized `autoPause: false` in `POST /sandboxes` whenever the caller left `lifecycle.onTimeout` / `lifecycle["on_timeout"]` unset, because the local default (`kill`) was folded into the payload before the request was built. That collapsed two distinct states at the API boundary — "no preference expressed" and "explicitly chose `kill`" — so the service could not own or evolve its own default without SDKs silently overriding it. ## Change `autoPause` is now sent only when a timeout action was actually chosen: | `lifecycle` | wire | | --- | --- | | not configured | `autoPause` omitted | | `onTimeout: 'kill'` | `autoPause: false` | | `onTimeout: 'pause'` | `autoPause: true` | An omitted `onTimeout` still resolves to `kill` locally for the existing `keepMemory` / `autoResume` validation, so no error paths change. A `null` `onTimeout` from an untyped caller counts as "not configured", matching how the SDKs already treat nullish option values. On the Python side the lifecycle normalization was duplicated verbatim between `sandbox_sync` and `sandbox_async`. It is now a single `build_lifecycle_config` in `e2b/sandbox/sandbox_api.py`, alongside the existing `build_iam_config` / `build_network_config` builders, so the two create paths cannot drift. ## Usage Nothing changes for callers that configure a lifecycle; the difference is only visible to callers that do not. ```ts import { Sandbox } from 'e2b' // No timeout lifecycle: autoPause is omitted and the API applies its default. await Sandbox.create() // Explicit action: autoPause: false / autoPause: true, as before. await Sandbox.create({ lifecycle: { onTimeout: 'kill' } }) await Sandbox.create({ lifecycle: { onTimeout: 'pause' } }) ``` ```python from e2b import Sandbox # No timeout lifecycle: auto_pause is omitted and the API applies its default. Sandbox.create() # Explicit action: autoPause: false / autoPause: true, as before. Sandbox.create(lifecycle={"on_timeout": "kill"}) Sandbox.create(lifecycle={"on_timeout": "pause"}) ``` The async Python SDK behaves identically via `AsyncSandbox.create`. ## Tests Request-level regression coverage for all three cases, plus the two "lifecycle present but no action" shapes an untyped caller can produce: - `packages/js-sdk/tests/sandbox/lifecycleRequest.test.ts` — msw captures the create body; needs no credentials. - `packages/python-sdk/tests/shared/sandbox/test_lifecycle_request.py` — parametrized over the sync and async create paths, asserting the serialized `NewSandbox` payload. Both also assert that `autoPauseMemory` still accompanies an explicit pause, since it is built from the same normalized action. Run locally: `pnpm run format`, `pnpm run lint` and `pnpm run typecheck` are clean. `packages/js-sdk` `tests/sandbox` is 250/251 (the one failure, `network.test.ts > injected header is reflected by the httpbin sidecar`, fails on `404: template 'httpbin' not found` — the sidecar template is unavailable in this environment and is unrelated to this change). `packages/python-sdk` `tests/shared/sandbox`, `tests/sync/sandbox_sync/test_create.py` and `tests/async/sandbox_async/test_create.py` pass, including the suites that create real sandboxes. Against the live API, creating a sandbox with no lifecycle, with `on_timeout: "kill"` and with `on_timeout: "pause"` reports `on_timeout` of `kill`, `kill` and `pause` respectively — so the API's current default matches the previous client-side default and there is no observable behavior change today, while the default now lives on the server. ## Notes for review - `autoResume` is still always sent (`{ enabled: false }` when unset), which is the same class of question for that field. I left it alone to keep this change to what the issue describes — happy to follow up if the API should own that default too. - No Linear MCP was available in this environment, so no Linear issue is linked; the GitHub issue above is the tracking item. <div><a href="https://cursor.com/agents/bc-4a366453-8e1e-4a02-97f4-f38f2a302c7b?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/automations/8e94ee92-9b0d-11f1-ba66-0e7d0216e441"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/view-automation-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/view-automation-light.png"><img alt="View Automation" width="141" height="28" src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com> |
||
|
|
39fc1d71af | [skip ci] Release new versions | ||
|
|
6824cdf313 |
feat(sdk): route sandbox egress through your own SOCKS5 proxy (BYOP) (#1688)
Drafts the SDK surface for [bring your own proxy](https://e2b-docs-byop-egress-proxy.mintlify.site/network/byop): `network.egressProxy` / `network["egress_proxy"]` on sandbox create, on `updateNetwork` / `update_network`, and in what `getInfo` / `get_info` reports back. Tunneling happens on the host after the allow and deny lists are evaluated, so nothing runs inside the sandbox and code running there can neither see the proxy nor route around it. ## The spec pin comes first The pinned infra spec marked `egressProxy` `x-not-implemented: true`, which Redocly's `filter-out` decorator drops from both generated clients — so the field did not exist in `schema.gen.ts` or in the Python client models, and no handwritten surface could reach it. [infra@0716edb9e8](https://github.com/e2b-dev/infra/commit/0716edb9e840f110c5f87c186876c01e61553098) removes the flag, so the first commit bumps `spec/infra-ref` and re-runs codegen rather than hand-writing the wire types. The pin picks up three other spec changes, and all of them are invisible to the SDKs: `AdminTeamRunningSandboxCounts`, the dead `NodeDetail.cachedBuilds` field, and `/admin/sandboxes/running-counts` are admin-tagged, and the envd spec is byte-identical between the two commits (verified by comparing the `packages/envd/spec` trees at both refs). `make codegen` could not run here because the VM has no Docker, so the spec was replaced with the byte-identical upstream file at the new pin and the two REST generators were run natively with the pinned `@redocly/cli` and `e2b-openapi-python-client`. ## Usage Create a sandbox that tunnels its egress: ```ts import { Sandbox } from 'e2b' const sandbox = await Sandbox.create({ network: { egressProxy: { address: 'proxy.example.com:1080', username: 'proxy-user', password: 'proxy-password', }, }, }) ``` ```python from e2b import Sandbox sandbox = Sandbox.create( network={ "egress_proxy": { "address": "proxy.example.com:1080", "username": "proxy-user", "password": "proxy-password", }, }, ) ``` It composes with the rest of the network configuration — here everything except `api.example.com` is denied, and what is allowed goes through your proxy: ```ts await Sandbox.create({ network: { allowOut: ['api.example.com'], denyOut: ({ allTraffic }) => [allTraffic], egressProxy: { address: 'proxy.example.com:1080' }, }, }) ``` ```python Sandbox.create( network={ "allow_out": ["api.example.com"], "deny_out": lambda ctx: [ctx.all_traffic], "egress_proxy": {"address": "proxy.example.com:1080"}, }, ) ``` Set or replace it on a sandbox that is already running, with no restart. The update replaces the whole configuration instead of merging into it, so an update that leaves the proxy out stops tunneling: ```ts await sandbox.updateNetwork({ allowOut: ['api.example.com'], denyOut: ({ allTraffic }) => [allTraffic], egressProxy: { address: 'proxy.example.com:1080' }, }) // Stop tunneling: an update without egressProxy clears it await sandbox.updateNetwork({}) ``` ```python sandbox.update_network({ "allow_out": ["api.example.com"], "deny_out": lambda ctx: [ctx.all_traffic], "egress_proxy": {"address": "proxy.example.com:1080"}, }) # Stop tunneling: an update without egress_proxy clears it sandbox.update_network({}) ``` Read the active proxy back: ```ts const info = await sandbox.getInfo() console.log(info.network?.egressProxy) // { address: 'proxy.example.com:1080', username: 'proxy-user' } ``` ```python info = sandbox.get_info() print(info.network["egress_proxy"]) # {'address': 'proxy.example.com:1080', 'username': 'proxy-user'} ``` ## Design notes - **`SandboxEgressProxyOpts` in, `SandboxEgressProxyInfo` out.** The API never returns the password, so the result type does not have the field — the same split as `SandboxNetworkRule` / `SandboxNetworkRuleInfo`. `fromApiEgressProxy` / `_from_client_egress_proxy` map the generated type at the boundary and drop a password even if a future API version starts echoing one back, so the type cannot quietly become a lie. - **The body is rebuilt from known fields**, as `buildIamBody` already does, so stray keys on the caller's object never reach the wire and a later mutation of it cannot alter an in-flight request. - **No client-side validation.** Address form, port range, hostname resolution, the internal-range rejection and the password-without-username rule are all the server's — it is the only side that can check them, and each already comes back as a readable API error. - **`null` never reaches a consumer.** The wire field is nullable; both SDKs normalize it (absent key in Python, `undefined` in JS), and an explicit `null` / `None` from an untyped caller is treated as "no proxy" on the way in. - Both types are exported from the flat entry points (`index.ts`, `__all__`). ## Testing Unit-level in both SDKs — msw in JS (12 tests), the shared builders in Python (11 tests, covering sync and async since they share the builders). Integration coverage is not included on purpose: tunneling needs a SOCKS5 proxy reachable from E2B's infrastructure, which CI has no way to stand up, and the feature is gated behind a private-beta team flag. `pnpm run format`, `pnpm run lint` and `pnpm run typecheck` are clean repo-wide. The remaining test failures in this environment are all `AuthenticationException` / missing `E2B_API_KEY` in pre-existing integration suites; no credentials were available on the VM. ## Notes - BYOP is available on E2B Cloud and in BYOC. A sandbox that names a proxy on a deployment built from open source `e2b-dev/infra` is rejected as unsupported by the orchestrator, which is why the field carried `x-not-implemented` upstream for a while. - No Linear MCP was available in this run, so no issue is linked. <div><a href="https://cursor.com/agents/bc-653eef78-87bb-5c9c-92d8-e573cd7ba5be?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/automations/8e94ee92-9b0d-11f1-ba66-0e7d0216e441"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/view-automation-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/view-automation-light.png"><img alt="View Automation" width="141" height="28" src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a> </div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com> |
||
|
|
e09b318f8c |
test: write test fixtures to temp dirs instead of the repo tree (#1689)
Clone of #1665, opened in response to `/sdk claim` on that PR. The original #1665 (branch `bangui`, by @mishushakov) can be closed in favour of this one — the tree here is byte-for-byte identical to its head, and the original commit is carried over unmodified so authorship and the `Co-Authored-By` trailer are preserved. ## What changed Eight test files created their fixtures outside a temporary destination, so running the suite left directories behind in the working tree. The five CLI template tests called `fs.mkdtemp` with a bare relative prefix. `mkdtemp` does not imply `os.tmpdir()` — a relative prefix resolves against `process.cwd()`, so each run created `packages/cli/e2b-<name>-testXXXXXX/`. They now join the prefix onto `os.tmpdir()`. On the JS SDK side, `getAllFilesInPath` and `spoolTarArchive` wrote into `__dirname` and now `mkdtemp` under `os.tmpdir()`. `build.test.ts` built its file context at `tests/template/folder`, relying on the implicit caller-directory context; it now creates the context under `os.tmpdir()` and passes it explicitly via `Template({ fileContextPath })`, matching what the Python mirror in `test_build.py` already does with `tempfile.mkdtemp` and `file_context_path`. The Python suite needed no changes: every host-side write already goes through `tmp_path`, `tempfile.mkdtemp`, or `TemporaryDirectory`. No usage examples apply — this is a test-only change with no user-facing surface. ## Verification Re-ran everything on this branch rather than relying on the original PR's numbers: - `packages/cli`: full suite green, 17 files / 109 tests passed. - `packages/js-sdk`: the two util test files, 24 tests passed. - `git status` is clean after both runs, with no leftover fixture directories anywhere in the tree — which is the behaviour this change is about. - `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` all clean across the JS and Python packages; `format` produced no diff. No changeset: test-only changes do not ship in either published package. Fixes SDK-334 <div><a href="https://cursor.com/agents/bc-7faf51ae-b169-4787-9d84-a50ec291b656?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/automations/3b1a5376-9bd3-11f1-ba66-0e7d0216e441"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/view-automation-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/view-automation-light.png"><img alt="View Automation" width="141" height="28" src="https://cursor.com/assets/images/view-automation-dark.png"></picture></a> </div> --------- Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com> |
||
|
|
5951f14e81 | [skip ci] Release new versions | ||
|
|
6248b12a5e |
feat(sdk): remove the deprecated accessToken option (#1680)
Removes the deprecated `accessToken` / `access_token` option from both
SDKs, along with its `E2B_ACCESS_TOKEN` environment fallback and the
`Authorization: Bearer` header it produced. The option was already
deprecated in both SDKs — `connectionConfig.ts` and
`connection_config.py` both pointed at `apiHeaders` / `api_headers` as
the replacement — and E2B access tokens are no longer accepted for API
authentication, so resolving one and putting it on the wire was dead
weight. Requests now authenticate with the API key alone.
Callers who need a bearer token for a custom deployment pass it
explicitly, which is what the deprecation notice already told them to
do:
```ts
// Before
const sandbox = await Sandbox.create({ accessToken: token })
// After
const sandbox = await Sandbox.create({
apiHeaders: { Authorization: `Bearer ${token}` },
})
```
```python
# Before
config = ConnectionConfig(access_token=token)
# After
config = ConnectionConfig(api_headers={"Authorization": f"Bearer {token}"})
```
`Sandbox.envd_access_token` / `traffic_access_token` are unrelated
per-sandbox tokens and are unaffected, as is the volume client's `token`
(which never read the env var — there's a test asserting exactly that).
Part of
[SDK-6](https://linear.app/e2b/issue/SDK-6/mark-e2b-access-token-as-deprecated-inside-all-code-references).
The CLI half is stacked on top in #1679.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ce634ab5f2 | [skip ci] Release new versions | ||
|
|
07eb9be196 |
feat(sdk): resolve iam token placeholders in network transform callbacks (#1616)
Stacked on #1606 (`iam-sdk-feature`) — merge that one first. This is the second half of SDK-245: it makes the workload tokens registered by `Sandbox.create`'s `iam` option usable, by letting a network rule's `transform` be a **callback** that receives placeholder strings the egress proxy resolves per request. `iam.tokens.aws` is the literal string `${e2b.identity.tokens.aws}` (the frozen backend spelling — a placeholder can only select a persisted named token, never an inline audience or claim). The SDK never resolves it: the wire payload carries the placeholder and the proxy substitutes a freshly minted JWT-SVID when it forwards the request, so the token value never reaches SDK-side code or the sandbox. Referencing a name that isn't registered in `iam.tokens` fails with `InvalidArgumentError` / `InvalidArgumentException` listing the names that are — the proxy never turns an unregistered name into a token, so a typo would otherwise surface as a confusing auth failure at the destination. `updateNetwork` / `update_network` accepts the same callbacks, but its payload carries no `iam` config, so token names can't be validated client-side there and any name resolves to its placeholder. Static `transform: { headers }` objects keep working unchanged (including hand-written `${e2b.identity.tokens.<name>}` strings, which stay the escape hatch for tokens the SDK doesn't know about). Only `{ iam }` is exposed on the context for now — `${e2b.sandboxId}` / `${e2b.teamId}` / `${e2b.executionId}` from the older prototype are not part of the current backend design, so `sandbox` can be added later when there is something to resolve. ## Usage ```ts import { Sandbox, Secret } from 'e2b' const sandbox = await Sandbox.create({ iam: { tokens: { aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }), }, }, network: { // Only allow egress to hosts that have rules registered. allowOut: ({ rules }) => [...rules.keys()], rules: { 'api.internal.example.com': [ { transform: ({ iam }) => ({ headers: { Authorization: `Bearer ${iam.tokens.aws}` }, }), }, ], }, }, }) ``` ```python from e2b import Sandbox, Secret sandbox = Sandbox.create( iam={ "tokens": { "aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID"), }, }, network={ "allow_out": lambda ctx: list(ctx.rules.keys()), "rules": { "api.internal.example.com": [ { "transform": lambda ctx: { "headers": {"Authorization": f"Bearer {ctx.iam.tokens['aws']}"}, }, }, ], }, }, ) ``` Both send: ```json { "iam": { "tokens": { "aws": { "audience": "sts.amazonaws.com", "tokenType": "JWT-SVID" } } }, "network": { "allowOut": ["api.internal.example.com"], "rules": { "api.internal.example.com": [ { "transform": { "headers": { "Authorization": "Bearer ${e2b.identity.tokens.aws}" } } } ] } } } ``` ## Notes - `allowOut` / `deny_out` selectors run **before** transforms are resolved, so `ctx.rules` still hands back the rules you passed — a rule's `transform` there is the union (object or callback), not the materialized object. The `getInfo` view keeps its own narrowed `SandboxNetworkRuleInfo` type. - Token names are validated where they are registered and again before interpolation: a name cannot be empty or contain `{`, `}` or control characters. The proxy reads a placeholder up to its first `}`, so `a}b` would mint the unrelated token `a` and leave `b}` as literal text, and a `{` in a name can open a second placeholder. The interpolation check is what covers `updateNetwork`, where any name the callback looks up becomes a placeholder without passing through the `iam` config. - Every lookup form on `iam.tokens` is guarded, not just `[name]`: Python's map is a `Mapping` whose `__getitem__` owns resolution (so `.get('typo')` raises instead of returning `None`), and membership (`'aws' in ctx.iam.tokens` / `'aws' in iam.tokens`) answers "is it registered?" without raising so a callback can branch on it. Lookup checks own keys only, so an unregistered name colliding with an object member (`constructor`, `__proto__`) reports as unregistered instead of resolving to a built-in; the four properties the runtime itself reads (`toJSON`, `then`, `toString`, `valueOf`) still resolve normally, so serializing, awaiting or coercing the map does not trip the guard. - A callback must be synchronous and return a plain transform object; a promise (from an `async` callback), an array, a `Map`/`Date`/class instance, or a missing return value is rejected with an actionable error rather than silently creating a rule with no headers. The awaitable is closed/caught so you don't also get an unawaited-coroutine warning or an unhandled rejection. ## Tests New payload-level tests: JS `tests/sandbox/networkTransform.test.ts` (msw), Python `tests/shared/sandbox/test_network_transform.py` — shared rather than mirrored into the sync and async suites, since they only exercise the shared builders. They cover placeholder resolution, enumerating and membership-testing registered tokens, `JSON.stringify` of the context not tripping the guard, static transforms staying byte-identical, `transform: null`, the unregistered-name rejection through both `[name]` and `.get()`, the no-`iam` rejection, non-transform and `async` return values, unusable token names (both braces, a smuggled placeholder, a newline, empty) at registration and on the update path, and the permissive `updateNetwork` path. Verified against production on all three surfaces (JS, sync Python, async Python), where: 1. a static transform carrying `Bearer ${e2b.identity.tokens.aws}` is accepted by `validateNetworkRules` and round-trips through `getInfo` / `get_info` unchanged; 2. the callback-resolved payload reaches the API and is answered with the expected team-gating error (`400: Sandbox IAM workload tokens are not available for your team.`), since `iam` is still feature-flagged; 3. a misspelled or unusable token name is rejected client-side before any request is made. Proxy-side substitution of the placeholder ships separately in belt (EN-1864); until then the header value is forwarded verbatim, which is why there is no end-to-end injection test here. Part of SDK-245. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
64b25bb37b |
feat(sdk): add iam workload identity option and Secret.iamToken helper (#1606)
Implements the sandbox workload identity (IAM) feature from the [infra spec](https://github.com/e2b-dev/belt/blob/main/spec/openapi-infra.yml) (`SandboxIam` / `SandboxIamTokens` / `SandboxIamToken`, already present in the pinned spec and generated clients) across the JS SDK and the sync and async Python SDKs. `Sandbox.create` gains an `iam` option whose non-empty `tokens` map enables workload identity, and a new `Secret` class (exported from both main packages) provides `iamToken` / `iam_token` to define the token values, per the SDK design. The design doc's `filePath` field is deliberately omitted until it lands in the OpenAPI spec, and plain `{ audience, tokenType }` objects are accepted alongside `Secret.iamToken` results. The SDK builds the request body from only the known token fields (stray properties never reach the wire, undefined-valued map entries count as empty) and rejects tokens missing `audience`/`tokenType` (`token_type` in Python) with `InvalidArgumentError` / `InvalidArgumentException`. Covered by request-body tests (msw in JS, `NewSandbox` payload tests in Python) since the backend feature is team-gated; all three surfaces were also smoke-tested end-to-end against production, where the payload is parsed and answered with the expected team-gating error. Fixes SDK-245. ## Usage ```ts import { Sandbox, Secret } from 'e2b' const sandbox = await Sandbox.create({ iam: { tokens: { aws: Secret.iamToken({ audience: 'sts.amazonaws.com', tokenType: 'JWT-SVID' }), }, }, }) ``` ```python from e2b import Sandbox, Secret sandbox = Sandbox.create( iam={ "tokens": { "aws": Secret.iam_token(audience="sts.amazonaws.com", token_type="JWT-SVID"), }, }, ) ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6acbeb39ee | [skip ci] Release new versions | ||
|
|
cab27aa6fa |
fix(sdk): clean up sandbox when MCP gateway startup fails (#1548)
## Problem Fixes #1498. `Sandbox.create` allocates a remote sandbox before starting `mcp-gateway`. If gateway startup fails, creation throws before the sandbox object is returned. As a result, the caller has no sandbox ID to clean up, and the orphaned sandbox continues consuming resources until it times out. This state transition exists in synchronous Python, asynchronous Python, and JavaScript/TypeScript. ## Changes - Add a rollback boundary around MCP gateway startup in all three SDK implementations: on failure, best-effort kill the newly allocated sandbox, then re-raise. - Surface gateway startup failure as `SandboxError` (JS) / `SandboxException` (Python) with a `Failed to start MCP gateway: <stderr>` message. Previously the intended message was unreachable dead code — foreground `commands.run` already throws on non-zero exit — so callers got a bare `CommandExitError`/`CommandExitException`. - In async Python, re-raise `asyncio.CancelledError` from the best-effort `kill()` so caller cancellation (e.g. `asyncio.timeout`) is honored; only ordinary cleanup failures are suppressed and never mask the original error. - Add integration coverage for synchronous Python, asynchronous Python, and TypeScript. The tests pin the sandbox to the base template (which has no `mcp-gateway` binary) so gateway startup genuinely fails after allocation. - Add a patch changeset for `e2b` and `@e2b/python-sdk`. ## Usage Behavior No API changes. A failed creation no longer leaves a sandbox behind, and the error is now descriptive: ```ts try { const sandbox = await Sandbox.create({ mcp: { ... } }) } catch (err) { // err is SandboxError: "Failed to start MCP gateway: <stderr>" // the allocated sandbox has already been killed — no orphan is left running } ``` ## Validation All three integration tests verified against real infra: creation rejects with the documented error and no sandbox remains. ## Notes Supersedes #1547 by @hxaxd (squash-merged into this branch to preserve attribution). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: 苏紫辰 <155808914+hxaxd@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e6111419b5 | [skip ci] Release new versions | ||
|
|
d5a382ed67 |
chore(js-sdk): bump undici to ^7.29.0 and optional undici8 to 8.10.0 (#1645)
Bumps both undici dependencies in the js-sdk past the 2026-07-24 security advisories: the required `undici` from `^7.28.0` to `^7.29.0`, and the optional `undici8` (`npm:undici@…`) from 8.8.0 to 8.10.0. Both releases patch one High ([GHSA-4cwx-7wf7-3272](https://github.com/nodejs/undici/security/advisories/GHSA-4cwx-7wf7-3272), cache-control parsing / cross-user disclosure) and four Medium advisories, clearing the open Dependabot alerts for undici; 8.10.0 additionally fixes HTTP/2 request settling, refused-stream retries and GOAWAY handling, which we exercise because every dispatcher the SDK builds sets `allowH2: true`. A root `pnpm.overrides` entry (`undici@>=7.0.0 <7.29.0`) is included because miniflare pins undici at exactly 7.28.0, which would otherwise keep a vulnerable copy in the lockfile; with it, the lockfile carries only 7.29.0 and 8.10.0. No code change was needed and there is no user-facing API change — 7.29.0 still requires Node `>=20.18.1` and 8.10.0 still requires `>=22.19.0`, matching the `UNDICI_8_MIN_NODE` gate in `packages/js-sdk/src/undici.ts`, so `getUndiciPackageCandidates()` picks the same package on the same Node versions. `format`, `lint` and `typecheck` pass, `tests/undici.test.ts` is 9/9, and `test:cf` was run to confirm miniflare still boots on the overridden undici. The remaining vitest projects need `E2B_API_KEY`, which isn't available locally, so they're left to CI. A patch changeset for `e2b` is included. Linear: [SDK-317](https://linear.app/e2b/issue/SDK-317/js-sdk-bump-optional-undici8-dependency-to-8100) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2d2823c94a | [skip ci] Release new versions | ||
|
|
88f41f3927 |
fix(python-sdk): port current JS stripAnsi regex to strip_ansi_escape_codes (#1545)
## Summary The Python SDK's `strip_ansi_escape_codes` (used to clean template build log messages) still used the old ansi-regex pattern, while the JS SDK's `stripAnsi` was rewritten in #895. This ports the current JS regex to Python so both SDKs clean logs identically: OSC sequences (hyperlinks, window titles) are matched non-greedily up to the first string terminator — including content spanning newlines — and CSI sequences are stripped without requiring a terminator. Following review feedback, both implementations now also strip the remaining ECMA-48 string controls — DCS (Sixel, tmux passthrough), SOS, PM, and APC — through their string terminator, so control payloads don't leak into cleaned logs. This goes beyond upstream `chalk/ansi-regex`, click, and Rich, none of which fully strip DCS payloads, and restores what the old Python pattern handled. Also mirrors the Python test suite into the JS SDK (which previously had no `stripAnsi` tests) — 20 identical cases per side — and verified byte-for-byte identical output between the two implementations on all of them. Includes a patch changeset for `e2b` and `@e2b/python-sdk`. ## Example Log messages that previously leaked OSC or DCS sequences into template build output are now cleaned: ```python from e2b.template.utils import strip_ansi_escape_codes strip_ansi_escape_codes("\x1b]8;;https://e2b.dev\x07E2B\x1b]8;;\x07") # "E2B" strip_ansi_escape_codes("\x1b]0;title\nstill title\x07done") # "done" strip_ansi_escape_codes("\x1b[38:2::255:0:0mRED\x1b[0m") # "RED" strip_ansi_escape_codes("\x1bPq#0;2;0;0;0~~@@\x1b\\image") # "image" (Sixel DCS) ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
86f7b8e2f8 |
fix(js-sdk): export the Git argument and status types (#1642)
Carries the change from #1635 (by @karpovantonme) into `main` as a single squash commit — #1635 was retargeted at `fix/export-git-argument-types`, merged there, and this PR promotes that branch. `Git.reset()`, `Git.restore()` and `Git.status()` are public, but the types naming their arguments and results were not reachable from the package entry point. `src/index.ts` re-exported fifteen `Git*` types and omitted `GitResetMode`, `GitResetOpts` and `GitRestoreOpts`. `GitStatusLabel` was worse off — `src/sandbox/git/index.ts` re-exported `GitBranches`, `GitConfigScope`, `GitFileStatus` and `GitStatus` from `./utils` but not `GitStatusLabel`, so it was unreachable from anywhere in the package, even though it is the type of `GitFileStatus.status`. The practical effect: you could call the methods, but you could not name what you pass them, so you could not write a typed wrapper. ```ts // before — all four fail import type { GitResetMode, GitResetOpts, GitRestoreOpts, GitStatusLabel, } from 'e2b' // the workaround people end up with type ResetMode = Parameters<Git['reset']>[0] extends { mode?: infer M } ? M : never ``` ```ts // after import { Sandbox } from 'e2b' import type { GitResetMode, GitResetOpts, GitStatusLabel } from 'e2b' async function hardResetTo(sbx: Sandbox, repo: string, target: string) { const mode: GitResetMode = 'hard' const opts: GitResetOpts = { mode, target, cwd: repo } return sbx.git.reset(opts) } function isBlocking(status: GitStatusLabel) { return status === 'conflict' || status === 'deleted' } ``` ## On SDK parity Python already exports `GitResetMode` (`e2b.GitResetMode`), so this brings JS up to it. The other three have no Python counterpart by design: the sync and async implementations take keyword arguments rather than option objects, so there is nothing shaped like `GitResetOpts`/`GitRestoreOpts`, and `GitFileStatus.status` is typed as a plain `str` there, so there is no `GitStatusLabel` either. Nothing to mirror on the Python side. ## Notes - Type-only re-exports, no runtime change. Changeset included (`e2b`: patch). - No test: a missing re-export is invisible to `tsc --noEmit` because `packages/js-sdk/tsconfig.json` includes only `src`, so an `import type … from '../src'` test passes either way. A declaration-reading test was dropped from #1635 during review. ## Not touched The same gap exists for a few non-Git types — `FilesystemListOpts`, `WatchOpts`, `PtyCreateOpts` and `PtyConnectOpts` are exported from their own modules but not from `src/index.ts`. Scope kept to the Git surface, as in #1635. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Anton Karpov <30812217+karpovantonme@users.noreply.github.com> Co-authored-by: Anton Karpov <karpovantonme@gmail.com> |
||
|
|
7a1fe4528c | [skip ci] Release new versions | ||
|
|
2821fb0b69 |
feat(sdk): route volume content to BYOC cluster domain (#1634)
When a team is connected to a custom (BYOC) cluster, the volume API now returns that cluster's domain in the create and get responses. The JS and Python (sync + async) SDKs use this domain as the destination for volume content requests instead of the default api.<E2B_DOMAIN> host, falling back to the configured domain when none is returned. The domain field is read defensively from the response until spec/infra-ref is bumped to the infra commit that adds it and `make codegen` regenerates the typed schema. Claude-Session: https://claude.ai/code/session_01212WCmNz1prPKrjhTv2PDj --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Matt Brockman <matt.brockman@e2b.dev> |
||
|
|
9ef3f1dbbe | [skip ci] Release new versions | ||
|
|
1ebe925ee0 |
fix(js-sdk): detect web platform objects by shape, not by class (#1618)
SDK-299
## Symptom
Two shapes of failure, one cause.
Every control-plane call crashing in an app that embeds the SDK next to
a server shim:
```
TypeError: Failed to parse URL from [object Request]
at fetch (…/undici/index.js:157:10)
at wrapped (…/e2b/src/undici.ts:126)
```
…and, quietly, uploads that arrive at the sandbox containing the eight
bytes `[object Blob]` instead of the file.
## Root cause
`value instanceof Blob` does not answer *"is this a Blob"*, it answers
*"was this minted by the `Blob` class this module happens to see"*. In a
Node process those are different questions: libraries replace the web
globals exactly the way they replace `globalThis.fetch` —
`@hono/node-server` installs its own `Request`, remix's
`installGlobals()` swaps `Request`/`Blob`/`File`, `web-streams-polyfill`
swaps `ReadableStream`, jsdom-style test environments bring their own
copies of all of them — and values also cross realms (`node:vm`,
`worker_threads`). `src/undici.ts` already late-binds the global `fetch`
for this reason; the brand checks never got the same treatment.
It is reachable with a **single** shim install, no exotic dependency
duplication:
1. `openapi-fetch` captures `Request: CustomRequest =
globalThis.Request` when the client is created (`dist/index.mjs:11`) and
mints every request from it,
2. `EnvdApiClient` is built once in the `Sandbox` constructor and stored
(`src/sandbox/index.ts:201`), so it outlives anything that swaps the
global afterwards,
3. from then on the SDK checks each request against a class that did not
mint it. Which copy "wins" the global is import-order dependent, so the
crash appears and disappears with unrelated dependency changes.
Verified locally, frame for frame: real `undici`/`undici8` throw
`TypeError: Failed to parse URL from [object Request]` for **any**
`Request` they did not mint — including Node's native one — so the
destructure in `toUndiciRequestInput` is load-bearing and a missed brand
check is fatal rather than merely slower.
## The whole family
Every brand check on the data path had the same defect, and each one
failed differently:
| Site | Misfires on | User-visible effect |
| --- | --- | --- |
| `undici.ts` `toUndiciRequestInput` | foreign `Request` | **every API
call throws** `Failed to parse URL from [object Request]` |
| `api/inflight.ts` `limitConcurrency` | foreign `Request` | abort
signal ignored while the request waits for a slot |
| `utils.ts` `toBlob` | foreign `Blob` | upload body is the text
`"[object Blob]"` |
| `utils.ts` `toBlob` | foreign `ReadableStream` | upload body is the
text `"[object ReadableStream]"` |
| `utils.ts` `toUploadBody` (gzip) | foreign `ReadableStream` |
`pipeThrough(new CompressionStream())` never settles — the upload hangs
|
| `utils.ts` `toUploadBody` | foreign `ReadableStream` | file buffered
into memory instead of streamed (OOM on large files) |
| `filesystem/index.ts` `hasStreamableData` | foreign `ReadableStream` |
same, plus the multipart path is chosen for a stream |
| `volume/index.ts` `readFile` | foreign `Blob`/`ArrayBuffer` |
**returns an empty file** |
| `undici.ts` `toUndiciRequestInput` (body) | foreign `Request`'s stream
body | body sent as the text `"[object ReadableStream]"` |
The `Blob`/stream rows are silent data corruption, confirmed against
real undici:
```js
await new Response(foreignBlob).text() // → "[object Blob]"
await new Response(foreignStream).text() // → "[object ReadableStream]"
```
## Fix
New internal `src/is.ts` asks what a value *is*: `instanceof` stays the
fast path, then it falls back to the members and `Symbol.toStringTag`
the platform guarantees (`isRequestLike`, `isBlobLike`,
`isReadableStreamLike`, `isArrayBufferLike`). Nothing is added to the
public surface.
Detection alone is not enough where the SDK hands data back to the
platform — the platform brand-checks too, and a detected-but-not-adopted
foreign stream would be stringified instead of buffered, i.e. worse than
before. So the conversions adopt what they detect:
- `toBlob` copies a foreign `Blob`'s bytes (`new Blob([await
data.arrayBuffer()], { type: data.type })`) and pumps a foreign stream
through a native one via its reader;
- the adoption itself is not class-dependent, which took two rounds to
get right (see *Adoption* below);
- `toUploadBody` returns `{ body, streamed }` instead of leaving
`filesystem`/`volume` to re-derive "did it stream?" with another brand
check on the result — only that function knows the decision it made.
### What used to break
```ts
import { serve } from '@hono/node-server' // installs its own globalThis.Request
import { Sandbox } from 'e2b'
const sandbox = await Sandbox.create()
await sandbox.files.write('/tmp/a.txt', 'hi') // TypeError: Failed to parse URL from [object Request]
```
```ts
import { ReadableStream } from 'web-streams-polyfill' // not the native class
// Used to upload the literal text "[object ReadableStream]"; with gzip it hung.
await sandbox.files.write('/tmp/big.bin', bigPolyfillStream, { gzip: true })
```
### Adoption
The platform accepts exactly two kinds of stream body: **its own
class**, and **any async iterable** (verified against `undici@8`/Node —
everything else is stringified). Async iterability is the half that
survives a replaced global, since a native stream stays async-iterable
even when `globalThis.ReadableStream` is a polyfill. Hence two helpers,
each named for the contract it satisfies:
- `toDispatchableStream` — for request bodies: passes through the
platform's own class *or* an async iterable, adopts the rest. Adopting
on `!(stream instanceof ReadableStream)` alone would have been
class-dependent in the same way as the bug: with a polyfilled global, a
perfectly good native stream fails the check and gets re-wrapped into a
polyfill instance the platform likes *less*.
- `toNativeStream` — for `pipeThrough(new CompressionStream(…))`, the
stricter consumer: it insists on its own class, so async iterability is
not enough there and every foreign stream is adopted.
Everything that isn't already a stream reaches the gzip path through
`toBlob`, whose result is always a native `Blob`, so the gzip path needs
no blob branch of its own. That in turn means nothing ever calls
`stream()` on a `Blob` the SDK didn't make, so `isBlobLike` only
requires `arrayBuffer` beyond the tag — one less requirement is one less
implementation whose upload would silently be the text `"[object
Blob]"`.
The same reasoning applies one level down: a `Request` from another
fetch implementation exposes *that* implementation's stream as its
`body`, so accepting the Request without adopting its body would only
move the stringification, not remove it.
## Tests
`tests/foreignPlatformObjects.ts` provides the fixtures: `ForeignBlob`
and `foreignReadableStream` are separate implementations rather than
subclasses (a subclass still passes `instanceof`, so it would prove
nothing), and `foreignRequestClasses()` returns two sibling subclasses
of the native `Request` — instances of one are fully functional Requests
that the other disowns, which is what the shims actually produce.
- `tests/is.test.ts` — the four predicates, including the negatives that
keep them honest (a `URL`, a string, an `IncomingMessage`-shaped `{ url,
method, headers }`).
- `tests/utils.test.ts` — content round-trips for
`toBlob`/`toUploadBody` across native/foreign `Blob`s and streams, plus
a gzip round-trip through `DecompressionStream` for all four input
kinds.
- `tests/undici.test.ts` — a disowned `Request` reaches undici
destructured as `(url, init)`; and, on Node, the same request goes
through the **real** undici `fetch` (via `MockAgent`, so no network) and
is matched on method, path and headers.
- `tests/api/inflight.test.ts` — an already-aborted disowned `Request`
rejects with `AbortError` without consuming a slot.
Each adoption rule has a test that fails without it: dropping the
async-iterable clause fails the "does not re-wrap a native stream when
the global class was replaced" case, using `toDispatchableStream` in the
gzip path fails the async-iterable gzip case, and dropping the body
adoption fails with `expected '[object ReadableStream]' to be 'hello'`.
Red/green: with `src/` reverted, the three Request tests fail (the
undici one with the production error, `ERR_INVALID_URL { input: '[object
Request]' }`) and the data-path tests fail with `expected '[object
Blob]' to be 'hello'` / `expected '[object ReadableStream]' to be
'hello'`.
Verification run: unit + `connectionConfig` (129), `tests/sandbox/files`
+ `tests/volume` against prod (92 passed, real streamed/gzipped
uploads), all four changed files green on Node, Bun, Deno and workerd,
plus `tsc`, `lint`, `prettier` and `build`.
## Out of scope
`network.rules instanceof Map` (`sandboxApi.ts`) and the `instanceof
Error` checks are left alone: nothing replaces `globalThis.Map`, and the
errors are ours. Python needs no counterpart — its REST stack takes
bytes/iterables and has no equivalent brand checks.
Supersedes #1610 (@himself65), which fixed the `Request` half of this
and diagnosed the crash; the reproduction there is what led to auditing
the rest.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
2df7651ee6 |
test(sdk): run firewall transform tests against an httpbin sidecar sandbox (#1631)
Follow-up to #1632, which added the template this depends on. Now rebased onto `main`, so this is just the test change. ## Problem The firewall transform tests asserted header injection by curling `httpbin.e2b.team`, an externally hosted service the suite had to keep alive. ## Fix Starts a sidecar sandbox from the `httpbin` template instead: the rule is keyed on the sidecar's `getHost(8080)` and the assertion reads the injected header back from `/headers`, in the JS, sync Python, and async Python suites. The sidecar's ready command has already passed by the time `create` resolves, so the server is serving and no readiness polling is needed. The template name lives in one fixture per SDK — `httpbinTemplate` in `tests/template.ts` and the `httpbin_template` fixture in `conftest.py`. Also drops two comments merged in #1632 that claimed the tests spawn `e2b/httpbin`. The bare alias is what resolves, same as `base` — the team slug only appears in the display name. ⚠️ Do not merge before **Build and push prepared templates** has been dispatched with `template: httpbin` — the tests resolve the template by name and fail until it exists on the E2B team. Verified against production: all three tests pass with the injected header reflected by the sidecar, spawning the template by its bare alias with a key that owns it — the same situation as CI. SDK-304 Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
4a2571d321 |
test(js-sdk): wait for workers.dev propagation in the CF deploy suite (#1592)
## Problem The `cloudflare-deploy` CI job intermittently fails with `non-JSON response (404): <!DOCTYPE html>...` ([example run](https://github.com/e2b-dev/E2B/actions/runs/30009788154/job/89214641280)). The truncated HTML boilerplate is easy to mistake for a Cloudflare captcha/challenge page, but it's the standard Cloudflare **404** page: each `wrangler deploy --temporary` lands on a brand-new account subdomain (`e2b-js-sdk-smoke.<random>.workers.dev`), and Cloudflare serves "nothing is here yet" until the route propagates to the edge. The in-test retry window (initial attempt + 10 retries × 3s ≈ 35s) wasn't always enough. ## Fix - `setup.mts`: after the deploy, poll the worker URL until the worker itself answers (405 to GET — the worker is POST-only), with a 240s deadline. Tests only start once the route is live. Only the propagation 404 and thrown fetch errors (transient DNS/connect) keep the poll waiting — any other status (403 challenge, 500 from a broken worker, ...) is a real failure and fails the setup immediately, with the error page's `<title>` in the message. - `run.test.ts`: retry only on the propagation 404 / `fetch failed`, so other Cloudflare error pages propagate on the first attempt; include the page `<title>` in the `non-JSON response` error, since the truncated body is boilerplate shared by every Cloudflare error page. Test-only change, no changeset. ## Verification Ran `pnpm test:cf:deploy` against real Cloudflare (both revisions): ``` Deployed: https://e2b-js-sdk-smoke.quick-bike.workers.dev Worker route not live yet (404), waiting... Worker route not live yet (404), waiting... Worker is live. Test Files 1 passed (1) Tests 1 passed (1) ``` The fresh subdomain served 404 for ~6s post-deploy — exactly the failure mode from CI — then the suite passed on the first test attempt. `pnpm run format`, `lint`, and `typecheck` pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6733f36755 |
fix(sdk): align Python Fedora/Alpine image defaults with JS (#1625)
Python `from_fedora_image` defaulted to `fedora:42` (end-of-life) and `from_alpine_image` to `alpine:3.22`, while JS already pinned `fedora:44`/`alpine:3.24` — the same call produced a different base image per SDK. Aligns Python; also fixes the JS type docs, which still named the old defaults. ```python Template().from_fedora_image() # fedora:44 (was fedora:42) Template().from_alpine_image() # alpine:3.24 (was alpine:3.22) ``` Follow-up to #1612; both defaults are still unreleased. |
||
|
|
1504fbc843 |
SDK: fromFedoraImage/fromAlpineImage/fromArchImage helpers (#1612)
## What Adds the missing non-Debian base-image convenience helpers to **both SDKs**, mirroring the existing `fromUbuntuImage`/`fromDebianImage`/`fromPythonImage`/`fromNodeImage`/`fromBunImage`: - **JS/TS** (`packages/js-sdk`): `fromFedoraImage(variant?)`, `fromAlpineImage(variant?)`, `fromArchImage(variant?)` + unit tests - **Python** (`packages/python-sdk`): `from_fedora_image(variant)`, `from_alpine_image(variant)`, `from_arch_image(variant)` + sync/async unit tests ## Why This is the **customer-facing half** of infra **#3381** (distro-aware template provisioning). The engine now builds + boots Ubuntu/Debian/Fedora/RHEL-family/Arch/Alpine on real KVM; before this PR the SDK exposed distro helpers for the Debian family only, so Fedora/Alpine/Arch were reachable only via the generic `fromImage()`. These give them first-class parity. ## Verification (honest) - **New helper unit tests pass locally** — JS `fromDistroImages.test.ts` → 6/6 green (`vitest`, no auth). Python `test_from_distro_images.py` (sync + async) committed. - **Full integration suite**: requires E2B API keys — fails locally with `AuthenticationError` **identically on `main`** (215/187/29), i.e. **zero regression** from this change; CI runs it with secrets. - Lint scoped to the touched files. ## Not in this PR The public **docs** still state *"only Debian-based images … Alpine/RedHat not supported"* — but that text lives in **`e2b-dev/docs`**, not this monorepo, so it's a **separate docs PR** (being opened against `e2b-dev/docs`). Flagging so this + that land together. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
ee0ad25117 |
docs(sdk): rename team to project in snapshot docstrings (#1562)
Renames team → project terminology in the JS and Python SDK snapshot docstrings: the `list_snapshots`/snapshot list `name` filter example now reads `"my-project/my-snapshot"`, and `SnapshotInfo.names` is documented as "including project slug and tag (e.g. project-slug/my-snapshot:v2)". Documentation-only — no exported names, runtime behavior, or wire protocol change; generated API clients and `spec/openapi.yml` are intentionally untouched until the backend exposes project-named endpoints. Includes a patch changeset for `e2b` and `@e2b/python-sdk`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cf8296cf89 | [skip ci] Release new versions | ||
|
|
178e267ba2 |
fix(js-sdk): bump deprecated glob@^11 to ^13 (#1613)
Closes #1611. `e2b` declared `"glob": "^11.1.0"`, and glob 11 is deprecated on npm, so **every** `npm install` of any project that depends on `e2b` — directly or transitively — printed a deprecation warning. Downstream packages can't silence it themselves: npm `overrides` and `npm-shrinkwrap.json` only apply to the top-level project being installed, not to a transitive dependency's own range. It can only be fixed here. Thanks @clayboby for the report and the verification work. ## Before / after ```console $ npm install e2b@2.36.0 # before npm warn deprecated glob@11.1.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. added 37 packages in 1s $ npm install e2b # after (this branch, packed locally) added 26 packages in 1s ``` No API change — this is a dependency bump. The 37 → 26 package drop comes from glob 13 moving its CLI (and `jackspeak`/`@isaacs/cliui`/`string-width`/… ) out to a separate `glob-bin` package. ## Why ^13 is safe glob 12 and 13 only made **CLI-only** breaking changes, per [glob's changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md): - **v12** — "Remove the unsafe `--shell` option." - **v13** — "Move the CLI program out to a separate package, `glob-bin`." The SDK's only use of glob is `getAllFilesInPath` in the template build path (`src/template/utils.ts`, loaded via `dynamicImport('glob')`), which touches the named async export `glob(pattern, opts)`, the options `ignore` / `withFileTypes` / `dot` / `cwd`, and `Path#isDirectory()` / `#fullpath()` / `#relative()`. All unchanged in 13. glob 13.0.6's `engines` (`18 || 20 || >=22`) satisfy the SDK's (`>=20.18.1 <21 || >=22`), and it's still dual CJS/ESM, so both build outputs resolve it. ## Also in this PR: `"types": ["node"]` in the js-sdk tsconfig glob 13 pulls `minipass@^7.1.3`, which removed the `/// <reference types="node" />` that TypeScript 7's native `tsc` was (accidentally) relying on to see Node globals — it doesn't auto-include `node_modules/@types`. Without this, the bump fails `tsc --noEmit` with ~25 `TS2591 Cannot find name 'process'/'Buffer'` errors. Requesting `node` explicitly is the right fix and makes the typecheck independent of a transitive dependency's d.ts. ## Verification - `tsc --noEmit` clean for js-sdk and cli; `pnpm run lint` / `format` clean; `tsdown` build clean and `glob` still emitted as an external `dynamicImport("glob")`, not inlined. - `getAllFilesInPath` unit suite (17 tests: ignore patterns, dotfiles/dotdirs, recursive dirs, deterministic sort, `.` pattern) green against the real glob 13.0.6 on Node, **Bun 1.3.14, and Deno 2.8.1**. - Full `unit` + `connectionConfig` projects: 401 passed / 30 skipped against prod. - Full `template` project: 133 passed / 3 skipped, including real end-to-end template builds that exercise `COPY` (the glob path). - Packed the tarball and installed it into a scratch project to confirm the warning is actually gone (output above), plus CJS `require('e2b')` and ESM `import 'e2b'` both load. ## Not fixed here `@e2b/cli` installs still warn, via `@npmcli/package-json@5.2.1 → glob@10.5.0`. Clearing that needs `@npmcli/package-json@7`, whose `engines` (`^20.17.0 || >=22.9.0`) are narrower than the CLI's own (`>=20.18.1 <21 || >=22`, so Node 22.0–22.8 would drop out) — separate change, separate decision. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b5119539ca |
fix(js-sdk): share lazy fetcher loading and drop the new Function import trick (#1607)
Extracts the duplicated api/envd fetcher-loading logic into shared
`createRuntimeFetch` + `buildDispatchedFetch` helpers in `undici.ts`,
fixing two behaviors along the way: a failed fetcher build is no longer
cached forever (the next request retries, with a guarded
compare-and-clear so a stale awaiter can't clobber a newer in-flight
build — covered by a regression test reproducing the microtask
interleaving), and the no-undici fallback now late-binds
`globalThis.fetch` so fetch replacements installed after the first
request (msw, instrumentation) are picked up.
`loadUndici` now uses the shared `dynamicImport` helper, whose import is
kept opaque to downstream bundlers via `webpackIgnore`/`@vite-ignore`
annotations instead of the `new Function('return import(...)')` trick —
so environments that disallow code generation from strings (CSP,
`--disallow-code-generation-from-strings`) now load undici normally
instead of silently degrading to the global fetch. The now-internal
`toUndiciRequestInput`/`UndiciRequestInit` are no longer exported. No
user-facing API changes; verified with the unit suites (lint/typecheck
clean) plus real API integration tests through the new dispatcher path.
### Test-suite fallout from the import fix
Dropping the `new Function` trick exposed a hidden test dependency: that
trick throws under vitest's vm evaluation, so every vitest run had
silently fallen back to the msw-patchable global fetch. With module
loading un-broken, the Node test runs dispatched through real undici,
bypassing msw's `globalThis.fetch` patch — mocked requests escaped to
the real API (real 404s in the tags suite, 3-minute hangs in the
abortSignal suites waiting for msw's `request:start`, and 28 real
template builds per run from the stacktrace suite).
Fixed centrally: `tests/globalFetchFallback.setup.ts`, registered via
`setupFiles` for the unit and template projects, mocks
`buildDispatchedFetch` to run the SDK's real undici-unavailable fallback
(late-bound `globalThis.fetch`), so msw suites need no per-file mock and
future msw suites are covered automatically. Suites that inject their
own `loadUndici` (the api/envd transport tests) keep it, so the
dispatcher wiring itself stays covered. Verified under Node, Bun, and
Cloudflare workerd.
Closes SDK-290
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
59c6996a1e | [skip ci] Release new versions | ||
|
|
4fcf7cb150 |
feat: sync API specs from infra and belt with Copybara (#1564)
The specs in `spec/` were copied from their source repos by hand and had
drifted ~2,400 lines behind infra, so they are now imported with
Copybara (`copy.bara.sky`, run in a pinned Docker image by
`scripts/fetch-spec.sh`): `make codegen` re-fetches them at the commits
pinned in `spec/infra-ref` and `spec/belt-ref` before generating, and
the generated-files CI check fails if the tracked copies don't match the
pins. Regenerating from the current pins picks up the accumulated spec
changes in the generated JS/Python clients (renamed request schemas,
`SandboxNetworkConfig`, `SandboxIam` workload identity,
`FILE_TYPE_SYMLINK`, access-token auth deprecation, volume path-metadata
tweaks). The one handwritten SDK change follows from that: the public
`FileType` enums gain a `SYMLINK` member (JS and both Python surfaces)
so entries envd reports as symlinks show up in `files.list()` and
`getInfo()`/`get_info()` instead of being silently skipped as unknown
types. The custom `spec/remove_extra_tags.py` tag-filtering script is
replaced by Redocly CLI's `filter-in` decorator (`redocly.yaml`), which
produces identical generated JS output; a `filter-out` decorator
additionally drops any operation or component schema the upstream specs
mark `x-not-implemented: true` (currently the SOCKS5
`SandboxEgressProxyConfig`/`egressProxy` surface, which infra flagged as
spec-only); each SDK's bundle now goes to its own gitignored
`spec/openapi_generated.<api>.yml` instead of both pipelines overwriting
one shared file; Python client models now list fields in spec order
instead of alphabetical (mechanical reordering only — construct models
with keyword args). Spec fetches try whatever GitHub token is available
and fall back to the tracked copies with a warning (the public infra
specs also fetch anonymously); in CI a short-lived belt-scoped token is
minted from the org-wide Autofixer GitHub App (no new secrets), so fork
PRs simply fall back for the belt spec; the CI workflows also cache the
Copybara image alongside the codegen image, and the previously ignored
`CODEGEN_IMAGE` env is honored by the Makefile.
## Usage
```sh
# update the specs: bump a pin, then regenerate
echo <infra-commit-sha> > spec/infra-ref
make codegen
# fetch a single spec without regenerating
pnpm fetch:api-spec # spec/openapi.yml from infra
pnpm fetch:envd-spec # spec/envd/ from infra
pnpm fetch:volume-spec # spec/openapi-volumecontent.yml from belt
# try the latest spec without touching the pin
E2B_INFRA_REF=main pnpm fetch:api-spec
# change which endpoint tags an SDK exposes
$EDITOR redocly.yaml && make codegen
```
```ts
// symlinks are now visible in the filesystem API (JS; same shape in Python)
const entries = await sandbox.files.list('/home/user')
const link = entries.find((e) => e.type === FileType.SYMLINK)
console.log(link?.symlinkTarget)
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ada1744cf1 |
test(js-sdk): run the template test suite on Bun (#1600)
## Description Adds `--project template` to `test:bun` so the Bun CI leg runs the template suite, matching the Deno leg (#1595). No code changes are needed: the template suite previously failed under Bun because Bun's JavaScriptCore elides tail-call frames and the fixed-depth stack walk attributed build errors one frame past the user's call site (the workaround attempt in #1596 was closed in favor of #1599). With #1599's boundary-based frame selection (now merged), the suite passes under Bun as-is. The CI workflow already passes `E2B_API_KEY`/`E2B_DOMAIN` to the Bun leg, and the matrix comment (updated in #1595) already covers Bun re-running API-backed suites, so `package.json` is the only change. ## Testing Full `test:bun` (unit + connectionConfig + template) green locally on Bun 1.3.14 against the real API: 530 passed, 35 skipped, 0 failed — including all 34 stack-trace/caller-directory tests that pin exact user call-site line/columns, the frames Bun used to elide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |