From 03f91207d8ded505cc3b6a5fabf68821b628b780 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 29 Sep 2026 10:35:36 +0800 Subject: [PATCH 1/4] Guide E2B provider setup with SDK template discovery --- .../api-reference/core/sandbox-manager.mdx | 10 +- apps/docs/content/docs/public-api.mdx | 2 +- apps/docs/content/guide-sources.json | 4 +- apps/docs/openapi/core-api.yaml | 125 ++++++++++++++++++ apps/docs/openapi/sources.json | 6 +- apps/docs/scripts/check-python.test.mjs | 2 +- .../features/sandbox/SandboxSetupWizard.tsx | 94 +++++++++++-- apps/web/src/lib/locale-strings.ts | 13 ++ contracts/agents-api/core.openapi.yaml | 115 ++++++++++++++++ contracts/agents-api/sandbox-deployment.md | 13 ++ docs/api/README.md | 2 +- .../agents-client/src/sandbox-client.test.ts | 20 +++ packages/agents-client/src/sandbox-client.ts | 20 +++ services/agents-api/cmd/server/main.go | 12 ++ services/agents-api/internal/api/handler.go | 1 + .../internal/api/sandbox_e2b_discovery.go | 85 ++++++++++++ .../api/sandbox_e2b_discovery_test.go | 58 ++++++++ .../internal/api/sandbox_manager.go | 2 + .../internal/sandbox/e2b/discovery_test.go | 36 +++++ .../internal/sandbox/e2b/provider.go | 85 +++++++++++- .../agents-api/tools/e2b-provider/README.md | 11 ++ .../tools/e2b-provider/discovery_test.py | 85 ++++++++++++ .../agents-api/tools/e2b-provider/provider.py | 17 ++- services/agents-api/tools/e2b-provider/sdk.py | 51 ++++++- 24 files changed, 840 insertions(+), 29 deletions(-) create mode 100644 services/agents-api/internal/api/sandbox_e2b_discovery.go create mode 100644 services/agents-api/internal/api/sandbox_e2b_discovery_test.go create mode 100644 services/agents-api/internal/sandbox/e2b/discovery_test.go create mode 100644 services/agents-api/tools/e2b-provider/discovery_test.py diff --git a/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx b/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx index f05ee6363..5cb345694 100644 --- a/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx +++ b/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx @@ -36,6 +36,14 @@ _openapi: Requires the current generation. Maintenance preserves execution, cleanup and history. Resume requires successfully activated provider configuration. + - content: >- + Core key only. Uses a transient E2B credential and endpoint through + the pinned SDK helper; returns safe template metadata. Does not save + the credential or allocate compute. + - content: >- + Core key only. Reads one template through the pinned SDK helper with a + transient E2B credential. Returns ready builds only, without + allocating compute. - content: >- Core key only. Does not grant project resource access. Responses contain only explicit safe fields. @@ -58,4 +66,4 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} - \ No newline at end of file + \ No newline at end of file diff --git a/apps/docs/content/docs/public-api.mdx b/apps/docs/content/docs/public-api.mdx index 977e5fc76..2ac78b736 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, 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) | +| `sandbox/deployment[/maintenance]`, `sandbox/e2b/templates[/{template_id}/builds]`, `sandbox/enrollment-tokens`, `sandbox/nodes[/{node_id}[/allocations]]` | Core Web server or operator scripts with the Core key: sandbox deployment and read-only E2B template discovery, 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 52367c9b3..d55b0d1d8 100644 --- a/apps/docs/content/guide-sources.json +++ b/apps/docs/content/guide-sources.json @@ -8,7 +8,7 @@ "docs/configuration.md": "d892fc8dda2ead6041b154e0f8104a7720e0df96d2058c161c86403f774f7752", "docs/web/core-connection.md": "46f86520e70bf41079708e2c398655aa9087ad9488c8e661d9a73035d11098c4", "docs/getting-started/quickstart.md": "5effeffcfb343f7939910f0c1758e1492d49e7bd66c409b1819cefe67af592d7", - "docs/api/README.md": "f6b22aec751a5e733e21dcbe8991747a032bcab0a9e7b5b7814711b47d5d664e", + "docs/api/README.md": "0ce9d3dc76e84cb6d768e23589ff48ae9d5cb7079b30994bcd895b77d17afbf0", "contracts/agents-api/execution-tools.md": "e0b61f6c0c236c362186c5f6d0ad69a16dd8a8d9afabe1329a1fe22e5f4a71e9", "docs/api/public-agent-api.md": "18829b366bd2e49fe5beb76dc8a709bc1aa8e35d4fbfd2484a0c52b46cf75417", "contracts/agents-api/environments.md": "89ffc903ee44d00ee87c16950202a3ca4c8345160780dcdd4ed2a71b25b1c2dc", @@ -36,7 +36,7 @@ "content/docs/bootstrap-projects-keys.zh.mdx": "509b4dcea4c865d6834bcaef78baa681180d48274931230f958a39e46011c88f", "content/docs/quickstart.mdx": "92f62b5df213c363bd7148de0462b2ca66f2054f4458d790196e75daa678bb24", "content/docs/quickstart.zh.mdx": "c5f119b100b102918a45763834d0ed7fd436479fb411e16a62a408411dbcb3e1", - "content/docs/public-api.mdx": "ae6e7279136625e9030603ae0399edd97690e905d55ee8787caedfea01306b7c", + "content/docs/public-api.mdx": "03007980296ed7bd4b601c26f3c992ea38f64ccb16a20306533b80011e869976", "content/docs/public-api.zh.mdx": "fb0efb9be2fba3f4a3dc71bf053d2c7eddffbfe010f59fa5a1ae60a3f068545b", "content/docs/agents-and-tools.mdx": "78789f7e6a1d327ff514d872b9f8b55a88754ffbfc34b7b10e5f7c115f242e3c", "content/docs/agents-and-tools.zh.mdx": "cc8d4e0ef0ea894f068f3bbd143d5ee70c6346bf98f425b1d5fe07ec3931f128", diff --git a/apps/docs/openapi/core-api.yaml b/apps/docs/openapi/core-api.yaml index 445c9cad1..24cfc9eef 100644 --- a/apps/docs/openapi/core-api.yaml +++ b/apps/docs/openapi/core-api.yaml @@ -3410,6 +3410,93 @@ paths: $ref: '#/components/schemas/store.SandboxMaintenanceRequest' description: Maintenance state required: true + /core/v1/sandbox/e2b/templates: + post: + description: Core key only. Uses a transient E2B credential and endpoint through the pinned SDK helper; returns safe template metadata. Does not save the credential or allocate compute. + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/api.SandboxE2BDiscoveryResult' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/api.CoreErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/api.CoreErrorResponse' + '503': + description: Service Unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/api.CoreErrorResponse' + security: + - DeploymentAdminAuth: [] + summary: List templates visible to an E2B credential + tags: + - Sandbox Manager + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/api.SandboxE2BDiscoveryInput' + description: Transient E2B connection + required: true + /core/v1/sandbox/e2b/templates/{template_id}/builds: + post: + description: Core key only. Reads one template through the pinned SDK helper with a transient E2B credential. Returns ready builds only, without allocating compute. + parameters: + - schema: + type: string + description: Template ID + in: path + name: template_id + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/api.SandboxE2BDiscoveryResult' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/api.CoreErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/api.CoreErrorResponse' + '503': + description: Service Unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/api.CoreErrorResponse' + security: + - DeploymentAdminAuth: [] + summary: List ready builds for an E2B template + tags: + - Sandbox Manager + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/api.SandboxE2BDiscoveryInput' + description: Transient E2B connection + required: true /core/v1/sandbox/enrollment-tokens: post: description: Core key only. Does not grant project resource access. Responses contain only explicit safe fields. @@ -4384,6 +4471,26 @@ components: runtime: $ref: '#/components/schemas/sandbox.RuntimeRelease' type: object + api.SandboxE2BDiscoveryInput: + properties: + api_key: + type: string + api_url: + type: string + domain: + type: string + type: object + api.SandboxE2BDiscoveryResult: + properties: + builds: + items: + $ref: '#/components/schemas/e2b.ReadyBuild' + type: array + templates: + items: + $ref: '#/components/schemas/e2b.TemplateSummary' + type: array + type: object api.SandboxE2BInput: properties: api_key: @@ -4653,6 +4760,24 @@ components: service: $ref: '#/components/schemas/coremetrics.ServiceState' type: object + e2b.ReadyBuild: + properties: + cpus: + type: integer + id: + type: string + memory_mib: + type: integer + type: object + e2b.TemplateSummary: + properties: + id: + type: string + names: + items: + type: string + type: array + type: object sandbox.DeploymentSpec: properties: resources: diff --git a/apps/docs/openapi/sources.json b/apps/docs/openapi/sources.json index 9eda88e73..480627266 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": "9dda7145c814f41564459fc721eab5f8de4fbbde19377b8d393330aed5b62fbb", + "contracts/agents-api/core.openapi.yaml": "814e9e628f1388e5a409a8402441f8d26cacf8f12a08285bb05974f1ddba302d", "contracts/agents-api/runtime.openapi.yaml": "b3fb025b20a0bcf4635d60339479b2b07f6dbed80f51e72f40a63b31e42b5f4a" }, "outputs": { @@ -39,8 +39,8 @@ "content/docs/api-reference/core/skills.mdx": "ed899ade7b032b1732b3097d81419a8fdbb9ead0594fca9f2107da644b97031f", "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": "275df1b60daf38d35f81db0c183fffca8984bf552f8638d820e4e52d4281d6fe", + "content/docs/api-reference/core/sandbox-manager.mdx": "466e37449faedc71e7c17e7da14264973a066cf7f770c018be0ba932a66bd5e5", + "openapi/core-api.yaml": "e3b735dff5879d31f66ad7ba360211a169a54e7a3a0fa3df4c71116ed0aac36e", "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/docs/scripts/check-python.test.mjs b/apps/docs/scripts/check-python.test.mjs index e6bce3365..75b0a9518 100644 --- a/apps/docs/scripts/check-python.test.mjs +++ b/apps/docs/scripts/check-python.test.mjs @@ -21,7 +21,7 @@ test('route gate and Python regressions honor the selected interpreter without s cwd: appRoot, env: { ...process.env, OAC_TEST_OFFICIAL_SDK_PYTHON: selected }, encoding: 'utf8', }) assert.equal(result.status, 0, result.stdout + result.stderr) - if (mode === 'routes') assert.match(result.stdout, /Contract operations:\s+130/) + if (mode === 'routes') assert.match(result.stdout, /Contract operations:\s+132/) else assert.match(result.stderr, /Ran 2 tests/) } const invocations = fs.readFileSync(log, 'utf8').trim().split('\n').map(line => JSON.parse(line)) diff --git a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx index 96e146e03..07511804e 100644 --- a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx +++ b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx @@ -1,9 +1,9 @@ -import type { InitializeSandboxDeployment, SandboxProvider, SandboxResources, SandboxRuntimeRelease, SandboxSpecification } from "@agents-core-web/agents-client"; +import type { InitializeSandboxDeployment, SandboxE2BReadyBuild, SandboxE2BTemplate, SandboxProvider, SandboxResources, SandboxRuntimeRelease, SandboxSpecification } from "@agents-core-web/agents-client"; import { useQuery } from "@tanstack/react-query"; import { AnimatePresence } from "motion/react"; import * as m from "motion/react-m"; import { ArrowLeft, ArrowRight, Box, Cloud, Cpu, Server, SlidersHorizontal, type LucideIcon } from "lucide-react"; -import { useId, useRef, useState, type FormEvent, type ReactNode } from "react"; +import { useEffect, useId, useRef, useState, type FormEvent, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; @@ -16,12 +16,23 @@ import type { MessageKey } from "../../lib/locale-strings"; import { sandboxConfigurationRejection } from "../../lib/sandbox-labels"; import { defaultSandboxResources, distributionRuntime, savedSpecification, validSandboxResources } from "./deployment-specification"; import { isRuntimeRelease, isRuntimeReleaseField, RUNTIME_RELEASE_FIELDS } from "./runtime-release"; +import { sandboxAdmin } from "./sandbox-queries"; import "./sandbox-wizard.css"; type Where = "nodes" | "direct"; type Step = "where" | "backend" | "e2b" | "size" | "review" | "advanced"; type Preset = "small" | "standard" | "large"; type Size = Preset | "current" | "custom"; +type E2BService = "sandbase" | "official" | "custom"; +const E2B_PRESETS = { + sandbase: { apiURL: "https://sandbox.sandbase.ai", domain: "sandbox.sandbase.ai" }, + official: { apiURL: "https://api.e2b.app", domain: "e2b.app" }, +} as const; +function e2bService(apiURL?: string): E2BService { + if (!apiURL) return "sandbase"; + if (apiURL === E2B_PRESETS.official.apiURL) return "official"; + return apiURL === E2B_PRESETS.sandbase.apiURL ? "sandbase" : "custom"; +} const MIB = 2 ** 20; const EASE = [0.16, 1, 0.3, 1] as const; @@ -109,8 +120,15 @@ 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 [service, setService] = useState(e2bService(current?.e2bAPIURL)); + const [apiURL, setAPIURL] = useState(current?.e2bAPIURL ?? E2B_PRESETS[e2bService(current?.e2bAPIURL) as keyof typeof E2B_PRESETS]?.apiURL ?? ""); + const [domain, setDomain] = useState(current?.e2bDomain ?? E2B_PRESETS[e2bService(current?.e2bAPIURL) as keyof typeof E2B_PRESETS]?.domain ?? ""); + const [templates, setTemplates] = useState([]); + const [builds, setBuilds] = useState([]); + const [selectedTemplate, setSelectedTemplate] = useState(current?.e2bTemplate?.split(":")[0] ?? ""); + const [discovery, setDiscovery] = useState<"idle" | "loading" | "ready" | "error">("idle"); + const [buildDiscovery, setBuildDiscovery] = useState<"idle" | "loading" | "ready" | "error">("idle"); + const [discoveryRetry, setDiscoveryRetry] = useState(0); const [runtime, setRuntime] = useState>({}); const [busy, setBusy] = useState(false); const [dockerConfirmed, setDockerConfirmed] = useState(current?.provider === "docker"); @@ -122,6 +140,35 @@ export function SandboxSetupWizard({ coreUrl, current, disabled, switching = fal const address = coreUrl || installation.data?.public_url || null; const configuration = installation.data?.configuration ?? null; + useEffect(() => { + setTemplates([]); setBuilds([]); setDiscovery("idle"); setBuildDiscovery("idle"); + if (provider !== "e2b" || !apiKey.trim() || !validEndpoint(apiURL.trim(), domain.trim())) return; + const controller = new AbortController(); + const timer = window.setTimeout(() => { + setDiscovery("loading"); + void sandboxAdmin.listE2BTemplates({ api_key: apiKey.trim(), api_url: apiURL.trim(), domain: domain.trim() }, { signal: controller.signal }) + .then((items) => { if (!controller.signal.aborted) { setTemplates(items); setDiscovery("ready"); } }) + .catch(() => { if (!controller.signal.aborted) setDiscovery("error"); }); + }, 500); + return () => { window.clearTimeout(timer); controller.abort(); }; + }, [provider, apiKey, apiURL, domain, discoveryRetry]); + + useEffect(() => { + setBuilds([]); setBuildDiscovery("idle"); + if (discovery !== "ready" || !selectedTemplate || !templates.some((item) => item.id === selectedTemplate)) return; + const controller = new AbortController(); + setBuildDiscovery("loading"); + void sandboxAdmin.listE2BReadyBuilds(selectedTemplate, { api_key: apiKey.trim(), api_url: apiURL.trim(), domain: domain.trim() }, { signal: controller.signal }) + .then((items) => { if (!controller.signal.aborted) { setBuilds(items); setBuildDiscovery("ready"); } }) + .catch(() => { if (!controller.signal.aborted) setBuildDiscovery("error"); }); + return () => controller.abort(); + }, [discovery, selectedTemplate, templates, apiKey, apiURL, domain, discoveryRetry]); + + function changeConnection(key: string, url: string, dataDomain: string) { + setApiKey(key); setAPIURL(url); setDomain(dataDomain); + setTemplate(""); setSelectedTemplate(""); setTemplates([]); setBuilds([]); + } + // A release the administrator entered comes first, then the saved one of the same backend, // then the one this console distributes (the release its node installer verifies). const matched = useQuery({ queryKey: ["sandbox-runtime-release"], queryFn: ({ signal }) => distributionRuntime(signal).catch(() => null), staleTime: Infinity, retry: false }); @@ -209,17 +256,44 @@ export function SandboxSetupWizard({ coreUrl, current, disabled, switching = fal page = (
+ + + - setApiKey(event.target.value)} /> + changeConnection(event.target.value, apiURL, domain)} /> + + {discovery === "loading" ?

{t("Loading templates…")}

: null} + {discovery === "error" ?

{t("Could not load templates. Check the key and provider connection.")}

: null} + {discovery === "ready" && templates.length === 0 ?

{t("No templates are visible to this key.")}

: null} + + - - setTemplate(event.target.value)} placeholder="oac-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b" autoComplete="off" spellCheck={false} aria-invalid={Boolean(template && !validTemplate(template.trim()))} /> + {buildDiscovery === "loading" ?

{t("Loading ready builds…")}

: null} + {buildDiscovery === "error" ?

{t("Could not load builds.")}

: null} + {buildDiscovery === "ready" && builds.length === 0 ?

{t("This template has no ready builds.")}

: null} + + - - setAPIURL(event.target.value)} placeholder="https://sandbox.example.com" autoComplete="off" spellCheck={false} /> + + changeConnection(apiKey, event.target.value, domain)} placeholder="https://sandbox.example.com" autoComplete="off" spellCheck={false} /> - setDomain(event.target.value)} placeholder="sandbox.example.com" autoComplete="off" spellCheck={false} /> + changeConnection(apiKey, apiURL, event.target.value)} placeholder="sandbox.example.com" autoComplete="off" spellCheck={false} />