From 81663faf274efc18f3e26905f85ff0f661275d59 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 28 Sep 2026 15:34:50 +0800 Subject: [PATCH] Support custom E2B endpoints for managed sandboxes --- apps/docs/content/docs/admin-api.mdx | 6 ++- apps/docs/content/docs/configure.mdx | 6 ++- apps/docs/content/docs/install.mdx | 2 + apps/docs/content/docs/public-api.mdx | 2 +- apps/docs/content/guide-sources.json | 16 +++--- apps/docs/openapi/core-api.yaml | 8 +++ apps/docs/openapi/sources.json | 4 +- .../features/overview/getting-started.test.ts | 2 +- .../sandbox/SandboxDeploymentSettings.tsx | 5 +- .../sandbox/SandboxSetupWizard.test.ts | 18 +++++++ .../features/sandbox/SandboxSetupWizard.tsx | 32 ++++++++++-- apps/web/src/lib/locale-strings.ts | 4 ++ contracts/agents-api/core.openapi.yaml | 8 +++ contracts/agents-api/sandbox-deployment.md | 16 ++++-- deploy/install/install.py | 11 ++-- deploy/install/sandbox_setup.py | 33 ++++++++++++ deploy/install/test_sandbox_setup.py | 12 +++++ docs/api/README.md | 2 +- docs/api/web-management.md | 6 ++- docs/configuration.md | 6 ++- docs/getting-started/install.md | 2 + .../agents-client/src/sandbox-client.test.ts | 4 +- packages/agents-client/src/sandbox-client.ts | 13 +++-- .../agents-api/cmd/server/managed_setup.go | 3 +- .../internal/api/sandbox_deployment_setup.go | 4 +- .../db/queries/sandbox_deployment_setup.sql | 1 + .../agents-api/internal/db/sqlc/models.go | 2 + .../db/sqlc/runtime_deployment.sql.go | 4 +- .../internal/db/sqlc/runtime_nodes.sql.go | 4 +- .../db/sqlc/sandbox_deployment_setup.sql.go | 9 +++- .../internal/sandbox/e2b/endpoint.go | 52 +++++++++++++++++++ .../internal/sandbox/e2b/endpoint_test.go | 37 +++++++++++++ .../internal/sandbox/e2b/provider.go | 9 ++-- .../internal/store/runtime_node_types.go | 2 + .../store/sandbox_deployment_mutations.go | 12 +++++ .../store/sandbox_deployment_setup.go | 10 +++- .../store/sandbox_deployment_switch_test.go | 37 +++++++++++++ .../migrations/000079_e2b_custom_endpoint.sql | 18 +++++++ .../agents-api/tools/e2b-provider/README.md | 4 ++ .../tools/e2b-provider/deployment_test.py | 23 +++++++- .../agents-api/tools/e2b-provider/provider.py | 15 ++++-- .../tools/e2b-provider/provider_test.py | 26 ++++++++++ services/agents-api/tools/e2b-provider/sdk.py | 13 +++-- .../agents-api/tools/e2b-provider/state.py | 16 ++++-- .../tools/e2b-provider/state_test.py | 19 +++++++ 45 files changed, 482 insertions(+), 56 deletions(-) create mode 100644 apps/web/src/features/sandbox/SandboxSetupWizard.test.ts create mode 100644 services/agents-api/internal/sandbox/e2b/endpoint.go create mode 100644 services/agents-api/internal/sandbox/e2b/endpoint_test.go create mode 100644 services/agents-api/migrations/000079_e2b_custom_endpoint.sql diff --git a/apps/docs/content/docs/admin-api.mdx b/apps/docs/content/docs/admin-api.mdx index 1e9b830cc..29b18a34c 100644 --- a/apps/docs/content/docs/admin-api.mdx +++ b/apps/docs/content/docs/admin-api.mdx @@ -95,7 +95,11 @@ Runtime selection. POST initializes it; PUT replaces the complete selection usin Response `resources.allocations` and `resources.pending` are cleanup counts, not CPU, memory or disk settings. E2B accepts a write-only key and exact template build instead of a node Runtime release, and provisions without a node installation. E2B -may omit `resources` to adopt the validated build's CPU and memory; responses show +may omit `resources` to adopt the validated build's CPU and memory. An E2B-compatible +service may also supply paired `e2b.api_url` and `e2b.domain`; omitted selectors +use official E2B. Responses expose these addresses but never the key, and changing +them requires the same drained maintenance transition as changing the template. +Responses show the build as read at selection time in `e2b.template_build`. Microsandbox responses return its idle `suspension` policy; other providers return null. diff --git a/apps/docs/content/docs/configure.mdx b/apps/docs/content/docs/configure.mdx index 11653093f..c8d9876b1 100644 --- a/apps/docs/content/docs/configure.mdx +++ b/apps/docs/content/docs/configure.mdx @@ -159,7 +159,11 @@ file. The database owns the saved values. E2B takes no `runtime`, and Web sends no resources for it: Core adopts the CPU and memory of the ready template build `template-id:build-uuid`, and supplied values must -match it. Docker has no separate disk quota. +match it. For an E2B-compatible service, enter both its HTTPS API origin and +sandbox data-plane domain in the setup wizard. Leaving both blank uses official +E2B. The API host must be the data-plane domain or one of its subdomains. +Changing either address requires draining the deployment in maintenance. +Docker has no separate disk quota. Changing the provider, the size or the Runtime applies to the whole deployment; see the **Nodes** page, and note that this flow will change in a coming release. A successful diff --git a/apps/docs/content/docs/install.mdx b/apps/docs/content/docs/install.mdx index 4778a096b..9dd105b93 100644 --- a/apps/docs/content/docs/install.mdx +++ b/apps/docs/content/docs/install.mdx @@ -116,6 +116,8 @@ and rerunning `install.sh` accepts no flag except `--install-dir`. | `--accept-docker-risks` | | With `--sandbox docker`: accept Docker's [weaker isolation](#sandbox-backend) without the prompt; required without a terminal | | `--e2b-api-key-file FILE` | | With `--sandbox e2b`: absolute path of a private file (no group or other access, at most 4 KiB) holding the E2B API key | | `--e2b-template ID:BUILD` | | With `--sandbox e2b`: the ready template build, `template-id:build-uuid` | +| `--e2b-api-url URL` | Official E2B | With `--sandbox e2b`: compatible service HTTPS API origin; use together with `--e2b-domain` | +| `--e2b-domain DOMAIN` | Official E2B | With `--sandbox e2b`: sandbox data-plane DNS suffix; use together with `--e2b-api-url` | | `--core-only` | | [Mode](#modes): Core and PostgreSQL, without Web | | `--web-only` | | [Mode](#modes): Web only, connected to an existing Core. Needs `--core-url` and `--core-key-file` | | `--core-url URL` | | With `--web-only`: origin of the existing Core, HTTPS or loopback HTTP. Seeds `web.core_url` | diff --git a/apps/docs/content/docs/public-api.mdx b/apps/docs/content/docs/public-api.mdx index 13cec4d9c..977e5fc76 100644 --- a/apps/docs/content/docs/public-api.mdx +++ b/apps/docs/content/docs/public-api.mdx @@ -54,7 +54,7 @@ with the Core key. | `projects/{project_id}/environments/{environment_id}/executor-credentials[/{key_id}]` | Executor credentials for a self-hosted Environment | [Executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-executor-credentials.md) | | `installation` | Public URL, API base URL, source commit, the installer's process settings and what is bound to the public URL; available before any deployment | [Installation](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/installation.md) | | `metrics` | Core's own process metrics | [Core metrics](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core-metrics.md) | -| `sandbox/deployment[/maintenance]`, `sandbox/enrollment-tokens`, `sandbox/nodes[/{node_id}[/allocations]]` | Sandbox deployment, node enrollment tokens and nodes | [Sandbox deployment](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/sandbox-deployment.md), [node operations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md), [node host history](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/node-host-history.md) | +| `sandbox/deployment[/maintenance]`, `sandbox/enrollment-tokens`, `sandbox/nodes[/{node_id}[/allocations]]` | Sandbox deployment, including E2B API endpoint and data-plane domain, node enrollment tokens and nodes | [Sandbox deployment](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/sandbox-deployment.md), [node operations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md), [node host history](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/node-host-history.md) | | `harnesses`, `harnesses/{harness}/model-provider` | Supported harnesses and each harness's deployment default model provider (write-only key) | [Model execution](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/model-execution.md#deployment-defaults) | ## Machine connection API diff --git a/apps/docs/content/guide-sources.json b/apps/docs/content/guide-sources.json index e768b0c2b..52367c9b3 100644 --- a/apps/docs/content/guide-sources.json +++ b/apps/docs/content/guide-sources.json @@ -4,11 +4,11 @@ "docs/getting-started/README.md": "e4ca3bcbfdcbf3832c1d9e0525f98083ca11a4e42148f9f45090bbe1bc0f2b5c", "docs/design-principles.md": "5e1bff3cdcc77cd02ad692f67977cb4e0a5654989982d722bac0e3c79dbf82cb", "docs/web/architecture.md": "23e5758d72687c2c821975f326f24a789545ce0b0f566802cbe901edf2ebe3f1", - "docs/getting-started/install.md": "15cfe81d4005a7337ac82bf6c35a8b441e9f9b0f2a2299021ad6c539f3f336a3", - "docs/configuration.md": "7fe061b93a09c0f7baaa70f33f0863d8b29d308bbbc9b0e42b35cbef0ef0c648", + "docs/getting-started/install.md": "e3e26cb260d48308c12687eb3975d9bb1004c894edca449a6c5d0f66ccbf33f5", + "docs/configuration.md": "d892fc8dda2ead6041b154e0f8104a7720e0df96d2058c161c86403f774f7752", "docs/web/core-connection.md": "46f86520e70bf41079708e2c398655aa9087ad9488c8e661d9a73035d11098c4", "docs/getting-started/quickstart.md": "5effeffcfb343f7939910f0c1758e1492d49e7bd66c409b1819cefe67af592d7", - "docs/api/README.md": "f8c3872b012af7c490a06578c7d56560e0ec5998865ef043523bc66cc10ae4be", + "docs/api/README.md": "f6b22aec751a5e733e21dcbe8991747a032bcab0a9e7b5b7814711b47d5d664e", "contracts/agents-api/execution-tools.md": "e0b61f6c0c236c362186c5f6d0ad69a16dd8a8d9afabe1329a1fe22e5f4a71e9", "docs/api/public-agent-api.md": "18829b366bd2e49fe5beb76dc8a709bc1aa8e35d4fbfd2484a0c52b46cf75417", "contracts/agents-api/environments.md": "89ffc903ee44d00ee87c16950202a3ca4c8345160780dcdd4ed2a71b25b1c2dc", @@ -16,7 +16,7 @@ "docs/getting-started/self-hosted.md": "3f1b402cf28bb320ba414eaf36110e9df4d792b81da0456a3c42d469357d8d99", "apps/web/public/onboarding/monitor-en.webp": "29dc220cb1250c7016b7c4bf7f30e9510b07c07b814a48c3aa9303b2d76f20b2", "docs/web/README.md": "3b96e673591da67a6f666dddafd49a565c066c347bdfa2eaba0f92ab03daf514", - "docs/api/web-management.md": "5480bfcf9dd24a8ffc8d442e4d8582e007dadd22dba6a686d71b4bede0616506", + "docs/api/web-management.md": "c15abb5c82d6ab3c3206f7f4912b3162f0408220b23e668e80af12e11115cba0", "contracts/agents-api/runtime-observability-api.md": "cd46e777fe716a7e76374f4655c96dfbad9d8be1bfca6203ed6118ab87874c2b", "docs/getting-started/operations.md": "abc965e6ba19d2a2c148c8b5afde7203ca39891383840d85379d0d433b39df1d", "apps/docs/scripts/guides.json": "9d1ba424f8fadd65ffa6175eaa9c7bc4878d3e1437971099812c984e1e593080" @@ -28,15 +28,15 @@ "content/docs/concepts.zh.mdx": "c60644f395dbac5ee607cdb053249e05ec8352d28de97ef1c38d6c1e4443bbb1", "content/docs/execution-model.mdx": "b3c928e353e173aa7eea5c4fbad06b6896fd1e97f69c87701de8af39fae054fe", "content/docs/execution-model.zh.mdx": "9976a9203c6a83d49c2c753e3a8528a369078e87aeb88850654da948ac8c17e1", - "content/docs/install.mdx": "439809c11f5d805e6c6f964c779ec01468dcdad88bd11a56bb180a6e6a13b98a", + "content/docs/install.mdx": "2b896e56ce970255605a40f6f6ea57228e5309bfd6aef9cc00776453ab218275", "content/docs/install.zh.mdx": "7ed0b726cf721b18e85b89145ef8ebd2f5d382afd0489633a7b63962aa2fc4ed", - "content/docs/configure.mdx": "4ab159c54f7b9c6e1a2d0c3873de5e3641b48c4e1badf47f63311bef94f8778f", + "content/docs/configure.mdx": "9180f070dadaa5deda52182fff0f8aa7a1143bf5c8f55f48d72bf26f17b4b20c", "content/docs/configure.zh.mdx": "a0c3a1df3e9f75e672e1d0a8c68c82c24ac36a8479d10a951de441960d07927f", "content/docs/bootstrap-projects-keys.mdx": "4a39e95986851828e3fbcc0550398146de9bb37cfe22f10df566c51a911ca9ed", "content/docs/bootstrap-projects-keys.zh.mdx": "509b4dcea4c865d6834bcaef78baa681180d48274931230f958a39e46011c88f", "content/docs/quickstart.mdx": "92f62b5df213c363bd7148de0462b2ca66f2054f4458d790196e75daa678bb24", "content/docs/quickstart.zh.mdx": "c5f119b100b102918a45763834d0ed7fd436479fb411e16a62a408411dbcb3e1", - "content/docs/public-api.mdx": "a3d39d873054614bdf410d8c2a1e5c33320f1f3bffe155657b64579ea28fdc48", + "content/docs/public-api.mdx": "ae6e7279136625e9030603ae0399edd97690e905d55ee8787caedfea01306b7c", "content/docs/public-api.zh.mdx": "fb0efb9be2fba3f4a3dc71bf053d2c7eddffbfe010f59fa5a1ae60a3f068545b", "content/docs/agents-and-tools.mdx": "78789f7e6a1d327ff514d872b9f8b55a88754ffbfc34b7b10e5f7c115f242e3c", "content/docs/agents-and-tools.zh.mdx": "cc8d4e0ef0ea894f068f3bbd143d5ee70c6346bf98f425b1d5fe07ec3931f128", @@ -51,7 +51,7 @@ "public/images/source/apps/web/public/onboarding/monitor-en.webp": "29dc220cb1250c7016b7c4bf7f30e9510b07c07b814a48c3aa9303b2d76f20b2", "content/docs/console.mdx": "a708f26941840375299f14b26dc69a278c1f44a7aedca90d50bb806ba05685c0", "content/docs/console.zh.mdx": "72f953ba46dcc95933773ac0c4dffdc0d0a499be9d9a1cb9467db2659f1fa10d", - "content/docs/admin-api.mdx": "23b0ebdbb3ab93cee6f93e083026318465fae581546de5af9953fbf779444452", + "content/docs/admin-api.mdx": "242265ebfa548b4fca41973b34822135434c25be679dad8ed0f6ccec8e9e4d46", "content/docs/admin-api.zh.mdx": "07006de30df3080e54c3cd79ed0ea5337dce557aa2f11afea84edb7f92884d38", "content/docs/observability.mdx": "4894808be45568b3d6e1039183350a5315dbbde1144db94cc592eb8bd8c433e4", "content/docs/observability.zh.mdx": "9574b3412a5754f41ab2d3a16062ac1cad06766be481a8e6bff1b84acbde7e7e", diff --git a/apps/docs/openapi/core-api.yaml b/apps/docs/openapi/core-api.yaml index 472e1f59e..445c9cad1 100644 --- a/apps/docs/openapi/core-api.yaml +++ b/apps/docs/openapi/core-api.yaml @@ -4388,6 +4388,10 @@ components: properties: api_key: type: string + api_url: + type: string + domain: + type: string template: type: string type: object @@ -5131,8 +5135,12 @@ components: type: object store.SandboxE2BView: properties: + api_url: + type: string credential_configured: type: boolean + domain: + type: string template: type: string template_build: diff --git a/apps/docs/openapi/sources.json b/apps/docs/openapi/sources.json index ddcaef8ac..9eda88e73 100644 --- a/apps/docs/openapi/sources.json +++ b/apps/docs/openapi/sources.json @@ -1,7 +1,7 @@ { "sources": { "contracts/agents-api/openapi.yaml": "a8896e8b67d78ed0608c7cc5636077ec33f4b4895a04bd6cfb8b038afe941e01", - "contracts/agents-api/core.openapi.yaml": "488a50fa308194224ee5ee879b7f6e38166d1b08dedc2bbc5bb39f38a844c87f", + "contracts/agents-api/core.openapi.yaml": "9dda7145c814f41564459fc721eab5f8de4fbbde19377b8d393330aed5b62fbb", "contracts/agents-api/runtime.openapi.yaml": "b3fb025b20a0bcf4635d60339479b2b07f6dbed80f51e72f40a63b31e42b5f4a" }, "outputs": { @@ -40,7 +40,7 @@ "content/docs/api-reference/core/vaults.mdx": "12d0aac96cd4a3472ef4eaa287f7945f78862017956e79bfc3ed0ef8e772e78a", "content/docs/api-reference/core/credentials.mdx": "27d586b9a8fd738b9947802f32c80458195370252886bc26abedccf2125ee981", "content/docs/api-reference/core/sandbox-manager.mdx": "36f2895b9969d27be843d5cebf935bf14ca9400ad076e56074a25905e3df5d1b", - "openapi/core-api.yaml": "9872f80cec2661c8539b6652a294c16c668f581399afd20f76b198279faae9ed", + "openapi/core-api.yaml": "275df1b60daf38d35f81db0c183fffca8984bf552f8638d820e4e52d4281d6fe", "content/docs/api-reference/core/meta.json": "588b163ed2de2100b0f8376688274f80698a6bf6ac037eef56d17c93fa8a5062", "content/docs/api-reference/core/index.mdx": "ddf0a8d87a6c21761beac703a32f2a40486a1c74abb341bd726f40c44a866405", "content/docs/api-reference/machine/sandbox-node.mdx": "5b367a08d43cea82686345d2ba1c75cc90627a7b2970fe42a0df9d77fb3e1e80", diff --git a/apps/web/src/features/overview/getting-started.test.ts b/apps/web/src/features/overview/getting-started.test.ts index 04c6905f5..4b29eff2f 100644 --- a/apps/web/src/features/overview/getting-started.test.ts +++ b/apps/web/src/features/overview/getting-started.test.ts @@ -34,7 +34,7 @@ describe("Getting started steps", () => { expect(sandboxes(fleet(deployment(), []))).toMatchObject({ state: "todo", action: "add-node" }); expect(sandboxes(fleet(deployment(), [node("n1", { provider_ready: false }), node("n2", { online: false })]))).toMatchObject({ state: "todo", action: "nodes" }); expect(sandboxes(fleet(deployment()))).toMatchObject({ state: "done" }); - const e2b = (status: string | null) => deployment({ provider: "e2b", mode: "direct", e2b: { template: "t", credential_configured: true, template_build: { status, resources: { cpus: 2, memory_mib: 2048, root_disk_mib: null } } } }); + const e2b = (status: string | null) => deployment({ provider: "e2b", mode: "direct", e2b: { template: "t", api_url: "https://api.e2b.app", domain: "e2b.app", credential_configured: true, template_build: { status, resources: { cpus: 2, memory_mib: 2048, root_disk_mib: null } } } }); expect(sandboxes(fleet(e2b("building"), []))).toMatchObject({ state: "todo", cloud: true }); expect(sandboxes(fleet(e2b("ready"), []))).toMatchObject({ state: "done", cloud: true }); // Saved before Core recorded the build: Core admitted it, so it counts as ready. diff --git a/apps/web/src/features/sandbox/SandboxDeploymentSettings.tsx b/apps/web/src/features/sandbox/SandboxDeploymentSettings.tsx index 2f4043148..f570839aa 100644 --- a/apps/web/src/features/sandbox/SandboxDeploymentSettings.tsx +++ b/apps/web/src/features/sandbox/SandboxDeploymentSettings.tsx @@ -48,6 +48,8 @@ export function SandboxDeploymentSettings({ deployment, disabled, fresh, onMaint {deployment.provider === "e2b" ?
{t("Immutable Runtime template")}
{deployment.e2b?.template || t("Unknown state")}
+
{t("Sandbox API URL")}
{deployment.e2b?.api_url}
+
{t("Sandbox data-plane domain")}
{deployment.e2b?.domain}
{t("Template build")}
{[ @@ -68,7 +70,8 @@ export function SandboxDeploymentSettings({ deployment, disabled, fresh, onMaint {!changing ? : <> { + it("accepts the official default and a paired compatible service", () => { + expect(validEndpoint("", "")).toBe(true); + expect(validEndpoint("https://sandbox-test.sandbase.ai", "sandbox-test.sandbase.ai")).toBe(true); + }); + + it("rejects incomplete, local and malformed selectors before submission", () => { + expect(validEndpoint("https://sandbox-test.sandbase.ai", "")).toBe(false); + expect(validEndpoint("https://localhost", "sandbox-test.sandbase.ai")).toBe(false); + expect(validEndpoint("https://127.0.0.1", "sandbox-test.sandbase.ai")).toBe(false); + expect(validEndpoint("https://bad-.example", "good.example")).toBe(false); + expect(validEndpoint("https://good.example", "-bad.example")).toBe(false); + expect(validEndpoint("https://unrelated.example", "sandbox-test.sandbase.ai")).toBe(false); + }); +}); diff --git a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx index 2f5dedb54..96e146e03 100644 --- a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx +++ b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx @@ -32,6 +32,21 @@ function validTemplate(value: string): boolean { return build !== undefined && /[^0-]/.test(build); } +export function validEndpoint(apiURL: string, domain: string): boolean { + const publicName = (host: string) => host.length <= 253 && host.includes(".") && !/^[0-9.]+$/.test(host) && + !host.endsWith(".local") && !host.endsWith(".localhost") && + host.split(".").every((label) => label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label)); + if (!apiURL && !domain) return true; + if (!apiURL || !domain || apiURL.length > 512 || !publicName(domain)) return false; + try { + const url = new URL(apiURL); + return url.protocol === "https:" && url.origin === apiURL && !url.username && !url.password && + !url.port && publicName(url.hostname) && (url.hostname === domain || url.hostname.endsWith(`.${domain}`)); + } catch { + return false; + } +} + /** * Per-sandbox presets around the deployment default: half and double of it. * Disks apply to microsandbox only, the one provider that enforces them. @@ -79,7 +94,7 @@ function presetOf(provider: SandboxProvider, resources: SandboxResources): Prese export function SandboxSetupWizard({ coreUrl, current, disabled, switching = false, onSubmit }: { /** The deployment's read-only Core address. */ coreUrl: string; - current?: { provider: SandboxProvider; specification?: SandboxSpecification; e2bTemplate?: string }; + current?: { provider: SandboxProvider; specification?: SandboxSpecification; e2bTemplate?: string; e2bAPIURL?: string; e2bDomain?: string }; disabled: boolean; switching?: boolean; onSubmit: (input: InitializeSandboxDeployment) => Promise; @@ -94,6 +109,8 @@ export function SandboxSetupWizard({ coreUrl, current, disabled, switching = fal const [size, setSize] = useState(current?.specification ? presetOf(current.provider, current.specification.resources) ?? "current" : "standard"); const [apiKey, setApiKey] = useState(""); const [template, setTemplate] = useState(current?.e2bTemplate ?? ""); + const [apiURL, setAPIURL] = useState(current?.e2bAPIURL === "https://api.e2b.app" ? "" : current?.e2bAPIURL ?? ""); + const [domain, setDomain] = useState(current?.e2bDomain === "e2b.app" ? "" : current?.e2bDomain ?? ""); const [runtime, setRuntime] = useState>({}); const [busy, setBusy] = useState(false); const [dockerConfirmed, setDockerConfirmed] = useState(current?.provider === "docker"); @@ -113,7 +130,7 @@ export function SandboxSetupWizard({ coreUrl, current, disabled, switching = fal const needsRuntime = provider === "docker" || provider === "microsandbox"; const runtimeReady = !needsRuntime || isRuntimeRelease(release); // Core keeps no key across a change: E2B always needs one. - const e2bReady = provider !== "e2b" || (apiKey.trim().length > 0 && validTemplate(template.trim())); + const e2bReady = provider !== "e2b" || (apiKey.trim().length > 0 && validTemplate(template.trim()) && validEndpoint(apiURL.trim(), domain.trim())); // Core sizes E2B sandboxes from the template build, so E2B sends no resources. const sized = provider !== null && provider !== "e2b"; const sizeReady = provider !== null && (!sized || validSandboxResources(provider, resources)); @@ -151,7 +168,8 @@ export function SandboxSetupWizard({ coreUrl, current, disabled, switching = fal provider, ...(sized ? { resources } : {}), ...(needsRuntime ? { runtime: release as SandboxRuntimeRelease } : {}), - ...(provider === "e2b" ? { e2b: { api_key: apiKey.trim(), template: template.trim() } } : {}), + ...(provider === "e2b" ? { e2b: { api_key: apiKey.trim(), template: template.trim(), + ...(apiURL.trim() ? { api_url: apiURL.trim(), domain: domain.trim() } : {}) } } : {}), }); } catch (error) { // A configuration Core rejected is explained here; the page reports every other failure. @@ -197,6 +215,12 @@ export function SandboxSetupWizard({ coreUrl, current, disabled, switching = fal setTemplate(event.target.value)} placeholder="oac-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b" autoComplete="off" spellCheck={false} aria-invalid={Boolean(template && !validTemplate(template.trim()))} /> + + setAPIURL(event.target.value)} placeholder="https://sandbox.example.com" autoComplete="off" spellCheck={false} /> + + + setDomain(event.target.value)} placeholder="sandbox.example.com" autoComplete="off" spellCheck={false} /> +