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>
API specs
Most files in this directory are owned by other repositories and are synced
here with Copybara (config in
../copy.bara.sky) — don't edit them by hand; change them in their
source repository and re-sync:
openapi.yml,envd/envd.yaml,envd/filesystem/,envd/process/are owned by the infra repository, pinned byinfra-ref.openapi-volumecontent.ymlis owned by the private belt repository, pinned bybelt-ref.
Fetches authenticate with a GitHub token when available (GITHUB_TOKEN, or
being logged in with gh auth login); the public infra specs also fetch
anonymously, while the volume-content spec needs a token with read access
to belt. When a fetch fails, make codegen warns and falls back to the
tracked copy.
make codegen re-fetches all of them at their pinned commits before
generating the clients, and the generated-files CI check fails if the
tracked copies don't match the pins. The files are stored byte-identical to
upstream. To update the specs, point the pin at a newer commit and re-run
make codegen. To fetch without regenerating:
pnpm fetch:api-spec # openapi.yml
pnpm fetch:envd-spec # envd spec
pnpm fetch:volume-spec # openapi-volumecontent.yml
E2B_INFRA_REF=main pnpm fetch:api-spec # try the latest without moving the pin
E2B_BELT_REF=main pnpm fetch:volume-spec
The remaining files (mcp-server.json, envd/buf-*.gen.yaml) are owned by
this repository. The SDK generate pipelines filter openapi.yml down to the
tags each SDK exposes with Redocly CLI (see ../redocly.yaml) before
generating the clients.