6824cdf313
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>
2 lines
41 B
Plaintext
2 lines
41 B
Plaintext
0716edb9e840f110c5f87c186876c01e61553098
|