main
1045 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b8029973aa |
fix(sdk): guard egressProxy shape in JS and drop null proxy credentials (#1757)
## Summary Follow-up to #1688. Three fixes on the `network.egressProxy` / `network["egress_proxy"]` paths a caller reaches by bypassing the types, plus the one changelog snippet that does not run. **The guards disagreed.** Python raised `InvalidArgumentException` for a proxy without a string `address`; JS had none, and since `buildEgressProxyBody` rebuilds the body from the known fields, an address that isn't there simply vanished and the caller got an API error about a `{}` they never wrote. Nothing leaked — `address` is `required` in the spec, so all of these already failed closed at the API — but the error named the wrong thing. ```ts function buildEgressProxyBody(egressProxy: SandboxEgressProxyOpts) { + if (!isPlainObject(egressProxy) || typeof egressProxy.address !== 'string') { + throw new InvalidArgumentError( + `network egressProxy must be an object with a string 'address' (e.g. 'proxy.example.com:1080').` + ) + } ``` | caller passes | JS before | JS now | Python | |---|---|---|---| | `{}` | `{"egressProxy":{}}` | `InvalidArgumentError` | `InvalidArgumentException` | | `{address: 1080}` | `{"egressProxy":{"address":1080}}` | `InvalidArgumentError` | `InvalidArgumentException` | | `'proxy.example.com:1080'` | `{"egressProxy":{}}` — address dropped | `InvalidArgumentError` | `InvalidArgumentException` | **`null` credentials reached the wire.** Both SDKs checked `!== undefined` / `in`, so a `null` / `None` username or password serialized as a JSON null the API rejects (`type: string`). Reading a credential out of an unset environment variable is the way to land there, and it is the one case where the caller means "this proxy takes no credentials" — both now skip a nullish credential (`!= null` in JS, `.get(...) is not None` in Python). ```ts await Sandbox.create({ network: { egressProxy: { address: 'proxy.example.com:1080', // Unset in the environment; the proxy takes no credentials. username: process.env.PROXY_USER, }, }, }) ``` ```python Sandbox.create( network={ "egress_proxy": { "address": "proxy.example.com:1080", "username": os.environ.get("PROXY_USER"), }, }, ) ``` **The published `get_info` snippet.** The 2.41.0 entry in both changelogs printed `info.network["egress_proxy"]`, but `SandboxInfo.network` is `Optional` and `egress_proxy` is `NotRequired` — pyright rejects it (`reportOptionalSubscript`, `reportTypedDictNotRequiredAccess`) and it raises at runtime for a sandbox without a proxy, unlike the TS line right above it (`info.network?.egressProxy`). It now reads `print((info.network or {}).get("egress_proxy"))`. Tests cover the three rejected JS shapes and the null credentials in both SDKs. Link to Devin session: https://app.devin.ai/sessions/216bb02ad7924e4ba5f2968aaadc7938 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> |
||
|
|
182b498f66 |
chore: point README docs links at docs.e2b.dev (#1754)
Follow-up to #1724. The docs site moved to its own subdomain, so the links that PR tagged point at the pre-migration domain. `e2b.dev/docs` returns a 308 to `docs.e2b.dev/`, and the subdomain has no `/docs` path prefix, so `e2b.dev/docs/code-interpreting` maps to `docs.e2b.dev/code-interpreting`. The UTM query string is preserved across the redirect, so attribution was not broken. This removes the redirect hop. Six links across the three packaged READMEs. Changeset covers the same three packages as #1724. This does not touch the root `README.md`, which has its own docs links on the old domain. That is queued separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e139fc38e0 | [skip ci] Release new versions | ||
|
|
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> |
||
|
|
b17b7262e4 |
fix(python-sdk): keep streamed request bodies unbuffered across retries (#1718)
## Summary pyqwest's retry middleware keeps a request replayable by mirroring a non-`bytes` body into memory as it is sent, so a streamed upload through the shared retrying transports (`files.write` of a file-like object, `volume.write_file`) reached the wire in chunks yet accumulated its whole body in RAM. curioswitch/pyqwest#219 (released in pyqwest 0.10.0) adds `RetryMode.UNBUFFERED`, which drops that copy: a streamed body is handed to the next attempt only while nothing has been read from it. That is exactly what the SDK's connect-only retry policy needs — pyqwest raises the builtin `ConnectionError` (the only thing `should_retry_response` retries) only before the request body was written, so the stream is still untouched on every failure we retry. `bytes` bodies (unary RPCs, in-memory writes) stay replayable in either mode. Both `ConnectionRetryTransport`s (sync + async) now declare it: ```python def should_retry_request(self, request: Request) -> RetryMode: return RetryMode.UNBUFFERED ``` The pyqwest pin moves to `>=0.10.0,<0.11`, the release shipping `RetryMode`. Tests (`tests/test_retry_stream_buffering.py`): - both transports override `should_retry_request` with `RetryMode.UNBUFFERED` (the inherited hook returns `True` — buffered) - untouched streams are retried after a simulated connect failure (sync + async) - peak allocation stays far below body size while streaming (tracemalloc, 16 MiB body), and a stream that failed after its first chunk is *not* replayed - the httpx→pyqwest adapters hand the body to the retry middleware as a stream, not flattened bytes Link to Devin session: https://app.devin.ai/sessions/895a6967064e425abfc84b8cfbc32910 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
d55ddb8b5c |
test(python-sdk): drop build API path encoding integration tests (#1713)
Supersedes #1709 — claimed via `/sdk claim` by @mishushakov. This is a clone of #1709: the original commit (`65c96289`) is applied unmodified, so the tree here is byte-identical to that PR's head and the original commit authorship and `Co-authored-by` trailer are preserved. The branch was already current with `main` (1 commit ahead, 0 behind), so no merge was needed. The contents were not reviewed or changed. Please close #1709 in favour of this PR. The original description follows verbatim. --- ## Summary Removes `tests/shared/template/test_build_api_path_encoding.py`. Path encoding is already covered by `tests/shared/api/test_encode_path_param.py`. ## Verification ```bash cd packages/python-sdk uv run pytest tests/shared/api/test_encode_path_param.py tests/shared -q # 175 passed, 1 skipped ``` [Slack Thread](https://e2b-team.slack.com/archives/D0962B9UKEE/p1786973222264879?thread_ts=1786973222.264879&cid=D0962B9UKEE) <div><a href="https://cursor.com/agents/bc-57d27899-5769-437a-a242-7956988cdb1f?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> |
||
|
|
666241d474 |
refactor(python-sdk): unify the pyqwest connection pools (#1692)
Claimed from #1659 on `/sdk claim` by the PR's own author (@mishushakov, org member). The original commit is carried over untouched, so authorship and the `Co-Authored-By` trailer are preserved — only PR ownership moves. **Please close #1659 in favour of this PR** (`Closes` does not auto-close pull requests, and this automation has no write access to do it). Closes [SDK-291](https://linear.app/e2b/issue/SDK-291/python-sdk-unify-pyqwest-connection-pools-once-all-http-traffic-is-off). ## What changes Every persistent HTTP stack in the Python SDK — control-plane REST, the envd HTTP API, the envd RPC clients, and the volume content API — now draws its connection pool from `e2b.api.client_sync`/`client_async` keyed on `(proxy, idle read bound, HTTP version)`, instead of each caching one of its own; reqwest pools per host internally, so one pool serves the API host and every per-sandbox host without interference, and because envd RPC and the envd HTTP API hit the same host an active sandbox needs a single HTTP/2 connection instead of one per stack. Two accessors expose it (`get_pyqwest_transport` for connectrpc, `get_httpx_transport` for the generated httpx clients) while per-layer concerns stay above the pool, so `PlainHTTPErrorTransport` becomes a stateless per-client wrapper and Connect-error normalization stays RPC-only. Streamed downloads keep a pool of their own — the only one carrying the idle `read_timeout`, since reqwest's read timer runs during body send and TTFB and would otherwise cut off long uploads. Sharing puts the sandbox health probe on the connection the failed RPC was using, so `tests/test_shared_transport_pool.py` pins that at the frame level with a new multi-connection HTTP/2 server serving both routes on one pool: an `RST_STREAM` kills only the stream and the probe reuses the same connection (which is also the proof the pool is genuinely shared), while a dropped TCP connection makes reqwest redial — both still answer, so `handle_rpc_exception_with_health` keeps telling a wedged connection apart from a dead sandbox. **No user-facing API change**, so there are no usage examples to add — the public surface, timeouts, retry policy, and proxy handling are all unchanged, and JS has no counterpart since pyqwest pools are Python-only. ## Added while claiming One regression test the original was missing (`test_{sync,async}_closing_one_client_leaves_the_shared_pool_open` in `tests/test_api_client_transport.py`). The refactor's docstrings promise that "closing an httpx client leaves the pool intact for the other clients on it", and that promise is now load-bearing process-wide rather than per-stack, but nothing asserted it: pyqwest pools *are* closable (`SyncHTTPTransport.close`/`HTTPTransport.aclose`) and every httpx client in the SDK holds the same cached adapter over one. The existing tests all close their clients inside `finally` and then reset the caches, so a close that reached the pool would go unnoticed. The new tests round-trip against the local echo server, then close the control-plane client and assert that both a sibling client (the envd HTTP API) and the pool the envd RPC stack executes on directly still work. Verified in the pinned dependency that `PyqwestTransport`/`AsyncPyqwestTransport` inherit httpx's no-op `close`/`aclose` and never touch the wrapped pool, and confirmed the assertions are not vacuous: forwarding the adapter's `close()` to the pool makes both of them fail with `RuntimeError: Executing request on already closed transport`. ## Verification - `uv run pytest tests/*.py -q` — 264 passed (262 before the added test). - `uv run pytest tests/shared -q` — 128 passed, 1 skipped. - `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` — clean. - Checked the two claims from the original description that a reader would have to take on trust: pyqwest's retry middleware does mirror non-`bytes` request bodies in RAM (`RetryingRequestContent` accumulates every chunk into a `bytearray` to make the body replayable), which is why template context uploads deliberately keep their own non-retrying transport — and why the same buffering applies to volume uploads and envd `files.write` on the shared retrying pool, a pre-existing issue on `main` filed as [SDK-332](https://linear.app/e2b/issue/SDK-332/python-sdk-streamed-uploads-are-mirrored-in-ram-by-the-pyqwest-retry) rather than something this PR introduces. ## Notes for review - RPC and envd HTTP now multiplex on one HTTP/2 connection and share its concurrent-stream budget (Go's default is 250, and hyper dials a second connection when one saturates) — low risk, but a real behavior change under heavy per-sandbox concurrency. - `get_envd_transport` survives only as an alias of `get_transport` because external consumers (`e2b-code-interpreter`) call it; prefer `get_transport` inside the SDK. - The changeset from the original PR is carried over unchanged (`@e2b/python-sdk` patch); the added test needs none of its own. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <div><a href="https://cursor.com/agents/bc-bfb51e4b-1116-42f4-8622-ba3bdeacf11a?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> |
||
|
|
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> |
||
|
|
e2eebd570f |
fix(python-sdk): URL-encode namespaced template IDs and aliases (#1691)
Claimed clone of #1520 (EN-1379), rebuilt on current `main`. Please close #1520 in favour of this PR. ## Summary Namespaced template IDs and aliases contain a slash, but the Python SDK interpolated them into the request path unencoded, so `Template.exists("namespace/name")` requested `/templates/aliases/namespace/name` instead of `/templates/aliases/namespace%2Fname` — the slash split the route rather than staying inside one path segment. A new `encode_path_param` helper percent-encodes the `template_id` and `alias` path params across every template build-API call site, in both the sync and async implementations. This matches the JS SDK, which already encodes path params: `openapi-fetch`'s default path serializer runs each value through `encodeURIComponent`, so no JS change is needed. ## Usage ```python from e2b import Template # Namespaced templates now resolve to /templates/aliases/my-team%2Fmy-template Template.exists("my-team/my-template") # ... and to /templates/my-team%2Fmy-template/tags Template.get_tags("my-team/my-template") ``` ```python from e2b import AsyncTemplate await AsyncTemplate.exists("my-team/my-template") ``` ## Changes on top of #1520 - Merged current `main` (the original branch was 26 commits behind), and confirmed the fix still covers every path-param call site after the merge. - Added `tests/shared/template/test_build_api_path_encoding.py`: the original PR only unit-tested the helper, which would not catch a call site that forgot to encode, nor httpx decoding `%2F` back into a separator. The new tests drive the sync and async build APIs through an `httpx.MockTransport` and assert the raw request path for both a namespaced alias and a namespaced template ID. Verified they fail when the encoding is removed. ## Tests - `pnpm run format`, `pnpm run lint`, `pnpm run typecheck` — all clean. - `uv run pytest tests/shared` in `packages/python-sdk` — 139 passed, 1 skipped. A changeset is included (`@e2b/python-sdk` patch); this is a Python-only change, so the JS SDK is not bumped. <div><a href="https://cursor.com/agents/bc-538dde5d-ca2f-4beb-8471-be70e265337d?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 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Tomas Valenta <49156497+ValentaTomas@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com> |
||
|
|
fc34961205 |
test(python-sdk): drop httpcore-era stream reader tests after the pyqwest migration (#1690)
Claimed from #1656 via `/sdk claim` (requested by @mishushakov, the original author). Same single commit, original authorship preserved. **Supersedes #1656, which should be closed in favor of this PR** — I don't have write access to close it myself. --- `tests/test_file_stream_reader.py` was written against httpcore and never migrated with the rest of the pyqwest stack — it builds bare `httpx.Client()` instances, so it still passes green while exercising a transport the SDK no longer ships. Both of its load-bearing premises are dead: - `_active_connections()` read `client._transport._pool.connections`, an httpcore-only internal. `PyqwestTransport` has no `_pool` at all. - `request.extensions["timeout"]["read"]` is no longer a per-chunk idle bound. The pyqwest adapter collapses read/write into one whole-operation deadline and exits the timeout scope before the body streams, so it bounds nothing after the response head. This deletes the five tests that asserted only httpcore behavior (both idle-timeout tests, the slow-consumer test, both abandoned-reader tests) plus the helper, and re-anchors the remaining eight on `response.is_closed` — `FileStreamReader.close()`'s actual contract, transport-agnostic and stronger than the pool check, since the context-manager tests now also assert the response stays open mid-stream. Also removes `tests/bugs/`, whose sole file was a permanently `@pytest.mark.skip`'d pyautogui repro against the `desktop` template. The real streaming-idle coverage against actual pyqwest transports already lives in `tests/test_volume_client.py`; the SDK stopped sending per-request timeouts on streamed reads for this same reason in `e2b/sandbox_sync/filesystem/filesystem.py`. Test-only, so no changeset — matching the repo convention for `test(...)` PRs. ## Usage examples None — this PR touches only `packages/python-sdk/tests/`. There is no change to any public API, so no user-facing usage differs. ## Verification Re-ran the original PR's checks on this branch: ``` $ uv run pytest tests/test_file_stream_reader.py -v 8 passed in 0.31s # was 13 $ uv run pytest tests/*.py -q 245 passed in 15.16s # full python-sdk unit suite $ uv run make format # ruff format . -> 403 files left unchanged $ uv run make lint # ruff check . -> All checks passed! $ uv run make typecheck # ty check -> All checks passed! ``` Also confirmed nothing else in the repo references the deleted `tests/bugs/`, `test_envelope_decode`, or `_active_connections`. Closes SDK-324 <div><a href="https://cursor.com/agents/bc-11701ccd-9302-40dc-b58b-33e570bed5c4?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> |
||
|
|
02ba746e9f |
fix(deps): patch 4 advisories found by dependency audit (3 high, 1 medium) (#1685)
Daily dependency vulnerability audit. `pnpm audit` reported 8 findings across 2 packages (3 distinct advisories, all high), and `pip-audit` reported 1 (medium). All 4 have published patches, and every one is applied here. Both ecosystems now report clean. All findings were cross-referenced against the GitHub Advisory Database via `gh api /advisories/<ghsa>` to confirm severity and first-patched version before bumping. ## Advisories fixed | Severity | CVSS | Advisory | Package | Was | Now | | --- | --- | --- | --- | --- | --- | | High | 7.5 | [CVE-2026-14257](https://github.com/advisories/GHSA-mh99-v99m-4gvg) | `brace-expansion` | 1.1.16 / 2.1.2 / 5.0.7 | 1.1.18 / 2.1.4 / 5.0.9 | | High | 7.5 | [CVE-2026-69152](https://github.com/advisories/GHSA-rgw5-rvv9-x895) | `brace-expansion` | 1.1.16 / 2.1.2 / 5.0.7 | 1.1.18 / 2.1.4 / 5.0.9 | | High | 7.5 | [GHSA-5p4m-2wfm-xmqj](https://github.com/advisories/GHSA-5p4m-2wfm-xmqj) (no CVE assigned) | `js-yaml` | 3.15.0 / 4.3.0 | 3.15.1 / 4.3.1 | | Medium | 5.3 | [CVE-2026-71554](https://github.com/advisories/GHSA-6hr6-w5qg-qmwg) | `h2` | 4.3.0 | 4.4.1 | The two `brace-expansion` CVEs are handled together because the second one bypasses the mitigation added for the first, so only the 1.1.18 / 2.1.4 / 5.0.9 line is safe against both. Note that the existing overrides already covered earlier rounds of these same advisories — they were pinning 1.1.13 / 2.1.2 / 5.0.6 and js-yaml 3.15.0 / 4.2.0, which have since been superseded. ## Why each one matters here **`brace-expansion` (high, DoS).** Reachable through `glob > minimatch > brace-expansion`, and `glob` is a *production* dependency of the published `e2b` JS SDK — so this is the one finding that was not dev-only. Worth noting for reviewers: `glob@13.0.6` requires `minimatch@^10.2.2`, which in turn requires `brace-expansion@^5.0.8`, so a fresh `npm install e2b` already resolves the patched 5.0.9 on its own. No `js-sdk` manifest change is needed and end users were not exposed; the override bump is what keeps this repo's own lockfile and CI off the vulnerable versions. **`js-yaml` (high, quadratic CPU in `!!omap`).** Dev-tooling only, via `@changesets/read > ... > read-yaml-file` and `knip`. **`h2` (medium, duplicate `Host` header / request smuggling).** A production dependency of the Python SDK. Bumping `uv.lock` alone would only fix this repo's dev environment, since `uv.lock` does not constrain downstream installs — so the floor in `pyproject.toml` is raised too, which is what actually prevents a consumer from resolving the vulnerable 4.3.0 or 4.4.0. `h2` 4.4.1 declares `requires_python >=3.10`, matching the SDK's own `requires-python`, so no supported Python version is dropped. This is the only user-facing change in the PR and it carries a `patch` changeset. This one is below the high/critical bar the audit normally acts on, and is included because the remediation is a single in-range floor bump on a dependency that ships to users. ## Changes - `package.json` — retarget the `brace-expansion` and `js-yaml` pnpm overrides at the new patched versions. - `pnpm-lock.yaml`, `packages/python-sdk/uv.lock` — regenerated. - `packages/python-sdk/pyproject.toml` — `h2>=4,<5` becomes `h2>=4.4.1,<5`. - `.changeset/bump-h2-4-4-1.md` — `patch` for `@e2b/python-sdk`. No source code changed; this is dependency metadata only. ## Verification All three audits are clean after the change: ```bash pnpm audit # No known vulnerabilities found pnpm audit --prod # No known vulnerabilities found cd packages/python-sdk && uv run --with pip-audit pip-audit # No known vulnerabilities found ``` `pnpm run format`, `pnpm run lint`, and `pnpm run typecheck` all pass with no diff. Tests: 256 Python unit tests, 101 CLI tests, and 345 JS SDK tests pass. The remaining suites could not run in this environment because no `E2B_API_KEY` was available — every one of those failures is an `AuthenticationError: API key is required` / `E2B_API_KEY must be set` from a live-sandbox integration test, and none is related to this diff. **The credential-gated integration suites should be confirmed green in CI before merge.** ```bash cd packages/python-sdk && uv run pytest tests --ignore=tests/async --ignore=tests/sync --ignore=tests/bugs --ignore=tests/shared -q # 256 passed cd packages/cli && npx vitest run # 101 passed | 8 skipped cd packages/js-sdk && npx vitest run --project unit --project connectionConfig --project template # 345 passed; 267 failures, all missing-API-key ``` ## Note on PR structure The audit task asks for one PR per vulnerability. This run was scoped to a single branch, so all 4 advisories are grouped here. That grouping is also the correct shape for the two `brace-expansion` CVEs, which share one fix and cannot be split. If separate PRs are preferred, the three commits on this branch are already split by advisory group and can be cherry-picked apart. <div><a href="https://cursor.com/agents/bc-c4b46d1f-a426-45b9-9c03-46e5decd398d?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/979f8043-9b01-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> |
||
|
|
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>
|
||
|
|
f5d702a520 | [skip ci] Release new versions | ||
|
|
0d507cd53d |
fix(python-sdk): restore the http2 parameter on the transport factories (#1671)
The pyqwest migration in 2.38.0 dropped the `http2` parameter from `get_transport` and `get_envd_transport` (added deliberately in #1347, 2.32.0) and collapsed the transport cache key to the proxy alone, so `e2b-code-interpreter`'s Jupyter requests — `get_transport(self.connection_config, http2=False)` — now raise `TypeError: get_transport() got an unexpected keyword argument 'http2'`; that is already live, since `e2b = "^2.26.0"` resolves to 2.38.x, and it blocks the Python half of code-interpreter [#328](https://github.com/e2b-dev/code-interpreter/pull/328). pyqwest supports the capability, it just was not threaded through: this restores the pre-2.38.0 signature (so no consumer code changes, only an `e2b` floor bump) by passing `http_version=None if http2 else HTTPVersion.HTTP1` into the pyqwest transports, and puts the HTTP version back into both cache keys — without that, whichever caller asks second is handed a transport of the wrong version. The default is unchanged: `None` leaves TLS connections to ALPN (HTTP/2 against the E2B API) and uses HTTP/1 for plaintext, exactly as today. HTTP/1.1 is not cosmetic for the consumer — with HTTP/2 multiplexing, abandoning a request only resets its stream, so the code-interpreter server never sees the `http.disconnect` it needs to interrupt the kernel, while HTTP/1.1's one connection per request closes the connection and the server observes it. ## Usage Both factories are internal (nothing is exported from `e2b/__init__.py`), so there is no public API change; consumers reaching into them get the 2.32.0 call back: ```python from e2b.api.client_sync import get_transport, get_envd_transport # Unchanged: ALPN negotiates the version (HTTP/2 against the E2B API). transport = get_transport(config) # Its own pool, pinned to HTTP/1.1, so a cancelled request closes the # connection and the server observes the disconnect. http1 = get_transport(config, http2=False) envd_http1 = get_envd_transport(config, http2=False) ``` The async mirror (`e2b.api.client_async`) is identical. ## Tests Six new cases in `packages/python-sdk/tests/test_api_client_transport.py`, sync and async: cache separation and identity across `http2` / proxy / `for_streaming`, the `http_version` value actually reaching the pyqwest transport (`[None, HTTP1, HTTP1]`), and a round trip proving the pinned transport works. The negotiated version can't be observed locally — the test echo server is plaintext, where both settings speak HTTP/1 — so it is asserted at the constructor, with the reason in a comment; it was verified by hand against `https://api.e2b.app/health` via the `pyqwest.access` logger, which shows `"HTTP/2 200 OK"` on the default and `"HTTP/1.1 200 OK"` with `http2=False` on both factories (and confirms `httpx.Response.http_version` is unreliable through the adapter — it reports HTTP/1.1 either way). 256 unit tests pass, plus `make lint`, `make typecheck` and `make format`. No JS change: its transport is an undici-dispatcher `fetch` with no HTTP-version knob, and the JS half of code-interpreter #328 is a clean bump. Closes [SDK-335](https://linear.app/e2b/issue/SDK-335/python-sdk-get-transport-lost-its-http2-parameter-in-2380-breaking-e2b) 🤖 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> |
||
|
|
11912ffa04 |
refactor(python-sdk): share one envd HTTP client across the sync sandbox modules (#1655)
The sync flavor built four envd HTTP clients per sandbox — `Filesystem`, `Commands` and `Pty` each constructed their own — while the async flavor built one in `Sandbox.__init__` and threaded it down; this builds it once on the sync side too and passes it into the three modules. No functional change: `get_envd_transport` already caches the pyqwest transport per `(proxy, for_streaming)` process-wide, so those four clients already shared one connection pool — the cost was a few `httpx.Client` wrappers per sandbox, plus a sync/async divergence that CLAUDE.md and TASTE.md both ask us to avoid. It also clears the last cosmetic differences between the two flavors: async `Commands`/`Pty` swap their `_check_health` lambda closure for the sync side's attribute + method, sync `Commands`/`Pty` drop a write-only `_envd_api_url`, and async `Filesystem` builds its RPC client first to match sync — the three constructor pairs now differ only in the sync/async client and RPC class names. All constructors touched are internal, so there is no public API change and nothing to show as a usage example. Verified with 336 unit tests, 92 sync and 89 async integration tests against prod (`commands`, `pty`, `files`), plus `make lint` and `make typecheck`. Closes [SDK-322](https://linear.app/e2b/issue/SDK-322/python-sdk-share-one-envd-http-client-across-the-sync-sandbox-modules) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6acbeb39ee | [skip ci] Release new versions | ||
|
|
b048369307 |
feat(python-sdk): move the envd HTTP API client onto pyqwest (#1623)
## What Tracked in [SDK-265](https://linear.app/e2b/issue/SDK-265) (part of the [SDK-268](https://linear.app/e2b/issue/SDK-268) stack). Stacked on #1603, at the top of the pyqwest stack (#1601 → #1602 → #1603 → this). Migrate the envd HTTP API client — sandbox file transfers (`files.read`/`write`), health checks — from httpx-native transports to pyqwest via the httpx adapter, and dedupe the transport plumbing that #1558 (envd RPC) and #1601 (REST) each carried a copy of. With this, all Python SDK traffic runs on pyqwest: REST control plane (#1601), envd RPC (#1558, connectrpc), envd HTTP API (this PR); the volume content client (#1602) and template build uploads (#1603) sit below this one in the stack. ## How **Shared plumbing** (first commit): `e2b.api` becomes the canonical home for the proxy narrowing (`proxy_to_config`, with stack-neutral error messages), the pool tuning, and the flavor `ConnectionRetryTransport` + a new `retrying_http_transport(proxy, read_timeout=None)` factory; `e2b.envd.client_sync/client_async` import them instead of defining their own (envd RPC behavior unchanged, pools stay separate — unification is SDK-291). **envd HTTP API** (second commit): - `get_envd_transport(config, for_streaming=False)` returns pyqwest-adapter transports cached per `(proxy, streaming)`; `get_envd_api(config, base_url, for_streaming=False)` builds the httpx client with sandbox headers + logging hooks. The per-thread (sync) / per-loop (async) client caching in `Filesystem`/`Commands`/`Pty`/`AsyncSandbox` is gone — one shared client per module, same rationale as the `ApiClient` simplification in #1601. - **Streamed downloads**: the streaming transport carries a 60s `read_timeout` — an idle bound that resets on every read, capping stalls without limiting total transfer time. It gets a dedicated pool because reqwest's read timer keeps ticking while a request body is sent and while waiting for the response head, so on the shared transport it would cut off uploads and slow unary responses. An explicit `request_timeout` becomes the whole-transfer deadline (adapter semantics) and is sent only when the caller set one; `stream_idle_timeout` stays honored on the async client via `wait_for` per read (so values above 60s work and `0` disables), and is documented as ignored on the sync client, which cannot interrupt a blocking read. Mirrors #1602's volume design. - **Uploads**: buffered uploads keep `request_timeout` as a whole-request deadline; streamed (file-like) uploads carry no client-side timeout and are bounded server-side (envd's idle read timeout) — both exactly the JS SDK's behavior (`getSignal` for buffered, no signal for streams). - **Multipart**: `files=` uploads go out as httpx's `MultipartStream`, which implements *both* `SyncByteStream` and `AsyncByteStream`. The pyqwest 0.7 adapter's sync content conversion matched `AsyncByteStream` first and raised `TypeError("unreachable")` from inside the body iterator, surfacing as a `WriteError` mid-request ("http2 error: stream error sent by user"). Fixed upstream in [pyqwest#196](https://github.com/curioswitch/pyqwest/pull/196), which matches the sync case first — so this PR carries no workaround (the stack requires **pyqwest 0.9**, set in #1601). The regression test stays, now covering the upstream fix. - The stream readers map the transport's idle timeout (builtin `TimeoutError` under pyqwest) to the documented `httpx.ReadTimeout`; `handle_envd_api_transport_exception`'s health-probe path keeps working because the adapter maps HTTP/2 stream resets to `httpx.RemoteProtocolError`. - **RPC logging**: the `LoggingInterceptor` docstring no longer promises its own removal. pyqwest does log requests ([pyqwest#197](https://github.com/curioswitch/pyqwest/pull/197)), but on process-wide `pyqwest`/`pyqwest.access` loggers that can't carry the per-sandbox `logger` and don't see streamed messages or the Connect error code of a stream that fails inside a `200 OK` — so the interceptor stays, with those loggers below it. [pyqwest#192](https://github.com/curioswitch/pyqwest/pull/192), the middleware it referenced, was closed in favor of #197. - **Transports**: rebased onto #1603 on pyqwest 0.9, so the envd HTTP API transports are the stock `PyqwestTransport`/`AsyncPyqwestTransport` (the SDK's adapter subclasses are gone as of #1601 — 0.9 strips the `Host` header and maps timeouts itself) with `follow_redirects=False` and, for the streaming pool, the transport-wide `read_timeout`. ## Testing - Unit: envd transport keying (streaming vs regular vs REST pools), `get_envd_api` wiring (headers, transports), multipart regression through a local server, stream-reader timeout mapping + per-read idle bound (`tests/test_file_stream_reader.py`), rewritten client-lifecycle tests (shared across threads). 236 unit tests green; lint + typecheck green. - Integration against production sandboxes: full `files` suites sync+async (123 tests — these caught the multipart bug), `commands` + `pty` suites both flavors (57 tests). All green. ## Usage example No API changes: ```python sbx = Sandbox.create() sbx.files.write("hello.txt", "hi") # multipart/octet-stream over pyqwest with sbx.files.read("hello.txt", format="stream") as stream: for chunk in stream: # stalls bounded by 60s idle read timeout ... ``` Only visible behavior shift: on the **sync** client, `files.read(..., format="stream", stream_idle_timeout=...)` is now a documented no-op (the transport-wide 60s idle bound applies); the async client honors it as before. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b3a7c9f44a |
feat(python-sdk): move template build-context uploads onto pyqwest (#1603)
## What Stacked on #1602 (which is stacked on #1601). Migrates the **template build-context uploads** (streaming the build archive to S3 presigned URLs in `build_api.upload_file`) onto [pyqwest](https://github.com/curioswitch/pyqwest) via its httpx-compatible transport adapter. Originally deferred from #1601 because S3 presigned URLs reject chunked transfer encoding and Content-Length framing through reqwest was unverified. Verified at the wire level (raw-socket capture server): httpx's Content-Length — derived from the spooled archive (sync) or set explicitly on the async-iterator body (async) — is forwarded by the adapter and reqwest keeps Content-Length framing for streamed bodies, no chunked fallback. > [!NOTE] > Rebased onto #1601, which locks **pyqwest 0.9.0**. Two knock-on changes here: the upload client uses the stock `PyqwestTransport`/`AsyncPyqwestTransport` (0.9.0's adapter subsumes what the SDK's transport subclasses did, so #1601 deleted them), and it builds its proxy from `proxy_to_config(...)` following #1601's rename. ## How - `e2b/template_sync/build_api.py` / `template_async/build_api.py`: `upload_file` uses a one-off pyqwest transport instead of the generated client's httpx transport. - **Redirects stay with the httpx client.** pyqwest 0.9.0 makes reqwest's internal redirect following configurable, so it's turned off on the upload transport: otherwise reqwest would replay the entire archive body against a new location without httpx knowing. The httpx client inherits the API client's `follow_redirects` (off), matching the httpx transport this replaced — so an unexpected hop surfaces as a failed upload rather than a silent re-upload. - `verify_ssl=False` on the generated client is no longer honored for uploads (pyqwest has no insecure-TLS option), and `http2=False` is gone (S3 negotiates HTTP/1.1 via ALPN anyway). - The 1-hour upload timeout now bounds the entire upload rather than each socket write — arguably the intended meaning for that endpoint. ## Testing - `tests/{sync,async}/*/test_upload_file.py` (the #1243 regression tests — Content-Length present and equal to the body, no chunked encoding) pass through pyqwest; the capture handlers now compare header names case-insensitively since hyper lowercases them where httpcore title-cased. - New in both mirrors: `test_upload_file_leaves_redirects_to_httpx` — a 307 on the upload URL surfaces as `FileUploadException` and the capture server sees exactly one PUT, guarding against reqwest silently following the hop and replaying the archive. - Lint (`ruff`), typecheck (`ty`), upload-file suites: green (10/10). ## Usage example No API changes — template builds upload their context exactly as before: ```python from e2b import Template template = Template().from_image("ubuntu:22.04").copy("data/", "/data") Template.build(template, alias="my-template") # archive upload now goes through pyqwest ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
458c2c4362 |
feat(python-sdk): move the volume content client onto pyqwest (#1602)
## What Stacked on #1601. Migrates the **volume content client** (`Volume`/`AsyncVolume` file operations) onto [pyqwest](https://github.com/curioswitch/pyqwest) via its httpx-compatible transport adapter — the same stock httpx transport adapter + connection-retry stack the REST API client uses after #1601. Originally deferred from #1601 because `Volume.read_file(format="stream")` relied on httpx's per-read `read` timeout as an *idle* timeout, which the adapter can't express per request (it converts the httpx timeout dict into a whole-request deadline, and the sync adapter doesn't bound body reads at all). Unblocked by pyqwest's transport-constructor `read_timeout`, which maps to reqwest's `ClientBuilder::read_timeout` — verified behaviorally (local slow-chunk server, sync + async) to be a true per-read idle timeout: it resets after each successful read, covers body reads, and a healthy stream longer than the timeout completes untouched. > [!NOTE] > Rebased onto #1601, which maps `httpx.Proxy` onto pyqwest's `Proxy` object and locks pyqwest 0.9.0. Following that: this PR builds its transport from `proxy_to_config(...)` instead of `proxy_to_url(...)`, uses the stock `PyqwestTransport`/`AsyncPyqwestTransport` (0.9.0's adapter drops the redundant `Host` header and maps pyqwest's timeouts and connection, network, and protocol failures to their httpx counterparts, so the SDK's transport subclasses are gone), and turns reqwest's internal redirects off so httpx owns them, as the generated volume client expects. ## How - `e2b/volume/client_sync/__init__.py` / `client_async/__init__.py` move to the same stock adapter + connection-retry stack as the API client. Caches become process-global, keyed by (proxy, streaming) — previously one pool per thread (sync) / per event loop (async). - Streamed downloads go through a **dedicated streaming transport** with `read_timeout=60s`. It can't live on the shared transport: reqwest's read timer keeps running while a request body is sent and while waiting for the response head (verified empirically — a 2.4 s upload against a 0.5 s `read_timeout` dies mid-send), so a shared `read_timeout` would cut off `write_file` uploads and slow unary responses longer than the idle bound. Uploads and unary calls stay on a transport without it, bounded by their whole-request deadlines as before. - The 60 s default matches the JS SDK exactly: JS bounds stream start by `requestTimeoutMs` (60 s default) and idle gaps by `streamIdleTimeoutMs ?? requestTimeoutMs`; the Python streaming transport's `read_timeout` bounds the response head and each idle gap at 60 s, resetting on every chunk, wire-only (a slow consumer doesn't trip it — verified). - `AsyncVolume.read_file` keeps honoring an explicit `stream_idle_timeout` **per call**, the same way JS honors `streamIdleTimeoutMs` and #1558 bounds stream setup: `asyncio.wait_for` around each read (response head and every chunk). Explicit values run on the *regular* transport, so a value above the 60 s transport bound isn't capped by it and `0` disables idle bounding entirely, restoring the previous contract. The sync client keeps the parameter but **ignores** it — it has no way to interrupt a blocking read into the Rust transport, so its bound must live in the transport. - Streamed reads are sent without a per-request timeout so the adapter imposes no whole-request deadline on long downloads; an explicitly passed `request_timeout` becomes the total-transfer deadline. - A stalled read surfaces as `httpx.ReadTimeout`, keeping the established contract: the 0.9.0 adapter maps its own timeouts, and the async flavor remaps the per-read `stream_idle_timeout` (an `asyncio.wait_for` expiry) to match. - Proxy narrowing follows #1601: `str`, `httpx.URL`, and reducible `httpx.Proxy` values work; inexpressible extras raise `InvalidArgumentException`. ## Testing - `tests/test_volume_client.py` rewritten: process-global transport caching (shared across threads and event loops), streaming vs regular transport separation, plus end-to-end streamed reads through `Volume.read_file`/`AsyncVolume.read_file` against a local chunked server — a healthy stream longer than the idle timeout completes (proves the timeout resets per read), a mid-body stall raises `httpx.ReadTimeout`, a slow response head on a *non-streamed* read is not cut off by the idle bound, and a slow response head on a streamed read is (JS handshake-timeout parity). Async `stream_idle_timeout`: an explicit value aborts a stall, a value above the transport bound isn't capped by it, and `0` disables idle bounding. - Volume content integration tests couldn't run end-to-end (the test team's key gets `403: use of volumes is not enabled`); the mock-transport volume content tests and the local-server stream tests cover that path. - Lint (`ruff`), typecheck (`ty`), unit suite: green. ## Usage example No API changes for the common path: ```python volume = Volume.connect(volume_id, token=token) stream = volume.read_file("big.bin", format="stream") # stalls bounded by the for chunk in stream: # transport-wide idle read ... # timeout (httpx.ReadTimeout) volume.read_file("big.bin", format="stream", stream_idle_timeout=5) # sync: accepted, ignored async_volume = await AsyncVolume.connect(volume_id, token=token) stream = await async_volume.read_file( "big.bin", format="stream", stream_idle_timeout=5 # async: honored per read, ) # 0 disables idle bounding ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a874ced97a |
feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter (#1601)
## What Migrate all httpx REST API client traffic in the Python SDK — the E2B control plane (sandbox lifecycle, listing, templates, volumes control plane) — to [pyqwest](https://github.com/curioswitch/pyqwest) (Rust reqwest/hyper), using its httpx-compatible transport adapter (`pyqwest.httpx.PyqwestTransport` / `AsyncPyqwestTransport`). The generated openapi client and `ApiClient`/`AsyncApiClient` keep their httpx surface — logging event hooks, per-request timeouts, headers, and redirects behave as before — only the transport underneath is swapped. envd RPC already runs on pyqwest via connectrpc (#1558). This PR touches only the control-plane client; the rest of the stack builds on it: #1623 (envd HTTP API client), #1602 (volume content client), #1603 (template uploads). Requires **pyqwest 0.9** — pinned in `pyproject.toml` (`>=0.9.0,<0.10`) with `uv.lock` refreshed. 0.8 brought the `Proxy` object ([pyqwest#194](https://github.com/curioswitch/pyqwest/pull/194)) and request loggers ([pyqwest#197](https://github.com/curioswitch/pyqwest/pull/197)); 0.9 ([release notes](https://github.com/curioswitch/pyqwest/discussions/214)) folds the two adapter workarounds this PR used to carry into the adapter itself and makes redirect handling configurable, so the SDK no longer subclasses the adapter at all. ## How - `e2b/api/client_sync/__init__.py` / `client_async/__init__.py`: `get_transport` now returns a pyqwest-backed httpx transport — a `SyncHTTPTransport`/`HTTPTransport` (`tls_include_system_certs=True`, proxy, pool tuning mapped from `E2B_KEEPALIVE_EXPIRY`/`E2B_MAX_KEEPALIVE_CONNECTIONS`), wrapped in a `ConnectionRetryTransport` for connect-only retries honoring `E2B_CONNECTION_RETRIES`, wrapped in the stock `PyqwestTransport`/`AsyncPyqwestTransport` httpx adapter. - pyqwest transports are thread-safe and loop-independent (I/O runs on a Rust tokio runtime), so the caches are process-global keyed by proxy — previously one pool per thread (sync) / per event loop (async). - **`ApiClient` sheds its threading machinery**: the `transport_factory`/`async_transport_factory` plumbing, the thread-local `httpx.Client` cache, and the per-loop `WeakKeyDictionary` of `AsyncClient`s are gone. A single lazily-created httpx client (the generated base behavior, the same shape the volume client already uses) serves all threads and event loops; `httpx.Client` is documented thread-safe and nothing below it is loop-bound. Closing that client can't tear down the shared pool — the adapter transports don't override `close()`/`aclose()`. - **Host header** (upstream in 0.9): sending the `Host` header httpx auto-adds on an HTTP/2 connection makes the E2B API edge reset the stream with `PROTOCOL_ERROR` (reproduced with plain pyqwest against `api.e2b.app`); hyper derives `Host`/`:authority` from the URL. The adapter now skips a `host` header matching the URL, so the SDK-side strip is gone — and unlike that strip, a genuinely custom `Host` override is still forwarded. - **Timeout exceptions** (upstream in 0.9): pyqwest raises the builtin `TimeoutError`; the adapter maps it to `httpx.ReadTimeout` both while awaiting the response head and while reading the body, preserving the `httpx.TimeoutException` contract for callers. Connection, network, and protocol failures likewise arrive as `httpx.ConnectError`/`ConnectTimeout`, `httpx.ReadError`/`WriteError`, and `httpx.RemoteProtocolError` instead of leaking pyqwest/builtin types. - **Redirects**: the pyqwest transports are built with `follow_redirects=False` (0.9 made it configurable; reqwest's default is to follow). Otherwise redirects are followed inside the transport, hiding 3xx responses from httpx and leaving `response.history` empty — even though the generated clients ask for no redirect following. httpx owns them again, as with the transports this replaced. - **Proxy**: `proxy=` accepts a URL string, `httpx.URL`, or an `httpx.Proxy` — including its credentials (sent as `Proxy-Authorization`) and any headers configured for the proxy, via pyqwest's `Proxy` object. `proxy_to_config` normalizes all three into a `ProxyConfig` tuple that both keys the transport cache and builds the `pyqwest.Proxy`, so the same proxy URL with different credentials or headers gets its own pool. A per-proxy `ssl_context` has no counterpart and raises `InvalidArgumentException` rather than being silently dropped. (`ProxyConfig` is a `NamedTuple`, not a frozen dataclass: `tests/test_env_var_parsing.py` reloads `e2b.api`, and a dataclass `__eq__` compares class identity, so keys built before and after a reload would silently stop matching.) - **`ProxyTypes` is ours now**: the public type of the `proxy` option (already exported from `e2b`) used to be imported at runtime from httpx's private `_types` module in eleven modules. It is defined there as `Union[str, URL, Proxy]` — exactly the three forms the SDK's two narrowers accept — so it's spelled out once in `e2b.connection_config` and imported from there. Same public name, same type to a type checker, no private-module dependency, and a place for a pyqwest proxy type to land as the remaining transports move off httpx. `e2b.envd.client_shared.proxy_to_url` took a bare `object` while `e2b.api.proxy_to_config` took `Optional[ProxyTypes]`; both now say the same thing. `isinstance` narrowing stays rather than duck-typing `.url`/`.auth` — httpx is a required dependency here (the generated REST client *is* an httpx client, and envd file transfers use httpx directly), so probing attributes would trade a clear `InvalidArgumentException` on a mistyped argument for no dependency savings. - **Request logs**: pyqwest logs one line per request on the `pyqwest.access` logger and lifecycle records on `pyqwest`, both at `DEBUG` — the transport-level diagnostics httpcore used to provide, now that httpcore is out of the path. Noted on `get_transport`; the SDK's own `logger` option is unchanged and sits above it on the httpx client. - **HTTP/2**: negotiated via ALPN for TLS connections (reqwest default), equivalent to the `http2=True` transports this replaces. ## What stays behind (handled by the stacked PRs) - **envd HTTP API client** (file transfers, health checks): #1623, which also dedupes the transport plumbing this PR and #1558 each carry a copy of (the proxy narrowing, pool tuning, retry transport — envd keeps byte-identical duplicates until then). - **Volume content client**: its streaming download relies on httpx's per-read `read` timeout as an *idle* timeout, which the adapter can't express per request — #1602. - **Template build context upload**: one-off httpx client PUTing to S3 presigned URLs — #1603. ## Timeout semantics note `request_timeout` was previously httpx's per-phase timeout (connect/read/write each bounded separately, so a slow multi-phase request could exceed it in total). Through the adapter it becomes an overall deadline per API call (async: headers + body; sync: up to response headers). For the SDK's REST calls — all unary with small JSON bodies — this is a tightening, arguably closer to what `request_timeout` promises. ## Testing - `tests/test_api_client_transport.py` rewritten for the new semantics: global per-proxy transport caching, a single httpx client shared across threads/loops (including 32-way concurrent request tests against a local server), timeout → `httpx.ReadTimeout` mapping for both the response head and a stalled body (slow/stalling local server), redirects surfacing to httpx (302 returned as-is, `response.history` populated when the caller opts in), the connection-only retry policy, `proxy_to_config` conversion, and sync+async round-trips through a real local HTTP server exercising pyqwest end to end. The two host-header unit tests are gone with the subclasses they tested — that behavior is the adapter's now. - Two tests cover the pyqwest proxy/logging surface: an echo server standing in for a proxy asserts that the absolute-form request target, `Proxy-Authorization`, and the extra proxy header actually arrive, and the `pyqwest.access` record is asserted for an API call. - On pyqwest 0.9.0 from PyPI: `uv sync --locked`, unit suite (`tests/*.py`, 238 passed), `ruff check`, `ty check` — all green. - Integration against the production API (real key) was run on 0.8.0: `tests/sync/api_sync`, `tests/async/api_async`, create/kill/timeout/connect — all green. (These initially failed with `RemoteProtocolError: StreamReset` until the host header stopped being forwarded, so they genuinely exercise the new stack; that fix now comes from the adapter.) ## Usage example No API changes for the common path: ```python from e2b import Sandbox sbx = Sandbox.create() # control-plane calls now go through pyqwest Sandbox.list() sbx.kill() ``` Proxy handling — URL strings and `httpx.Proxy` objects work, credentials and proxy headers included: ```python Sandbox.create(proxy="http://user:pass@localhost:8030") # ok (unchanged) Sandbox.create(proxy=httpx.Proxy("http://localhost:8030", auth=("user", "pass"))) # sent as Proxy-Authorization Sandbox.create(proxy=httpx.Proxy("http://localhost:8030", headers={"X-Auth": "t"})) # sent to the proxy Sandbox.create(proxy=httpx.Proxy("https://localhost:8030", ssl_context=ctx)) # raises InvalidArgumentException ``` `ProxyTypes` — already exported from `e2b` — is now defined by the SDK rather than re-exported from `httpx._types`, with the same three members: ```python from e2b import ProxyTypes # Union[str, httpx.URL, httpx.Proxy] ``` Transport-level HTTP logs, replacing the httpcore records this migration removes: ```python import logging logging.basicConfig() logging.getLogger("pyqwest.access").setLevel(logging.DEBUG) Sandbox.create() # DEBUG pyqwest.access - HTTP Request: POST https://api.e2b.app/sandboxes "HTTP/2 201 Created" ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
998e560a1a |
fix(python-sdk): relax wcmatch constraint to >=10.1,<12 (#1638)
|
||
|
|
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 | ||
|
|
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> |
||
|
|
45d26792f1 |
chore(deps-dev): bump datamodel-code-generator from 0.34.0 to 0.64.0 in /packages/python-sdk in the uv group across 1 directory (#1621)
> [!NOTE]
> Manual follow-up commit on top of Dependabot's bump (addressing review
feedback): the codegen image pin was out of sync, so this PR also bumps
it and carries the regenerated output.
### Manual changes on top of the bump
- `codegen.Dockerfile` bumped from `datamodel-code-generator==0.34.0` to
`0.64.0`. `pyproject.toml`'s `codegen` group and the Dockerfile must
stay in sync (the comment above the group says so) — CI's `Generated
files` check regenerates from the image, so leaving the image at
`0.34.0` would make `make init` produce output CI rejects.
- `packages/python-sdk/e2b/sandbox/mcp.py` regenerated with `0.64.0`.
Two output changes:
- builtin generics (`list[str]`, `dict[str, Any]` instead of
`List`/`Dict`), fine on the SDK's `>=3.10` floor;
- `additionalProperties: false` in `spec/mcp-server.json` is now honored
as PEP 728 `closed=True` (`0.34.0` silently dropped it), and `TypedDict`
is imported from `typing_extensions` accordingly.
- `typing-extensions>=4.1.0` → `>=4.10.0`. `closed=True` is evaluated at
class-creation time, i.e. on `import e2b`, and 4.10.0 is the first
release whose `TypedDict` accepts the keyword (4.9.0 raises `TypeError:
_TypedDictMeta.__new__() got an unexpected keyword argument 'closed'`).
- Changeset added (`patch` for `@e2b/python-sdk`), since the regenerated
file and the dependency floor ship to users.
Nothing changes for callers at runtime — `McpServer` is still a plain
dict at the call site:
```python
from e2b import Sandbox
sbx = Sandbox.create(mcp={"duckduckgo": {}, "brave": {"braveApiKey": "..."}})
```
The `closed` types are also inert for type checkers in practice, because
the public `McpServer` is `Union[BaseMcpServer, GitHubMcpServer]` and
the second arm is a `Dict[str, ...]`. Verified: `pyright` and `mypy`
both clean against the snippet above, `ruff`/`ty`/`pnpm typecheck`
clean, 283 offline Python unit tests pass, and regenerating with the
full pinned toolchain (`python:3.10` + `black==26.3.1` + the other
Dockerfile pins) reproduces the committed file byte-for-byte.
---
Bumps the uv group with 1 update in the /packages/python-sdk directory:
[datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator).
Updates `datamodel-code-generator` from 0.34.0 to 0.64.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/koxudaxi/datamodel-code-generator/releases">datamodel-code-generator's
releases</a>.</em></p>
<blockquote>
<h2>0.64.0</h2>
<h2>Breaking Changes</h2>
<h3>Code Generation Changes</h3>
<ul>
<li>Self-referencing fields are now quoted with
<code>--disable-future-imports</code> - When
<code>--disable-future-imports</code> is set (no <code>from __future__
import annotations</code> and no native PEP 649 deferred evaluation on
Python < 3.14), self-referencing and forward-referencing field
annotations in regular <code>BaseModel</code> classes are now emitted as
quoted forward references instead of bare names. Previously such
annotations were left unquoted, producing invalid code that raised
<code>NameError</code> (Ruff F821) at class-evaluation time. Output for
the common case (with <code>from __future__ import annotations</code> or
Python 3.14 native deferred annotations) is unchanged. Users who
snapshot/golden-file generated output for the
<code>--disable-future-imports</code> configuration with
self-referencing models will see the annotation change from unquoted to
quoted, e.g. <code>children: Optional[List[Node]]</code> →
<code>children: Optional[List["Node"]]</code>. (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3387">#3387</a>)</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>Update CHANGELOG for 0.63.0 by <a
href="https://github.com/dcg-generated-docs"><code>@dcg-generated-docs</code></a>[bot]
in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3345">koxudaxi/datamodel-code-generator#3345</a></li>
<li>Deduplicate module content builder by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3346">koxudaxi/datamodel-code-generator#3346</a></li>
<li>Deduplicate import reference helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3348">koxudaxi/datamodel-code-generator#3348</a></li>
<li>Refactor jsonschema root model registration by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3352">koxudaxi/datamodel-code-generator#3352</a></li>
<li>Refactor XML Schema literal helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3349">koxudaxi/datamodel-code-generator#3349</a></li>
<li>Move builtin formatter helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3351">koxudaxi/datamodel-code-generator#3351</a></li>
<li>Deduplicate Pydantic v2 config helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3350">koxudaxi/datamodel-code-generator#3350</a></li>
<li>Deduplicate DataType type hint rendering by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3354">koxudaxi/datamodel-code-generator#3354</a></li>
<li>Fix <code>constr()</code> for string fields carrying
minItems/maxItems by <a
href="https://github.com/DarkaMaul"><code>@DarkaMaul</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3353">koxudaxi/datamodel-code-generator#3353</a></li>
<li>Cover non-finite import idempotence by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3367">koxudaxi/datamodel-code-generator#3367</a></li>
<li>Deduplicate input text detection by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3357">koxudaxi/datamodel-code-generator#3357</a></li>
<li>Remove stale protobuf coverage pragma by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3358">koxudaxi/datamodel-code-generator#3358</a></li>
<li>Cover explicit null OpenAPI media schemas by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3360">koxudaxi/datamodel-code-generator#3360</a></li>
<li>Simplify Python version feature checks by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3361">koxudaxi/datamodel-code-generator#3361</a></li>
<li>Speed up CI checks by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3378">koxudaxi/datamodel-code-generator#3378</a></li>
<li>Add maintainer link to docs footer and README by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3379">koxudaxi/datamodel-code-generator#3379</a></li>
<li>Use builtin formatter in CI by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3380">koxudaxi/datamodel-code-generator#3380</a></li>
<li>Split coverage by OS by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3381">koxudaxi/datamodel-code-generator#3381</a></li>
<li>Simplify import removal cleanup by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3362">koxudaxi/datamodel-code-generator#3362</a></li>
<li>Pin deprecation warning stacklevel by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3363">koxudaxi/datamodel-code-generator#3363</a></li>
<li>Pin public module exports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3364">koxudaxi/datamodel-code-generator#3364</a></li>
<li>Cover to_hashable branch cases by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3366">koxudaxi/datamodel-code-generator#3366</a></li>
<li>Cover stable toposort behavior by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3369">koxudaxi/datamodel-code-generator#3369</a></li>
<li>Extract registry render helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3371">koxudaxi/datamodel-code-generator#3371</a></li>
<li>Fix minItems for arrays of URI strings by <a
href="https://github.com/sjh9714"><code>@sjh9714</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3377">koxudaxi/datamodel-code-generator#3377</a></li>
<li>Deduplicate config value validators by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3372">koxudaxi/datamodel-code-generator#3372</a></li>
<li>Cover CLI option metadata helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3374">koxudaxi/datamodel-code-generator#3374</a></li>
<li>Cover Pydantic v2 version fallback by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3368">koxudaxi/datamodel-code-generator#3368</a></li>
<li>Fix nullable JSON Schema const enums by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3355">koxudaxi/datamodel-code-generator#3355</a></li>
<li>Pin patchable generation seams by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3365">koxudaxi/datamodel-code-generator#3365</a></li>
<li>Cover utility helper behavior by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3375">koxudaxi/datamodel-code-generator#3375</a></li>
<li>Cover DefaultPutDict behavior by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3376">koxudaxi/datamodel-code-generator#3376</a></li>
<li>Cover validator config normalization by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3373">koxudaxi/datamodel-code-generator#3373</a></li>
<li>Avoid expensive runtime type checks by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3382">koxudaxi/datamodel-code-generator#3382</a></li>
<li>Avoid eager builtin formatter import by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3383">koxudaxi/datamodel-code-generator#3383</a></li>
<li>Avoid eager TOML parser import by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3384">koxudaxi/datamodel-code-generator#3384</a></li>
<li>Stabilize msgspec payload tests by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3385">koxudaxi/datamodel-code-generator#3385</a></li>
<li>Avoid eager input parser imports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3386">koxudaxi/datamodel-code-generator#3386</a></li>
<li>Avoid eager parser model imports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3388">koxudaxi/datamodel-code-generator#3388</a></li>
<li>Avoid eager AsyncAPI converter imports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3389">koxudaxi/datamodel-code-generator#3389</a></li>
<li>Dispose parser on parse errors by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3390">koxudaxi/datamodel-code-generator#3390</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/koxudaxi/datamodel-code-generator/blob/main/CHANGELOG.md">datamodel-code-generator's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/koxudaxi/datamodel-code-generator/releases/tag/0.64.0">0.64.0</a>
- 2026-06-14</h2>
<h2>Breaking Changes</h2>
<h3>Code Generation Changes</h3>
<ul>
<li>Self-referencing fields are now quoted with
<code>--disable-future-imports</code> - When
<code>--disable-future-imports</code> is set (no <code>from __future__
import annotations</code> and no native PEP 649 deferred evaluation on
Python < 3.14), self-referencing and forward-referencing field
annotations in regular <code>BaseModel</code> classes are now emitted as
quoted forward references instead of bare names. Previously such
annotations were left unquoted, producing invalid code that raised
<code>NameError</code> (Ruff F821) at class-evaluation time. Output for
the common case (with <code>from __future__ import annotations</code> or
Python 3.14 native deferred annotations) is unchanged. Users who
snapshot/golden-file generated output for the
<code>--disable-future-imports</code> configuration with
self-referencing models will see the annotation change from unquoted to
quoted, e.g. <code>children: Optional[List[Node]]</code> →
<code>children: Optional[List["Node"]]</code>. (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3387">#3387</a>)</li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>Update CHANGELOG for 0.63.0 by <a
href="https://github.com/dcg-generated-docs"><code>@dcg-generated-docs</code></a>[bot]
in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3345">koxudaxi/datamodel-code-generator#3345</a></li>
<li>Deduplicate module content builder by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3346">koxudaxi/datamodel-code-generator#3346</a></li>
<li>Deduplicate import reference helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3348">koxudaxi/datamodel-code-generator#3348</a></li>
<li>Refactor jsonschema root model registration by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3352">koxudaxi/datamodel-code-generator#3352</a></li>
<li>Refactor XML Schema literal helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3349">koxudaxi/datamodel-code-generator#3349</a></li>
<li>Move builtin formatter helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3351">koxudaxi/datamodel-code-generator#3351</a></li>
<li>Deduplicate Pydantic v2 config helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3350">koxudaxi/datamodel-code-generator#3350</a></li>
<li>Deduplicate DataType type hint rendering by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3354">koxudaxi/datamodel-code-generator#3354</a></li>
<li>Fix <code>constr()</code> for string fields carrying
minItems/maxItems by <a
href="https://github.com/DarkaMaul"><code>@DarkaMaul</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3353">koxudaxi/datamodel-code-generator#3353</a></li>
<li>Cover non-finite import idempotence by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3367">koxudaxi/datamodel-code-generator#3367</a></li>
<li>Deduplicate input text detection by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3357">koxudaxi/datamodel-code-generator#3357</a></li>
<li>Remove stale protobuf coverage pragma by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3358">koxudaxi/datamodel-code-generator#3358</a></li>
<li>Cover explicit null OpenAPI media schemas by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3360">koxudaxi/datamodel-code-generator#3360</a></li>
<li>Simplify Python version feature checks by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3361">koxudaxi/datamodel-code-generator#3361</a></li>
<li>Speed up CI checks by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3378">koxudaxi/datamodel-code-generator#3378</a></li>
<li>Add maintainer link to docs footer and README by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3379">koxudaxi/datamodel-code-generator#3379</a></li>
<li>Use builtin formatter in CI by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3380">koxudaxi/datamodel-code-generator#3380</a></li>
<li>Split coverage by OS by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3381">koxudaxi/datamodel-code-generator#3381</a></li>
<li>Simplify import removal cleanup by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3362">koxudaxi/datamodel-code-generator#3362</a></li>
<li>Pin deprecation warning stacklevel by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3363">koxudaxi/datamodel-code-generator#3363</a></li>
<li>Pin public module exports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3364">koxudaxi/datamodel-code-generator#3364</a></li>
<li>Cover to_hashable branch cases by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3366">koxudaxi/datamodel-code-generator#3366</a></li>
<li>Cover stable toposort behavior by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3369">koxudaxi/datamodel-code-generator#3369</a></li>
<li>Extract registry render helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3371">koxudaxi/datamodel-code-generator#3371</a></li>
<li>Fix minItems for arrays of URI strings by <a
href="https://github.com/sjh9714"><code>@sjh9714</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3377">koxudaxi/datamodel-code-generator#3377</a></li>
<li>Deduplicate config value validators by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3372">koxudaxi/datamodel-code-generator#3372</a></li>
<li>Cover CLI option metadata helpers by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3374">koxudaxi/datamodel-code-generator#3374</a></li>
<li>Cover Pydantic v2 version fallback by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3368">koxudaxi/datamodel-code-generator#3368</a></li>
<li>Fix nullable JSON Schema const enums by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3355">koxudaxi/datamodel-code-generator#3355</a></li>
<li>Pin patchable generation seams by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3365">koxudaxi/datamodel-code-generator#3365</a></li>
<li>Cover utility helper behavior by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3375">koxudaxi/datamodel-code-generator#3375</a></li>
<li>Cover DefaultPutDict behavior by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3376">koxudaxi/datamodel-code-generator#3376</a></li>
<li>Cover validator config normalization by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3373">koxudaxi/datamodel-code-generator#3373</a></li>
<li>Avoid expensive runtime type checks by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3382">koxudaxi/datamodel-code-generator#3382</a></li>
<li>Avoid eager builtin formatter import by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3383">koxudaxi/datamodel-code-generator#3383</a></li>
<li>Avoid eager TOML parser import by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3384">koxudaxi/datamodel-code-generator#3384</a></li>
<li>Stabilize msgspec payload tests by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3385">koxudaxi/datamodel-code-generator#3385</a></li>
<li>Avoid eager input parser imports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3386">koxudaxi/datamodel-code-generator#3386</a></li>
<li>Avoid eager parser model imports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3388">koxudaxi/datamodel-code-generator#3388</a></li>
<li>Avoid eager AsyncAPI converter imports by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3389">koxudaxi/datamodel-code-generator#3389</a></li>
<li>Dispose parser on parse errors by <a
href="https://github.com/koxudaxi"><code>@koxudaxi</code></a> in <a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/pull/3390">koxudaxi/datamodel-code-generator#3390</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/53a25ab8ddb132ac68a2795247fc855b8f445d84"><code>53a25ab</code></a>
Fast path schema output (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3410">#3410</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/ee2087f32e6100f5c3642e7ea8506aa38e9df26c"><code>ee2087f</code></a>
Skip discriminator import scan (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3411">#3411</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/bdf5ddfc27f94a06ba8d289759193bb09daadd34"><code>bdf5ddf</code></a>
fix: quote self-referencing fields when --disable-future-imports is set
(<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3387">#3387</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/ad4ec877fa6708baebdaaf820171d24bfe5bf0cb"><code>ad4ec87</code></a>
Cache payload validation strategies (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3409">#3409</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/b191d52a0a1d83edeac9553119f70b2f5c131126"><code>b191d52</code></a>
Shard Python tests (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3408">#3408</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/29dd6d74c95dd7799d51f2c707db24862809eb23"><code>29dd6d7</code></a>
Cache parsed sources (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3407">#3407</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/93e2fe3cf5774d5e4d2083fac365e5bcbf0a647a"><code>93e2fe3</code></a>
Defer generation refresh (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3406">#3406</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/bb01d9c628f9077cc5dd72320ae60a18abd5b790"><code>bb01d9c</code></a>
Lazy root format exports (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3405">#3405</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/48237ed8c3af3bb58b5e6b274925ebb646412d3a"><code>48237ed</code></a>
Fast path JSON schemas (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3404">#3404</a>)</li>
<li><a
href="https://github.com/koxudaxi/datamodel-code-generator/commit/b21d106c88ac22f137cd4562389ad95a50c2e912"><code>b21d106</code></a>
Slot generation facts (<a
href="https://redirect.github.com/koxudaxi/datamodel-code-generator/issues/3403">#3403</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/koxudaxi/datamodel-code-generator/compare/0.34.0...0.64.0">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/e2b-dev/E2B/network/alerts).
</details>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com>
Co-authored-by: Claude <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) |