diff --git a/README.md b/README.md index 0170d3a94..af5f2fcc5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The OpenAI Agents API, open source and self-hosted. Run Codex, Claude Code or Mi - **Same API as OpenAI.** Set `OPENAI_BASE_URL` and the [official OpenAI SDK](https://developers.openai.com/api/docs/libraries), or plain HTTP, works unchanged. - **Your agent, your model.** Each Session runs Codex, Claude Code or MiniMax Code natively, as it runs on your machine, with the model provider you configure. See [Harness capabilities](https://openagentcore.dev/contracts/agents-api/harness-capabilities). -- **Compute that sleeps, workspaces that stay.** On microsandbox, which keeps every workspace on independent storage, idle sandboxes release compute and resume on a compatible node with their files intact. E2B pauses idle sandboxes in place. Docker and your own Linux, macOS or Windows machine also run Sessions. See [Sandbox Provider protocol](https://openagentcore.dev/docs/sandbox-provider). +- **Compute that sleeps, workspaces that stay.** On microsandbox, which keeps every workspace on independent storage, idle sandboxes release compute and resume on a compatible node with their files intact. E2B pauses idle sandboxes in place. Your own Linux, macOS or Windows machine also runs Sessions. See [Sandbox Provider protocol](https://openagentcore.dev/docs/sandbox-provider). - **Replaceable parts.** Sandbox Providers, workspace filesystems, Harnesses and model providers plug in through [defined protocols](AGENTS.md#protocols-at-every-boundary). ## Quickstart diff --git a/README.zh-CN.md b/README.zh-CN.md index a3d62b01b..0a420e5e2 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -16,7 +16,7 @@ - **与 OpenAI 相同的 API。** 设置 `OPENAI_BASE_URL` 后,[官方 OpenAI SDK](https://developers.openai.com/api/docs/libraries)或直接的 HTTP 调用无需改动即可使用。 - **你的 Agent,你的模型。** 每个 Session 原生运行 Codex、Claude Code 或 MiniMax Code,使用你配置的模型提供商,与在本机运行一致。详见 [Harness 能力](https://openagentcore.dev/zh/contracts/agents-api/harness-capabilities)。 -- **计算会休眠,工作区会保留。** 在 microsandbox 上(每个工作区都保存在独立存储中),空闲沙箱释放计算资源,并在兼容节点上带着文件恢复。E2B 会就地暂停空闲沙箱。Docker 以及你自己的 Linux、macOS 或 Windows 机器同样可以运行 Session。详见[沙箱提供商协议](https://openagentcore.dev/zh/docs/sandbox-provider)。 +- **计算会休眠,工作区会保留。** 在 microsandbox 上(每个工作区都保存在独立存储中),空闲沙箱释放计算资源,并在兼容节点上带着文件恢复。E2B 会就地暂停空闲沙箱。你自己的 Linux、macOS 或 Windows 机器同样可以运行 Session。详见[沙箱提供商协议](https://openagentcore.dev/zh/docs/sandbox-provider)。 - **部件可替换。** 沙箱提供商、工作区文件系统、Harness 和模型提供商都通过[既定协议](AGENTS.md#protocols-at-every-boundary)接入。 ## 快速开始 diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md index 462529674..28011b250 100644 --- a/apps/web/DESIGN.md +++ b/apps/web/DESIGN.md @@ -366,10 +366,9 @@ Dialogs are 448px Paper cards (960px when wide) with 8px corners, a 52px header - **ConfirmDialog**: the one grammar for destructive actions. The body states what will be deleted and its consequences; the footer holds Cancel (outline) and the confirm button (danger), whose label changes while busy. Core's reason for a rejection, or an uncertain-outcome warning, appears in red inside the dialog. The Skill page's delete dialogs follow the same grammar; deleting a whole Skill also requires typing its name. Archiving a project says in bold that it can't be undone, then how many active keys it revokes (the project read's count, or more when its loaded key list shows more) and that assets and accepted work stay; with active keys it too requires typing the project's name, shown in mono with its inner spaces kept (surrounding spaces are forgiven, Unicode compared in NFC). While the project list is read again Archive waits; if that read failed, a red line says the count may be out of date and Archive stays disabled. - **Key dialogs**: name fields carry their rules in a help tip and their problem in red underneath. The issued key appears in a read-only field with a copy button, under a notice that it is shown once; only "I've saved this key" dismisses it. Closing the dialog moves the key into a pending notice card on the page. - **Executor credential dialog** (640px): the shown-once notice, then a prompt to save the JSON privately before Done. Download credential file is primary; Copy credential is secondary. Installation commands are not repeated here. Done forgets the credential; closing preserves it in the pending card. The native installer reads the unchanged JSON file: its absolute path is entered during interactive installation or passed with `--credential-file`; tokens never enter command arguments. -- **Add node**: the Limits choice first (Recommended, which leaves the limits for Core to derive from the host once it enrolls, or Custom with the two numbers), then the one-time command in a Terminal block (expiry countdown and Copy command in its header), the three progress steps, and, once the installer's minute passes, an amber card with the reason and a copyable system-service log command. Below, the Host requirements Hairline disclosure is open until this browser has shown it once. The docker group's root-equivalent access is stated once, in **Use Docker instead of microsandbox?**. Do not expose an ordinary-user installation command or user-service prerequisites. The command downloads from the installation's public URL, never the browser's address, so it works as shown on any host. Until the installation is read, a line says it is being checked; a failed read, a public URL other machines can't use (loopback or not HTTPS), or a console without the provider's node files replaces the limits with one line saying why (the failed read with Try again), and the footer offers nothing to generate. Once the node is ready, while Getting started is open, one line under the green status names the next step (set a default model provider, or finish Getting started) with a text action to Settings → Models and credentials or the Overview. +- **Add node**: the Limits choice first (Recommended, which leaves the limits for Core to derive from the host once it enrolls, or Custom with the two numbers), then the one-time command in a Terminal block (expiry countdown and Copy command in its header), the three progress steps, and, once the installer's minute passes, an amber card with the reason and a copyable system-service log command. Below, the Host requirements Hairline disclosure is open until this browser has shown it once. Do not expose an ordinary-user installation command or user-service prerequisites. The command downloads from the installation's public URL, never the browser's address, so it works as shown on any host. Until the installation is read, a line says it is being checked; a failed read, a public URL other machines can't use (loopback or not HTTPS), or a console without the provider's node files replaces the limits with one line saying why (the failed read with Try again), and the footer offers nothing to generate. Once the node is ready, while Getting started is open, one line under the green status names the next step (set a default model provider, or finish Getting started) with a text action to Settings → Models and credentials or the Overview. - **Clean up the host**: after a node is removed, a dialog gives the host's uninstall command in the same Terminal block, a Graphite line that it deletes no sandboxes, volumes or images; the uninstaller prints what it keeps. The command requires root or sudo; there is no user-service alternative. A node enrolled with an earlier Core address adds an "Old Core address gone?" disclosure with the `--force` form. The command, too, downloads from the public URL, which the dialog reads again if it is not at hand: until then one line says it is being checked, a failed read says so with Try again, and a public URL other machines can't use (loopback, or none) gets a line saying the service stays on the host and no command can be given. Done dismisses it and focus returns to the page heading. -- **Use Docker?**: choosing a backend that declares cautions in sandbox setup, Docker today, titles the dialog "Use {backend}?" and lists each caution as a plain list item: for Docker, that containers share the host kernel and that the node's account joins the docker group, which is root-equivalent on the host. The footer holds Use Docker (outline) and Back (primary), which takes focus; Back, closing or Escape leaves the choice unmade. -- **Edit node**: the name, then the Limits choice, Recommended showing the node's `recommended_capacity` while Core has one, or Custom with the numbers. The Nodes list and a node's Capacity show "Active / limit" for Docker and microsandbox alike, so a saved limit shows where it was set. +- **Edit node**: the name, then the Limits choice, Recommended showing the node's `recommended_capacity` while Core has one, or Custom with the numbers. The Nodes list and a node's Capacity show "Active / limit", so a saved limit shows where it was set. - **How to call**: wherever a new key is shown, a card under it gives three copyable samples, each a Margin Gray block with a Hairline and its label and copy button in a header row: a Shell block exporting `OPENAI_BASE_URL` (the installation's API base URL) and `OPENAI_API_KEY` (the new key) together, then curl and Python (with the pinned SDK), each listing the project's Agents and creating a Session with a first message (`environment`, an inline `agent` with `model: ""`, and `input`). A copy the clipboard refuses selects the sample and says so in red underneath. One Graphite line says to put a model the model provider serves in place of ``, and that running an Agent needs a model provider: in each request, saved on the Agent, or the deployment default. An active project's page shows the same samples as a section without any key: the Shell block exports a quoted placeholder, and a Graphite line above the samples says to use a key issued for this project, shown only once at issuance. When the public address is loopback, a note above the samples says the API is reachable only on the Core machine; without a public address only a note to set one shows. Before the installation is read, a skeleton holds the first sample's place. ### Navigation @@ -405,7 +404,7 @@ Signing in and the console tour share one frame: a dark stage on the left (alway The first card on the Overview while any step is to do: a card header ("Getting started", "n of 4 done", a help tip, then a ghost Take the tour button and an icon button that hides it) over four rows split by Faint Rules. Each row has a 22px numbered ring (a check on the tile wash when done), a 13px/600 title over one 12.5px Graphite line, a status dot (Done in green, To do in Pencil, Checking pending, Unknown for a failed read) and one outline action while the step is to do: Set up sandboxes, Add node, Open Nodes or Open sandbox backend; Open Settings; Create project (which continues to the new project's first key) or Issue key; See how to call (the newest active project, preferring one with an active key), or Projects and keys without an active project. Add node, Create project and Issue key open their page with the dialog already open; Open Settings brings the Models and credentials tab to the top of the page body and focuses the default Harness's Set or Replace; See how to call opens the project and, once its keys, usage and address are read, brings its How to call heading to the top of the page body, focused. Only the page body scrolls; the page header stays. Every step done turns it into one line, "You're set", with Take the tour and Dismiss; it stays, through the tour, until dismissed, and the checklist does not come back on its own. The choice is kept per installation in the browser, also while the deployment cannot be read; Show Getting started, a quiet row above the sidebar's account controls, opens it again at any time. ### Sandbox setup -Setting up hosted sandboxes is a set of pages inside Settings → Sandbox and scheduling, one decision each: where sandboxes run (own machines or E2B), then the backend, or for E2B the Connect E2B page (service, API key, template build and endpoint), then the size of each sandbox (three presets; E2B skips it, since each sandbox takes the template build's size), then a review. Choices are large cards that advance on a click; short indigo dashes show the progress; pages slide and blur across. The backend page compares microsandbox and Docker behind a help tip; microsandbox comes first, preselected (a saved backend stays selected), with a neutral Recommended pill beside its title. Docker takes a confirmation (see Dialogs) once per visit to setup; a saved Docker deployment has already made it. The review states where sandboxes run, the size, each declared connection value (an empty optional one reads Provider default) and the Core address, read-only: the console never asks for it. A loopback address carries an amber line under it: only the Core machine reaches it. When Core rejects the configuration for it (E2B with a loopback `public_url`), a red-tinted block under the review states the reason and adds Managed in Settings, which leads to Settings → Service and deployment. A save attempt clears the transient E2B key. Initial setup then asks for it again, with a link to that step; an update may leave it blank to keep the committed key. Advanced settings, one link away on the review page of a backend that declares a default size, hold the exact CPU, memory and disk values; E2B has none, so the review offers no link. A change keeps the saved size while the backend stays the same (a saved size outside the presets is offered as Current). Same-backend editing starts at size or the E2B connection with the provider fixed. With the saved key kept, the template build stays an editable text field of the saved value; entering a key loads the build list again. It is an online configuration update, including when older sandboxes remain: existing node identities and resource ownership are retained. Changing the backend or E2B team requires reset and then a new setup. E2B updates can omit the key to retain it; every explicitly entered key takes the verified replacement path and advances the target generation on success, including the same value. Rejections remain inline with a safe reason and a deliberate way back to reset; never infer teams from a key, auto-reset or auto-resubmit. Optional explanations sit behind help tips; errors and safety consequences remain visible. +Setting up hosted sandboxes is a set of pages inside Settings → Sandbox and scheduling, one decision each: where sandboxes run (own machines or E2B), then the backend, or for E2B the Connect E2B page (service, API key, template build and endpoint), then the size of each sandbox (three presets; E2B skips it, since each sandbox takes the template build's size), then a review. Choices are large cards that advance on a click; short indigo dashes show the progress; pages slide and blur across. The backend page offers microsandbox, preselected (a saved backend stays selected), with a neutral Recommended pill beside its title. The review states where sandboxes run, the size, each declared connection value (an empty optional one reads Provider default) and the Core address, read-only: the console never asks for it. A loopback address carries an amber line under it: only the Core machine reaches it. When Core rejects the configuration for it (E2B with a loopback `public_url`), a red-tinted block under the review states the reason and adds Managed in Settings, which leads to Settings → Service and deployment. A save attempt clears the transient E2B key. Initial setup then asks for it again, with a link to that step; an update may leave it blank to keep the committed key. Advanced settings, one link away on the review page of a backend that declares a default size, hold the exact CPU, memory and disk values; E2B has none, so the review offers no link. A change keeps the saved size while the backend stays the same (a saved size outside the presets is offered as Current). Same-backend editing starts at size or the E2B connection with the provider fixed. With the saved key kept, the template build stays an editable text field of the saved value; entering a key loads the build list again. It is an online configuration update, including when older sandboxes remain: existing node identities and resource ownership are retained. Changing the backend or E2B team requires reset and then a new setup. E2B updates can omit the key to retain it; every explicitly entered key takes the verified replacement path and advances the target generation on success, including the same value. Rejections remain inline with a safe reason and a deliberate way back to reset; never infer teams from a key, auto-reset or auto-resubmit. Optional explanations sit behind help tips; errors and safety consequences remain visible. ### Sandbox reset Sandbox configuration offers an explicit reset ([semantics](../../contracts/agents-api/sandbox-deployment.md#reset)) through its panels and confirmation dialogs. The confirmation keeps to one concise consequence paragraph, two mode choices, the auto deadline and footer actions. Put cleanup sequencing and preservation details in help tips. State directly that hosted work is archived, remaining active work may be cancelled, archived Sessions cannot resume and unpersisted workspace contents may be lost. Further consequences live behind the reset impact help tip. diff --git a/apps/web/e2e/console.ts b/apps/web/e2e/console.ts index 14381899f..03d2e3fdd 100644 --- a/apps/web/e2e/console.ts +++ b/apps/web/e2e/console.ts @@ -14,9 +14,9 @@ export const FIXTURE_CORE_KEY = "fixture-core-key-3f9a2c71"; * (loopback: only the Core machine reaches the API, and E2B is rejected) or "stale" (public, * with a node enrolled with an earlier address), and `installers: "none"` a console without its node installation payload, so it serves neither * the node nor the self-hosted installer. `nodeArtifacts` lists the providers the console has - * node files for, both by default; as in the console, microsandbox needs Docker's files too. + * node files for, microsandbox by default. */ -export interface FixtureOptions { fresh?: boolean; sandbox?: "configured" | "none" | "e2b"; nodes?: "none"; installation?: "public" | "local" | "stale"; installers?: "none"; nodeArtifacts?: ("docker" | "microsandbox")[] } +export interface FixtureOptions { fresh?: boolean; sandbox?: "configured" | "none" | "e2b"; nodes?: "none"; installation?: "public" | "local" | "stale"; installers?: "none"; nodeArtifacts?: "microsandbox"[] } /** Fresh fixture state: signed out ("login") or already signed in ("authenticated"). */ export async function resetFixture(request: APIRequestContext, auth: "login" | "authenticated" = "authenticated", options: FixtureOptions = {}) { diff --git a/apps/web/e2e/data/routes.mjs b/apps/web/e2e/data/routes.mjs index adddd9dea..505d83b96 100644 --- a/apps/web/e2e/data/routes.mjs +++ b/apps/web/e2e/data/routes.mjs @@ -83,9 +83,9 @@ export function buildDemo(now = Math.floor(Date.now() / 1000), publicUrl = "http }); sessions.sort((a, b) => b.created_at - a.created_at); const nodes = [ - { rollout: { state: "ready", ready_generation: 1 }, id: "node-local", name: "core-01", provider: "docker", online: true, provider_ready: true, cpu_count: 16, available_memory_bytes: 38 * 2 ** 30, available_disk_bytes: 410 * 2 ** 30, running: 5, snapshots: 2, last_seen_at: new Date((now - 8) * 1000).toISOString(), max_active: 8, max_retained: 16, recommended_capacity: { max_active: 6, max_retained: 6 }, active: 5, reserved: 1, retained: 2, cleanup_pending: 0, created_at: new Date((now - 86400 * 30) * 1000).toISOString() }, - { rollout: { state: "failed", ready_generation: null, diagnostic: "host_unsupported" }, id: "node-gpu", name: "gpu-worker-02", provider: "docker", online: true, provider_ready: false, cpu_count: 32, available_memory_bytes: 12 * 2 ** 30, available_disk_bytes: 96 * 2 ** 30, running: 7, snapshots: 5, last_seen_at: new Date((now - 12) * 1000).toISOString(), max_active: 8, max_retained: 16, recommended_capacity: null, active: 7, reserved: 0, retained: 5, cleanup_pending: 1, created_at: new Date((now - 86400 * 12) * 1000).toISOString() }, - { rollout: { state: "unknown", ready_generation: 1 }, id: "node-edge", name: "edge-03", provider: "docker", online: false, provider_ready: false, cpu_count: 8, available_memory_bytes: null, available_disk_bytes: null, running: 0, snapshots: 0, last_seen_at: new Date((now - 5400) * 1000).toISOString(), max_active: 4, max_retained: 8, recommended_capacity: null, active: 0, reserved: 0, retained: 0, cleanup_pending: 0, created_at: new Date((now - 86400 * 3) * 1000).toISOString() }, + { rollout: { state: "ready", ready_generation: 1 }, id: "node-local", name: "core-01", provider: "microsandbox", online: true, provider_ready: true, cpu_count: 16, available_memory_bytes: 38 * 2 ** 30, available_disk_bytes: 410 * 2 ** 30, running: 5, snapshots: 2, last_seen_at: new Date((now - 8) * 1000).toISOString(), max_active: 8, max_retained: 16, recommended_capacity: { max_active: 6, max_retained: 6 }, active: 5, reserved: 1, retained: 2, cleanup_pending: 0, created_at: new Date((now - 86400 * 30) * 1000).toISOString() }, + { rollout: { state: "failed", ready_generation: null, diagnostic: "host_unsupported" }, id: "node-gpu", name: "gpu-worker-02", provider: "microsandbox", online: true, provider_ready: false, cpu_count: 32, available_memory_bytes: 12 * 2 ** 30, available_disk_bytes: 96 * 2 ** 30, running: 7, snapshots: 5, last_seen_at: new Date((now - 12) * 1000).toISOString(), max_active: 8, max_retained: 16, recommended_capacity: null, active: 7, reserved: 0, retained: 5, cleanup_pending: 1, created_at: new Date((now - 86400 * 12) * 1000).toISOString() }, + { rollout: { state: "unknown", ready_generation: 1 }, id: "node-edge", name: "edge-03", provider: "microsandbox", online: false, provider_ready: false, cpu_count: 8, available_memory_bytes: null, available_disk_bytes: null, running: 0, snapshots: 0, last_seen_at: new Date((now - 5400) * 1000).toISOString(), max_active: 4, max_retained: 8, recommended_capacity: null, active: 0, reserved: 0, retained: 0, cleanup_pending: 0, created_at: new Date((now - 86400 * 3) * 1000).toISOString() }, ]; const hosted = sessions.filter((session) => session.environment.type === "openai_hosted"); const allocations = hosted.map((session, index) => { @@ -104,7 +104,7 @@ export function buildDemo(now = Math.floor(Date.now() / 1000), publicUrl = "http if (session.environment.type === "self_hosted") return { ...base, environment_id: session.environment.id, mode: "self_hosted", provider_type: null, instance: { kind: "self_hosted_connection", allocation_id: null, device_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", allocation_created_at: null, observed_at: null, started_at: null, memory: null }; const allocation = allocations.find((entry) => entry.session_id === session.id); const sleeping = allocation.compute_phase === "suspended"; - return { ...base, environment_id: session.environment.id, mode: "openai_hosted", provider_type: "docker", instance: { kind: "managed_allocation", allocation_id: allocation.id, device_id: null, connection_generation: null }, lifecycle_state: sleeping ? "sleeping" : "active", status: "observed", reason: null, allocation_created_at: session.created_at, observed_at: now - 2, started_at: session.created_at, memory: sleeping ? null : { usage_bytes: Math.floor((0.4 + rand() * 1.4) * 2 ** 30), limit_bytes: 2 * 2 ** 30 } }; + return { ...base, environment_id: session.environment.id, mode: "openai_hosted", provider_type: "microsandbox", instance: { kind: "managed_allocation", allocation_id: allocation.id, device_id: null, connection_generation: null }, lifecycle_state: sleeping ? "sleeping" : "active", status: "observed", reason: null, allocation_created_at: session.created_at, observed_at: now - 2, started_at: session.created_at, memory: sleeping ? null : { usage_bytes: Math.floor((0.4 + rand() * 1.4) * 2 ** 30), limit_bytes: 2 * 2 ** 30 } }; }); // Added after everything else is generated, so the seeded data above does not change. agents.push(...providerAgentDefinitions.map((definition, index) => ({ diff --git a/apps/web/e2e/fixture-console.mjs b/apps/web/e2e/fixture-console.mjs index 79f5c5d46..f261b3781 100644 --- a/apps/web/e2e/fixture-console.mjs +++ b/apps/web/e2e/fixture-console.mjs @@ -86,17 +86,17 @@ function unconfiguredDeployment(generation = 0, ownerEpoch = 3) { } function configuredDeployment() { - return { installation_id: INSTALLATION_ID, provider: "docker", credential_configured: false, configuration: {}, metadata: {}, core_url: publicUrl(), reset: null, rollout: nodeRollout(), owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, running: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 } }, specification_digest: "fixture", suspension: null }; + return { installation_id: INSTALLATION_ID, provider: "microsandbox", credential_configured: false, configuration: {}, metadata: {}, core_url: publicUrl(), reset: null, rollout: nodeRollout(), owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, running: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 } }, specification_digest: "fixture", suspension: { idle_seconds: 300, retention_seconds: 86400 } }; } /** The E2B template build as Core read it when the selection was saved. */ const templateBuild = { status: "ready", resources: { cpus: 2, memory_mib: 2048, root_disk_mib: 10240 } }; // E2B runs sandboxes in its cloud: no nodes, only what Core holds there. function e2bDeployment() { - return { ...configuredDeployment(), provider: "e2b", mode: "direct", rollout: noNodeRollout(), resources: { allocations: 3, running: 3, pending: 1 }, specification: { resources: { cpus: 2, memory_mib: 2048 } }, configuration: { template: "oac-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b", api_url: "https://api.e2b.app", domain: "e2b.app" } , credential_configured: true, metadata: { template_build: templateBuild } }; + return { ...configuredDeployment(), provider: "e2b", mode: "direct", rollout: noNodeRollout(), suspension: null, resources: { allocations: 3, running: 3, pending: 1 }, specification: { resources: { cpus: 2, memory_mib: 2048 } }, configuration: { template: "oac-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b", api_url: "https://api.e2b.app", domain: "e2b.app" } , credential_configured: true, metadata: { template_build: templateBuild } }; } -function reset(mode = "login", fresh = false, sandbox = "configured", nodes = "demo", address = "public", installers = true, artifacts = "docker,microsandbox") { +function reset(mode = "login", fresh = false, sandbox = "configured", nodes = "demo", address = "public", installers = true, artifacts = "microsandbox") { // Self-hosted Sessions get their remote_url from public_url, as in Core. const screenshots = process.env.OAC_WEB_SCREENSHOT_DEMO === "1"; const now = Math.floor(Date.now() / 1000); @@ -521,7 +521,7 @@ async function executorCredentialRoute(request, response, projectId, environment */ function registeredNode(nodeId) { const { enrollment_id = null, ...limits } = state.enrollment ?? { max_active: 1, max_retained: 1 }; - return { rollout: { state: "unknown", ready_generation: null }, id: nodeId, name: nodeId, provider: state.deployment?.provider ?? "docker", core_url: publicUrl(), enrollment_id, online: false, provider_ready: false, cpu_count: null, available_memory_bytes: null, available_disk_bytes: null, running: 0, snapshots: 0, last_seen_at: null, recommended_capacity: null, ...limits, active: 0, reserved: 0, retained: 0, cleanup_pending: 0, created_at: new Date().toISOString() }; + return { rollout: { state: "unknown", ready_generation: null }, id: nodeId, name: nodeId, provider: state.deployment?.provider ?? "microsandbox", core_url: publicUrl(), enrollment_id, online: false, provider_ready: false, cpu_count: null, available_memory_bytes: null, available_disk_bytes: null, running: 0, snapshots: 0, last_seen_at: null, recommended_capacity: null, ...limits, active: 0, reserved: 0, retained: 0, cleanup_pending: 0, created_at: new Date().toISOString() }; } const HARNESS_PROVIDER = /^\/harnesses\/([^/]+)\/model-configuration$/; diff --git a/apps/web/e2e/nodes.spec.ts b/apps/web/e2e/nodes.spec.ts index bb0889aa6..46782ce19 100644 --- a/apps/web/e2e/nodes.spec.ts +++ b/apps/web/e2e/nodes.spec.ts @@ -26,10 +26,10 @@ test("adds a node: host requirements, a root/sudo command, a countdown, the same await expect(add.getByLabel("One-time enrollment command without sudo", { exact: true })).toHaveCount(0); await add.getByRole("radio", { name: "Custom" }).click(); await add.getByLabel("Sandboxes at once").fill("3"); + await add.getByLabel("Retained sandboxes").fill("3"); const tokenRequest = () => page.waitForRequest((sent) => sent.method() === "POST" && sent.url().endsWith("/core/v1/sandbox/enrollment-tokens")); const issued = tokenRequest(); await add.getByRole("button", { name: "Generate command" }).click(); - // Docker never suspends, so it retains exactly the sandboxes it runs. expect((await issued).postDataJSON()).toEqual({ max_active: 3, max_retained: 3 }); const field = add.getByLabel("One-time enrollment command", { exact: true }); await expect(field).toHaveValue(/enroll_fixture_/); @@ -42,7 +42,7 @@ test("adds a node: host requirements, a root/sudo command, a countdown, the same await expect(add.getByText("If the command is interrupted or the download stalls, run it again: unfinished downloads restart, and verified files are reused.")).toBeVisible(); await expect(add.getByRole("timer")).toHaveText(/^Expires in (1:00:00|59:\d\d)$/); const progress = add.getByRole("status", { name: "Registration progress" }); - await expect(progress).toHaveText(/Waiting for registration.*Connect.*Docker check/); + await expect(progress).toHaveText(/Waiting for registration.*Connect.*microsandbox check/); // Closing keeps the command for the next opening. Meanwhile another command's node, with the // same limits, registers: it reports another enrollment ID, so it is not this command's node. @@ -53,7 +53,7 @@ test("adds a node: host requirements, a root/sudo command, a countdown, the same await page.getByRole("button", { name: "Add node" }).click(); await expect(field).toHaveValue(first); await expect(page.getByRole("table", { name: "Sandbox nodes" })).toContainText("edge-05"); - await expect(progress).toHaveText(/Waiting for registration.*Connect.*Docker check/); + await expect(progress).toHaveText(/Waiting for registration.*Connect.*microsandbox check/); // Once expired, a new command is issued only on request, for the same limits. await page.clock.fastForward("01:00:30"); @@ -79,7 +79,7 @@ test("adds a node: host requirements, a root/sudo command, a countdown, the same watch.expiredWatch.observe(document.body, { childList: true, subtree: true, characterData: true }); }); await page.getByRole("button", { name: "Add node" }).click(); - await expect(progress).toHaveText(/Registered · edge-04.*Waiting to connect.*Docker check/); + await expect(progress).toHaveText(/Registered · edge-04.*Waiting to connect.*microsandbox check/); expect(await page.evaluate(() => { const watch = window as unknown as { expiredShown: boolean; expiredWatch: MutationObserver }; watch.expiredWatch.disconnect(); @@ -95,7 +95,7 @@ test("adds a node: host requirements, a root/sudo command, a countdown, the same // Connected but not ready, it keeps waiting for its provider; once ready, the node is connected. await setNode(request, { id: "node-new", online: true }); await expect(problem).toContainText("Sandbox provider unavailable"); - await expect(progress).toContainText("Waiting for Docker"); + await expect(progress).toContainText("Waiting for microsandbox"); await setNode(request, { id: "node-new", provider_ready: true }); await expect(progress).toHaveText("edge-04 · Connected"); await add.getByRole("button", { name: "Done" }).click(); @@ -145,7 +145,7 @@ test("issues no command before the installation is read, for a loopback public U await openConsole(page, request, "nodes", { nodeArtifacts: [] }); await page.getByRole("button", { name: "Refresh sandbox state" }).click(); await page.getByRole("button", { name: "Add node" }).click(); - await expect(add.getByRole("status")).toHaveText("This console has no node files for Docker. Install Core from the offline bundle, or add the release artifacts and rerun ./install.sh."); + await expect(add.getByRole("status")).toHaveText("This console has no node files for microsandbox. Install Core from the offline bundle, or add the release artifacts and rerun ./install.sh."); await expect(add.getByRole("button", { name: "Generate command" })).toHaveCount(0); expect(await writes(request)).toEqual([]); // Rerunning ./install.sh adds them: reopening reads the console again, without a reload. @@ -187,7 +187,6 @@ test("marks a node on an old Core address in its row, beside each node's limit", await expect(row).toContainText("Old address"); await expect(row).toContainText("Remove and add again"); await expect(row).not.toContainText("Available"); - // Docker nodes show their limit too. await expect(row).toContainText("5 / 8"); }); @@ -224,34 +223,6 @@ test("sets up own-machine sandboxes page by page", async ({ page, request }) => await page.getByRole("button", { name: "Settings", exact: true }).click(); }); -test("preselects microsandbox and asks once before switching to Docker", async ({ page, request }) => { - await openConsole(page, request, "settings?id=sandbox", { sandbox: "none" }); - await page.getByRole("button", { name: "Own machines" }).click(); - const microsandbox = page.getByRole("button", { name: "microsandbox Recommended" }); - const docker = page.getByRole("button", { name: "Docker", exact: true }); - const confirm = page.getByRole("dialog", { name: "Use Docker?" }); - const sizeStep = page.getByRole("heading", { name: "How big is each sandbox?" }); - await expect(microsandbox).toHaveAttribute("aria-pressed", "true"); - - // Keeping microsandbox is the default action; it leaves microsandbox selected. - await docker.click(); - const keep = confirm.getByRole("button", { name: "Back" }); - await expect(keep).toBeFocused(); - await keep.click(); - await expect(confirm).toBeHidden(); - await expect(microsandbox).toHaveAttribute("aria-pressed", "true"); - - // Confirmed, Docker is selected and the wizard does not ask again. - await docker.click(); - await confirm.getByRole("button", { name: "Use Docker" }).click(); - await expect(sizeStep).toBeVisible(); - await page.getByRole("button", { name: "Back", exact: true }).click(); - await expect(docker).toHaveAttribute("aria-pressed", "true"); - await docker.click(); - await expect(sizeStep).toBeVisible(); - await expect(confirm).toHaveCount(0); -}); - test("saves E2B without opening Add node, as it has no machines", async ({ page, request }) => { let submitted: Record | null = null; page.on("request", (sent) => { @@ -313,8 +284,8 @@ test("shows the retained E2B build while a replacement key is checked", async ({ }); test("edits only the saved backend, preserving a custom size", async ({ page, request }) => { - const current = { resources: { cpus: 7, memory_mib: 8192 } }; - let deployment = { configuration: {}, metadata: {}, credential_configured: false, installation_id: "94be54a1-138c-4f30-bc87-b13686272dbe", provider: "docker", core_url: "https://core.example", reset: null, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 0, preparing: 0, failed: 0, unknown: 0 } }, + const current = { resources: { cpus: 7, memory_mib: 8192, root_disk_mib: 10240 }, workspace: { attachment: "host_directory", user_xattr: true, capacity_quota: false } }; + let deployment = { configuration: {}, metadata: {}, credential_configured: false, installation_id: "94be54a1-138c-4f30-bc87-b13686272dbe", provider: "microsandbox", core_url: "https://core.example", reset: null, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 0, preparing: 0, failed: 0, unknown: 0 } }, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 0, running: 0, pending: 0 }, specification: current, specification_digest: "e".repeat(64), suspension: null }; let submitted: Record | null = null; @@ -332,7 +303,7 @@ test("edits only the saved backend, preserving a custom size", async ({ page, re await expect(page.getByRole("button", { name: "E2B", exact: true })).toHaveCount(0); await page.getByRole("button", { name: /^Current/ }).click(); await page.getByRole("button", { name: "Save configuration" }).click(); - await expect.poll(() => submitted).toMatchObject({ provider: "docker", expected_generation: 1, resources: current.resources }); + await expect.poll(() => submitted).toMatchObject({ provider: "microsandbox", expected_generation: 1, resources: current.resources }); expect(submitted).not.toHaveProperty("core_url"); }); @@ -367,7 +338,7 @@ test("renames a node and sets how many sandboxes run on it at once", async ({ pa await page.getByRole("row").filter({ hasText: "core-01" }).getByRole("button", { name: "Edit node" }).click(); const edit = page.getByRole("dialog", { name: "Edit node" }); await edit.getByLabel("Name").fill("core-01-large"); - await expect(edit).toContainText("Recommended: 6 at once."); + await expect(edit).toContainText("Recommended: 6 at once, 6 retained."); await edit.getByRole("button", { name: "Use recommended" }).click(); await expect(edit.getByLabel("Sandboxes at once")).toHaveValue("6"); await edit.getByRole("button", { name: "Save" }).click(); diff --git a/apps/web/e2e/sandbox-generation.spec.ts b/apps/web/e2e/sandbox-generation.spec.ts index a94ac1432..f84af2adc 100644 --- a/apps/web/e2e/sandbox-generation.spec.ts +++ b/apps/web/e2e/sandbox-generation.spec.ts @@ -64,7 +64,7 @@ test("online configuration retains existing resources and stops rapid polling wh await page.getByRole("button", { name: /^Large/ }).click(); const saved = await saveConfiguration(page); expect(saved.response.ok()).toBe(true); - expect(saved.input).toMatchObject({ provider: "docker", expected_generation: 1 }); + expect(saved.input).toMatchObject({ provider: "microsandbox", expected_generation: 1 }); const current = await saved.response.json() as SandboxDeployment; expect(current.generation).toBe(2); expect(current.resources.allocations).toBe(before.allocations.length); @@ -240,7 +240,7 @@ test("uncertain configuration refresh preserves a draft until its generation cha const edit = page.getByRole("dialog", { name: "Change resources", exact: true }); await edit.getByRole("button", { name: /^Large/ }).click(); const wizard = edit.getByRole("region", { name: "Change the sandbox configuration", exact: true }); - const draftSize = await fact(wizard, "Each sandbox").innerText(); + const draftSize = (await fact(wizard, "Each sandbox").textContent()) ?? ""; expect(draftSize).not.toBe(committedSize); await failNext(request, { method: "PUT", path: deploymentPath, status: 503, message: "Configuration outcome is unconfirmed." }); const result = await saveConfiguration(page); diff --git a/apps/web/e2e/sandbox-lifecycle.spec.ts b/apps/web/e2e/sandbox-lifecycle.spec.ts index ffb53a7e7..f1894c504 100644 --- a/apps/web/e2e/sandbox-lifecycle.spec.ts +++ b/apps/web/e2e/sandbox-lifecycle.spec.ts @@ -37,12 +37,6 @@ test("shows each node's retained limit and elapsed phase time without inventing await page.screenshot({ path: testInfo.outputPath("sandbox-metrics-zh.png"), fullPage: true }); }); -test("hides the retained limit when the provider declares no suspension support", async ({ page, request }) => { - await openConsole(page, request, "nodes"); - await expect(page.getByRole("table", { name: "Sandbox nodes" })).toBeVisible(); - await expect(page.getByRole("columnheader", { name: "Retained / limit" })).toHaveCount(0); -}); - test("edits deployment idle time without changing generation", async ({ page, request }) => { // Settings shows the deployment only; Nodes reads the node inventory. diff --git a/apps/web/src/features/dashboard/runtime-history.test.ts b/apps/web/src/features/dashboard/runtime-history.test.ts index df9454bd4..fc22ec350 100644 --- a/apps/web/src/features/dashboard/runtime-history.test.ts +++ b/apps/web/src/features/dashboard/runtime-history.test.ts @@ -73,7 +73,7 @@ function history(overrides: Partial = {}): RuntimeHistory { environment_id: environmentID, allocation_id: allocationID, started_at: { seconds: 10, nanoseconds: 500_000_000 }, - provider_type: "docker", + provider_type: "microsandbox", points: [{ start: 100, end: 130, first_observed_at: 110, last_observed_at: 110, observation_count: 1, observed_count: 1, unavailable_count: 0, diff --git a/apps/web/src/features/fleet/SandboxResetNotice.test.tsx b/apps/web/src/features/fleet/SandboxResetNotice.test.tsx index 79341708d..994113c5e 100644 --- a/apps/web/src/features/fleet/SandboxResetNotice.test.tsx +++ b/apps/web/src/features/fleet/SandboxResetNotice.test.tsx @@ -6,7 +6,7 @@ import { SandboxResetNotice } from "./SandboxResetNotice"; const reset: SandboxReset = { clear: "auto", requested_at: "2026-09-27T10:00:00Z", deadline_at: "2026-09-27T11:00:00Z", forced_at: null, remaining: { busy: 1, idle: 0, cleanup: 2, on_offline_nodes: 1, offline_nodes: [{ node_id: "n1", name: "Node 1", resources: 1 }] } }; -const deployment = (reset: SandboxReset | null): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "docker", core_url: "http://core", reset, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 3, running: 3, pending: 0 }, suspension: null }); +const deployment = (reset: SandboxReset | null): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", reset, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 3, running: 3, pending: 0 }, suspension: null }); const render = (value: SandboxDeployment | undefined, failed = false) => renderToStaticMarkup( {}} />); describe("reset notices on read-only surfaces", () => { diff --git a/apps/web/src/features/fleet/fleet-queries.test.ts b/apps/web/src/features/fleet/fleet-queries.test.ts index aabdcc5c4..f7f04995e 100644 --- a/apps/web/src/features/fleet/fleet-queries.test.ts +++ b/apps/web/src/features/fleet/fleet-queries.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { sandboxAdmin, sandboxDeploymentQuery, sandboxScope } from "../sandbox/sandbox-queries"; import { fleetQuery } from "./fleet-queries"; -const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "docker", core_url: "http://core", owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, suspension: null, +const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, suspension: null, reset: { clear: "auto", requested_at: "2026-09-27T10:00:00Z", deadline_at: "2026-09-27T11:00:00Z", forced_at: null, remaining: { busy: 1, idle: 0, cleanup: 0, on_offline_nodes: 0, offline_nodes: [] } } }; afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); diff --git a/apps/web/src/features/fleet/use-sandbox-fleet.test.tsx b/apps/web/src/features/fleet/use-sandbox-fleet.test.tsx index 49a8ba840..4f978a739 100644 --- a/apps/web/src/features/fleet/use-sandbox-fleet.test.tsx +++ b/apps/web/src/features/fleet/use-sandbox-fleet.test.tsx @@ -10,7 +10,7 @@ import { fleetQuery, type FleetSnapshot } from "./fleet-queries"; import { FleetReadNotice } from "./FleetReadNotice"; import { fleetSnapshot, useSandboxFleet } from "./use-sandbox-fleet"; -const configured: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "docker", core_url: "http://core", reset: null, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, suspension: null }; +const configured: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", reset: null, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, suspension: null }; function Probe() { const { state, deployment } = useSandboxFleet(); diff --git a/apps/web/src/features/overview/getting-started.test.ts b/apps/web/src/features/overview/getting-started.test.ts index aaffd5da0..e46950b4f 100644 --- a/apps/web/src/features/overview/getting-started.test.ts +++ b/apps/web/src/features/overview/getting-started.test.ts @@ -6,7 +6,7 @@ import { checklistStorageKey, checklistView, gettingStartedSteps, nextStepAfterN import { node, project } from "./test-fixtures"; const deployment = (overrides: Partial = {}): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, - rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "docker", core_url: "http://core", reset: null, owner_epoch: 1, generation: 1, mode: "nodes", + rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", reset: null, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 0, running: 0, pending: 0 }, suspension: null, ...overrides, }); const fleet = (value: SandboxDeployment, nodes = [node("n1")]): FleetState => ({ status: "ready", snapshot: { deployment: value, nodes, allocations: [], loadedAt: 0 }, targetGeneration: value.generation, refreshing: false, error: null }); @@ -27,7 +27,7 @@ describe("Getting started steps", () => { }); it("does not report ready during reset or an unreadable deployment, even with ready nodes", () => { - for (const backend of ["docker", "e2b"] as const) { + for (const backend of ["microsandbox", "e2b"] as const) { const input = { fleet: fleet(deployment({ provider: backend })), localOnly: false, projects: [project("p")], sessions: 1, harnesses: [harness("codex", { default: true, model_configuration: { ...provider, harness: "codex" } })] }; for (const sandboxReset of [true, "failed", undefined] as const) { @@ -53,7 +53,7 @@ describe("Getting started steps", () => { } }); it("keeps local-only installations to do even with a ready node or cloud deployment", () => { - for (const provider of ["docker", "e2b"] as const) { + for (const provider of ["microsandbox", "e2b"] as const) { const steps = gettingStartedSteps({ sandboxReset: false, fleet: fleet(deployment({ provider, mode: provider === "e2b" ? "direct" : "nodes" })), projects: [], sessions: 1, harnesses: [], localOnly: true }); expect(steps.sandboxes).toMatchObject({ state: "todo", action: "nodes", cloud: provider === "e2b" }); } diff --git a/apps/web/src/features/overview/test-fixtures.ts b/apps/web/src/features/overview/test-fixtures.ts index 98229483c..bc46a793b 100644 --- a/apps/web/src/features/overview/test-fixtures.ts +++ b/apps/web/src/features/overview/test-fixtures.ts @@ -35,7 +35,7 @@ export function session(id: string, overrides: Partial = {}): Agen export function node(id: string, overrides: Partial = {}): SandboxNode { return { - id, name: id, provider: "docker", online: true, provider_ready: true, + id, name: id, provider: "microsandbox", online: true, provider_ready: true, rollout: { state: overrides.online === false ? "unknown" : "ready", ready_generation: 1 }, cpu_count: 8, available_memory_bytes: 1024, available_disk_bytes: 2048, running: 1, snapshots: 0, last_seen_at: "2026-09-24T00:00:00Z", @@ -48,7 +48,7 @@ export function node(id: string, overrides: Partial = {}): SandboxN export function hostedObservation(sessionId: string, projectId: string, overrides: Partial = {}): OwnedRuntimeObservation { return { id: sessionId, object: "agent.runtime_observation", session_id: sessionId, resolved_at: 1_000, - environment_id: `env_${sessionId}`, mode: "openai_hosted", provider_type: "docker", + environment_id: `env_${sessionId}`, mode: "openai_hosted", provider_type: "microsandbox", instance: { kind: "managed_allocation", allocation_id: `alloc_${sessionId}`, device_id: null, connection_generation: null }, lifecycle_state: "active", status: "observed", reason: null, allocation_created_at: 100, observed_at: 1_000, started_at: 400, memory: { usage_bytes: 100, limit_bytes: 400 }, diff --git a/apps/web/src/features/sandbox/SandboxResetControls.test.tsx b/apps/web/src/features/sandbox/SandboxResetControls.test.tsx index f8678adae..e4330cad9 100644 --- a/apps/web/src/features/sandbox/SandboxResetControls.test.tsx +++ b/apps/web/src/features/sandbox/SandboxResetControls.test.tsx @@ -5,7 +5,7 @@ import { SandboxResetControls } from "./SandboxResetControls"; const reset: SandboxReset = { clear: "auto", requested_at: "2020-01-01T10:00:00Z", deadline_at: "2020-01-01T11:00:00Z", forced_at: null, remaining: { busy: 2, idle: 1, cleanup: 3, on_offline_nodes: 2, offline_nodes: [{ node_id: "offline-a", name: "Offline A", resources: 2 }] } }; -const deployment = (value: SandboxReset | null): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "docker", core_url: "https://core.example", reset: value, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 5, running: 5, pending: 1 }, suspension: null }); +const deployment = (value: SandboxReset | null): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "https://core.example", reset: value, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 5, running: 5, pending: 1 }, suspension: null }); const render = (value: SandboxReset | null, stale = false) => renderToStaticMarkup( true} onCancel={async () => true} />); describe("authoritative reset progress", () => { diff --git a/apps/web/src/features/sandbox/SandboxRolloutSummary.test.tsx b/apps/web/src/features/sandbox/SandboxRolloutSummary.test.tsx index 732cc1cf7..3dcb80fea 100644 --- a/apps/web/src/features/sandbox/SandboxRolloutSummary.test.tsx +++ b/apps/web/src/features/sandbox/SandboxRolloutSummary.test.tsx @@ -7,7 +7,7 @@ import { SandboxRolloutSummary } from "./SandboxRolloutSummary"; import { NodeRolloutStatus } from "./NodeRolloutStatus"; import { SandboxDeploymentSettings } from "./SandboxDeploymentSettings"; -const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, installation_id: "i", owner_epoch: 1, generation: 4, provider: "docker", mode: "nodes", core_url: "https://core.example", resources: { allocations: 8, running: 8, pending: 2 }, reset: null, suspension: null, +const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, installation_id: "i", owner_epoch: 1, generation: 4, provider: "microsandbox", mode: "nodes", core_url: "https://core.example", resources: { allocations: 8, running: 8, pending: 2 }, reset: null, suspension: null, rollout: { state: "settled", previous_generation_sandboxes: 8, nodes: { ready: 1, preparing: 0, failed: 2, unknown: 4 } } }; describe("authoritative configuration rollout", () => { diff --git a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx index abd9a6c59..f475fe255 100644 --- a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx +++ b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx @@ -408,7 +408,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab } >
    - {confirming ? sandboxProviderSetup(confirming).cautions.map((caution) =>
  • {text(caution)}
  • ) : null} + {confirming ? sandboxProviderSetup(confirming).cautions.map((caution: Parameters[0]) =>
  • {text(caution)}
  • ) : null}
, document.body, diff --git a/apps/web/src/features/sandbox/console-config.test.ts b/apps/web/src/features/sandbox/console-config.test.ts index 23834faba..b21c1eb30 100644 --- a/apps/web/src/features/sandbox/console-config.test.ts +++ b/apps/web/src/features/sandbox/console-config.test.ts @@ -4,10 +4,10 @@ import { nodeFilesAvailable, sandboxConsoleConfig } from "./console-config"; afterEach(() => vi.unstubAllGlobals()); describe("bundled console capabilities", () => { it("uses the existing console login without sending a project or admin bearer", async () => { - const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ node_installer: true, node_installer_sha256: "a".repeat(64), node_artifacts: ["docker"] }))); + const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ node_installer: true, node_installer_sha256: "a".repeat(64), node_artifacts: ["microsandbox"] }))); vi.stubGlobal("fetch", fetch); const controller = new AbortController(); - expect(await sandboxConsoleConfig(controller.signal)).toEqual({ node_installer: true, node_installer_sha256: "a".repeat(64), node_artifacts: ["docker"] }); + expect(await sandboxConsoleConfig(controller.signal)).toEqual({ node_installer: true, node_installer_sha256: "a".repeat(64), node_artifacts: ["microsandbox"] }); expect(fetch).toHaveBeenCalledWith("/console/config", { credentials: "include", signal: controller.signal }); }); it.each([{}, { node_installer: "true", node_installer_sha256: "a".repeat(64) }, { node_installer: true, node_installer_sha256: "bad" }])("does not enable installation without a verified digest %j", async (body) => { @@ -23,14 +23,14 @@ describe("bundled console capabilities", () => { vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ node_installer: true, node_installer_sha256: "a".repeat(64), ...body })))); return sandboxConsoleConfig(new AbortController().signal); }; - const docker = await read({ node_artifacts: ["docker"] }); - expect(nodeFilesAvailable(docker, "docker")).toBe(true); - expect(nodeFilesAvailable(docker, "microsandbox")).toBe(false); + const files = await read({ node_artifacts: ["microsandbox"] }); + expect(nodeFilesAvailable(files, "microsandbox")).toBe(true); + expect(nodeFilesAvailable(files, "e2b")).toBe(false); // An absent, null or malformed value reports none. - for (const body of [{}, { node_artifacts: null }, { node_artifacts: "docker" }, { node_artifacts: { docker: true } }]) { + for (const body of [{}, { node_artifacts: null }, { node_artifacts: "microsandbox" }, { node_artifacts: { microsandbox: true } }]) { const config = await read(body); expect(config.node_artifacts).toEqual([]); - expect(nodeFilesAvailable(config, "docker")).toBe(false); + expect(nodeFilesAvailable(config, "microsandbox")).toBe(false); } }); }); diff --git a/apps/web/src/features/sandbox/deployment-specification.test.ts b/apps/web/src/features/sandbox/deployment-specification.test.ts index 0c9e1cf26..ba1e7bf0b 100644 --- a/apps/web/src/features/sandbox/deployment-specification.test.ts +++ b/apps/web/src/features/sandbox/deployment-specification.test.ts @@ -5,10 +5,10 @@ import { deploymentContract, type SandboxProvider, type SandboxSpecification } f describe("deployment resources", () => { it("preserves saved resources only for the same provider", () => { const current: SandboxSpecification = { resources: { cpus: 7, memory_mib: 8192 } }; - expect(savedSpecification("docker", "docker", current)).toEqual(current); - expect(savedSpecification("docker", "docker", current)).not.toBe(current); - expect(savedSpecification("microsandbox", "docker", current)).toBeNull(); - expect(savedSpecification("e2b", "docker", current)).toBeNull(); + expect(savedSpecification("microsandbox", "microsandbox", current)).toEqual(current); + expect(savedSpecification("microsandbox", "microsandbox", current)).not.toBe(current); + expect(savedSpecification("e2b", "microsandbox", current)).toBeNull(); + expect(savedSpecification("microsandbox", "e2b", current)).toBeNull(); expect(savedSpecification("e2b", "e2b", { resources: current.resources })).toEqual({ resources: current.resources }); }); it("proposes a copy of each declared default size, which the declared bounds accept", () => { @@ -23,10 +23,10 @@ describe("deployment resources", () => { } }); it("respects CPU, memory and supported disk bounds", () => { - expect(validSandboxResources("docker", { cpus: 0, memory_mib: 2048 })).toBe(false); + expect(validSandboxResources("e2b", { cpus: 0, memory_mib: 2048 })).toBe(false); expect(validSandboxResources("e2b", { cpus: 256, memory_mib: 2048 })).toBe(false); - expect(validSandboxResources("docker", { cpus: 2, memory_mib: 511 })).toBe(false); - expect(validSandboxResources("docker", { cpus: 2, memory_mib: 1048577 })).toBe(false); + expect(validSandboxResources("e2b", { cpus: 2, memory_mib: 511 })).toBe(false); + expect(validSandboxResources("e2b", { cpus: 2, memory_mib: 1048577 })).toBe(false); expect(validSandboxResources("e2b", { cpus: 2, memory_mib: 2048, root_disk_mib: 1024 })).toBe(false); expect(validSandboxResources("microsandbox", { cpus: 2, memory_mib: 2048, root_disk_mib: 1023 }, { ...deploymentContract.providers.microsandbox.workspace, capacity_quota: false })).toBe(false); }); @@ -39,13 +39,11 @@ describe("deployment resources", () => { expect(validSandboxResources("microsandbox", { ...resources, root_disk_mib: 0 }, workspace)).toBe(false); expect(validSandboxResources("microsandbox", { ...resources, environment_disk_mib: 8192 }, workspace)).toBe(false); expect(validSandboxResources("microsandbox", resources, { ...workspace, user_xattr: false })).toBe(false); - expect(validSandboxResources("docker", { cpus: 2, memory_mib: 2048 }, workspace)).toBe(false); expect(validSandboxResources("microsandbox", { ...resources, environment_disk_mib: 8192 }, { ...workspace, capacity_quota: true })).toBe(true); }); it("takes the workspace requirement from the selected storage", () => { expect(selectedWorkspace("microsandbox", { tenant_capacity_mib: 0 })).toMatchObject({ capacity_quota: false }); expect(selectedWorkspace("microsandbox", { tenant_capacity_mib: 1024 })).toMatchObject({ capacity_quota: true }); expect(selectedWorkspace("microsandbox", null)).toBeUndefined(); - expect(selectedWorkspace("docker", { tenant_capacity_mib: 0 })).toBeUndefined(); }); }); diff --git a/apps/web/src/features/sandbox/enrollment-command.test.ts b/apps/web/src/features/sandbox/enrollment-command.test.ts index 4f669c51c..982ce2878 100644 --- a/apps/web/src/features/sandbox/enrollment-command.test.ts +++ b/apps/web/src/features/sandbox/enrollment-command.test.ts @@ -56,7 +56,7 @@ process.exit(process.env.SCENARIO==='installer failure'?7:0);`); const digest = scenario === "checksum mismatch" ? "0".repeat(64) : createHash("sha256").update(payload).digest("hex"); const token = "fixture'one-time"; try { - const command = scenario === "uninstall" ? nodeUninstallCommand({ sourceUrl: "http://localhost:8080", installationId: "fixture-installation", scriptDigest: digest }) : nodeInstallCommand({ token, coreUrl: "http://127.0.0.1:8091", sourceUrl: "http://localhost:8080", provider: "docker", installationId: "fixture-installation", scriptDigest: digest }); + const command = scenario === "uninstall" ? nodeUninstallCommand({ sourceUrl: "http://localhost:8080", installationId: "fixture-installation", scriptDigest: digest }) : nodeInstallCommand({ token, coreUrl: "http://127.0.0.1:8091", sourceUrl: "http://localhost:8080", provider: "microsandbox", installationId: "fixture-installation", scriptDigest: digest }); const proxy = "http://fixture:private%20password@proxy.example:3128"; const proxyEnv: Record = { http_proxy: undefined, https_proxy: undefined, no_proxy: undefined, HTTP_PROXY: undefined, HTTPS_PROXY: undefined, NO_PROXY: undefined, ALL_PROXY: undefined }; if (["uppercase proxy", "root proxy", "empty proxy"].includes(scenario)) Object.assign(proxyEnv, { HTTP_PROXY: proxy, HTTPS_PROXY: proxy, NO_PROXY: "core.example,localhost" }); @@ -111,7 +111,7 @@ process.exit(process.env.SCENARIO==='installer failure'?7:0);`); expect(invocation.stdin).toBe(scenario === "uninstall" ? "" : `${token}\n`); expect(invocation.args.some((arg) => arg.includes("one-time"))).toBe(false); expect(Object.values(invocation.env).some((value) => value.includes("one-time"))).toBe(false); - expect(invocation.args.slice(1)).toEqual(scenario === "uninstall" ? ["--uninstall", "--installation-id", "fixture-installation"] : [...(scenario === "no color" ? ["--no-color"] : []), "--enrollment-token-stdin", "--source-url", "http://localhost:8080", "--core-url", "http://127.0.0.1:8091", "--provider", "docker", "--installation-id", "fixture-installation"]); + expect(invocation.args.slice(1)).toEqual(scenario === "uninstall" ? ["--uninstall", "--installation-id", "fixture-installation"] : [...(scenario === "no color" ? ["--no-color"] : []), "--enrollment-token-stdin", "--source-url", "http://localhost:8080", "--core-url", "http://127.0.0.1:8091", "--provider", "microsandbox", "--installation-id", "fixture-installation"]); expect(invocation.mode & 0o077).toBe(0); if (scenario === "as root" || scenario === "root proxy") { expect(readdirSync(root)).not.toContain("sudo.json"); diff --git a/apps/web/src/features/sandbox/sandbox-deployment-write.test.ts b/apps/web/src/features/sandbox/sandbox-deployment-write.test.ts index d1066ddd3..13485840d 100644 --- a/apps/web/src/features/sandbox/sandbox-deployment-write.test.ts +++ b/apps/web/src/features/sandbox/sandbox-deployment-write.test.ts @@ -5,7 +5,7 @@ import { sandboxAdmin, sandboxDeploymentQuery } from "./sandbox-queries"; import { writeSandboxDeployment } from "./sandbox-deployment-write"; import { sandboxWriteOwnershipQuery } from "./sandbox-write-ownership"; -const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", owner_epoch: 1, generation: 1, provider: "docker", core_url: "http://core", mode: "nodes", reset: null, resources: { allocations: 0, running: 0, pending: 0 }, suspension: null }; +const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", owner_epoch: 1, generation: 1, provider: "microsandbox", core_url: "http://core", mode: "nodes", reset: null, resources: { allocations: 0, running: 0, pending: 0 }, suspension: null }; function deferred() { let resolve!: (value: T) => void; let reject!: (error: Error) => void; diff --git a/apps/web/src/features/sandbox/sandbox-page-ownership.test.tsx b/apps/web/src/features/sandbox/sandbox-page-ownership.test.tsx index dddbf62b0..fa7086132 100644 --- a/apps/web/src/features/sandbox/sandbox-page-ownership.test.tsx +++ b/apps/web/src/features/sandbox/sandbox-page-ownership.test.tsx @@ -47,14 +47,14 @@ describe("sandbox page ownership", () => { expect(cloud).toContain("Nodes"); expect(cloud).toContain("There are no nodes to manage."); expect(cloud).not.toContain("Change resources"); - const hosts = render(, cache("docker")); + const hosts = render(, cache("microsandbox")); expect(hosts).toContain("node-enrollment"); expect(hosts).toContain("Add your first node"); expect(hosts).not.toContain("Reset deployment"); }); it("opens editing deliberately and keeps the System landing page to one entry", () => { - const client = cache("docker"); + const client = cache("microsandbox"); const configuration = render(, client); expect(configuration).toContain("Change resources"); expect(configuration).toContain("Reset deployment"); @@ -67,7 +67,7 @@ describe("sandbox page ownership", () => { }); it("shares pending and uncertain write ownership across both pages", () => { - const client = cache("docker"); + const client = cache("microsandbox"); const attempt = beginSandboxWrite(client)!; expect(render(, client)).toContain("Saving sandbox change"); expect(render(, client)).toContain("Saving sandbox change"); diff --git a/apps/web/src/features/sandbox/sandbox-queries.test.ts b/apps/web/src/features/sandbox/sandbox-queries.test.ts index 08854805d..30a12ae13 100644 --- a/apps/web/src/features/sandbox/sandbox-queries.test.ts +++ b/apps/web/src/features/sandbox/sandbox-queries.test.ts @@ -9,7 +9,7 @@ const reset: SandboxReset = { remaining: { busy: 1, idle: 0, cleanup: 1, on_offline_nodes: 1, offline_nodes: [{ node_id: "node-a", name: "Offline host", resources: 1 }] }, }; const deployment = (overrides: Partial = {}): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, - rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "installation-a", provider: "docker", core_url: "https://core.example", reset, + rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "installation-a", provider: "microsandbox", core_url: "https://core.example", reset, owner_epoch: 1, generation: 2, mode: "nodes", resources: { allocations: 2, running: 2, pending: 0 }, suspension: null, ...overrides, }); const clients: QueryClient[] = []; diff --git a/apps/web/src/features/sandbox/use-sandbox-page-state.test.tsx b/apps/web/src/features/sandbox/use-sandbox-page-state.test.tsx index ae16d744a..0977b8533 100644 --- a/apps/web/src/features/sandbox/use-sandbox-page-state.test.tsx +++ b/apps/web/src/features/sandbox/use-sandbox-page-state.test.tsx @@ -6,7 +6,7 @@ import { node } from "../overview/test-fixtures"; import { sandboxAdmin, sandboxDeploymentQuery, sandboxSnapshotQuery } from "./sandbox-queries"; import { useSandboxPageState } from "./use-sandbox-page-state"; -const configured: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "docker", core_url: "http://core", reset: null, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, suspension: null }; +const configured: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", reset: null, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, suspension: null }; const resetting: SandboxDeployment = { ...configured, reset: { clear: "auto", requested_at: "2026-09-27T10:00:00Z", deadline_at: "2026-09-27T11:00:00Z", forced_at: null, remaining: { busy: 1, idle: 0, cleanup: 0, on_offline_nodes: 0, offline_nodes: [] } } }; const clients: QueryClient[] = []; function cache() { @@ -47,11 +47,11 @@ describe("Nodes shared deployment evidence", () => { it("uses another page's reset and completion immediately despite a fresh cached idle snapshot", () => { const client = cache(); client.setQueryData(sandboxDeploymentQuery.queryKey, resetting); - expect(render(client)).toContain("docker/auto/0"); + expect(render(client)).toContain("microsandbox/auto/0"); expect(render(client)).toContain('data-compatible="false"'); expect(render(client)).toContain('data-inventory-loading="true"'); client.setQueryData(sandboxDeploymentQuery.queryKey, { ...resetting, reset: { ...resetting.reset!, clear: "force", forced_at: "2026-09-27T10:01:00Z" } }); - expect(render(client)).toContain("docker/force/0"); + expect(render(client)).toContain("microsandbox/force/0"); client.setQueryData(sandboxDeploymentQuery.queryKey, { ...configured, provider: "", mode: "", generation: 2 }); expect(render(client)).toContain("setup/none/0"); expect(client.getQueryData(sandboxSnapshotQuery.queryKey)?.deployment).toEqual(configured); @@ -67,9 +67,9 @@ describe("Nodes shared deployment evidence", () => { it("retains older generation nodes but rejects a different installation, owner, provider or reset lifecycle", () => { const client = cache(); client.setQueryData(sandboxDeploymentQuery.queryKey, configured); - expect(render(client)).toContain("docker/none/1"); + expect(render(client)).toContain("microsandbox/none/1"); client.setQueryData(sandboxDeploymentQuery.queryKey, { ...configured, generation: 2 }); - expect(render(client)).toContain("docker/none/1"); + expect(render(client)).toContain("microsandbox/none/1"); expect(render(client)).toContain('data-older="true"'); for (const change of [{ installation_id: "next" }, { owner_epoch: 2 }, { mode: "direct" as const }, { provider: "e2b" as const }]) { client.setQueryData(sandboxDeploymentQuery.queryKey, { ...configured, ...change }); diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index 624b6d3fd..bed69b0bc 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -66,7 +66,7 @@ Each code returns HTTP 400 with `type: "invalid_request_error"`. A missing, malf | `harness_config_invalid` | `harness_config` | omitted | The deployment default's native parameters are unsupported or invalid | | `invalid_sandbox_configuration` | `resources.cpus` | `min`: 1, `max`: 255 | The CPU count is outside the supported bounds | | `invalid_sandbox_configuration` | `resources.memory_mib` | `min`: 512, `max`: 1048576 | Memory is outside the supported bounds | -| `invalid_sandbox_configuration` | `resources.root_disk_mib` or `resources.environment_disk_mib` | `min`: 1024 for microsandbox (`environment_disk_mib` also accepts 0); `min`: 0, `max`: 0 for Docker and E2B | Disk capacity is missing or unsupported by the provider | +| `invalid_sandbox_configuration` | `resources.root_disk_mib` or `resources.environment_disk_mib` | `min`: 1024 for microsandbox (`environment_disk_mib` also accepts 0); `min`: 0, `max`: 0 for E2B | Disk capacity is missing or unsupported by the provider | Bounds are validation constants, never submitted values. Node names are limited in bytes; Project and key names in trimmed Unicode characters without control characters. Only the first failure is reported, in this order: model provider URL, protocol, key, limits, then the Harness's protocol; sandbox resources CPU, memory, then disk. Model-provider field errors inside a `model_provider` object keep that object's field as `param`. An unknown sandbox provider returns an error without these fields. diff --git a/contracts/agents-api/environment-executor-credentials.md b/contracts/agents-api/environment-executor-credentials.md index a71761c74..c59f25204 100644 --- a/contracts/agents-api/environment-executor-credentials.md +++ b/contracts/agents-api/environment-executor-credentials.md @@ -89,7 +89,7 @@ The installer derives this route from the returned `remote_url` and does not fol ## Revoked or rotated credential -When Core permanently rejects the daemon (enrollment 401 or 409, a permanent WebSocket rejection, or a daemon version from another Core distribution), a daemon started by `oac-daemon start` prints the reason once and exits with an error, and starting it again tries enrollment once and exits again. A Docker Runtime (`oac-daemon connect`) prints the reason once and makes no further requests until it is stopped, then exits successfully, so a supervisor that restarts on exit does not loop; started again, it tries enrollment once and parks again. Transient failures keep the normal reconnect behavior and never replay execution. +When Core permanently rejects the daemon (enrollment 401 or 409, a permanent WebSocket rejection, or a daemon version from another Core distribution), a daemon started by `oac-daemon start` prints the reason once and exits with an error, and starting it again tries enrollment once and exits again. A sandbox Runtime (`oac-daemon connect`) prints the reason once and makes no further requests until it is stopped, then exits successfully, so a supervisor that restarts on exit does not loop; started again, it tries enrollment once and parks again. Transient failures keep the normal reconnect behavior and never replay execution. A machine reconnects only with its bound `key_id`, rotated to a new secret that replaces the credential file at its configured path; the [self-hosted guide](../../docs/getting-started/self-hosted.md#rotate-or-revoke) gives the steps. A new `key_id` cannot reconnect an Environment that is already bound: issuing it succeeds, but enrollment with it returns 409. Rotation does not reinstall Harnesses, change the workspace or replace native history; never create a new Session history to recover a credential. diff --git a/contracts/agents-api/environments.md b/contracts/agents-api/environments.md index 179e3c545..742b32a4c 100644 --- a/contracts/agents-api/environments.md +++ b/contracts/agents-api/environments.md @@ -8,7 +8,7 @@ Related owners: - [Environment files](./environment-files.md): the Files API on a live workspace. - [Executor credentials](./environment-executor-credentials.md): enrollment, the installation grant and connection status of a `self_hosted` machine. -- [Sandbox deployment](./sandbox-deployment.md): which Sandbox Provider (E2B, Docker or microsandbox) hosts `openai_hosted` Environments. +- [Sandbox deployment](./sandbox-deployment.md): which Sandbox Provider (E2B or microsandbox) hosts `openai_hosted` Environments. - [Core–Runtime protocol](../../docs/runtime-protocol.md): the `runtime_prepare` transfer and every other wire message. - [Runtime and outer isolation](../../docs/concepts.md#runtime-and-outer-isolation): the daemon runs tools with its launching user's permissions; isolation comes from the outer Environment. @@ -71,7 +71,7 @@ Both placements run the same Runtime: the daemon, the selected Harness, native t ### Hosted (`openai_hosted`) -The deployment's configured Sandbox Provider (E2B, Docker or microsandbox, see [sandbox deployment](./sandbox-deployment.md)) hosts the Environment. [Harness capabilities](./harness-capabilities.md) lists which Harnesses run there. +The deployment's configured Sandbox Provider (E2B or microsandbox, see [sandbox deployment](./sandbox-deployment.md)) hosts the Environment. [Harness capabilities](./harness-capabilities.md) lists which Harnesses run there. - Session creation, with or without initial input, commits the Session, Environment and retry identity before the Worker provisions compute. A creation interrupted before bootstrap is recovered without repeating the Provider's Create. - Provisioning needs no caller action; the Session stays idle until a Turn starts. diff --git a/contracts/agents-api/harness-capabilities.md b/contracts/agents-api/harness-capabilities.md index 83cc92f0f..9600fb44c 100644 --- a/contracts/agents-api/harness-capabilities.md +++ b/contracts/agents-api/harness-capabilities.md @@ -12,7 +12,7 @@ Anything this page does not list follows the Harness's native behavior with the | Admitted | Core admits it through the same Runtime path, but no real-model acceptance has run on that placement | | Rejected | Core rejects the request before execution | -Placements are `none` (no Environment), hosted (`openai_hosted`) and self-hosted (`self_hosted`). Hosted acceptance ran on Docker nodes; E2B and microsandbox run the same Runtime and adapters, and every Verified hosted cell counts as Admitted there. Self-hosted acceptance ran on Linux machines; the [self-hosted guide](../../docs/getting-started/self-hosted.md#platforms) lists the supported platforms. A verified operation is verified on its own, not in every combination with other options; combinations that a profile rejects are listed in the operation's contract. +Placements are `none` (no Environment), hosted (`openai_hosted`) and self-hosted (`self_hosted`). E2B and microsandbox run the same Runtime and adapters, and every Verified hosted cell counts as Admitted on both. Self-hosted acceptance ran on Linux machines; the [self-hosted guide](../../docs/getting-started/self-hosted.md#platforms) lists the supported platforms. A verified operation is verified on its own, not in every combination with other options; combinations that a profile rejects are listed in the operation's contract. ## Execution and input @@ -65,4 +65,4 @@ These operations need a workspace, so they apply to hosted and self-hosted place | [Plugin MCP over stdio](./environments.md#plugin-mcp) | Verified: hosted, self-hosted | Verified: self-hosted; admitted: hosted | Verified: self-hosted; admitted: hosted | | Plugin MCP over HTTP | Admitted, with literal headers or HTTPS bearer | Admitted, with literal headers or HTTPS bearer | Verified: self-hosted; admitted: hosted; literal headers or HTTPS bearer | -External workspace storage additionally requires the selected Harness declaration and Runtime to support `retained_native_history`; Docker and E2B, which keep their own disks, do not. microsandbox always uses external storage, so MiniMax Code, which does not declare the capability, cannot run on it. The [Workspace filesystem protocol](../../docs/workspace-provider.md) owns storage combination admission. +External workspace storage additionally requires the selected Harness declaration and Runtime to support `retained_native_history`; E2B, which keeps its own disks, does not. microsandbox always uses external storage, so MiniMax Code, which does not declare the capability, cannot run on it. The [Workspace filesystem protocol](../../docs/workspace-provider.md) owns storage combination admission. diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index c6f6e8ede..3f340e04e 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -34,7 +34,7 @@ flowchart TB | Declaration | The Harness's maximum capabilities, placements, configuration and pure admission restrictions, read by Core admission and the Runtime | `internal/harnessconfig//declaration.go` | | Runtime binding | The declaration plus discovery, installed factories and verified capabilities | `apps/daemon/internal/agent//declaration.go` | -An Environment supplies execution resources. Managed E2B, Docker and microsandbox machines and application-owned machines differ in provisioning and connection; the connected Runtime uses this same contract. The daemon runs on Linux, macOS and Windows, managed Providers are Linux-only, and each adapter qualifies its own platforms ([self-hosted platforms](../../docs/getting-started/self-hosted.md#platforms)). Native factories receive capabilities only after the Runtime has loaded the bound installed snapshot ([capability preparation](./environments.md#runtime-capability-preparation)). Model providers supply model communication settings, not Turn scheduling or native process ownership. +An Environment supplies execution resources. Managed E2B and microsandbox machines and application-owned machines differ in provisioning and connection; the connected Runtime uses this same contract. The daemon runs on Linux, macOS and Windows, managed Providers are Linux-only, and each adapter qualifies its own platforms ([self-hosted platforms](../../docs/getting-started/self-hosted.md#platforms)). Native factories receive capabilities only after the Runtime has loaded the bound installed snapshot ([capability preparation](./environments.md#runtime-capability-preparation)). Model providers supply model communication settings, not Turn scheduling or native process ownership. ## Steps diff --git a/contracts/agents-api/node-generation-protocol.md b/contracts/agents-api/node-generation-protocol.md index a7a7e997a..b6ce0652a 100644 --- a/contracts/agents-api/node-generation-protocol.md +++ b/contracts/agents-api/node-generation-protocol.md @@ -2,12 +2,12 @@ title: "Sandbox node protocol" --- -The sandbox node protocol connects Core to a Docker or microsandbox host, which runs Provider operations against its local provider and reports readiness, host measurements and the deployment generations it holds. +The sandbox node protocol connects Core to a microsandbox host, which runs Provider operations against its local provider and reports readiness, host measurements and the deployment generations it holds. | | | | --- | --- | | Caller | Core, the only lifecycle owner; the node never retries a mutation or schedules work | -| Implementations | The sandbox node program with the Docker and microsandbox Providers | +| Implementations | The sandbox node program with the microsandbox Provider | | Code | [`services/core/internal/sandbox/node`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/node) (`wire.go`, `generation_wire.go`) | | Transport | One WebSocket, `GET /api/v1/sandbox-node/connect`; the HTTP routes a node uses to enroll and read its configuration are in the [machine connection API](./machine-api.md#node-routes) | | Version rule | Exact match on `node.ProtocolVersion`, see [Frames and version](#frames-and-version) | diff --git a/contracts/agents-api/runtime-observability-api.md b/contracts/agents-api/runtime-observability-api.md index d5066a752..79939c624 100644 --- a/contracts/agents-api/runtime-observability-api.md +++ b/contracts/agents-api/runtime-observability-api.md @@ -35,7 +35,7 @@ The list has one row for every Session of every Project that is not deleted, inc "session_id": "6c77d3a2-71d6-4ed5-884f-687aecda02a3", "environment_id": "6c02fb71-5fa8-4298-93e8-57c6625a3fc2", "mode": "openai_hosted", - "provider_type": "docker", + "provider_type": "microsandbox", "instance": { "kind": "managed_allocation", "allocation_id": "d23ab94e-e40b-45bd-93a2-444f1f74642b", @@ -81,7 +81,7 @@ This returns one `RuntimeObservation`, without `disk`. It accepts no query param | `session_id` | string | The Session. | | `environment_id` | string or null | Null only for mode `none`. | | `mode` | enum | `none`, `self_hosted` or `openai_hosted`. | -| `provider_type` | string or null | Source kind, such as `docker`, `microsandbox` or `e2b`; null when no provider was read. Treat an unknown value as a new kind, not an error. | +| `provider_type` | string or null | Source kind, `microsandbox` or `e2b`; null when no provider was read. Treat an unknown value as a new kind, not an error. | | `instance` | object | The current compute identity; see [`RuntimeInstance`](#runtimeinstance). | | `lifecycle_state` | enum or null | Core's own lifecycle view of a managed allocation; null for `none` and `self_hosted`. See below. | | `status` | enum | `observed`, `unsupported` or `unavailable`. | @@ -117,7 +117,7 @@ This returns one `RuntimeObservation`, without `disk`. It accepts no query param ### `disk` -Only list rows carry `disk`: null, or `{usage_bytes, limit_bytes}` with the rules of `memory`. E2B fills it when the sandbox reports both its disk usage and a nonzero capacity. Docker and microsandbox return null. A non-null `disk` appears only on an `observed` row. +Only list rows carry `disk`: null, or `{usage_bytes, limit_bytes}` with the rules of `memory`. E2B fills it when the sandbox reports both its disk usage and a nonzero capacity. microsandbox returns null. A non-null `disk` appears only on an `observed` row. ### Status and reason diff --git a/contracts/agents-api/runtime-observability.md b/contracts/agents-api/runtime-observability.md index 14e04d22a..d519abf17 100644 --- a/contracts/agents-api/runtime-observability.md +++ b/contracts/agents-api/runtime-observability.md @@ -18,13 +18,13 @@ none: tenant_id -> session_id (no Session-owned Runtime instance) The resolver (`services/core/internal/deployment/observation.go`) reads the Session, its Environment, the current allocation and the Session's measured usage from the database. A Session, daemon connection, process, container and native Harness Session are different identities and never stand in for one another. -Managed Docker, microsandbox and E2B allocations are observed. `none` and `self_hosted` Sessions are `unsupported`; Core never attributes shared host statistics to an `environment:none` Session. +Managed microsandbox and E2B allocations are observed. `none` and `self_hosted` Sessions are `unsupported`; Core never attributes shared host statistics to an `environment:none` Session. Every managed allocation is read through the deployment's selected Sandbox Provider, which verifies the allocation's installation (`provider_key`) and labels or equivalent ownership data before it returns values. Before any provider read, the allocation state decides some rows: `creating` or no allocation yet gives `allocation_pending`, and `cleanup_pending` or `released` gives `runtime_not_running`. A provider read that exceeds its deadline gives `sample_timeout`, a not-running result `runtime_not_running`, and an unavailable result `sample_unavailable`. Any other error, an ownership mismatch or an invalid sample fails the read. `Observe` belongs to the [Sandbox Provider protocol](../../docs/sandbox-provider.md); `services/core/internal/runtimeobs/source.go` owns the observation types and the `Source` view of a Provider. Core loads the selected Provider and its registered kind once per page and uses that same immutable Provider for every read on that page, reading each running target with `Observe`. Without a selection the load returns typed `ErrUnavailable`, which produces `sample_unavailable` without a provider type. Other load errors follow the provider-read error rules above. -An observation's provider type is that registered kind: `docker`, `microsandbox` or `e2b`. Reconfiguration affects later pages; it cannot change the provider type or Provider selected for an in-flight page. Generation routers continue to resolve each allocation through its recorded deployment generation. +An observation's provider type is that registered kind: `microsandbox` or `e2b`. Reconfiguration affects later pages; it cannot change the provider type or Provider selected for an in-flight page. Generation routers continue to resolve each allocation through its recorded deployment generation. ## Sample semantics @@ -40,10 +40,6 @@ Every measurement is optional. A present zero is an observed zero; an absent val ## Provider mapping -### Docker - -One non-streaming Inspect and Stats read of the owned container. Cumulative CPU time comes from the cgroup counter, memory usage from the current cgroup usage, and CPU and memory capacity from the container's configured limits. The container's `StartedAt` is the incarnation start, so a container restart resets compute uptime. A missing or stopped container is `runtime_not_running`. Disk is null. - ### microsandbox A suspended allocation is `runtime_not_running` without calling the helper. Otherwise Core sends the helper a `metrics` request for the exact compute recorded on the allocation. Under the allocation lock, the helper checks the sandbox's identity through the pinned SDK, runs the pinned `msb metrics --format json` CLI read, and checks the identity again; the sandbox must be running or draining. The [microsandbox helper](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/microsandbox-provider/README.md) owns that request. diff --git a/contracts/agents-api/sandbox-deployment.md b/contracts/agents-api/sandbox-deployment.md index 3f3ed1d88..83b56bed8 100644 --- a/contracts/agents-api/sandbox-deployment.md +++ b/contracts/agents-api/sandbox-deployment.md @@ -34,12 +34,12 @@ POST and PUT take the same complete selection and require `expected_generation` | Field | Meaning | | --- | --- | | `expected_generation` | Required nonnegative integer from GET; never refresh and replay it automatically | -| `provider` | Exactly one of `docker`, `microsandbox`, `e2b` | -| `resources` | Per-sandbox limits, below; required for Docker and microsandbox, optional for E2B | -| `configuration` | The provider's public selectors. E2B: the immutable `template` build and the optional paired `api_url` and `domain`. Docker and microsandbox accept only `{}` or omission | -| `credential` | The provider's write-only credential. E2B: `{api_key}`, required at first setup and omitted on PUT to keep the current key; a null or empty key is invalid. Docker and microsandbox reject it | +| `provider` | Exactly one of `microsandbox`, `e2b` | +| `resources` | Per-sandbox limits, below; required for microsandbox, optional for E2B | +| `configuration` | The provider's public selectors. E2B: the immutable `template` build and the optional paired `api_url` and `domain`. microsandbox accepts only `{}` or omission | +| `credential` | The provider's write-only credential. E2B: `{api_key}`, required at first setup and omitted on PUT to keep the current key; a null or empty key is invalid. microsandbox rejects it | -The request has no Core address. Core derives the deployment's `core_url` from the installation public URL, [`OAC_PUBLIC_URL`](../../docs/configuration.md#settings): the origin nodes and sandbox guests use to reach Core. A request that contains `core_url` is rejected with 400 `invalid_request` like any other unknown member. E2B guests reach Core from E2B's cloud, so an E2B selection is rejected with 409 `sandbox_configuration_error` while the public URL is loopback. Docker and microsandbox selections accept a loopback public URL, which serves only local development because a guest's loopback address does not reach its host. Changing the public URL is an installation change; see [Change the public URL](../../docs/getting-started/public-address.md#changing-the-public-url). +The request has no Core address. Core derives the deployment's `core_url` from the installation public URL, [`OAC_PUBLIC_URL`](../../docs/configuration.md#settings): the origin nodes and sandbox guests use to reach Core. A request that contains `core_url` is rejected with 400 `invalid_request` like any other unknown member. E2B guests reach Core from E2B's cloud, so an E2B selection is rejected with 409 `sandbox_configuration_error` while the public URL is loopback. A microsandbox selection accepts a loopback public URL, which serves only local development because a guest's loopback address does not reach its host. Changing the public URL is an installation change; see [Change the public URL](../../docs/getting-started/public-address.md#changing-the-public-url). ### Resources @@ -47,16 +47,16 @@ The request has no Core address. Core derives the deployment's `core_url` from t | --- | --- | | `cpus` | Integer, 1 through 255 | | `memory_mib` | Integer, 512 through 1048576 MiB | -| `root_disk_mib` | microsandbox: at least 1024 MiB; Docker and E2B: omitted or zero | -| `environment_disk_mib` | microsandbox: each new workspace's admitted capacity on workspace storage, where zero (the default) requests no quota and a positive value must be at least 1024 MiB and requires enforced quotas; Docker and E2B: omitted or zero | +| `root_disk_mib` | microsandbox: at least 1024 MiB; E2B: omitted or zero | +| `environment_disk_mib` | microsandbox: each new workspace's admitted capacity on workspace storage, where zero (the default) requests no quota and a positive value must be at least 1024 MiB and requires enforced quotas; E2B: omitted or zero | These limits describe each sandbox. A node's `max_active` and `max_retained` are separate reservation limits, set as [Node capacity](#nodes-and-allocations) describes; no host measurement permits exceeding either once set. Native providers may reject values that pass these bounds. -Docker applies the CPU and memory limits and checks the running container's limits and exact image; it has no hard root or workspace disk quota. E2B CPU and memory must equal the exact ready template build, which Core validates through the pinned SDK before saving; disk capacity stays part of the template. Neither provider accepts a disk quota it cannot enforce. An E2B selection may omit `resources`: Core then stores the build's CPU count and memory as `cpus` and `memory_mib`, returned in `specification.resources` without disk fields. On restart Core loads the committed E2B selection without validating the template build again, so an E2B outage never blocks inspection or cleanup; new selections still require validation. +E2B CPU and memory must equal the exact ready template build, which Core validates through the pinned SDK before saving; disk capacity stays part of the template. E2B accepts no disk quota it cannot enforce. An E2B selection may omit `resources`: Core then stores the build's CPU count and memory as `cpus` and `memory_mib`, returned in `specification.resources` without disk fields. On restart Core loads the committed E2B selection without validating the template build again, so an E2B outage never blocks inspection or cleanup; new selections still require validation. microsandbox configures the CPUs, memory and a managed root disk. It runs every Environment on [workspace storage](../../docs/workspace-provider.md) mounted at `/environment`. The [microsandbox helper](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/microsandbox-provider/README.md) describes how restore handles these limits. -The optional response `specification.workspace` is an immutable capability receipt derived from the selected [workspace filesystem](../../docs/workspace-provider.md), not an independently writable setting. It contains only `attachment`, `user_xattr` and `capacity_quota`; filesystem configuration, paths and credentials stay outside the sandbox specification. Core validates the combined declarations before saving, and retained generation validation and node construction use this receipt. microsandbox always has the receipt, and a deployment saved before workspace storage is selected returns `workspace_storage_required`. Docker and E2B have no receipt and keep their own disks. Create and Resume provide a binding exactly when the frozen generation has the receipt. +The optional response `specification.workspace` is an immutable capability receipt derived from the selected [workspace filesystem](../../docs/workspace-provider.md), not an independently writable setting. It contains only `attachment`, `user_xattr` and `capacity_quota`; filesystem configuration, paths and credentials stay outside the sandbox specification. Core validates the combined declarations before saving, and retained generation validation and node construction use this receipt. microsandbox always has the receipt, and a deployment saved before workspace storage is selected returns `workspace_storage_required`. E2B has no receipt and keeps its own disks. Create and Resume provide a binding exactly when the frozen generation has the receipt. Select the filesystem configuration before the microsandbox deployment. Selecting the filesystem does not alter existing generations or allocations; the next sandbox update derives the receipt. A filesystem selection must support the provider's declared requirements. An existing deployment requires the same `attachment` and `user_xattr`; the selection may change `capacity_quota`, after which new workspaces are refused until a sandbox update sets a matching `environment_disk_mib`. Existing workspaces keep the capacity they were admitted with. @@ -66,7 +66,7 @@ E2B uses `configuration.template` in `template-id:build-uuid` form; the build UU ### Configuration discovery -`POST /core/v1/sandbox/providers/{provider}/discovery` takes exactly `configuration`, `credential` and `query` objects, at most 64 KiB, and runs for at most 30 seconds. Unknown members and null objects are rejected. The credential is used only for that request and is never stored or returned; discovery saves nothing, changes no deployment, allocates nothing and never proves that a selection will be admitted. Docker and microsandbox reject it with 400 `sandbox_operation_unsupported`. +`POST /core/v1/sandbox/providers/{provider}/discovery` takes exactly `configuration`, `credential` and `query` objects, at most 64 KiB, and runs for at most 30 seconds. Unknown members and null objects are rejected. The credential is used only for that request and is never stored or returned; discovery saves nothing, changes no deployment, allocates nothing and never proves that a selection will be admitted. microsandbox rejects it with 400 `sandbox_operation_unsupported`. Post `{"configuration": {…}, "credential": {…}, "query": {"field": "…", "path": […]}}`, where `field` is a field the provider declares as discovered and `path` holds the values selected at the earlier levels. The result is `{"options": [{"value": "…", "label": "…", "resources": {"cpus": 2, "memory_mib": 2048}, "complete": true}]}`, possibly empty. An option with `complete: true` can be saved as the field's value; the others open the next level. Any other field returns 400 `invalid_request_error`. @@ -74,12 +74,12 @@ E2B declares `template` as discovered. An empty `path` lists the key's templates ## Safe response -GET and successful writes return `installation_id`, `provider`, `core_url` (read-only: the installation public URL, present even before configuration), `mode`, `generation`, `owner_epoch`, `reset`, `rollout`, `suspension`, `resources` and `credential_configured`. A configured deployment also returns `specification`, `specification_digest`, `configuration` and `metadata`: the adapter's public projection of its selectors and of the observations it recorded, never raw stored values or secrets. E2B returns `configuration.template`, `configuration.api_url`, `configuration.domain` and, once recorded, `metadata.template_build`. Docker and microsandbox return empty `configuration` and `metadata` objects and `credential_configured: false`; an unconfigured deployment has neither object. +GET and successful writes return `installation_id`, `provider`, `core_url` (read-only: the installation public URL, present even before configuration), `mode`, `generation`, `owner_epoch`, `reset`, `rollout`, `suspension`, `resources` and `credential_configured`. A configured deployment also returns `specification`, `specification_digest`, `configuration` and `metadata`: the adapter's public projection of its selectors and of the observations it recorded, never raw stored values or secrets. E2B returns `configuration.template`, `configuration.api_url`, `configuration.domain` and, once recorded, `metadata.template_build`. microsandbox returns empty `configuration` and `metadata` objects and `credential_configured: false`; an unconfigured deployment has neither object. - `metadata.template_build` is `{status, resources: {cpus, memory_mib, root_disk_mib}}`: the build as Core read it through the pinned SDK when the selection was saved. GET never calls E2B, so it stays cheap during an E2B outage. Validation admits only a `ready` build whose CPU count and memory equal the selected values; `root_disk_mib` is the build's native disk size, which Core does not enforce. Unknown values are null, `metadata: {}` means no observation was recorded, and an identical PUT without a credential does not refresh it. - `suspension` is `{idle_seconds, retention_seconds}`, Core's [suspension policy](../../docs/sandbox-provider.md#suspension), when the selected Provider declares checkpoint support; any other deployment, including an unconfigured one, returns null. - Request `resources` and response `specification.resources` are per-sandbox limits. Response `resources.allocations` and `resources.pending` count unreleased allocations and pending hosted Environments without an allocation; `resources.running` counts the unreleased allocations whose compute is running, so it excludes suspended ones. -- An unconfigured deployment has an empty provider and no specification. Docker and microsandbox use `mode: nodes`; E2B uses `mode: direct`, without a synthetic node. +- An unconfigured deployment has an empty provider and no specification. microsandbox uses `mode: nodes`; E2B uses `mode: direct`, without a synthetic node. - `generation` identifies the saved selection. `owner_epoch` fences the execution owner and node connections; it does not replace `expected_generation`. - `specification_digest` is the server's identity of the provider, limits and derived workspace capability receipt; enrollment echoes it unchanged. @@ -89,7 +89,7 @@ The typed `SandboxAdminClient` in `packages/agents-client` checks the deployment POST validates a candidate before persisting it and creates no compute, Session or model request. At the current generation an identical selection is a no-op; an old generation returns 409 `sandbox_generation_stale`, even for the same body. Missing prerequisites or a failed preparation leave the committed provider in place. A different backend requires a reset first. POST also initializes after a completed reset, using the reset's new generation. -PUT accepts the same provider while no reset is active. Send the observed generation once and never replay an uncertain write automatically. New allocations use the newly committed specification, and existing allocations keep their immutable deployment generation. A same-provider change retires no node, token or owner epoch and drains no execution: Docker and microsandbox nodes prepare the new target independently while serving their old pin, and E2B changes apply at once. +PUT accepts the same provider while no reset is active. Send the observed generation once and never replay an uncertain write automatically. New allocations use the newly committed specification, and existing allocations keep their immutable deployment generation. A same-provider change retires no node, token or owner epoch and drains no execution: microsandbox nodes prepare the new target independently while serving their old pin, and E2B changes apply at once. ### E2B key replacement @@ -192,7 +192,7 @@ One mutation gate serializes setup, PUT, reset, cancellation and finalization. A ## Nodes and allocations -Node capacity is set separately from the deployment specification. `POST /core/v1/sandbox/enrollment-tokens` accepts optional `max_active` and `max_retained` and returns `{token, expires_at, enrollment_id}`; `enrollment_id` is a public handle of that command, never a credential. Core stores the token with the limits as sent, and an omitted limit stays unset. When the node enrolls, Core copies each explicit limit to it unchanged and sets each unset limit to the [recommended capacity](#recommended-capacity) of the host measurements in the enrollment request and the current specification; an unset limit with no host measurements fails with 409 `sandbox_node_host_unmeasured` and leaves the token unused. When only `max_retained` is set, the derived `max_active` never exceeds it; when only `max_active` is set, the derived `max_retained` is raised to at least `max_active`. microsandbox uses both limits. Docker never suspends, so Core replaces its `max_retained` with `max_active`, here and in PATCH. E2B has no nodes and answers 409 `sandbox_deployment_conflict`. A node cannot submit limits, but an unset limit follows the host measurements the node reports at enrollment, bounded by 1 and 1000000. Its local checks may refuse a deployment it cannot run but never raise the limits. Explicit values and later PATCH edits bind: once a node is enrolled, Core derives nothing again. [Node capacity](../../docs/configuration.md#node-capacity) explains the limits for operators. +Node capacity is set separately from the deployment specification. `POST /core/v1/sandbox/enrollment-tokens` accepts optional `max_active` and `max_retained` and returns `{token, expires_at, enrollment_id}`; `enrollment_id` is a public handle of that command, never a credential. Core stores the token with the limits as sent, and an omitted limit stays unset. When the node enrolls, Core copies each explicit limit to it unchanged and sets each unset limit to the [recommended capacity](#recommended-capacity) of the host measurements in the enrollment request and the current specification; an unset limit with no host measurements fails with 409 `sandbox_node_host_unmeasured` and leaves the token unused. When only `max_retained` is set, the derived `max_active` never exceeds it; when only `max_active` is set, the derived `max_retained` is raised to at least `max_active`. microsandbox uses both limits. E2B has no nodes and answers 409 `sandbox_deployment_conflict`. A node cannot submit limits, but an unset limit follows the host measurements the node reports at enrollment, bounded by 1 and 1000000. Its local checks may refuse a deployment it cannot run but never raise the limits. Explicit values and later PATCH edits bind: once a node is enrolled, Core derives nothing again. [Node capacity](../../docs/configuration.md#node-capacity) explains the limits for operators. `GET /core/v1/sandbox/nodes` returns `{data: [...]}` with, for each node: `id`, `name`, `provider`, `online`, `last_seen_at`, `created_at`, `max_active`, `max_retained`, the counts `active`, `reserved`, `running`, `retained`, `snapshots` and `cleanup_pending`, `provider_ready`, the host measurements `cpu_count`, `available_memory_bytes` and `available_disk_bytes`, `rollout`, `recommended_capacity`, `enrollment_id` and `core_url`. `recommended_capacity` is `{max_active, max_retained}`, the [recommended capacity](#recommended-capacity) of the node's latest health and the current specification, or null until the node has reported a CPU count or effective cores and its total memory, and while the current specification does not validate. `enrollment_id` is the handle of the command that registered the node, or null when Core has none. `core_url` is the installation public URL at enrollment; a node whose `core_url` differs from the current public URL receives no new placements. Work already placed on it finishes there, including a placed Environment that has no allocation yet, and its retained sandboxes can still resume while the old address reaches Core. See [Change the public URL](../../docs/getting-started/public-address.md#changing-the-public-url). @@ -215,22 +215,22 @@ The recommendation is absent unless the host reports a CPU count or effective co Some fields keep one name across providers but differ in meaning, or do not apply. Deployment fields come from `GET /core/v1/sandbox/deployment`, node and allocation fields from the node routes, and Runtime fields from the [Runtime telemetry API](./runtime-observability-api.md), where `disk` appears only in the observation list and history is described under [Session Runtime history](./runtime-observability-api.md#session-runtime-history). -| Field | microsandbox | E2B | Docker | -| --- | --- | --- | --- | -| Independent workspace filesystem | Required: [Kernel NFS adapter](../../docs/workspace-provider.md#kernel-nfs-adapter), configured separately from compute | Unsupported | Unsupported | -| Deployment `specification.resources` | `cpus`, `memory_mib`, `root_disk_mib` and, as a quota request, `environment_disk_mib` | `cpus` and `memory_mib`, equal to the ready template build's and taken from it when omitted; no disk fields | `cpus` and `memory_mib`; no disk quota | -| Deployment `metadata.template_build` | Absent: `metadata` is empty | The build as Core read it when the selection was saved | Absent: `metadata` is empty | -| Deployment `suspension` | `{idle_seconds, retention_seconds}` | `{idle_seconds, retention_seconds}`; Core pauses an idle E2B sandbox with the SDK's native pause | `null` | -| Deployment `resources.allocations`, `resources.pending` | Totals across all nodes | Core's unreleased E2B sandboxes, and hosted Environments waiting for one | Totals across all nodes | -| Enrollment-token `max_active`, `max_retained` | Both limits apply; an omitted limit is derived at enrollment | 409 `sandbox_deployment_conflict`, after the 400 capacity checks; E2B has no nodes | `max_retained` always equals `max_active` | -| Node list and detail | Enrolled nodes | Empty list; detail returns 404 | Enrolled nodes | -| Node `retained`, `snapshots`, `max_retained` | Suspended sandboxes are `retained` minus `active`; `snapshots` also includes retained snapshots during restore or cleanup | Not applicable | Docker never suspends: `retained` equals `active`, `snapshots` is 0 and `max_retained` equals `max_active` | -| Node `host.available_disk_bytes` | Free space on the filesystem of the node state directory; sandbox disks have their own quotas | Not applicable | Free space on the filesystem of the node state directory, not a container's disk | -| Allocation `compute_phase`, `compute_phase_changed_at` | Includes `suspended`; the timestamp describes the current phase, not snapshot expiry | Not applicable: no node allocations | Always `disabled`, counted as running until release; the time is the allocation's creation | -| Runtime observation `memory` | From the VM: memory limit and usage | From E2B metrics: memory usage and limit | From Docker stats: memory limit and usage | -| Runtime observation `disk` | `null` | E2B `diskUsed` and `diskTotal`; `null` when the template does not report them | `null`: no disk quota | -| Runtime observation `lifecycle_state: sleeping` | While suspended | Never | Never | -| Runtime history CPU | Derived from cumulative CPU time | Mean of the utilization ratios E2B reported in each bucket | Derived from cumulative CPU time | +| Field | microsandbox | E2B | +| --- | --- | --- | +| Independent workspace filesystem | Required: [Kernel NFS adapter](../../docs/workspace-provider.md#kernel-nfs-adapter), configured separately from compute | Unsupported | +| Deployment `specification.resources` | `cpus`, `memory_mib`, `root_disk_mib` and, as a quota request, `environment_disk_mib` | `cpus` and `memory_mib`, equal to the ready template build's and taken from it when omitted; no disk fields | +| Deployment `metadata.template_build` | Absent: `metadata` is empty | The build as Core read it when the selection was saved | +| Deployment `suspension` | `{idle_seconds, retention_seconds}` | `{idle_seconds, retention_seconds}`; Core pauses an idle E2B sandbox with the SDK's native pause | +| Deployment `resources.allocations`, `resources.pending` | Totals across all nodes | Core's unreleased E2B sandboxes, and hosted Environments waiting for one | +| Enrollment-token `max_active`, `max_retained` | Both limits apply; an omitted limit is derived at enrollment | 409 `sandbox_deployment_conflict`, after the 400 capacity checks; E2B has no nodes | +| Node list and detail | Enrolled nodes | Empty list; detail returns 404 | +| Node `retained`, `snapshots`, `max_retained` | Suspended sandboxes are `retained` minus `active`; `snapshots` also includes retained snapshots during restore or cleanup | Not applicable | +| Node `host.available_disk_bytes` | Free space on the filesystem of the node state directory; sandbox disks have their own quotas | Not applicable | +| Allocation `compute_phase`, `compute_phase_changed_at` | Includes `suspended`; the timestamp describes the current phase, not snapshot expiry | Not applicable: no node allocations | +| Runtime observation `memory` | From the VM: memory limit and usage | From E2B metrics: memory usage and limit | +| Runtime observation `disk` | `null` | E2B `diskUsed` and `diskTotal`; `null` when the template does not report them | +| Runtime observation `lifecycle_state: sleeping` | While suspended | Never | +| Runtime history CPU | Derived from cumulative CPU time | Mean of the utilization ratios E2B reported in each bucket | ## Errors diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index 56cedb10e..89ab5fe75 100644 --- a/contracts/agents-api/zh/core-errors.md +++ b/contracts/agents-api/zh/core-errors.md @@ -1,7 +1,7 @@ --- title: "Core 管理错误" source: contracts/agents-api/core-errors.md -source_hash: 1424dc3288c244cb3be78bb2070530bdc0b5aec8474c1ab943611bee335fdca7 +source_hash: 771eca3c6af62cae0c43665da6cd03854c72e0befeaba57120bf6c2d13e346fe --- `/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。 @@ -68,7 +68,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 | `harness_config_invalid` | `harness_config` | 省略 | 部署默认配置的原生参数不受支持或无效 | | `invalid_sandbox_configuration` | `resources.cpus` | `min`:1,`max`:255 | CPU 数量超出支持范围 | | `invalid_sandbox_configuration` | `resources.memory_mib` | `min`:512,`max`:1048576 | 内存超出支持范围 | -| `invalid_sandbox_configuration` | `resources.root_disk_mib` 或 `resources.environment_disk_mib` | `min`:microsandbox 为 1024(`environment_disk_mib` 也接受 0);Docker 和 E2B 的 `min`:0,`max`:0 | 磁盘容量缺失或提供商不支持 | +| `invalid_sandbox_configuration` | `resources.root_disk_mib` 或 `resources.environment_disk_mib` | `min`:microsandbox 为 1024(`environment_disk_mib` 也接受 0);E2B 的 `min`:0,`max`:0 | 磁盘容量缺失或提供商不支持 | 这些边界是验证常量,绝不是提交的值。节点名称按字节数限制;项目名称和键名称按去除首尾空白后的 Unicode 字符数限制,且不得包含控制字符。系统仅按以下顺序报告第一个失败项:模型提供商 URL、协议、密钥、限制,然后是 Harness 协议;沙箱资源依次为 CPU、内存,然后是磁盘。`model_provider` 对象内的模型提供商字段错误仍以该对象的相应字段作为 `param`。未知的沙箱提供商返回一个不含这些字段的错误。 diff --git a/contracts/agents-api/zh/environment-executor-credentials.md b/contracts/agents-api/zh/environment-executor-credentials.md index d0926cce1..57f58674f 100644 --- a/contracts/agents-api/zh/environment-executor-credentials.md +++ b/contracts/agents-api/zh/environment-executor-credentials.md @@ -1,7 +1,7 @@ --- title: "执行器凭据" source: contracts/agents-api/environment-executor-credentials.md -source_hash: 9920c082498a3dd0ff4d7a20bed6ab3c43ed5a46a9a8e52c0efe72ce5683924b +source_hash: 706000252b81f220fb22ad28234328b38b425f145066b4fd7b4950cc56c89188 --- 执行器凭据允许 `oac-daemon` 为一个 `self_hosted` Environment 注册并连接。它只授权该 Environment 的私有 daemon 传输(`/api/v1/agent-daemon/*`),不授权 `/v1`、`/core/v1`、sandbox node 注册或项目资源。项目的 principal 是其执行 principal。Core 只保存密钥摘要。 @@ -91,7 +91,7 @@ Connected 表示 Environment 已连接,其设备和执行器凭据仍有当前 ## 已撤销或轮换的凭据 {#revoked-or-rotated-credential} -Core 永久拒绝 daemon 时(注册 401/409、永久 WebSocket 拒绝,或 daemon 版本来自其他 Core 分发),由 `oac-daemon start` 启动的 daemon 只打印一次原因并以错误退出,再次启动时只尝试一次注册,然后再次退出。Docker Runtime(`oac-daemon connect`)只打印一次原因,停止发请求直到被停止,随后成功退出,避免按退出重启的 supervisor 循环;再次启动时只尝试一次注册,然后再次停驻。临时故障保持正常重连行为,不重放执行。 +Core 永久拒绝 daemon 时(注册 401/409、永久 WebSocket 拒绝,或 daemon 版本来自其他 Core 分发),由 `oac-daemon start` 启动的 daemon 只打印一次原因并以错误退出,再次启动时只尝试一次注册,然后再次退出。沙箱 Runtime(`oac-daemon connect`)只打印一次原因,停止发请求直到被停止,随后成功退出,避免按退出重启的 supervisor 循环;再次启动时只尝试一次注册,然后再次停驻。临时故障保持正常重连行为,不重放执行。 机器只能使用绑定的 `key_id` 重连,轮换新密钥后替换配置路径的凭据文件;[自托管指南](../../../docs/zh/getting-started/self-hosted.md#rotate-or-revoke)提供步骤。新 `key_id` 无法重新连接已绑定的 Environment:签发成功,但用它注册返回 409。轮换不重装 Harness、不改变工作区、不替换原生历史;不要为恢复凭据创建新 Session 历史。 diff --git a/contracts/agents-api/zh/environments.md b/contracts/agents-api/zh/environments.md index b584015e7..8efbee683 100644 --- a/contracts/agents-api/zh/environments.md +++ b/contracts/agents-api/zh/environments.md @@ -1,7 +1,7 @@ --- title: "Environment 与模板" source: contracts/agents-api/environments.md -source_hash: 52d557886a595276c94f935b22531d84a712ceff0e64bf91e04749eed4366dc1 +source_hash: 6c8a1b26ed3e285a7ef5219d514d954e7c83e08c2d1c244b5669279fcd0b4c8a --- Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment 模板是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。 @@ -10,7 +10,7 @@ Environment 是 Session 的执行资源,包括 Harness 运行所在的机器 - [Environment files](environment-files.md):实时工作区上的 Files API。 - [执行器凭据](environment-executor-credentials.md):`self_hosted` 机器的注册、安装授权和连接状态。 -- [Sandbox deployment](sandbox-deployment.md):托管 `openai_hosted` Environment 的沙箱提供商(E2B、Docker 或 microsandbox)。 +- [Sandbox deployment](sandbox-deployment.md):托管 `openai_hosted` Environment 的沙箱提供商(E2B 或 microsandbox)。 - [Core–Runtime protocol](../../../docs/zh/runtime-protocol.md):`runtime_prepare` 传输及所有其他线上消息。 - [Runtime and outer isolation](../../../docs/zh/concepts.md#runtime-and-outer-isolation):daemon 使用启动用户的权限运行工具;隔离由外层 Environment 提供。 @@ -73,7 +73,7 @@ Core 在 Session 创建事务中创建 Environment 记录;Session upsert 会 ### 托管(`openai_hosted`) {#hosted-openai-hosted} -部署中配置的沙箱提供商(E2B、Docker 或 microsandbox,请参阅 [sandbox deployment](sandbox-deployment.md))承载 Environment。[Harness capabilities](harness-capabilities.md) 列出了可在其中运行的 Harnesses。 +部署中配置的沙箱提供商(E2B 或 microsandbox,请参阅 [sandbox deployment](sandbox-deployment.md))承载 Environment。[Harness capabilities](harness-capabilities.md) 列出了可在其中运行的 Harnesses。 - Session 创建时,无论是否包含初始输入,都会在 Worker 配置计算资源之前提交 Session、Environment 和重试身份。若创建在 bootstrap 前中断,可恢复时不会重复执行提供商的 Create。 - 置备无需调用方执行任何操作;在 Turn 启动之前,Session 会保持空闲。 diff --git a/contracts/agents-api/zh/harness-capabilities.md b/contracts/agents-api/zh/harness-capabilities.md index 9ffc01685..d97747f82 100644 --- a/contracts/agents-api/zh/harness-capabilities.md +++ b/contracts/agents-api/zh/harness-capabilities.md @@ -1,7 +1,7 @@ --- title: "Harness 能力" source: contracts/agents-api/harness-capabilities.md -source_hash: 8913afc18b99b847850a857e387e1a230085525ce5f1b28abd5a440abc9dcf16 +source_hash: 455e1a4ba4c672d818d76ecf7fc9db7fc432e72627692f5db0d10d95fecd73fa --- 本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `internal/harnessconfig//declaration.go` 中 Harness 的声明决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。 @@ -14,7 +14,7 @@ source_hash: 8913afc18b99b847850a857e387e1a230085525ce5f1b28abd5a440abc9dcf16 | 已准入 | Core 通过相同 Runtime 路径允许准入,但该位置尚未运行真实模型验收 | | 已拒绝 | Core 在执行前拒绝请求 | -部署位置为 `none`(无 Environment)、托管(`openai_hosted`)和自托管(`self_hosted`)。托管验收在 Docker 节点运行;E2B、microsandbox 使用相同 Runtime 与适配器,托管位置的每个已验证单元格在这两者中视为已准入。自托管验收在 Linux 机器运行;[自托管指南](../../../docs/zh/getting-started/self-hosted.md#platforms)列出支持平台。操作通过验证仅代表该操作本身,不代表与其他选项的所有组合;配置拒绝的组合列于对应操作契约。 +部署位置为 `none`(无 Environment)、托管(`openai_hosted`)和自托管(`self_hosted`)。E2B 和 microsandbox 使用相同 Runtime 与适配器,托管位置的每个已验证单元格在这两者中视为已准入。自托管验收在 Linux 机器运行;[自托管指南](../../../docs/zh/getting-started/self-hosted.md#platforms)列出支持平台。操作通过验证仅代表该操作本身,不代表与其他选项的所有组合;配置拒绝的组合列于对应操作契约。 ## 执行与输入 {#execution-and-input} @@ -67,4 +67,4 @@ Claude 结构化输出要求 medium verbosity,可与 `multi_agent`、Skill、P | [stdio Plugin MCP](environments.md#plugin-mcp) | 已验证:托管、自托管 | 已验证:自托管;已准入:托管 | 已验证:自托管;已准入:托管 | | HTTP Plugin MCP | 已准入,字面值头或 HTTPS bearer | 已准入,字面值头或 HTTPS bearer | 已验证:自托管;已准入:托管;字面值头或 HTTPS bearer | -外部工作区存储还要求所选 Harness 声明和 Runtime 支持 `retained_native_history`;使用自有磁盘的 Docker 和 E2B 不要求此能力。microsandbox 始终使用外部存储,因此未声明该能力的 MiniMax Code 无法在其上运行。[工作区文件系统协议](../../../docs/zh/workspace-provider.md)定义存储组合准入规则。 +外部工作区存储还要求所选 Harness 声明和 Runtime 支持 `retained_native_history`;使用自有磁盘的 E2B 不要求此能力。microsandbox 始终使用外部存储,因此未声明该能力的 MiniMax Code 无法在其上运行。[工作区文件系统协议](../../../docs/zh/workspace-provider.md)定义存储组合准入规则。 diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index c160dffb0..e46201a6b 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "Harness 协议" source: contracts/agents-api/harness-onboarding.md -source_hash: 9be8782db19e5d68342b751f9b9aeaeac4566e3489320294ca06fcebd422bf78 +source_hash: 258dc7cba6a5b2959d9b7b525ab5a6a9ef4ed53939e44276773b71095467946e --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code);**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。Harness 协议是 Runtime 与适配器之间的契约:适配器接口及其生命周期义务、声明、注册和验收。本文面向添加或维护 Harness 的开发者。 @@ -36,7 +36,7 @@ flowchart TB | 声明 | Harness 的最大能力、放置位置、配置和纯准入限制,由 Core 准入和 Runtime 共同读取 | `internal/harnessconfig//declaration.go` | | Runtime 绑定 | 声明加上发现、已安装工厂和已验证能力 | `apps/daemon/internal/agent//declaration.go` | -Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以及应用自有机器在预配和连接方式上有所不同;已连接的 Runtime 使用同一契约。daemon 运行于 Linux、macOS 和 Windows,受管提供商仅支持 Linux,并且每个适配器自行认定其支持的平台([self-hosted platforms](../../../docs/zh/getting-started/self-hosted.md#platforms))。只有在 Runtime 加载绑定的已安装快照后,原生工厂才会收到能力([capability preparation](environments.md#runtime-capability-preparation))。模型提供商负责模型通信设置,而不负责 Turn 调度或原生进程所有权。 +Environment 提供执行资源。受管 E2B 和 microsandbox 机器以及应用自有机器在预配和连接方式上有所不同;已连接的 Runtime 使用同一契约。daemon 运行于 Linux、macOS 和 Windows,受管提供商仅支持 Linux,并且每个适配器自行认定其支持的平台([self-hosted platforms](../../../docs/zh/getting-started/self-hosted.md#platforms))。只有在 Runtime 加载绑定的已安装快照后,原生工厂才会收到能力([capability preparation](environments.md#runtime-capability-preparation))。模型提供商负责模型通信设置,而不负责 Turn 调度或原生进程所有权。 ## 步骤 {#steps} diff --git a/contracts/agents-api/zh/node-generation-protocol.md b/contracts/agents-api/zh/node-generation-protocol.md index 64dd5a810..1fae3d016 100644 --- a/contracts/agents-api/zh/node-generation-protocol.md +++ b/contracts/agents-api/zh/node-generation-protocol.md @@ -1,15 +1,15 @@ --- title: "沙箱节点协议" source: contracts/agents-api/node-generation-protocol.md -source_hash: 3648a80013f1ae90031430c5bdf28d6ee5b59f55dda4b168dfeb99ab5bcec3e2 +source_hash: 78fca3a808fea42a96c020ce78178637e81d0c90ab7fe6827c011ec6f0b272c1 --- -沙箱节点协议连接 Core 与 Docker 或 microsandbox 主机:主机针对其本地提供商执行提供商操作,并报告就绪状态、主机测量值及其持有的部署代次。 +沙箱节点协议连接 Core 与 microsandbox 主机:主机针对其本地提供商执行提供商操作,并报告就绪状态、主机测量值及其持有的部署代次。 | | | | --- | --- | | 调用方 | Core,唯一的生命周期所有者;节点从不重试变更,也不调度工作 | -| 实现 | 沙箱节点程序,搭配 Docker 与 microsandbox 提供商 | +| 实现 | 沙箱节点程序,搭配 microsandbox 提供商 | | 代码 | [`services/core/internal/sandbox/node`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/node)(`wire.go`、`generation_wire.go`) | | 传输 | 一个 WebSocket,`GET /api/v1/sandbox-node/connect`;节点用于登记和读取配置的 HTTP 路由见[机器连接 API](machine-api.md#node-routes) | | 版本规则 | 与 `node.ProtocolVersion` 精确匹配,见[帧与版本](#frames-and-version) | diff --git a/contracts/agents-api/zh/runtime-observability-api.md b/contracts/agents-api/zh/runtime-observability-api.md index e03bd4b29..4eb72d523 100644 --- a/contracts/agents-api/zh/runtime-observability-api.md +++ b/contracts/agents-api/zh/runtime-observability-api.md @@ -1,7 +1,7 @@ --- title: "Runtime 遥测 API" source: contracts/agents-api/runtime-observability-api.md -source_hash: 7dcff114daa6c74d1ee08709f2f68d1b8d97c7225991669fb6431c684e34c26b +source_hash: fa3b6fa85dfed2399c687a3124b8ab6407dc8dfb3e0a3b32f86333dad0750c3c --- Core 通过 `/core/v1` 下的只读管理员路由报告托管 Runtime 和沙箱节点所使用的信息:当前 Runtime 观测值、单个 Session 的已存储 Runtime 历史记录,以及沙箱节点的主机观测值和历史记录。读取操作绝不创建、唤醒、续期或更改计算资源,也绝不向历史记录添加样本。[Runtime observability](runtime-observability.md) 定义了 Core 如何采集和保留这些值;[Console API usage](../../../docs/zh/web/console-api-usage.md) 列出了读取这些值的 Web 页面。 @@ -37,7 +37,7 @@ Authorization: Bearer "session_id": "6c77d3a2-71d6-4ed5-884f-687aecda02a3", "environment_id": "6c02fb71-5fa8-4298-93e8-57c6625a3fc2", "mode": "openai_hosted", - "provider_type": "docker", + "provider_type": "microsandbox", "instance": { "kind": "managed_allocation", "allocation_id": "d23ab94e-e40b-45bd-93a2-444f1f74642b", @@ -83,7 +83,7 @@ Authorization: Bearer | `session_id` | string | 对应的 Session。 | | `environment_id` | string 或 null | 仅当 mode 为 `none` 时为 null。 | | `mode` | enum | `none`、`self_hosted` 或 `openai_hosted`。 | -| `provider_type` | string 或 null | 来源类型,例如 `docker`、`microsandbox` 或 `e2b`;未读取任何提供商数据时为 null。未知值应视为新类型,而不是错误。 | +| `provider_type` | string 或 null | 来源类型,`microsandbox` 或 `e2b`;未读取任何提供商数据时为 null。未知值应视为新类型,而不是错误。 | | `instance` | object | 当前计算资源标识;请参阅 [`RuntimeInstance`](#runtimeinstance)。 | | `lifecycle_state` | enum 或 null | Core 自身对托管分配的生命周期视图;对于 `none` 和 `self_hosted` 为 null。请参阅下文。 | | `status` | enum | `observed`、`unsupported` 或 `unavailable`。 | @@ -119,7 +119,7 @@ Authorization: Bearer ### `disk` {#disk} -只有列表行包含 `disk`:其值为 null,或为遵循 `memory` 规则的 `{usage_bytes, limit_bytes}`。当沙箱同时报告磁盘使用量和非零容量时,E2B 会填充此对象。Docker 和 microsandbox 返回 null。非 null 的 `disk` 只出现在状态为 `observed` 的行中。 +只有列表行包含 `disk`:其值为 null,或为遵循 `memory` 规则的 `{usage_bytes, limit_bytes}`。当沙箱同时报告磁盘使用量和非零容量时,E2B 会填充此对象。microsandbox 返回 null。非 null 的 `disk` 只出现在状态为 `observed` 的行中。 ### 状态和原因 {#status-and-reason} diff --git a/contracts/agents-api/zh/runtime-observability.md b/contracts/agents-api/zh/runtime-observability.md index e19e67478..6efbbd646 100644 --- a/contracts/agents-api/zh/runtime-observability.md +++ b/contracts/agents-api/zh/runtime-observability.md @@ -1,7 +1,7 @@ --- title: "Runtime 可观测性" source: contracts/agents-api/runtime-observability.md -source_hash: 0ff8d5a694d934cdc384b859f8192772689c7a5b6599124a2051e978aa7b52d3 +source_hash: b77c76a6b8ed496dab56f3d62deb7f7f61066e0751bc4bd3b5373535b3cc7c05 --- 这是面向贡献者的契约,规定 Core 如何观测 Runtime 并保留其历史。路由和响应字段见 [Runtime telemetry API](runtime-observability-api.md)。代码位于 `services/core/internal/runtimeobs`(解析、源、采样器和导出)、`services/core/internal/runtimehistory`(历史查询和 PostgreSQL 存储)以及 `services/core/internal/runtimeobs/otlpexporter`。 @@ -20,13 +20,13 @@ none: tenant_id -> session_id (no Session-owned Runtime instance) 解析器(`services/core/internal/deployment/observation.go`)从数据库中读取 Session、其 Environment、当前分配以及 Session 的实测使用量。Session、守护进程连接、进程、容器和原生 Harness Session 是不同身份,彼此绝不能替代。 -托管 Docker、microsandbox 和 E2B 分配均会被观测。`none` 和 `self_hosted` Session 为 `unsupported`;Core 绝不会将共享主机统计信息归属于 `environment:none` Session。 +托管 microsandbox 和 E2B 分配均会被观测。`none` 和 `self_hosted` Session 为 `unsupported`;Core 绝不会将共享主机统计信息归属于 `environment:none` Session。 每个托管分配都通过部署所选的沙箱提供商读取;该提供商在返回数值前会验证分配的 installation(`provider_key`)以及分配标签或等效所有权数据。在读取任何提供商之前,部分行的结果由分配状态决定:处于 `creating` 状态或尚无分配时得到 `allocation_pending`,处于 `cleanup_pending` 或 `released` 状态时得到 `runtime_not_running`。提供商读取超出截止时间时得到 `sample_timeout`,返回未运行结果时得到 `runtime_not_running`,返回不可用结果时得到 `sample_unavailable`。任何其他错误、所有权不匹配或无效采样都会使读取失败。 `Observe` 属于 [沙箱提供商协议](../../../docs/zh/sandbox-provider.md);`services/core/internal/runtimeobs/source.go` 负责观测类型以及提供商的 `Source` 视图。Core 每页只加载一次所选提供商及其注册 kind,并在该页的每次读取中使用同一不可变提供商,用 `Observe` 读取每个运行中的目标。没有选择时,加载返回类型化的 `ErrUnavailable`,从而生成不含提供商类型的 `sample_unavailable`。其他加载错误遵循上述提供商读取错误规则。 -观测的提供商类型即该注册 kind:`docker`、`microsandbox` 或 `e2b`。重新配置会影响后续页面,但无法更改正在处理页面所选定的提供商类型或提供商。Generation 路由器仍会通过每个分配记录的部署代次解析该分配。 +观测的提供商类型即该注册 kind:`microsandbox` 或 `e2b`。重新配置会影响后续页面,但无法更改正在处理页面所选定的提供商类型或提供商。Generation 路由器仍会通过每个分配记录的部署代次解析该分配。 ## 采样语义 {#sample-semantics} @@ -42,10 +42,6 @@ none: tenant_id -> session_id (no Session-owned Runtime instance) ## 提供商映射 {#provider-mapping} -### Docker {#docker} - -对所属容器进行一次非流式 Inspect 和 Stats 读取。累计 CPU 时间来自 cgroup 计数器,内存使用量来自当前 cgroup 使用量,CPU 和内存容量来自容器配置的容量限制。容器的 `StartedAt` 是本次计算实例启动周期的开始时间,因此重启容器会重置计算运行时长。容器缺失或已停止时为 `runtime_not_running`。磁盘为 null。 - ### microsandbox {#microsandbox} 处于挂起状态的分配为 `runtime_not_running`,且不会调用 helper。否则,Core 会针对分配中记录的确切计算实例向 helper 发送 `metrics` 请求。在分配锁保护下,helper 通过固定版本 SDK 检查 sandbox 身份,运行固定版本的 `msb metrics --format json` CLI 读取,再次检查身份;sandbox 必须处于 running 或 draining 状态。[microsandbox helper](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/microsandbox-provider/README.md) 负责此请求。 diff --git a/contracts/agents-api/zh/sandbox-deployment.md b/contracts/agents-api/zh/sandbox-deployment.md index 45bf4aad2..8397320ba 100644 --- a/contracts/agents-api/zh/sandbox-deployment.md +++ b/contracts/agents-api/zh/sandbox-deployment.md @@ -1,7 +1,7 @@ --- title: "沙箱部署" source: contracts/agents-api/sandbox-deployment.md -source_hash: 27e0c5be13ea610bf488553702c6f8eca3e530544855e754789116cdb2ddc703 +source_hash: c16d11a873f0fc39288cd0b1cfbc9514a1392950f46fbaeb49503fa42f80ba9b --- 沙箱部署为 Core 管理的 `openai_hosted` 执行选择沙箱提供商和每个沙箱的资源。PostgreSQL 为每个安装维护一个当前有效选择;Web 和 Core API 写入同一配置。节点文件保存其已安装副本和特定于主机的路径,且不能覆盖其资源。Runtime 不属于该选择:每个节点运行其所属安装验证过的 Runtime。该选择独立于 Harness。部署可以保持未配置状态,没有节点;此时它拒绝托管准入。 @@ -36,12 +36,12 @@ POST 和 PUT 接受相同的完整选择,并要求提供先前 GET 返回的 ` | 字段 | 含义 | | --- | --- | | `expected_generation` | 必填的非负整数,来自 GET;绝不会自动刷新并重放 | -| `provider` | 必须是 `docker`、`microsandbox`、`e2b` 中恰好一个 | -| `resources` | 每个沙箱的限制,见下文;Docker 和 microsandbox 必填,E2B 可选 | -| `configuration` | 提供商的公开选择器。E2B:不可变的 `template` 构建以及可选且配套的 `api_url` 和 `domain`。Docker 和 microsandbox 仅接受 `{}` 或省略 | -| `credential` | 提供商的只写凭据。E2B:`{api_key}`,首次设置时必填,在 PUT 中省略以保留当前密钥;null 或空密钥无效。Docker 和 microsandbox 拒绝该字段 | +| `provider` | 必须是 `microsandbox`、`e2b` 中恰好一个 | +| `resources` | 每个沙箱的限制,见下文;microsandbox 必填,E2B 可选 | +| `configuration` | 提供商的公开选择器。E2B:不可变的 `template` 构建以及可选且配套的 `api_url` 和 `domain`。microsandbox 仅接受 `{}` 或省略 | +| `credential` | 提供商的只写凭据。E2B:`{api_key}`,首次设置时必填,在 PUT 中省略以保留当前密钥;null 或空密钥无效。microsandbox 拒绝该字段 | -请求中没有 Core 地址。Core 根据安装公开 URL [`OAC_PUBLIC_URL`](../../../docs/zh/configuration.md#settings)派生部署的 `core_url`:这是节点和沙箱客户机访问 Core 时使用的源地址。包含 `core_url` 的请求会像包含任何其他未知成员一样被拒绝,并返回 400 `invalid_request`。E2B 客户机从 E2B 云访问 Core,因此当公开 URL 为回环地址时,E2B 选择会被拒绝,并返回 409 `sandbox_configuration_error`。Docker 和 microsandbox 选择接受回环公开 URL,但这仅适用于本地开发,因为客户机的回环地址无法访问其主机。更改公开 URL 属于安装变更;参见[更改公开 URL](../../../docs/zh/getting-started/public-address.md#changing-the-public-url)。 +请求中没有 Core 地址。Core 根据安装公开 URL [`OAC_PUBLIC_URL`](../../../docs/zh/configuration.md#settings)派生部署的 `core_url`:这是节点和沙箱客户机访问 Core 时使用的源地址。包含 `core_url` 的请求会像包含任何其他未知成员一样被拒绝,并返回 400 `invalid_request`。E2B 客户机从 E2B 云访问 Core,因此当公开 URL 为回环地址时,E2B 选择会被拒绝,并返回 409 `sandbox_configuration_error`。microsandbox 选择接受回环公开 URL,但这仅适用于本地开发,因为客户机的回环地址无法访问其主机。更改公开 URL 属于安装变更;参见[更改公开 URL](../../../docs/zh/getting-started/public-address.md#changing-the-public-url)。 ### 资源 {#resources} @@ -49,17 +49,17 @@ POST 和 PUT 接受相同的完整选择,并要求提供先前 GET 返回的 ` | --- | --- | | `cpus` | 整数,1 到 255 | | `memory_mib` | 整数,512 到 1048576 MiB | -| `root_disk_mib` | microsandbox:至少 1024 MiB;Docker 和 E2B:省略或为零 | -| `environment_disk_mib` | microsandbox:工作区存储上每个新工作区的获准容量,零(默认值)表示不请求配额,正值必须至少为 1024 MiB 且要求强制执行配额;Docker 和 E2B:省略或为零 | +| `root_disk_mib` | microsandbox:至少 1024 MiB;E2B:省略或为零 | +| `environment_disk_mib` | microsandbox:工作区存储上每个新工作区的获准容量,零(默认值)表示不请求配额,正值必须至少为 1024 MiB 且要求强制执行配额;E2B:省略或为零 | 这些限制描述每个沙箱。节点的 `max_active` 和 `max_retained` 是独立的预留限制,设置方式见[节点与分配](#nodes-and-allocations);限制一经设置,任何主机测量值都不允许超过其中任何一个。即使数值满足这些边界,原生提供商仍可能拒绝这些值。 -Docker 应用 CPU 和内存限制,并检查运行中容器的限制和精确镜像;它没有硬性的根磁盘或工作区磁盘配额。E2B 的 CPU 和内存必须与精确的就绪模板构建一致,Core 会在保存前通过固定版本 SDK 进行验证;磁盘容量仍属于模板的一部分。两种提供商都不接受其无法强制执行的磁盘配额。E2B 选择可以省略 `resources`:此时 Core 将构建的 CPU 数量和内存存储为 `cpus` 和 `memory_mib`,并通过 `specification.resources` 返回,不带磁盘字段。重启时,Core 会加载已提交的 E2B 选择,而不会再次验证模板构建,因此 E2B 中断绝不会阻碍检查或清理;新选择仍需验证。 +E2B 的 CPU 和内存必须与精确的就绪模板构建一致,Core 会在保存前通过固定版本 SDK 进行验证;磁盘容量仍属于模板的一部分。E2B 不接受其无法强制执行的磁盘配额。E2B 选择可以省略 `resources`:此时 Core 将构建的 CPU 数量和内存存储为 `cpus` 和 `memory_mib`,并通过 `specification.resources` 返回,不带磁盘字段。重启时,Core 会加载已提交的 E2B 选择,而不会再次验证模板构建,因此 E2B 中断绝不会阻碍检查或清理;新选择仍需验证。 microsandbox 会配置 CPU、内存和托管根磁盘,并在挂载于 `/environment` 的[工作区存储](../../../docs/zh/workspace-provider.md)上运行每个 Environment。[microsandbox 辅助工具](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/microsandbox-provider/README.md)说明了 restore 如何处理这些限制。 -响应中可选的 `specification.workspace` 是从所选[工作区文件系统](../../../docs/zh/workspace-provider.md)派生的不可变能力回执,不是可独立写入的设置。它仅包含 `attachment`、`user_xattr` 和 `capacity_quota`;文件系统配置、路径和凭据不进入 Sandbox specification。Core 保存前校验组合声明,保留代次校验与节点构造均使用此回执。microsandbox 始终具备该回执,尚未选择工作区存储时保存部署会返回 `workspace_storage_required`。Docker 和 E2B 没有回执,继续使用自有磁盘。Create 和 Resume 必须且仅当固定代次具备该回执时提供绑定。 +响应中可选的 `specification.workspace` 是从所选[工作区文件系统](../../../docs/zh/workspace-provider.md)派生的不可变能力回执,不是可独立写入的设置。它仅包含 `attachment`、`user_xattr` 和 `capacity_quota`;文件系统配置、路径和凭据不进入 Sandbox specification。Core 保存前校验组合声明,保留代次校验与节点构造均使用此回执。microsandbox 始终具备该回执,尚未选择工作区存储时保存部署会返回 `workspace_storage_required`。E2B 没有回执,继续使用自有磁盘。Create 和 Resume 必须且仅当固定代次具备该回执时提供绑定。 先选择文件系统配置,再配置 microsandbox 部署。选择文件系统不会改变现有代次或 allocation;下一次 Sandbox 更新派生该回执。文件系统选择必须支持提供商声明的要求。现有部署要求相同的 `attachment` 和 `user_xattr`;选择可以更改 `capacity_quota`,此后在 Sandbox 更新设置相匹配的 `environment_disk_mib` 之前拒绝新工作区。既有工作区保持其获准时的容量。 @@ -69,7 +69,7 @@ E2B 使用 `template-id:build-uuid` 形式的 `configuration.template`;构建 ### 配置发现 {#configuration-discovery} -`POST /core/v1/sandbox/providers/{provider}/discovery` 恰好接受 `configuration`、`credential` 和 `query` 对象,总大小最多为 64 KiB,运行时间最多为 30 秒。未知成员和 null 对象会被拒绝。凭据仅用于该请求,绝不会被存储或返回;发现操作不会保存任何内容、不会更改部署、不会分配任何资源,也绝不会证明某项选择会被准入。Docker 和 microsandbox 会以 400 `sandbox_operation_unsupported` 拒绝该操作。 +`POST /core/v1/sandbox/providers/{provider}/discovery` 恰好接受 `configuration`、`credential` 和 `query` 对象,总大小最多为 64 KiB,运行时间最多为 30 秒。未知成员和 null 对象会被拒绝。凭据仅用于该请求,绝不会被存储或返回;发现操作不会保存任何内容、不会更改部署、不会分配任何资源,也绝不会证明某项选择会被准入。microsandbox 会以 400 `sandbox_operation_unsupported` 拒绝该操作。 发布 `{"configuration": {…}, "credential": {…}, "query": {"field": "…", "path": […]}}`,其中 `field` 是提供商声明为 discovered 的字段,`path` 是前几级已选择的值。结果为 `{"options": [{"value": "…", "label": "…", "resources": {"cpus": 2, "memory_mib": 2048}, "complete": true}]}`,可能为空。`complete: true` 的选项可保存为该字段的值,其余选项打开下一级。其他任何字段返回 400 `invalid_request_error`。 @@ -77,12 +77,12 @@ E2B 把 `template` 声明为 discovered。空 `path` 列出该密钥的模板, ## 安全响应 {#safe-response} -GET 和成功的写入操作会返回 `installation_id`、`provider`、`core_url`(只读:安装公开 URL,即使配置前也存在)、`mode`、`generation`、`owner_epoch`、`reset`、`rollout`、`suspension`、`resources` 和 `credential_configured`。已配置的部署还会返回 `specification`、`specification_digest`、`configuration` 和 `metadata`:这是适配器对其选择器及其记录的观测结果所作的公开投影,绝不会包含原始存储值或机密。E2B 返回 `configuration.template`、`configuration.api_url`、`configuration.domain`,并在记录后返回 `metadata.template_build`。Docker 和 microsandbox 返回空的 `configuration` 和 `metadata` 对象以及 `credential_configured: false`;未配置的部署则不含这两个对象。 +GET 和成功的写入操作会返回 `installation_id`、`provider`、`core_url`(只读:安装公开 URL,即使配置前也存在)、`mode`、`generation`、`owner_epoch`、`reset`、`rollout`、`suspension`、`resources` 和 `credential_configured`。已配置的部署还会返回 `specification`、`specification_digest`、`configuration` 和 `metadata`:这是适配器对其选择器及其记录的观测结果所作的公开投影,绝不会包含原始存储值或机密。E2B 返回 `configuration.template`、`configuration.api_url`、`configuration.domain`,并在记录后返回 `metadata.template_build`。microsandbox 返回空的 `configuration` 和 `metadata` 对象以及 `credential_configured: false`;未配置的部署则不含这两个对象。 - `metadata.template_build` 为 `{status, resources: {cpus, memory_mib, root_disk_mib}}`:这是保存选择时 Core 通过固定版本 SDK 读取的构建。GET 绝不会调用 E2B,因此 E2B 中断期间该操作仍保持低成本。验证仅接受 CPU 数量和内存与所选值一致的 `ready` 构建;`root_disk_mib` 是构建的原生磁盘大小,Core 不会强制执行该值。未知值为 null,`metadata: {}` 表示未记录任何观测,在不提供凭据的情况下提交完全相同的 PUT 也不会刷新它。 - 所选提供商声明 checkpoint 支持时,`suspension` 为 `{idle_seconds, retention_seconds}`,即 Core 的 [suspension policy](../../../docs/zh/sandbox-provider.md#suspension);其他部署(包括未配置的部署)返回 null。 - 请求中的 `resources` 和响应中的 `specification.resources` 是每个沙箱的限制。响应中的 `resources.allocations` 和 `resources.pending` 分别计算尚未释放的分配,以及尚未分配沙箱的待处理托管 Environment;`resources.running` 计算计算资源正在运行的未释放分配,因此不含已挂起的分配。 -- 未配置的部署具有空的提供商且没有 specification。Docker 和 microsandbox 使用 `mode: nodes`;E2B 使用 `mode: direct`,且没有合成节点。 +- 未配置的部署具有空的提供商且没有 specification。microsandbox 使用 `mode: nodes`;E2B 使用 `mode: direct`,且没有合成节点。 - `generation` 标识已保存的选择。`owner_epoch` 用于对执行所有者和节点连接进行栅栏隔离;它不能替代 `expected_generation`。 - `specification_digest` 是服务器对提供商、限制和派生的工作区能力回执的标识;注册过程会原样回显该值。 @@ -92,7 +92,7 @@ GET 和成功的写入操作会返回 `installation_id`、`provider`、`core_url POST 会在持久保存候选配置之前对其进行验证,并且不会创建计算资源、Session 或模型请求。在当前代次上,完全相同的选择是无操作;使用旧代次则返回 409 `sandbox_generation_stale`,即使请求体相同也是如此。缺少前置条件或准备失败时,已提交的提供商保持不变。更改提供商前必须先重置。POST 还会在重置完成后使用该重置的新代次进行初始化。 -仅在没有重置进行时,PUT 才接受同一提供商。只发送一次观测到的代次,绝不自动重放结果不确定的写入操作。新分配使用新提交的 specification,现有分配则保留其不可变的部署代次。同提供商更改不会停用任何节点、令牌或所有者 epoch,也不会排空任何执行:Docker 和 microsandbox 节点在继续提供旧固定版本的同时独立准备新目标,而 E2B 更改会立即生效。 +仅在没有重置进行时,PUT 才接受同一提供商。只发送一次观测到的代次,绝不自动重放结果不确定的写入操作。新分配使用新提交的 specification,现有分配则保留其不可变的部署代次。同提供商更改不会停用任何节点、令牌或所有者 epoch,也不会排空任何执行:microsandbox 节点在继续提供旧固定版本的同时独立准备新目标,而 E2B 更改会立即生效。 ### E2B 密钥替换 {#e2b-key-replacement} @@ -197,7 +197,7 @@ stateDiagram-v2 ## 节点与分配 {#nodes-and-allocations} -节点容量独立于部署 specification 设置。`POST /core/v1/sandbox/enrollment-tokens` 接受可选的 `max_active` 和 `max_retained`,并返回 `{token, expires_at, enrollment_id}`;`enrollment_id` 是该命令的公共句柄,绝不是凭据。Core 将令牌与发送的限制一同存储,省略的限制保持未设置。节点注册时,Core 把每个显式限制原样复制给节点,并把每个未设置的限制设为注册请求中的主机测量值与当前 specification 的[推荐容量](#recommended-capacity);限制未设置且没有主机测量值时,返回 409 `sandbox_node_host_unmeasured`,令牌保持未使用。只设置 `max_retained` 时,推导出的 `max_active` 不会超过它;只设置 `max_active` 时,推导出的 `max_retained` 会被提高到至少等于 `max_active`。microsandbox 会使用两个限制。Docker 绝不暂停,因此 Core 会在此处和 PATCH 中用 `max_active` 替换其 `max_retained`。E2B 没有节点,并返回 409 `sandbox_deployment_conflict`。节点不能提交限制,但未设置的限制取决于节点在注册时报告的主机测量值,范围限定在 1 到 1000000。其本地检查可以拒绝其无法运行的部署,但绝不会提高限制。显式值和之后的 PATCH 修改具有约束力:节点注册之后,Core 不再推导。[节点容量](../../../docs/zh/configuration.md#node-capacity)为操作员说明了这些限制。 +节点容量独立于部署 specification 设置。`POST /core/v1/sandbox/enrollment-tokens` 接受可选的 `max_active` 和 `max_retained`,并返回 `{token, expires_at, enrollment_id}`;`enrollment_id` 是该命令的公共句柄,绝不是凭据。Core 将令牌与发送的限制一同存储,省略的限制保持未设置。节点注册时,Core 把每个显式限制原样复制给节点,并把每个未设置的限制设为注册请求中的主机测量值与当前 specification 的[推荐容量](#recommended-capacity);限制未设置且没有主机测量值时,返回 409 `sandbox_node_host_unmeasured`,令牌保持未使用。只设置 `max_retained` 时,推导出的 `max_active` 不会超过它;只设置 `max_active` 时,推导出的 `max_retained` 会被提高到至少等于 `max_active`。microsandbox 会使用两个限制。E2B 没有节点,并返回 409 `sandbox_deployment_conflict`。节点不能提交限制,但未设置的限制取决于节点在注册时报告的主机测量值,范围限定在 1 到 1000000。其本地检查可以拒绝其无法运行的部署,但绝不会提高限制。显式值和之后的 PATCH 修改具有约束力:节点注册之后,Core 不再推导。[节点容量](../../../docs/zh/configuration.md#node-capacity)为操作员说明了这些限制。 `GET /core/v1/sandbox/nodes` 返回 `{data: [...]}`,其中每个节点包含 `id`、`name`、`provider`、`online`、`last_seen_at`、`created_at`、`max_active`、`max_retained`,计数项 `active`、`reserved`、`running`、`retained`、`snapshots` 和 `cleanup_pending`,以及 `provider_ready`、主机测量值 `cpu_count`、`available_memory_bytes` 和 `available_disk_bytes`、`rollout`、`recommended_capacity`、`enrollment_id` 和 `core_url`。`recommended_capacity` 是 `{max_active, max_retained}`,即节点最新健康状态与当前 specification 的[推荐容量](#recommended-capacity);在节点报告 CPU 数量或有效核数以及总内存之前,以及当前 specification 未通过校验时,为 null。`enrollment_id` 是注册该节点的命令的句柄;如果 Core 没有该句柄,则为 null。`core_url` 是注册时的安装公开 URL;`core_url` 与当前公开 URL 不同的节点不会收到新放置。已放置到该节点的工作会继续在那里完成,包括已放置但尚未获得分配的 Environment;只要旧地址仍能访问 Core,其保留沙箱仍可恢复。参见[更改公开 URL](../../../docs/zh/getting-started/public-address.md#changing-the-public-url)。 @@ -220,22 +220,22 @@ Core 用一条规则,从节点主机和单个沙箱的大小推导推荐值。 某些字段在不同提供商中保持同一名称,但含义不同或不适用。部署字段来自 `GET /core/v1/sandbox/deployment`,节点和分配字段来自节点路由,Runtime 字段来自 [Runtime 遥测 API](runtime-observability-api.md);其中 `disk` 仅出现在观测列表中,历史则在 [Session Runtime 历史](runtime-observability-api.md#session-runtime-history)下说明。 -| 字段 | microsandbox | E2B | Docker | -| --- | --- | --- | --- | -| 独立工作区文件系统 | 必需:[Kernel NFS adapter](../../../docs/zh/workspace-provider.md#kernel-nfs-adapter),与计算资源分开配置 | 不支持 | 不支持 | -| 部署 `specification.resources` | `cpus`、`memory_mib`、`root_disk_mib`,以及作为配额请求的 `environment_disk_mib` | `cpus` 和 `memory_mib`,必须等于就绪模板构建中的值;省略时取自该构建;无磁盘字段 | `cpus` 和 `memory_mib`;无磁盘配额 | -| 部署 `metadata.template_build` | 不存在:`metadata` 为空 | Core 保存选择时读取到的构建 | 不存在:`metadata` 为空 | -| 部署 `suspension` | `{idle_seconds, retention_seconds}` | `{idle_seconds, retention_seconds}`;Core 用 SDK 的原生 pause 暂停空闲的 E2B 沙箱 | `null` | -| 部署 `resources.allocations`、`resources.pending` | 所有节点的总数 | Core 中尚未释放的 E2B 沙箱,以及正在等待沙箱的托管 Environment | 所有节点的总数 | -| 注册令牌 `max_active`、`max_retained` | 两个限制均适用;省略的限制在注册时推导 | 先执行 400 容量检查,然后返回 409 `sandbox_deployment_conflict`;E2B 没有节点 | `max_retained` 始终等于 `max_active` | -| 节点列表和详情 | 已注册节点 | 空列表;详情返回 404 | 已注册节点 | -| 节点 `retained`、`snapshots`、`max_retained` | 已暂停沙箱数为 `retained` 减去 `active`;`snapshots` 还包含恢复或清理期间保留的快照 | 不适用 | Docker 绝不暂停:`retained` 等于 `active`,`snapshots` 为 0,`max_retained` 等于 `max_active` | -| 节点 `host.available_disk_bytes` | 节点状态目录所在文件系统的可用空间;沙箱磁盘有自己的配额 | 不适用 | 节点状态目录所在文件系统的可用空间,而不是容器的磁盘 | -| 分配 `compute_phase`、`compute_phase_changed_at` | 包含 `suspended`;时间表示当前阶段的起点,不是快照到期时间 | 不适用:没有节点分配 | 始终为 `disabled`,在释放前计为运行中;该时间为分配创建时间 | -| Runtime 观测 `memory` | 来自 VM:内存限制和使用量 | 来自 E2B 指标:内存使用量和限制 | 来自 Docker stats:内存限制和使用量 | -| Runtime 观测 `disk` | `null` | E2B `diskUsed` 和 `diskTotal`;模板未报告时为 `null` | `null`:无磁盘配额 | -| Runtime 观测 `lifecycle_state: sleeping` | 暂停期间 | 从不 | 从不 | -| Runtime 历史 CPU | 根据累计 CPU 时间派生 | 每个时间桶中 E2B 所报告使用率比值的平均值 | 根据累计 CPU 时间派生 | +| 字段 | microsandbox | E2B | +| --- | --- | --- | +| 独立工作区文件系统 | 必需:[Kernel NFS adapter](../../../docs/zh/workspace-provider.md#kernel-nfs-adapter),与计算资源分开配置 | 不支持 | +| 部署 `specification.resources` | `cpus`、`memory_mib`、`root_disk_mib`,以及作为配额请求的 `environment_disk_mib` | `cpus` 和 `memory_mib`,必须等于就绪模板构建中的值;省略时取自该构建;无磁盘字段 | +| 部署 `metadata.template_build` | 不存在:`metadata` 为空 | Core 保存选择时读取到的构建 | +| 部署 `suspension` | `{idle_seconds, retention_seconds}` | `{idle_seconds, retention_seconds}`;Core 用 SDK 的原生 pause 暂停空闲的 E2B 沙箱 | +| 部署 `resources.allocations`、`resources.pending` | 所有节点的总数 | Core 中尚未释放的 E2B 沙箱,以及正在等待沙箱的托管 Environment | +| 注册令牌 `max_active`、`max_retained` | 两个限制均适用;省略的限制在注册时推导 | 先执行 400 容量检查,然后返回 409 `sandbox_deployment_conflict`;E2B 没有节点 | +| 节点列表和详情 | 已注册节点 | 空列表;详情返回 404 | +| 节点 `retained`、`snapshots`、`max_retained` | 已暂停沙箱数为 `retained` 减去 `active`;`snapshots` 还包含恢复或清理期间保留的快照 | 不适用 | +| 节点 `host.available_disk_bytes` | 节点状态目录所在文件系统的可用空间;沙箱磁盘有自己的配额 | 不适用 | +| 分配 `compute_phase`、`compute_phase_changed_at` | 包含 `suspended`;时间表示当前阶段的起点,不是快照到期时间 | 不适用:没有节点分配 | +| Runtime 观测 `memory` | 来自 VM:内存限制和使用量 | 来自 E2B 指标:内存使用量和限制 | +| Runtime 观测 `disk` | `null` | E2B `diskUsed` 和 `diskTotal`;模板未报告时为 `null` | +| Runtime 观测 `lifecycle_state: sleeping` | 暂停期间 | 从不 | +| Runtime 历史 CPU | 根据累计 CPU 时间派生 | 每个时间桶中 E2B 所报告使用率比值的平均值 | ## 错误 {#errors} diff --git a/deploy/node/README.md b/deploy/node/README.md index 07e8ba6d6..abefa2129 100644 --- a/deploy/node/README.md +++ b/deploy/node/README.md @@ -5,7 +5,7 @@ These modules are packaged as `node-install.pyz`, which Web serves at `/node-ins | Module | Role | | --- | --- | | `node_install.py`, `node_spec.py`, `node_generations.py` | Node installer and generation helper. `node_spec.py` is generated by `go run ./services/core/cmd/specification-contract -write` | -| `host_docker.py`, `host_microsandbox.py` | One module per Sandbox Provider: its host checks, Runtime import, node configuration and uninstall cleanup. `node_install.py` selects the module in its `HOSTS` table by the provider Core chose and names no provider elsewhere; a new node Provider adds a module, a table entry and its name in the module lists of `node_generations.py`, `scripts/build-core-distribution.sh`, `scripts/core-distribution-manifest.py` and `test_node_helper_transfer.py` | +| `host_microsandbox.py` | One module per node Sandbox Provider: its host checks, Runtime import, node configuration and uninstall cleanup. `node_install.py` selects the module in its `HOSTS` table by the provider Core chose and names no provider elsewhere; a new node Provider adds a module, a table entry and its name in the module lists of `node_generations.py`, `scripts/build-core-distribution.sh`, `scripts/core-distribution-manifest.py` and `test_node_helper_transfer.py` | | `distribution.py` | Verified artifacts, private temporary files, release metadata and Docker image identity | | `install_display.py`, `node_output.py` | Terminal output | | `provider_assets.py` | Node provider artifact names, generated by `go run ./services/core/cmd/provider-artifacts` | @@ -15,15 +15,15 @@ These modules are packaged as `node-install.pyz`, which Web serves at `/node-ins The node installer runs as root and prepares the host for one node per installation. -- It creates or adopts the `oac-node` system user, adds it to the `docker` or `kvm` group (no other group), and installs one root-owned system service per installation that runs the node program as `User=oac-node`. Nodes on a host share that account, so a host serves one Core. -- Docker group membership makes that user, and so the node, root-equivalent on the host; that is inherent to Docker sandboxes. microsandbox needs only `kvm`, user KVM access and the Linux runtime libraries. +- It creates or adopts the `oac-node` system user, adds it to the `kvm` group (no other group), and installs one root-owned system service per installation that runs the node program as `User=oac-node`. Nodes on a host share that account, so a host serves one Core. +- microsandbox needs only `kvm`, user KVM access and the Linux runtime libraries. - Node configuration and identity live under `~/.oac/nodes//` in the node account's home (`/var/lib/oac-node`); microsandbox uses a separate short private Runtime home. - The node service owns its provider processes outside the Core container. `KillMode=process` keeps resident microVM and helper processes across a service restart. The service restarts after failures with no start limit, so a node outlasts a Core outage, and stops restarting when the node program exits 78 because Core answered 401 to its credential (a removed node). -- It never installs Docker, KVM or packages and never changes device permissions. It refuses SELinux-enforcing hosts and changes nothing when a check fails. The enrollment token comes only on standard input, never in arguments or the environment. +- It never installs KVM or packages and never changes device permissions. It refuses SELinux-enforcing hosts and changes nothing when a check fails. The enrollment token comes only on standard input, never in arguments or the environment. - Files the service account owns are read, written and deleted only with that account's credentials. The one exception is root removing the account's home after `userdel`, when no process can still run as that account. That work runs in a child that starts its own session with `/dev/null` as input, joins a new session keyring and dies with its parent; root shows its output only as plain text (terminal controls become `?`). SIGINT, SIGHUP and SIGTERM stop that child and what it started. - Root never runs a file the service account can write, opens a URL it wrote, or follows a link in its home. Capture the trusted bootstrap bytes before dropping to the service account and pass them through the fork; the service account writes its own retained generation helper. Never open the caller's private download directory to it or let root write into service-owned state. - The generated bootstrap passes only the six standard HTTP/HTTPS proxy and bypass variables through sudo and gives both spellings the lowercase value when present, even if empty, so curl, urllib and the Go registration command follow the same rules. The installation child keeps just those names beside its fixed environment. Proxy values stay out of arguments, unit files and diagnostics; never use broad sudo environment inheritance. The installation child uses them for downloads. The node service keeps the settings of the latest run in `/etc/oac-node/.env` (root, mode 0600, read through `EnvironmentFile=`; removed when a run has no proxy and by uninstall), with loopback added to `NO_PROXY`. -- `--uninstall` removes a node only after Core rejects its credential, except for a node that never registered and with `--force`, which Web offers when the old Core address no longer responds. It never touches sandboxes, volumes or Docker images (the Runtime image stays, with the command that removes it). Once Core confirmed the removal it also deletes the node's microsandbox store, which `--force` never does (it prints the command instead). It deletes the account only when the installer created it and no node remains, and otherwise removes only the groups it added. +- `--uninstall` removes a node only after Core rejects its credential, except for a node that never registered and with `--force`, which Web offers when the old Core address no longer responds. It never touches sandboxes or volumes. Once Core confirmed the removal it also deletes the node's microsandbox store, which `--force` never does (it prints the command instead). It deletes the account only when the installer created it and no node remains, and otherwise removes only the groups it added. - Never adopt or remove another installation's resources. ## Download contract @@ -61,13 +61,13 @@ A dropped generation can never be prepared or used again. A helper's exit is evi ### Installation payload -One installation serves exactly one release. A node gets its executable, private preparer and Runtime from the console that generated its command, and the Runtime stays on the node until uninstall. The console serves a single flat payload: `node-install.pyz`, `manifest.json`, `SHA256SUMS`, `runtime/seccomp.json` and the declared `artifacts/`. Artifact URLs stay pinned to the verified manifest. A node program that belongs to another installation of Core is refused with the remove, uninstall and add steps before the installer writes the node identity, imports the Runtime, registers the node or starts its service. +One installation serves exactly one release. A node gets its executable, private preparer and Runtime from the console that generated its command, and the Runtime stays on the node until uninstall. The console serves a single flat payload: `node-install.pyz`, `manifest.json`, `SHA256SUMS` and the declared `artifacts/`. Artifact URLs stay pinned to the verified manifest. A node program that belongs to another installation of Core is refused with the remove, uninstall and add steps before the installer writes the node identity, imports the Runtime, registers the node or starts its service. A published console payload keeps its metadata and artifact bytes. Publishing it again first validates all metadata and every existing declared artifact, then may atomically add only missing, checksum-matched declared artifacts; any conflict prevents every addition, and nothing is overwritten. ### Restart recovery -Missing Runtime bytes never move a placement to another Runtime. The node keeps the original generation and specification digest as unready and asks Core's authenticated configuration route for that exact generation before recovery. Missing seccomp bytes may leave an unready provider placeholder; a missing image or native artifact found by a provider probe keeps the generation unready, and rerunning the node's command restores the installation's missing bytes. +Missing Runtime bytes never move a placement to another Runtime. The node keeps the original generation and specification digest as unready and asks Core's authenticated configuration route for that exact generation before recovery. A missing image or native artifact found by a provider probe keeps the generation unready, and rerunning the node's command restores the installation's missing bytes. Preparation is serialized. Target and serving generations come first, with bounded progress on the others. Each attempt has a 30-minute deadline, and failures back off for 1, 2, 5 and 10 minutes, then at most 30. Nothing is prepared before the connection has delivered deployment facts. Conflicting bytes or a different retained specification refuse preparation. @@ -77,7 +77,7 @@ Preparation takes the same exclusive generation lease and installation lock as c Preparation writes the generation's final `.json` provider configuration, which is write-once: a copy of the node's base provider configuration with the generation's specification and number, written after Core authorized the generation and the node confirmed that the installation's Runtime is still available. Every generation of a node shares that Runtime and, for microsandbox, its store. Publication is atomic, so an interrupted preparation leaves either nothing or the complete file, and a rerun revalidates it. -Collection writes a digest-bound dropped marker under the generation lease after a fresh correlated Core drop grant. The marker prevents re-adoption; the small configuration and ownership journals stay as local identity records. It never removes the installation's Runtime, images or stores. Docker images belong to the host's shared daemon, and uninstall keeps them and prints the command that removes them. +Collection writes a digest-bound dropped marker under the generation lease after a fresh correlated Core drop grant. The marker prevents re-adoption; the small configuration and ownership journals stay as local identity records. It never removes the installation's Runtime, images or stores. ## Image identity diff --git a/deploy/node/host_docker.py b/deploy/node/host_docker.py deleted file mode 100644 index 52489e03b..000000000 --- a/deploy/node/host_docker.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Docker host preparation for a node; node_install.py selects it through its HOSTS table. - -Every function takes the installer module first, as node_generations.py does, so this file needs no import of it.""" -import json -import os -from pathlib import Path -import re -import shutil -import stat - -import distribution - -NAME = "Docker" -COMMAND = ("docker", "--host", "unix:///var/run/docker.sock") -SOCKET = Path("/var/run/docker.sock") -# The group that owns the provider's device; the node's service user joins only this one. -GROUP = "docker" -UNIT_AFTER = "network-online.target docker.service" -# Unpacked Runtime copies the Provider's store keeps on the disk beyond the one the import unpacks. -EXTRA_UNPACKED_COPIES = 0 -# A root-owned, empty Docker configuration for the service user's docker calls, so no -# CLI plugin or credential helper the service user controls runs in the installer. -CHILD_CONFIG = Path("/run/oac-node-docker") - - -def network(installation_id): - return "oac-node-" + installation_id - - -def networks(installer): - return installer.checked(list(COMMAND) + ["network", "ls", "--format", "{{.Name}}"], "Cannot inspect Docker networks").splitlines() - - -def containers(installer, installation_id): - return installer.checked(list(COMMAND) + ["ps", "--all", "--filter", "label=io.oac.installation=" + installation_id, - "--format", "{{.Names}}"], "Cannot inspect Docker containers").split() - - -def preflight(installer): - installer.checked(list(COMMAND) + ["info", "--format", "{{.ServerVersion}}"], "Docker access through /var/run/docker.sock is required") - - -def host_check(installer): - """Check Docker without installing it; return the group that grants access and the engine's details.""" - if shutil.which("docker") is None: - raise installer.InstallError("Docker Engine is not installed. Install it (https://docs.docker.com/engine/install/), " - "then rerun this command." + installer.NOTHING_CHANGED) - try: - info = SOCKET.stat() - except OSError: - info = None - if info is None or not stat.S_ISSOCK(info.st_mode): - raise installer.InstallError("Docker is not running: run `sudo systemctl enable --now docker`, then rerun this command." + installer.NOTHING_CHANGED) - if info.st_gid == 0 or stat.S_IMODE(info.st_mode) & 0o060 != 0o060: - raise installer.InstallError("Nodes reach Docker at /var/run/docker.sock through its group, but the socket is not " - "group-accessible (rootless Docker is not supported)." + installer.NOTHING_CHANGED) - try: - details = json.loads(installer.checked(list(COMMAND) + ["info", "--format", "{{json .}}"], "Docker is not running")) - except (installer.InstallError, ValueError): - raise installer.InstallError("Docker is not running: run `sudo systemctl enable --now docker`, then rerun this command." + installer.NOTHING_CHANGED) from None - # docker info's JSON uses the Engine API names (CpuCfsQuota), not the Go field names. - if details.get("MemoryLimit") is not True or details.get("CpuCfsQuota") is not True: - raise installer.InstallError("Docker on this host does not enforce CPU and memory limits; use cgroup v2, then rerun this command." + installer.NOTHING_CHANGED) - return installer.device_group(GROUP, SOCKET, info.st_gid), details - - -def capacity(installer, details): - return details.get("NCPU", 0), details.get("MemTotal", 0) - - -def other_node(installer, args): - # A network that no container uses is a leftover (Docker was down during an uninstall); the install reuses it. - if network(args.installation_id) in networks(installer) and containers(installer, args.installation_id): - raise installer.InstallError("Another node for this installation already uses this Docker engine (network " + network(args.installation_id) - + "). Remove it on the Nodes page and uninstall it first." + installer.NOTHING_CHANGED) - - -def configure(installer, root, args): - if getattr(args, "checkpoint_root", None) is not None: - raise installer.InstallError("Checkpoint root applies only to microsandbox nodes") - - -def native_config(installer, root, args, manifest, runtime_image): - return {"host": "unix:///var/run/docker.sock", "image": runtime_image, "network": network(args.installation_id), - "seccomp_file": str(root / "runtime/seccomp.json"), "nested_sandbox": True} - - -def runtime_imported(installer, root, args, manifest): - return distribution.docker_image(manifest, "runtime", COMMAND) is not None - - -def prepare_runtime(installer, root, args, manifest): - image = distribution.ensure_docker_image( - manifest, "runtime", lambda: distribution.runtime_archive(manifest, root, getattr(args, "disposable", False)), list(COMMAND)) - installer.drop_runtime_archives(root) - name = network(args.installation_id) - if name not in networks(installer): - installer.checked(list(COMMAND) + ["network", "create", name], "Cannot create node Docker network") - return image - - -def verify_retained(installer, stored, runtime_image): - if stored.get("native", {}).get("image") != runtime_image: - raise installer.node_spec.SpecificationError("Retained Docker image differs; preserve the node and inspect its configuration") - - -def image_available(installer, value): - seccomp = Path(value["native"]["seccomp_file"]) - installer.existing_file(seccomp) - json.loads(seccomp.read_text()) - raw = installer.checked(list(COMMAND) + ["image", "inspect", value["native"]["image"], "--format", "{{.Id}} {{.Os}}/{{.Architecture}}"], "Cannot inspect pinned image") - return raw.strip() == value["native"]["image"] + " linux/amd64" - - -def prepare_host(installer): - """Create the root-owned empty directory the service user's docker calls read.""" - path = CHILD_CONFIG - if not path.exists() and not path.is_symlink(): - path.mkdir(mode=0o755) - os.chmod(path, 0o755) - info = path.lstat() - if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or stat.S_IMODE(info.st_mode) & 0o022: - raise installer.InstallError(str(path) + " must be a directory only root can write; inspect the host.") - - -def child_environment(): - return {"DOCKER_CONFIG": str(CHILD_CONFIG)} - - -def release(installer, installation_id): - """Remove the node's Docker network; never containers, volumes or images.""" - if shutil.which("docker") is None: - return - name = network(installation_id) - try: - if name not in networks(installer): - return - remaining = containers(installer, installation_id) - except installer.InstallError: - print("Docker is unavailable, so network " + name + " was not removed.") - return - if remaining: - print("Kept Docker network " + name + ": these containers still exist: " + ", ".join(remaining)) - return - installer.checked(list(COMMAND) + ["network", "rm", name], "Cannot remove Docker network " + name) - - -def check_removal(installer, installation_id): - pass - - -def after_remove(installer, installation_id, native, delete_store): - """Name the Runtime image the uninstall keeps.""" - image = native.get("image") - # The service user can write provider.json; print only what an image ID can be. - if isinstance(image, str) and re.fullmatch(r"sha256:[0-9a-f]{64}", image): - print("Kept the Runtime image " + image + "; remove it with `sudo docker image rm " + image + "` if no other node uses it.") - - -def kept_stores(installer): - return [] diff --git a/deploy/node/node_generations.py b/deploy/node/node_generations.py index 6ba58aff8..10b86ae87 100644 --- a/deploy/node/node_generations.py +++ b/deploy/node/node_generations.py @@ -37,7 +37,7 @@ def helper_archive(args, installer): archive = io.BytesIO() source_dir = Path(installer.__file__).parent with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as package: - for name in ("node_install.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_docker.py", "host_microsandbox.py"): + for name in ("node_install.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_microsandbox.py"): package.write(source_dir / name, "__main__.py" if name == "node_install.py" else name) return archive.getvalue() diff --git a/deploy/node/node_install.py b/deploy/node/node_install.py index 722c7d169..6c2a6b8a2 100644 --- a/deploy/node/node_install.py +++ b/deploy/node/node_install.py @@ -31,7 +31,6 @@ import uuid import distribution -import host_docker import host_microsandbox import provider_assets import node_spec @@ -46,7 +45,7 @@ class InstallError(Exception): # The one place that names the Sandbox Providers: each module prepares its provider's host, and the # flow below calls them by the provider Core selected. -HOSTS = {"docker": host_docker, "microsandbox": host_microsandbox} +HOSTS = {"microsandbox": host_microsandbox} SELF = sys.modules[__name__] # Sudo mode: run as root, the installer prepares the host itself. The node runs as @@ -101,8 +100,8 @@ def checked(arguments, failure, explain=None, **kwargs): # The node program's failed local check before it contacts Core; only its fixed class code is shown. READINESS_CHECK = re.compile(rb"local provider readiness check failed \(([a-z_]{1,64})\)") READINESS_FAILURES = { - "provider_unavailable": "The sandbox provider does not answer on this host, for example because Docker is stopped.", - "host_unsupported": "This host lacks a capability the sandbox provider needs, such as Docker CPU and memory limits or read-write access to /dev/kvm.", + "provider_unavailable": "The sandbox provider does not answer on this host, for example because the host service is stopped.", + "host_unsupported": "This host lacks a capability the sandbox provider needs, such as read-write access to /dev/kvm.", "capacity_insufficient": "This host is too small for one sandbox.", "runtime_image_unavailable": "The sandbox provider does not have the Runtime image.", "artifacts_unavailable": "A sandbox provider file is missing or fails its checksum.", @@ -212,7 +211,7 @@ def metadata(source): or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest.get("images", {}).get("runtime", ""))): raise InstallError("Unsupported node distribution") distribution.image_identities(manifest, "runtime") - for name in dict.fromkeys(item["path"] for items in provider_assets.CATALOG.values() for item in items if item["role"] != "policy"): + for name in dict.fromkeys(item["path"] for items in provider_assets.CATALOG.values() for item in items): distribution.artifact(manifest, name) # Metadata stays on the console; artifact requests may redirect to the release. manifest["artifact_base_url"] = source + "/node-install/artifacts" @@ -264,26 +263,6 @@ def file_digest(path): return result.hexdigest() -def download(source, name, root, expected, replace=False): - target = root / name - safe_directory(target.parent) - if existing_file(target): - if file_digest(target) == expected: - return - if not replace: - raise InstallError("Installed node payload differs; refusing to overwrite it") - target.unlink() - with distribution.temporary_file(target) as temporary: - with os.fdopen(os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb") as output, fetch(source, name) as response: - for block in iter(lambda: response.read(1024 * 1024), b""): - output.write(block) - if file_digest(Path(temporary)) != expected: - raise InstallError("Node payload checksum mismatch: " + name) - os.replace(temporary, target) - if name.startswith("native/"): - os.chmod(target, 0o700) - - def drop_runtime_archives(root): """The imported image is the cache; the archives only hold disk.""" for name in ("images/runtime.tar", "images/runtime.tar.gz"): @@ -352,11 +331,10 @@ def require_disk(root, args, manifest, names): Provider's store may then keep more unpacked copies under the same disk.""" needed = 0 for name in names: - if name != "runtime/seccomp.json": - entry = distribution.artifact(manifest, name) - target = root / name - if not (target.is_file() and distribution.matches(target, entry)): - needed += entry["size"] - distribution.resumable(distribution.partial_path(target), entry["size"]) + entry = distribution.artifact(manifest, name) + target = root / name + if not (target.is_file() and distribution.matches(target, entry)): + needed += entry["size"] - distribution.resumable(distribution.partial_path(target), entry["size"]) host = HOSTS[args.provider] if not host.runtime_imported(SELF, root, args, manifest): archive = distribution.artifact(manifest, "images/runtime.tar.gz") @@ -376,9 +354,7 @@ def register_node(root, args, token, helper_archive=None, *, secret_path): """Download and verify the payload, prepare the Runtime and register; not the service.""" install_display.step("Downloading and verifying node files") manifest, sums = metadata(args.source_url) - names = provider_assets.artifacts(args.provider, ("node", "runtime", "policy")) - if "runtime/seccomp.json" not in sums: - raise InstallError("The distribution is missing required node checksums") + names = provider_assets.artifacts(args.provider, ("node", "runtime")) state = {"installation_id": args.installation_id, "provider": args.provider, "core_url": args.core_url, "source_commit": manifest["source_commit"], "generation": args.configuration["generation"], "specification_digest": args.configuration["specification_digest"]} @@ -388,14 +364,11 @@ def register_node(root, args, token, helper_archive=None, *, secret_path): require_disk(root, args, manifest, names) write_once(root / "installation.json", json_text(state)) for name in names: - if name == "runtime/seccomp.json": - download(args.source_url, name, root, sums[name], replace=args.disposable) - else: - target = root / name - safe_directory(target.parent) - existing_file(target) - distribution.obtain_artifact(manifest, name, target, args.disposable) - os.chmod(target, 0o700) + target = root / name + safe_directory(target.parent) + existing_file(target) + distribution.obtain_artifact(manifest, name, target, args.disposable) + os.chmod(target, 0o700) node_generations.install_helper(root, args, SELF, helper_archive) safe_directory(root / "state/node") lease_identity = {"installation_id": args.installation_id, "generation": args.configuration["generation"], @@ -799,7 +772,7 @@ def ours(account, recorded_uid=None): """The account the installer created or adopted. It has the service home, a nologin shell, the recorded uid when there is one, - neither uid nor gid 0, and no groups besides its own, docker and kvm.""" + neither uid nor gid 0, and no groups besides its own and kvm.""" if (account is None or account.pw_dir != str(SERVICE_HOME) or not account.pw_shell.endswith(("nologin", "false")) or recorded_uid not in (None, account.pw_uid) or account.pw_uid == 0 or account.pw_gid == 0): return False @@ -830,7 +803,7 @@ def other_node(args, provider): """Refuse a second sudo-mode Core on this host, or a second node for this installation. Sudo-mode nodes share the oac-node account, so one host serves one Core. Retained node - state is checked in the invoking user's home and, for Docker, on this engine; + state is checked in the invoking user's home; other users' homes are not searched or modified.""" for installation in node_records(): if installation != args.installation_id: @@ -857,7 +830,7 @@ def account_plan(): if account is not None and not ours(account, (record or {}).get("uid")): raise InstallError("An account named " + SERVICE_USER + " exists but was not created or adopted by this installer " "(home " + account.pw_dir + ", shell " + account.pw_shell + ", uid " + str(account.pw_uid) - + ", or groups beyond its own, docker and kvm). Rename or remove it, then rerun." + NOTHING_CHANGED) + + ", or groups beyond its own and kvm). Rename or remove it, then rerun." + NOTHING_CHANGED) if account is not None and account.pw_uid != SERVICE_UID: raise InstallError("The " + SERVICE_USER + " account has UID " + str(account.pw_uid) + ", but it must use UID " + str(SERVICE_UID) + " to match Core. Renumber or remove it, then rerun." + NOTHING_CHANGED) diff --git a/deploy/node/node_payload.py b/deploy/node/node_payload.py index 405f147d0..222e00e9e 100644 --- a/deploy/node/node_payload.py +++ b/deploy/node/node_payload.py @@ -18,7 +18,7 @@ def publish(destination, source): if destination.is_symlink(): raise PayloadError("Invalid node payload directory") manifest = json.loads((source / "manifest.json").read_text()) - names = ["node-install.pyz", "manifest.json", "SHA256SUMS", "runtime/seccomp.json"] + names = ["node-install.pyz", "manifest.json", "SHA256SUMS"] for logical in manifest.get("artifacts", {}): entry = artifact(manifest, logical) name = "artifacts/" + entry["filename"] diff --git a/deploy/node/node_spec.py b/deploy/node/node_spec.py index 809f020fb..43c95a15b 100644 --- a/deploy/node/node_spec.py +++ b/deploy/node/node_spec.py @@ -24,7 +24,7 @@ class UnrecordedIdentity(SpecificationError): # BEGIN GENERATED DEPLOYMENT CONTRACT # Generated from sandbox/deployment_contract.go; do not edit. -_CONTRACT = json.loads("{\"workspace_fields\":[\"attachment\",\"user_xattr\",\"capacity_quota\"],\"resources\":[{\"name\":\"cpus\",\"min\":1,\"max\":255,\"omit_zero\":false},{\"name\":\"memory_mib\",\"min\":512,\"max\":1048576,\"omit_zero\":false},{\"name\":\"root_disk_mib\",\"min\":0,\"max\":4294967295,\"omit_zero\":true},{\"name\":\"environment_disk_mib\",\"min\":0,\"max\":4294967295,\"omit_zero\":true}],\"providers\":{\"docker\":{\"mode\":\"nodes\",\"disk\":false,\"default_resources\":{\"cpus\":2,\"memory_mib\":2048}},\"e2b\":{\"mode\":\"direct\",\"disk\":false,\"default_resources\":null},\"microsandbox\":{\"mode\":\"nodes\",\"workspace\":{\"attachment\":\"host_directory\",\"user_xattr\":true},\"disk\":true,\"default_resources\":{\"cpus\":2,\"memory_mib\":4096,\"root_disk_mib\":8192}}},\"minimum_disk\":1024}") +_CONTRACT = json.loads("{\"workspace_fields\":[\"attachment\",\"user_xattr\",\"capacity_quota\"],\"resources\":[{\"name\":\"cpus\",\"min\":1,\"max\":255,\"omit_zero\":false},{\"name\":\"memory_mib\",\"min\":512,\"max\":1048576,\"omit_zero\":false},{\"name\":\"root_disk_mib\",\"min\":0,\"max\":4294967295,\"omit_zero\":true},{\"name\":\"environment_disk_mib\",\"min\":0,\"max\":4294967295,\"omit_zero\":true}],\"providers\":{\"e2b\":{\"mode\":\"direct\",\"disk\":false,\"default_resources\":null},\"microsandbox\":{\"mode\":\"nodes\",\"workspace\":{\"attachment\":\"host_directory\",\"user_xattr\":true},\"disk\":true,\"default_resources\":{\"cpus\":2,\"memory_mib\":4096,\"root_disk_mib\":8192}}},\"minimum_disk\":1024}") # END GENERATED DEPLOYMENT CONTRACT diff --git a/deploy/node/provider_assets.py b/deploy/node/provider_assets.py index d64a3aea8..50a0a87b0 100644 --- a/deploy/node/provider_assets.py +++ b/deploy/node/provider_assets.py @@ -1,7 +1,7 @@ # Code generated by go run ./services/core/cmd/provider-artifacts -write; DO NOT EDIT. import json -CATALOG = json.loads("{\n \"docker\": [\n {\n \"path\": \"native/bin/oac-node\",\n \"suffix\": \"sandbox-node\",\n \"role\": \"node\"\n },\n {\n \"path\": \"images/runtime.tar.gz\",\n \"suffix\": \"runtime.tar.gz\",\n \"role\": \"image\"\n },\n {\n \"path\": \"runtime/seccomp.json\",\n \"suffix\": \"seccomp.json\",\n \"role\": \"policy\"\n }\n ],\n \"microsandbox\": [\n {\n \"path\": \"native/bin/oac-node\",\n \"suffix\": \"sandbox-node\",\n \"role\": \"node\"\n },\n {\n \"path\": \"images/runtime.tar.gz\",\n \"suffix\": \"runtime.tar.gz\",\n \"role\": \"image\"\n },\n {\n \"path\": \"runtime/seccomp.json\",\n \"suffix\": \"seccomp.json\",\n \"role\": \"policy\"\n },\n {\n \"path\": \"native/bin/oac-microsandbox-provider\",\n \"suffix\": \"microsandbox-provider\",\n \"role\": \"runtime\"\n },\n {\n \"path\": \"native/microsandbox/msb\",\n \"suffix\": \"msb\",\n \"role\": \"runtime\"\n },\n {\n \"path\": \"native/microsandbox/libkrunfw.so.5.6.1\",\n \"suffix\": \"libkrunfw.so.5.6.1\",\n \"role\": \"runtime\"\n }\n ]\n}\n") +CATALOG = json.loads("{\n \"microsandbox\": [\n {\n \"path\": \"native/bin/oac-node\",\n \"suffix\": \"sandbox-node\",\n \"role\": \"node\"\n },\n {\n \"path\": \"images/runtime.tar.gz\",\n \"suffix\": \"runtime.tar.gz\",\n \"role\": \"image\"\n },\n {\n \"path\": \"native/bin/oac-microsandbox-provider\",\n \"suffix\": \"microsandbox-provider\",\n \"role\": \"runtime\"\n },\n {\n \"path\": \"native/microsandbox/msb\",\n \"suffix\": \"msb\",\n \"role\": \"runtime\"\n },\n {\n \"path\": \"native/microsandbox/libkrunfw.so.5.6.1\",\n \"suffix\": \"libkrunfw.so.5.6.1\",\n \"role\": \"runtime\"\n }\n ]\n}\n") def artifacts(provider, roles=None): return tuple(item["path"] for item in CATALOG[provider] if roles is None or item["role"] in roles) diff --git a/deploy/node/test_generation_review_regressions.py b/deploy/node/test_generation_review_regressions.py index 0fe09d62e..97c8345c2 100644 --- a/deploy/node/test_generation_review_regressions.py +++ b/deploy/node/test_generation_review_regressions.py @@ -74,7 +74,7 @@ def payload_fixture(self, directory): for name in ('node', 'helper'): raw = (name + ' verified artifact').encode() entries[name] = {'filename': name + '-' + source, 'size': len(raw), 'sha256': hashlib.sha256(raw).hexdigest()} - files = {'node-install.pyz': b'node', 'SHA256SUMS': b'fixture', 'runtime/seccomp.json': b'{}', 'manifest.json': json.dumps({'source_commit': source, 'artifacts': entries}).encode()} + files = {'node-install.pyz': b'node', 'SHA256SUMS': b'fixture', 'manifest.json': json.dumps({'source_commit': source, 'artifacts': entries}).encode()} for name, raw in files.items(): path = bundle / name; path.parent.mkdir(parents=True, exist_ok=True); path.write_bytes(raw) node_payload.publish(root / 'node-payload', bundle) @@ -83,7 +83,7 @@ def payload_fixture(self, directory): def test_publish_is_flat_and_adds_and_repairs_verified_missing_artifacts(self): with tempfile.TemporaryDirectory() as temp: root, bundle, payload, entries = self.payload_fixture(Path(temp)) - self.assertEqual(sorted(path.name for path in payload.iterdir()), ['SHA256SUMS', 'manifest.json', 'node-install.pyz', 'runtime']) + self.assertEqual(sorted(path.name for path in payload.iterdir()), ['SHA256SUMS', 'manifest.json', 'node-install.pyz']) (bundle / 'artifacts').mkdir() for name, entry in entries.items(): (bundle / 'artifacts' / entry['filename']).write_bytes((name + ' verified artifact').encode()) diff --git a/deploy/node/test_node_generations.py b/deploy/node/test_node_generations.py index 2d210261f..2c1ee0598 100644 --- a/deploy/node/test_node_generations.py +++ b/deploy/node/test_node_generations.py @@ -20,10 +20,10 @@ def setUp(self): self.root = Path(temporary.name) self.directory = self.root / "state/node/generations" self.directory.mkdir(parents=True, mode=0o700) - self.value = {"installation_id": "test-installation", "generation": 1, "provider": "docker", "core_url": "https://core.example/api/v1", - "native": {"image": "sha256:" + "a" * 64, "seccomp_file": str(self.root / "runtime/seccomp.json")}, + self.value = {"installation_id": "test-installation", "generation": 1, "provider": "microsandbox", "core_url": "https://core.example/api/v1", + "native": {"image": "sha256:" + "a" * 64}, "specification": {"resources": {"cpus": 1, "memory_mib": 1024}}} - self.args = SimpleNamespace(installation_id="test-installation", generation=1, specification_digest=node_spec.digest("docker", self.value["specification"])) + self.args = SimpleNamespace(installation_id="test-installation", generation=1, specification_digest=node_spec.digest("microsandbox", self.value["specification"])) node_generations.atomic_json(self.root / "provider.json", self.value) self.initialize_lease() for patch in (mock.patch.object(node_generations, "owned_root", return_value=(self.root, {"core_url": "https://core.example"})), @@ -38,7 +38,7 @@ def initialize_lease(self): def next_generation(self, resources): specification = {"resources": resources} - configuration = {"installation_id": "test-installation", "provider": "docker", "core_url": "https://core.example", "generation": 2, "specification": specification, "specification_digest": node_spec.digest("docker", specification)} + configuration = {"installation_id": "test-installation", "provider": "microsandbox", "core_url": "https://core.example", "generation": 2, "specification": specification, "specification_digest": node_spec.digest("microsandbox", specification)} self.args.generation, self.args.specification_digest = 2, configuration["specification_digest"] return configuration @@ -86,12 +86,12 @@ def test_busy_helper_refuses_collection_then_same_inode_collects(self): def test_collection_leaves_the_installations_runtime_and_other_generations(self): other = dict(self.value, generation=2) node_generations.atomic_json(self.directory / "2.json", other) - seccomp = self.root / "runtime/seccomp.json" - seccomp.parent.mkdir() - seccomp.write_text("{}") + runtime = self.root / "images/runtime.tar" + runtime.parent.mkdir() + runtime.write_text("{}") node_generations.collect(self.args, installer) installer.checked.assert_not_called() - self.assertTrue(seccomp.exists()) + self.assertTrue(runtime.exists()) self.assertEqual(node_generations.retained_configs(self.root, installer), {2: other}) def test_symlink_and_shared_inode_are_not_collection_authority(self): diff --git a/deploy/node/test_node_helper_transfer.py b/deploy/node/test_node_helper_transfer.py index 5e0193831..c77d0ea2b 100644 --- a/deploy/node/test_node_helper_transfer.py +++ b/deploy/node/test_node_helper_transfer.py @@ -31,7 +31,7 @@ def test_captured_archive_survives_removal_of_private_source(self): self.assertEqual(helper.read_bytes(), captured) self.assertEqual(stat.S_IMODE(helper.stat().st_mode), 0o600) with zipfile.ZipFile(helper) as archive: - self.assertEqual(set(archive.namelist()), {"__main__.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_docker.py", "host_microsandbox.py"}) + self.assertEqual(set(archive.namelist()), {"__main__.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_microsandbox.py"}) @unittest.skipUnless(hasattr(os, "fork") and os.geteuid() == 0, "requires a disposable Linux root test environment") def test_real_service_uid_cannot_read_private_download_but_can_install_snapshot(self): diff --git a/deploy/node/test_node_install.py b/deploy/node/test_node_install.py index c4efd748e..ec7b96b9c 100644 --- a/deploy/node/test_node_install.py +++ b/deploy/node/test_node_install.py @@ -23,12 +23,12 @@ from types import SimpleNamespace from unittest import mock -import host_docker import host_microsandbox import node_install as installer import node_spec REAL_FETCH = installer.fetch +REAL_GETADDRINFO = socket.getaddrinfo @@ -64,16 +64,15 @@ def setUp(self): self.addCleanup(temporary.cleanup) self.home = Path(temporary.name).resolve() self.args = argparse.Namespace(source_url="https://console.example", core_url="https://172.29.144.1:24443", - provider="docker", installation_id="94be54a1-138c-4f30-bc87-b13686272dbe") + provider="microsandbox", installation_id="94be54a1-138c-4f30-bc87-b13686272dbe") self.root = self.home / ".oac/nodes" / self.args.installation_id self.manifest = {"platform": "linux/amd64", "source_commit": "a" * 40, "images": {"runtime": "sha256:" + "b" * 64}, "image_manifest_digests": {"runtime": "sha256:" + "c" * 64}, "runtime_ref": "oac-runtime@sha256:" + "c" * 64, "microsandbox": {"runtime_sha256": "d" * 64, "firmware_sha256": "e" * 64}} - self.payloads = {name: b"fixture-payload-" + name.encode() for name in installer.provider_assets.artifacts("microsandbox", ("node", "policy", "runtime"))} + self.payloads = {name: b"fixture-payload-" + name.encode() for name in installer.provider_assets.artifacts("microsandbox", ("node", "runtime"))} self.payloads["images/runtime.tar.gz"] = gzip.compress(b"runtime archive") self.refresh_manifest() - self.containerd = False self.invalid_image = None self.image_present = False self.calls = [] @@ -88,9 +87,10 @@ def setUp(self): mock.patch.object(installer, "open_request", side_effect=self.configuration_response), mock.patch.object(installer.distribution.urllib.request, "build_opener", return_value=mock.Mock(open=self.artifact_response)), mock.patch.object(host_microsandbox, "home", return_value=self.home / "m"), + mock.patch.object(host_microsandbox, "capacity", return_value=(64, 256 << 30)), + mock.patch.object(host_microsandbox.socket, "getaddrinfo", side_effect=lambda host, port, **kwargs: REAL_GETADDRINFO(host, port, **kwargs) if host[0].isdigit() else [(2, 1, 6, "", ("203.0.113.5", port))]), mock.patch.object(installer, "fetch", side_effect=lambda source, name: io.BytesIO(self.payloads[name])), mock.patch.object(installer, "checked", side_effect=self.checked), - mock.patch.object(installer.distribution, "docker_command", side_effect=self.docker_command), mock.patch.object(installer.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, b"statically linked", b""))): patch.start() self.addCleanup(patch.stop) @@ -159,38 +159,24 @@ def checked(self, arguments, failure, **kwargs): if getattr(self, "fail_usermod", False): raise installer.InstallError(failure) self.joined = True - if "{{json .}}" in arguments: - return json.dumps({"MemoryLimit": True, "CpuCfsQuota": True, "NCPU": 8, "MemTotal": 16 << 30}) if "load" in arguments: self.image_present = True if "inspect" in arguments: if not self.image_present: raise installer.InstallError(failure) - if arguments[0] == "docker": - if self.containerd and arguments[arguments.index("inspect") + 1] == self.manifest["images"]["runtime"]: - raise installer.InstallError(failure) - identity = self.manifest["image_manifest_digests" if self.containerd else "images"]["runtime"] - return self.invalid_image or identity + " linux/amd64" - return json.dumps({"digest": self.manifest["runtime_ref"].split("@", 1)[1], "os": "linux", "architecture": "amd64"}) + return json.dumps({"digest": self.manifest["runtime_ref"].split("@", 1)[1], "os": "linux", "architecture": "amd64", **(self.invalid_image or {})}) return "" - def docker_command(self, arguments, **kwargs): - try: - return subprocess.CompletedProcess(arguments, 0, self.checked(arguments, "image missing", **kwargs), "") - except installer.InstallError: - return subprocess.CompletedProcess(arguments, 1, "", "") - def install(self): installer.prepare_service_node(self.args, "synthetic-once-token", installer.node_generations.helper_archive(self.args, installer)) - def test_docker_installs_matched_payload_registers_and_starts_persistent_service(self): + def test_microsandbox_installs_matched_payload_registers_and_starts_persistent_service(self): system = self.sudo_host() installer.install_system(self.args, "synthetic-once-token") config = json.loads((self.root / "provider.json").read_text()) - self.assertEqual(config["native"]["image"], self.manifest["images"]["runtime"]) + self.assertEqual(config["native"]["image"], self.manifest["runtime_ref"]) self.assertEqual(config["core_url"], self.args.core_url + "/api/v1") self.assertEqual(config["installation_id"], self.args.installation_id) - self.assertFalse((self.root / host_microsandbox.ARTIFACTS[0]).exists()) unit = (system / "units" / ("oac-node-" + self.args.installation_id + ".service")).read_text() self.assertIn(" run --config ", unit) self.assertIn("KillMode=process", unit) @@ -202,37 +188,16 @@ def test_docker_installs_matched_payload_registers_and_starts_persistent_service self.assertTrue(any("register" in call for call, _ in self.calls)) self.assertTrue(any("is-active" in call for call, _ in self.calls)) - def test_containerd_node_persists_actual_id_and_warm_retry_avoids_archive(self): - self.containerd = True - self.install() - expected = self.manifest['image_manifest_digests']['runtime'] - self.assertEqual(json.loads((self.root / 'provider.json').read_text())['native']['image'], expected) - before = (self.root / 'provider.json').read_bytes() - with mock.patch.object(installer.distribution, 'runtime_archive', side_effect=AssertionError('warm Runtime download')): - self.install() - self.assertEqual((self.root / 'provider.json').read_bytes(), before) - - def test_retained_provider_image_cannot_bypass_verified_selection(self): - self.install() - config = json.loads((self.root / 'provider.json').read_text()) - config['native']['image'] = 'sha256:' + 'f' * 64 - (self.root / 'provider.json').write_text(json.dumps(config)) - self.calls.clear() - with self.assertRaisesRegex(node_spec.SpecificationError, 'Retained Docker image differs'): - self.install() - self.assertFalse(any('register' in call or 'enable' in call for call, _ in self.calls)) - def test_wrong_loaded_runtime_cannot_register_or_write_provider_config(self): - for observed in ('sha256:' + 'f' * 64 + ' linux/amd64', self.manifest['images']['runtime'] + ' linux/arm64'): + for observed in ({'digest': 'sha256:' + 'f' * 64}, {'architecture': 'arm64'}): self.invalid_image = observed self.image_present = False - with self.subTest(observed=observed), self.assertRaisesRegex(installer.distribution.DistributionError, 'identity or platform'): + with self.subTest(observed=observed), self.assertRaisesRegex(installer.InstallError, 'identity or platform'): self.install() self.assertFalse((self.root / 'provider.json').exists()) self.assertFalse(any('register' in call or 'enable' in call for call, _ in self.calls)) def test_microsandbox_imports_image_and_allows_only_explicit_private_core_endpoint(self): - self.args.provider = "microsandbox" self.install() config = json.loads((self.root / "provider.json").read_text())["native"] # The image and artifact hashes come from the installation's verified manifest. @@ -257,32 +222,11 @@ def test_repeat_preserves_registration_and_recovers_service_start_failure(self): self.assertFalse(any("register" in call for call, _ in self.calls)) self.assertFalse(any("load" in call for call, _ in self.calls)) - def test_policy_download_after_sigkill_starts_clean(self): - root = self.home / "policy" - payload = self.payloads["runtime/seccomp.json"] - expected = hashlib.sha256(payload).hexdigest() - class Interrupted(io.BytesIO): - def read(self, size=-1): - if self.tell(): - os.kill(os.getpid(), signal.SIGKILL) - return super().read(1) - child = os.fork() - if child == 0: - with mock.patch.object(installer, "fetch", return_value=Interrupted(payload)): - installer.download(self.args.source_url, "runtime/seccomp.json", root, expected) - os._exit(1) - _, status = os.waitpid(child, 0) - self.assertEqual(os.waitstatus_to_exitcode(status), -signal.SIGKILL) - partial = root / "runtime/.seccomp.json.partial" - self.assertTrue(partial.exists()) - installer.download(self.args.source_url, "runtime/seccomp.json", root, expected) - self.assertFalse(partial.exists()) - self.assertEqual((root / "runtime/seccomp.json").read_bytes(), payload) - def test_retry_after_configuration_write_sigkill_publishes_complete_files(self): for name in ("installation.json", "provider.json", "registered.json"): with self.subTest(name=name): self.args.installation_id = name.replace(".json", "") + shutil.rmtree(self.home / "m", ignore_errors=True) self.root = self.home / ".oac/nodes" / self.args.installation_id partial = self.root / ("." + name + ".partial") original_open = os.open @@ -363,14 +307,13 @@ def test_changed_public_url_clears_unregistered_state_for_a_new_command(self): self.install() for name in ("installation.json", "provider.json", "state/node/identity.json", "registered.json"): self.assertFalse((self.root / name).exists(), name) - self.assertTrue((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).is_file()) + self.assertTrue((self.root / installer.provider_assets.artifacts("microsandbox", ("node",))[0]).is_file()) self.register_stderr = None self.args.core_url = "https://core-new.example" self.install() self.assertEqual(json.loads((self.root / "registered.json").read_text())["core_url"], "https://core-new.example") def test_microsandbox_registration_retry_retains_original_dns_policy(self): - self.args.provider = "microsandbox" self.fail_registration = True with self.assertRaises(installer.InstallError): self.install() @@ -384,7 +327,7 @@ def test_microsandbox_registration_retry_retains_original_dns_policy(self): def test_different_commit_provider_or_core_cannot_overwrite_retained_installation(self): self.install() original = (self.root / "provider.json").read_bytes() - for field, value in (("provider", "microsandbox"), ("core_url", "https://other.example")): + for field, value in (("core_url", "https://other.example"),): before = getattr(self.args, field) setattr(self.args, field, value) with self.assertRaisesRegex((installer.InstallError, node_spec.SpecificationError), "differs|prepares the changed|added with another Core address"): @@ -401,32 +344,28 @@ def test_corrupt_manifest_and_payload_fail_before_provider_use(self): with self.assertRaisesRegex(installer.InstallError, "manifest checksum"): self.install() self.refresh_manifest() - self.payloads[installer.provider_assets.artifacts("docker", ("node",))[0]] += b"corrupt" + self.payloads[installer.provider_assets.artifacts("microsandbox", ("node",))[0]] += b"corrupt" with self.assertRaisesRegex(installer.distribution.DistributionError, "published size|checksum"): self.install() self.assertFalse([call for call, _ in self.calls if "inspect" not in call]) - self.assertFalse((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).exists()) + self.assertFalse((self.root / installer.provider_assets.artifacts("microsandbox", ("node",))[0]).exists()) def test_installed_payload_and_config_are_not_overwritten(self): self.install() - target = self.root / installer.provider_assets.artifacts("docker", ("node",))[0] + target = self.root / installer.provider_assets.artifacts("microsandbox", ("node",))[0] target.write_bytes(b"existing-different-payload") with self.assertRaises(installer.distribution.DistributionError): self.install() self.assertEqual(target.read_bytes(), b"existing-different-payload") def test_warm_image_skips_archive_download_and_import_before_registration(self): - for provider in ("docker", "microsandbox"): - with self.subTest(provider=provider): - self.args.provider = provider - self.image_present = True - with mock.patch.object(installer.distribution, "runtime_archive") as archive: - installer.HOSTS[provider].prepare_runtime(installer, self.root, self.args, self.manifest) - archive.assert_not_called() + self.image_present = True + with mock.patch.object(installer.distribution, "runtime_archive") as archive: + installer.HOSTS["microsandbox"].prepare_runtime(installer, self.root, self.args, self.manifest) + archive.assert_not_called() self.assertFalse(any("load" in call for call, _ in self.calls)) def test_retry_after_unconfirmed_enrollment_preserves_imported_image(self): - self.args.provider = "microsandbox" self.fail_registration = True with self.assertRaises(installer.InstallError): self.install() @@ -458,11 +397,7 @@ def sudo_host(self, enforcing=False): (system / "systemd").mkdir(parents=True) (system / "run").mkdir() (system / "selinux").write_text("1" if enforcing else "0") - docker_socket = socket.socket(socket.AF_UNIX) - docker_socket.bind(str(system / "docker.sock")) - self.addCleanup(docker_socket.close) - os.chmod(system / "docker.sock", 0o660) - self.account, self.joined, self.docker_installed, self.device_group = None, False, True, "docker" + self.account, self.joined, self.kvm_present, self.device_group = None, False, True, "kvm" service_home = self.home / "service" self.root = service_home / ".oac/nodes" / self.args.installation_id @@ -472,13 +407,16 @@ def run_as(account, function, *arguments): self.service_steps.append("wait_ready" if function is installer.wait_ready else function.__name__) with mock.patch.object(installer.Path, "home", return_value=Path(account.pw_dir)): function(*arguments) - for patch in (mock.patch.multiple(host_docker, CHILD_CONFIG=system / "docker-config", SOCKET=system / "docker.sock"), + def kvm_stat(): + if not self.kvm_present: + raise OSError(errno.ENOENT, "no KVM") + return SimpleNamespace(st_mode=stat.S_IFCHR | 0o660, st_gid=1) + for patch in (mock.patch.object(host_microsandbox, "KVM", SimpleNamespace(stat=kvm_stat)), mock.patch.multiple(installer, SERVICE_HOME=service_home, SERVICE_UID=os.getuid(), SYSTEM_RECORDS=system / "etc", SYSTEM_UNITS=system / "units", SYSTEM_LOCKS=system / "run", SYSTEMD_RUNNING=system / "systemd", SELINUX_ENFORCE=system / "selinux", run_as=run_as, service_account=lambda: self.account), - mock.patch.object(installer.shutil, "which", side_effect=lambda tool: None if tool == "docker" and not self.docker_installed else "/usr/bin/" + tool), mock.patch.object(installer.grp, "getgrnam", side_effect=lambda name: SimpleNamespace(gr_mem=["oac-node"] if self.joined else [])), mock.patch.object(installer.grp, "getgrgid", side_effect=lambda gid: SimpleNamespace(gr_name=self.device_group)), mock.patch.object(installer.pwd, "getpwuid", side_effect=KeyError)): @@ -501,7 +439,7 @@ def test_sudo_summary_commands_retain_permissions_of_invoking_account(self): with self.subTest(uid=uid), mock.patch.dict(os.environ, {"SUDO_UID": uid}): stream = io.StringIO() with contextlib.redirect_stdout(stream): - installer.node_output.summary(self.root, self.args, "oac-node-example.service", "oac-node", "Docker") + installer.node_output.summary(self.root, self.args, "oac-node-example.service", "oac-node", "microsandbox") self.assertIn(" Status: " + prefix + "systemctl status oac-node-example.service", stream.getvalue()) self.assertIn(" Logs: " + prefix + "journalctl -u oac-node-example.service -f", stream.getvalue()) @@ -517,7 +455,7 @@ def test_completion_summary_follows_readiness_and_hides_enrollment_token(self): ready.side_effect = None action(self.args, "") text = stream.getvalue() - self.assertIn("\nStatus\n Core connection: connected\n Sandbox Provider: Docker (ready)\n", text) + self.assertIn("\nStatus\n Core connection: connected\n Sandbox Provider: microsandbox (ready)\n", text) self.assertIn("\nNode\n", text) self.assertIn("\nManage\n", text) self.assertIn(" Status: systemctl status oac-node-", text) @@ -535,7 +473,7 @@ def test_sudo_mode_prepares_the_host_and_reruns_without_changes(self): self.assertTrue(any(call[:3] == ["usermod", "--append", "--groups"] for call in commands)) self.assertIn(["systemctl", "enable", "--now", "oac-node-" + self.args.installation_id + ".service"], commands) unit = (system / "units" / ("oac-node-" + self.args.installation_id + ".service")).read_text() - for line in ("User=oac-node", "After=network-online.target docker.service", "WantedBy=multi-user.target", + for line in ("User=oac-node", "After=network-online.target", "WantedBy=multi-user.target", "RestartPreventExitStatus=78", "StartLimitIntervalSec=0"): self.assertIn(line, unit) self.assertNotIn("synthetic-once-token", unit) @@ -545,7 +483,7 @@ def test_sudo_mode_prepares_the_host_and_reruns_without_changes(self): self.assertTrue((self.root / "registered.json").exists()) self.assertFalse((self.root / ("oac-node-" + self.args.installation_id + ".service")).exists()) account = json.loads((system / "etc/account.json").read_text()) - self.assertEqual((account["created"], account["groups_added"]), (True, ["docker"])) + self.assertEqual((account["created"], account["groups_added"]), (True, ["kvm"])) self.assertEqual(stat.S_IMODE((system / "etc").stat().st_mode), 0o755) before = {path: path.read_bytes() for path in (system / "etc").iterdir()} self.calls.clear() @@ -591,14 +529,14 @@ def test_host_lock_is_private_and_exclusive(self): def test_sudo_mode_refusals_change_nothing(self): foreign = SimpleNamespace(pw_name="oac-node", pw_uid=4242, pw_gid=4242, pw_dir="/home/oac-node", pw_shell="/bin/bash") - for case, message in (("selinux", "SELinux is enforcing"), ("docker", "Docker Engine is not installed"), + for case, message in (("selinux", "SELinux is enforcing"), ("kvm", "KVM is unavailable"), ("account", "not created or adopted by this installer"), ("group", "belongs to the group disk"), ("home", "does not belong to the oac-node account")): with self.subTest(case=case): system = self.sudo_host(enforcing=case == "selinux") - self.docker_installed = case != "docker" + self.kvm_present = case != "kvm" self.account = foreign if case == "account" else None - self.device_group = "disk" if case == "group" else "docker" + self.device_group = "disk" if case == "group" else "kvm" if case == "home": # a home directory left without its account (self.home / "service").mkdir() self.calls.clear() @@ -646,8 +584,6 @@ def test_uninstall_deletes_the_store_once_core_confirmed_the_removal(self): self.sudo_host() installer.install_system(self.args, "synthetic-once-token") store = self.home / "m" - store.mkdir() - (store / "oac-installation.json").write_text(json.dumps({"installation_id": self.args.installation_id})) (store / "image").write_text("layers") output = self.uninstall_output(False) self.assertFalse(store.exists()) @@ -658,8 +594,6 @@ def test_forced_uninstall_keeps_the_store_and_prints_the_command(self): self.sudo_host() installer.install_system(self.args, "synthetic-once-token") store = self.home / "m" - store.mkdir() - (store / "oac-installation.json").write_text(json.dumps({"installation_id": self.args.installation_id})) output = self.uninstall_output(True) self.assertTrue(store.is_dir()) self.assertIn("Kept the microsandbox store " + str(store), output) @@ -722,8 +656,6 @@ def test_uninstall_keeps_the_store_when_core_was_never_asked(self): self.sudo_host() installer.install_system(self.args, "synthetic-once-token") store = self.home / "m" - store.mkdir() - (store / "oac-installation.json").write_text(json.dumps({"installation_id": self.args.installation_id})) shutil.rmtree(self.root) self.assertIn("Kept the microsandbox store " + str(store), self.uninstall_output(False)) self.assertTrue(store.is_dir()) @@ -733,6 +665,7 @@ def test_uninstall_refuses_a_linked_store_before_removing_anything(self): installer.install_system(self.args, "synthetic-once-token") elsewhere = self.home / "elsewhere" elsewhere.mkdir() + shutil.rmtree(self.home / "m") (self.home / "m").symlink_to(elsewhere, target_is_directory=True) with self.assertRaisesRegex(installer.InstallError, "symbolic link.*Nothing was changed"): self.uninstall_output(False) @@ -743,7 +676,7 @@ def test_uninstall_keeps_a_store_it_did_not_create(self): self.sudo_host() installer.install_system(self.args, "synthetic-once-token") store = self.home / "m" - store.mkdir() + (store / "oac-installation.json").unlink() self.assertIn("Kept the microsandbox store", self.uninstall_output(False)) self.assertTrue(store.is_dir()) @@ -780,7 +713,7 @@ def test_uninstall_leaves_an_adopted_account_as_found(self): self.assertIn("rm -f /home/me/.oac/node-bootstrap/*.pyz", output) commands = [call for call, _ in self.calls] self.assertNotIn(["userdel", "oac-node"], commands) - self.assertIn(["gpasswd", "--delete", "oac-node", "docker"], commands) + self.assertIn(["gpasswd", "--delete", "oac-node", "kvm"], commands) self.assertTrue((self.home / "service").is_dir()) self.assertEqual(stat.S_IMODE((self.home / "service").stat().st_mode), 0o755) self.assertFalse((system / "etc").exists()) @@ -828,12 +761,11 @@ def probe(): tty = "opened" except OSError: tty = "unavailable" - print("session-leader=%s stdin=%s docker-config=%s tty=%s" % (os.getsid(0) == os.getpid(), - os.readlink("/proc/self/fd/0"), os.environ["DOCKER_CONFIG"], tty)) + print("session-leader=%s stdin=%s tty=%s" % (os.getsid(0) == os.getpid(), + os.readlink("/proc/self/fd/0"), tty)) output = io.StringIO() self.service_step(probe, output) - self.assertIn("session-leader=True stdin=/dev/null docker-config=" + str(host_docker.CHILD_CONFIG) + " tty=unavailable", - output.getvalue()) + self.assertIn("session-leader=True stdin=/dev/null tty=unavailable", output.getvalue()) def test_service_user_output_reaches_the_terminal_as_plain_text(self): def forge(): @@ -847,21 +779,6 @@ def forge(): self.assertEqual(output.getvalue(), "?]52;c;ZWNobyBoaQ==?copied??[2KFinish with: sudo sh\ncaf\u00e9\n") self.assertEqual(errors.getvalue(), "?]0;title??warning\n") - def test_uninstall_prints_only_an_image_id_from_the_service_home(self): - root = self.home / "node" - root.mkdir() - (root / "provider.json").write_text(json.dumps({"native": {"image": "x\nFinish with: sudo sh"}})) - output = io.StringIO() - with mock.patch.object(installer.sys, "stdout", output): - installer.remove_node_files(root, self.args.installation_id, False) - self.assertNotIn("Finish", output.getvalue()) - root.mkdir() - (root / "provider.json").write_text(json.dumps({"native": {"image": "sha256:" + "b" * 64}})) - output = io.StringIO() - with mock.patch.object(installer.sys, "stdout", output): - installer.remove_node_files(root, self.args.installation_id, False) - self.assertIn("`sudo docker image rm sha256:", output.getvalue()) - def test_service_user_step_gets_its_own_session_keyring(self): libc = installer.ctypes.CDLL(None, use_errno=True) libc.syscall.restype = installer.ctypes.c_long @@ -974,7 +891,7 @@ def test_an_install_that_never_registered_continues_after_the_configuration_and_ self.install() self.fail_registration = False self.core_knows_node = False - program = installer.provider_assets.artifacts("docker", ("node",))[0] + program = installer.provider_assets.artifacts("microsandbox", ("node",))[0] self.payloads[program] = b"program of the upgraded Core" self.cpus = 4 self.refresh_manifest() @@ -993,7 +910,7 @@ def test_a_registered_node_keeps_its_state_when_the_configuration_changes(self): def test_a_registered_node_of_another_installation_says_to_remove_and_add_it_again(self): self.install() - program = self.root / installer.provider_assets.artifacts("docker", ("node",))[0] + program = self.root / installer.provider_assets.artifacts("microsandbox", ("node",))[0] saved = program.read_bytes() self.manifest["source_commit"] = "f" * 40 self.payloads[program.relative_to(self.root).as_posix()] = b"program of another installation" @@ -1007,7 +924,7 @@ def test_too_little_disk_is_refused_before_anything_downloads(self): with self.assertRaisesRegex(installer.InstallError, "Not enough disk space under .*[Ff]ree space, then rerun"): self.install() self.assertFalse((self.root / "installation.json").exists()) - self.assertFalse((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).exists()) + self.assertFalse((self.root / installer.provider_assets.artifacts("microsandbox", ("node",))[0]).exists()) def test_a_full_disk_in_the_service_step_has_its_own_message(self): def write(): @@ -1019,12 +936,8 @@ def write(): self.assertNotIn("OSError", errors.getvalue()) def test_the_runtime_archives_are_deleted_once_the_image_is_imported(self): - for provider in ("docker", "microsandbox"): - with self.subTest(provider=provider): - self.args.provider = provider - self.install() - self.assertEqual(list(self.root.glob("images/*")), []) - shutil.rmtree(self.root) + self.install() + self.assertEqual(list(self.root.glob("images/*")), []) def test_a_failed_local_provider_check_names_its_class_not_the_nodes_text(self): message = str(installer.registration_failure(b"x: local provider readiness check failed (host_unsupported): /home/secret")) @@ -1032,19 +945,6 @@ def test_a_failed_local_provider_check_names_its_class_not_the_nodes_text(self): self.assertIn("the token was not used", message) self.assertNotIn("secret", message) - def test_a_leftover_docker_network_without_containers_does_not_block_adding_the_node(self): - network = "oac-node-" + self.args.installation_id - containers = [""] - def docker(arguments, failure, **kwargs): - if "ps" in arguments: - return containers[0] - return network if "network" in arguments else "" - with mock.patch.object(installer, "checked", side_effect=docker), mock.patch.object(installer, "node_records", return_value=[]): - installer.other_node(self.args, "docker") - containers[0] = "oac-sandbox-1" - with self.assertRaisesRegex(installer.InstallError, "already uses this Docker engine"): - installer.other_node(self.args, "docker") - def test_a_metadata_body_cut_short_is_retried(self): class Cut(io.BytesIO): headers = {"Content-Length": "10"} diff --git a/deploy/node/test_node_proxy.py b/deploy/node/test_node_proxy.py index 557ec47d8..81603ffff 100644 --- a/deploy/node/test_node_proxy.py +++ b/deploy/node/test_node_proxy.py @@ -205,7 +205,7 @@ def test_service_keeps_the_installer_proxy_in_a_private_file(self): self.assertEqual(installer.service_environment({"no_proxy": "internal.invalid"}), "") with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "node.env" - unit = installer.system_unit(Path("/var/lib/oac-node/.oac/nodes/fixture"), "docker", path) + unit = installer.system_unit(Path("/var/lib/oac-node/.oac/nodes/fixture"), "microsandbox", path) self.assertIn("EnvironmentFile=-" + str(path) + "\n", unit) self.assertNotIn("private", unit) self.assertTrue(installer.write_service_environment(path, content)) diff --git a/deploy/node/test_node_readiness.py b/deploy/node/test_node_readiness.py index b0b3e726b..97eba4808 100644 --- a/deploy/node/test_node_readiness.py +++ b/deploy/node/test_node_readiness.py @@ -20,7 +20,7 @@ def setUp(self): temporary = tempfile.TemporaryDirectory(dir=base) self.addCleanup(temporary.cleanup) self.root = Path(temporary.name).resolve() - self.args = argparse.Namespace(core_url="https://core.example", provider="docker", + self.args = argparse.Namespace(core_url="https://core.example", provider="microsandbox", installation_id="94be54a1-138c-4f30-bc87-b13686272dbe") self.identity = {"node_id": "634d97be-e54d-40f0-9468-ae6b62be85bf", "installation_id": self.args.installation_id, "provider": self.args.provider, "deployment_generation": 1, "specification_digest": "b" * 64} diff --git a/deploy/node/test_node_spec.py b/deploy/node/test_node_spec.py index 68b3256d1..933447871 100644 --- a/deploy/node/test_node_spec.py +++ b/deploy/node/test_node_spec.py @@ -14,13 +14,13 @@ class SpecificationTests(unittest.TestCase): def setUp(self): self.args = argparse.Namespace(core_url="https://core.example", installation_id="94be54a1-138c-4f30-bc87-b13686272dbe", provider=None) - self.spec = {"resources": {"cpus": 2, "memory_mib": 4096}} - self.data = {"installation_id": self.args.installation_id, "provider": "docker", "generation": 3, - "specification": self.spec, "specification_digest": node_spec.digest("docker", self.spec), + self.spec = {"resources": {"cpus": 2, "memory_mib": 4096, "root_disk_mib": 8192}, "workspace": {"attachment": "host_directory", "user_xattr": True}} + self.data = {"installation_id": self.args.installation_id, "provider": "microsandbox", "generation": 3, + "specification": self.spec, "specification_digest": node_spec.digest("microsandbox", self.spec), "core_url": self.args.core_url} self.retained = {"core_url": self.args.core_url, "credential": "f" * 64, "identity": { "node_id": "634d97be-e54d-40f0-9468-ae6b62be85bf", "installation_id": self.args.installation_id, - "provider": "docker", "deployment_generation": 3, "specification_digest": self.data["specification_digest"]}} + "provider": "microsandbox", "deployment_generation": 3, "specification_digest": self.data["specification_digest"]}} def response(self): return io.BytesIO(json.dumps(self.data).encode()) @@ -75,14 +75,14 @@ def test_changed_generation_or_specification_rejects_retained_node(self): for change in (lambda: self.data.update(generation=4), lambda: self.spec["resources"].update(cpus=8)): original = copy.deepcopy(self.data) change() - self.data["specification_digest"] = node_spec.digest("docker", self.data["specification"]) + self.data["specification_digest"] = node_spec.digest("microsandbox", self.data["specification"]) with self.assertRaisesRegex(node_spec.SpecificationError, "prepares the changed sandbox configuration itself"): node_spec.fetch(self.args, "once", self.retained, mock.Mock(return_value=self.response())) self.data = original self.spec = self.data["specification"] def test_invalid_limits_digests_and_provider_assertions_are_rejected(self): - for field, value in (("cpus", True), ("memory_mib", 1), ("root_disk_mib", 8192), ("custom", 1)): + for field, value in (("cpus", True), ("memory_mib", 1), ("root_disk_mib", 1), ("custom", 1)): data = copy.deepcopy(self.data) data["specification"]["resources"][field] = value with self.subTest(field=field), self.assertRaises(node_spec.SpecificationError): @@ -90,15 +90,15 @@ def test_invalid_limits_digests_and_provider_assertions_are_rejected(self): self.data["specification_digest"] = "0" * 64 with self.assertRaises(node_spec.SpecificationError): node_spec.validate(self.data, self.args) - self.data["specification_digest"] = node_spec.digest("docker", self.spec) - self.args.provider = "microsandbox" + self.data["specification_digest"] = node_spec.digest("microsandbox", self.spec) + self.args.provider = "e2b" with self.assertRaises(node_spec.SpecificationError): node_spec.validate(self.data, self.args) def test_digest_is_independent_of_response_object_key_order(self): reordered = copy.deepcopy(self.spec) reordered["resources"] = dict(reversed(list(reordered["resources"].items()))) - self.assertEqual(node_spec.digest("docker", reordered), node_spec.digest("docker", self.spec)) + self.assertEqual(node_spec.digest("microsandbox", reordered), node_spec.digest("microsandbox", self.spec)) class SharedDeploymentContractTests(unittest.TestCase): diff --git a/deploy/test_install_source.py b/deploy/test_install_source.py index af326e433..acea5d7b7 100644 --- a/deploy/test_install_source.py +++ b/deploy/test_install_source.py @@ -28,7 +28,7 @@ def setUp(self): self.bundle.mkdir() self.revision = 'a' * 40 self.manifest = {'source_commit': self.revision, 'platform': 'linux/amd64', 'artifacts': {}} - for name, data in [('manifest.json', json.dumps(self.manifest).encode()), ('node-install.pyz', b'installer'), ('runtime/seccomp.json', b'{}')]: + for name, data in [('manifest.json', json.dumps(self.manifest).encode()), ('node-install.pyz', b'installer')]: target = self.bundle / name target.parent.mkdir(exist_ok=True) target.write_bytes(data) diff --git a/docs/architecture.md b/docs/architecture.md index cbc0328b7..d76556a9d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,7 +27,7 @@ The Runtime daemon initiates its authenticated WebSocket connection to Core. [AP | Core | Authenticate callers, resolve and freeze configuration, schedule Turns, handle cancellation and pending interactions, persist resources and execution facts in PostgreSQL | [Core service](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md) | | Sandbox Provider | Create, observe, renew and reclaim compute; supply Runtime startup input | [Sandbox Provider protocol](./sandbox-provider.md), [Runtime bootstrap](./runtime-bootstrap.md) | | Workspace filesystem adapter | Create, observe and delete durable Environment storage; validate and resolve its compute attachment | [Workspace filesystem protocol](./workspace-provider.md) | -| Sandbox node | Operate a Docker or microsandbox host and reconcile its assigned generation and allocations | [Sandbox node protocol](../contracts/agents-api/node-generation-protocol.md) | +| Sandbox node | Operate a microsandbox host and reconcile its assigned generation and allocations | [Sandbox node protocol](../contracts/agents-api/node-generation-protocol.md) | | Runtime | Prepare the workspace and capabilities, manage Session Executors, execute Turns and report events and receipts | [Core–Runtime protocol](./runtime-protocol.md) | | Harness adapter | Validate native configuration, invoke the upstream SDK or protocol, translate events and confirm native cleanup | [Harness protocol](../contracts/agents-api/harness-onboarding.md) | | Model provider | Serve the model protocol selected for the Harness | [Model execution](../contracts/agents-api/model-execution.md) | diff --git a/docs/concepts.md b/docs/concepts.md index b281a3023..e83e2215f 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -26,7 +26,7 @@ Creating or editing application assets, starting Sessions and submitting input r The Runtime daemon runs on Linux, macOS and Windows. Native platform behavior belongs to the Runtime and its Harness adapters; managed Sandbox Providers run Linux environments. -Tools run with the permissions of the account that launches the daemon. The daemon adds no filesystem, permission or network isolation. Use the outer Environment for isolation: a managed Docker, E2B or microsandbox environment, or a container or VM around a self-hosted machine's Runtime. Authentication, private storage, locks and process cleanup protect the connection and lifecycle, but tools running as the same user can access Runtime data. +Tools run with the permissions of the account that launches the daemon. The daemon adds no filesystem, permission or network isolation. Use the outer Environment for isolation: a managed E2B or microsandbox environment, or a container or VM around a self-hosted machine's Runtime. Authentication, private storage, locks and process cleanup protect the connection and lifecycle, but tools running as the same user can access Runtime data. ## Secrets and audit diff --git a/docs/configuration.md b/docs/configuration.md index 84c236da6..4e266334f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -55,7 +55,7 @@ Runtime settings live in Core's database. Change them in Web; scripts use the sa | Setting | Where in Web | Core API | Notes | | --- | --- | --- | --- | -| Sandbox backend: Docker, microsandbox or E2B | **Settings** → **Sandbox and scheduling**: the setup wizard, ending with **Save configuration** | `/core/v1/sandbox/deployment` | One backend per installation, chosen after the first sign-in. Another backend needs **Reset deployment** first; see [change the sandbox configuration](./getting-started/nodes.md#change-the-sandbox-configuration) | +| Sandbox backend: microsandbox or E2B | **Settings** → **Sandbox and scheduling**: the setup wizard, ending with **Save configuration** | `/core/v1/sandbox/deployment` | One backend per installation, chosen after the first sign-in. Another backend needs **Reset deployment** first; see [change the sandbox configuration](./getting-started/nodes.md#change-the-sandbox-configuration) | | Sandbox size, E2B key and template build | **Settings** → **Sandbox and scheduling** → **Change resources** | `/core/v1/sandbox/deployment` | Web proposes the [default size the Provider declares](./sandbox-provider.md#register-the-provider-kind). Existing sandboxes keep their size. The E2B key is write-only and encrypted | | Nodes and their capacity | **Nodes**: **Add node**, **Edit node** and removal | `/core/v1/sandbox/enrollment-tokens`, `/core/v1/sandbox/nodes` | See [Node capacity](#node-capacity) and [Add and manage nodes](./getting-started/nodes.md) | | Idle suspension | **Settings** → **Sandbox and scheduling** → **Automatic suspension** → **Idle before suspension (seconds)** → **Save** | `PATCH /core/v1/sandbox/deployment` | Deployment-wide; see [suspension policy](../contracts/agents-api/sandbox-deployment.md#suspension-policy) | @@ -68,7 +68,7 @@ Which Harnesses are enabled, and the default one, are process settings (`OAC_HAR ### Node capacity -A node's capacity is **Sandboxes at once** (`max_active`) and, for microsandbox only, **Retained sandboxes** (`max_retained`), with `max_retained >= max_active >= 1`. **Add node** keeps the recommended limits by default, which Core [derives from the host](../contracts/agents-api/sandbox-deployment.md#recommended-capacity) when the node registers, or you enter both numbers. Docker never suspends sandboxes, so Web doesn't ask for it and Core keeps `max_retained` equal to `max_active`. Change them later on **Nodes** with **Edit node**, which shows the recommendation and fills it in on request. Reservations and cleanup that is not confirmed count against capacity; full retained slots can [end an idle checkpoint's retention early](sandbox-provider.md#cold-replacement-after-checkpoint-retention); lowering a limit stops no running sandbox. A node's own files can't change its capacity, size or Runtime. +A node's capacity is **Sandboxes at once** (`max_active`) and **Retained sandboxes** (`max_retained`), with `max_retained >= max_active >= 1`. **Add node** keeps the recommended limits by default, which Core [derives from the host](../contracts/agents-api/sandbox-deployment.md#recommended-capacity) when the node registers, or you enter both numbers. Change them later on **Nodes** with **Edit node**, which shows the recommendation and fills it in on request. Reservations and cleanup that is not confirmed count against capacity; full retained slots can [end an idle checkpoint's retention early](sandbox-provider.md#cold-replacement-after-checkpoint-retention); lowering a limit stops no running sandbox. A node's own files can't change its capacity, size or Runtime. `OAC_EXECUTION_CONCURRENCY` is unrelated: it limits concurrent execution work in Core. @@ -105,20 +105,6 @@ Initialization prepares this directory; application services receive their secre The named Docker volume `_data` contains these paths. Docker manages Linux ownership on every host; each service mounts only its required subdirectories. Preserve this volume together with the project definition and public URL. Removing only the secret directories does not reset an installation; initialization refuses to start over an existing database. Core also binds the installation ID to its database. Runtime settings continue to live in [Core's database](#runtime-settings-web). -## Docker node configuration - -The node installer writes Docker’s host settings into the `native` object of the node’s configuration file, which only the Docker adapter reads. Deployment resources and capacity remain in [Core’s database](#runtime-settings-web). - -| Field | Installer value | Meaning | -| --- | --- | --- | -| `host` | `unix:///var/run/docker.sock` | Explicit Docker Engine socket | -| `image` | The Runtime image’s local ID after loading | The installation’s Runtime image. The host’s image store decides whether its local ID is the manifest’s `image_id` or `image_manifest_digest`, so the value is node-local | -| `network` | `oac-node-` | Runtime container network | -| `seccomp_file` | `/runtime/seccomp.json` | Matched distribution’s seccomp profile | -| `nested_sandbox` | `true` | Enables the Docker adapter’s init process and proc-mask configuration | - -The [Docker adapter](./sandbox-provider.md#docker-adapter) owns container isolation, volume layout and lifecycle behavior. - ## Installation directory The installer creates `~/.oac/core` by default (`$HOME/.oac/core` on Windows). Its files contain process settings and the native operator command; persistent service data lives in the [Compose data volume](#compose-installations). diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 17b8b6dc2..2f865495a 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -76,7 +76,7 @@ To move to a newer release, reinstall: [uninstall](./operations.md#uninstall) th ## Add sandbox capacity -The installer selects no sandbox backend. [Choose a sandbox backend](./sandbox-backends.md): microsandbox on your own Linux nodes with workspace storage from [Set up workspace storage](./workspace-storage.md) is the recommended self-managed deployment, and E2B needs no nodes. Applications can also run [self-hosted Sessions](./self-hosted.md) on their own machines. +The installer selects no sandbox backend. [Choose a sandbox backend](./sandbox-backends.md): microsandbox on your own Linux nodes with workspace storage from [Set up workspace storage](./workspace-storage.md) is the self-managed deployment, and E2B needs no nodes. Applications can also run [self-hosted Sessions](./self-hosted.md) on their own machines. ## Set a default model diff --git a/docs/getting-started/nodes.md b/docs/getting-started/nodes.md index 8e7fddf68..1a791b27f 100644 --- a/docs/getting-started/nodes.md +++ b/docs/getting-started/nodes.md @@ -2,7 +2,7 @@ title: "Add and manage nodes" --- -A node is a Linux host that runs sandboxes for Core-hosted Sessions. This guide is for operators who add, change and remove nodes when the sandbox backend (see [Choose a sandbox backend](./sandbox-backends.md)) is microsandbox or Docker; E2B needs no nodes. +A node is a Linux host that runs sandboxes for Core-hosted Sessions. This guide is for operators who add, change and remove nodes when the sandbox backend (see [Choose a sandbox backend](./sandbox-backends.md)) is microsandbox; E2B needs no nodes. Core places each new Session on a node with free capacity; the node creates the sandbox, and the sandbox connects back to Core. Machines that applications connect for their own Sessions are [self-hosted executors](./self-hosted.md), not nodes. You add a node by generating a command in Web and running it on the host. The [sandbox deployment contract](../../contracts/agents-api/sandbox-deployment.md) defines the sandbox settings and their change rules, and the [node protocol](../../contracts/agents-api/node-generation-protocol.md) defines how nodes prepare and keep Runtime generations. @@ -21,15 +21,14 @@ The Core host joins like any other host: to run sandboxes on it, add it as a nod - Linux amd64 with systemd; Python 3.9+, `curl`, `sha256sum` and `flock`; root or sudo. - SELinux not enforcing. The installer does not support hosts with enforcing SELinux. -- Docker: rootful Docker Engine running, its socket `/var/run/docker.sock` owned by the `docker` group with mode `0660`, enforcing CPU and memory limits (cgroup v2). -- microsandbox: `/dev/kvm` in the `kvm` group (hardware or nested virtualization), and the libraries microsandbox links (glibc). -- CPUs and memory for at least one sandbox of the installation's size, and about 3 GB of free disk while the Runtime image installs: its download and unpacked copy sit under `/var/lib/oac-node` until the import finishes, and the image takes about 1.5 GB in Docker's storage or the microsandbox store. The installer checks the space before it downloads and deletes the archives after the import, so the node keeps the image and under 0.2 GB of programs. +- `/dev/kvm` in the `kvm` group (hardware or nested virtualization), and the libraries microsandbox links (glibc). +- CPUs and memory for at least one sandbox of the installation's size, and about 3 GB of free disk while the Runtime image installs: its download and unpacked copy sit under `/var/lib/oac-node` until the import finishes, and the image takes about 1.5 GB in the microsandbox store. The installer checks the space before it downloads and deletes the archives after the import, so the node keeps the image and under 0.2 GB of programs. - Access to the console and Core at the public URL; sandboxes reach Core too. ## Add a node 1. In Web, open **Nodes** and choose **Add node**. -2. Keep **Recommended** limits, or choose **Custom** and set **Sandboxes at once** and, with microsandbox, **Retained sandboxes**: the node's [capacity](../configuration.md#node-capacity). **Edit node** shows the recommendation for the host and fills it in with **Use recommended**. +2. Keep **Recommended** limits, or choose **Custom** and set **Sandboxes at once** and **Retained sandboxes**: the node's [capacity](../configuration.md#node-capacity). **Edit node** shows the recommendation for the host and fills it in with **Use recommended**. 3. Choose **Generate command** and copy the command. It registers one node, once, and only if it runs within an hour; Web counts down and offers **Generate new command** when it expires. 4. Run it on the host. Web follows the node from registered to connected to ready. @@ -62,17 +61,14 @@ The node service keeps the proxy settings of the latest run, so it reaches Core | Item | Detail | | --- | --- | | Service user | System user `oac-node` with UID 65532, the UID of Core's service, home `/var/lib/oac-node` and no login shell. An existing account with that UID, that home and a nologin shell is adopted; any other account named `oac-node` is refused. If another account holds UID 65532, the installer stops and changes nothing | -| Group | The group that owns `/var/run/docker.sock` (Docker) or `/dev/kvm` (microsandbox): `docker` or `kvm`, nothing else | +| Group | The group that owns `/dev/kvm`: `kvm`, nothing else | | Service | `/etc/systemd/system/oac-node-.service`, a root-owned system unit with `User=oac-node`, enabled at boot. It restarts every 5 seconds while Core is unreachable and stops for good once Core no longer accepts the node | | Node state | `/var/lib/oac-node/.oac/nodes//`: identity, configuration, node files. microsandbox keeps its images and sandboxes under `/var/lib/oac-node/.oac/m/` | | Records | `/etc/oac-node/`: what the installer created or changed, used by reruns and uninstall, and the service's proxy settings | -| Docker | The Runtime image, imported once, and a network `oac-node-` | +| Runtime image | Imported once into the microsandbox store | | microsandbox network policy | Sandboxes may reach Core, DNS on the host and public addresses, and nothing else: no inbound connections and no private networks | -Root only prepares the account, the group and the unit; everything else, the Docker network included, runs as `oac-node`, in its own session without a terminal. The installer never installs Docker, KVM or packages, never starts Docker, never changes device permissions, sudoers, firewall or SELinux settings, and never touches other accounts. The node connects out to Core; Core needs no SSH or Docker TCP access to the host. - -> [!CAUTION] -> Docker mode is root-equivalent. Membership in the `docker` group lets `oac-node`, and so anything that controls the node, act as root on the host, and containers share the host kernel. Add Docker nodes only on hosts dedicated to sandboxes. microsandbox nodes need only the `kvm` group and run each sandbox in its own microVM. +Root only prepares the account, the group and the unit; everything else runs as `oac-node`, in its own session without a terminal. The installer never installs KVM or packages, never changes device permissions, sudoers, firewall or SELinux settings, and never touches other accounts. The node connects out to Core; Core needs no SSH access to the host. > [!WARNING] > microsandbox sandboxes cannot reach private addresses; see [Sandboxes cannot reach a private endpoint](./troubleshooting.md#sandboxes-cannot-reach-a-private-endpoint). @@ -87,7 +83,7 @@ Root only prepares the account, the group and the unit; everything else, the Doc - A run that fails before the node registers leaves nothing that Core knows. The next command, even after a configuration change, replaces the earlier attempt's configuration and any downloaded file that no longer matches, keeps the files that match and continues. - If the command expires during a slow download, registration fails with `The enrollment command expired or was already used`. The downloaded files are kept: generate a new command in Web and run it, and the download continues where it stopped. - Every file shows its progress. Failed or interrupted downloads keep what arrived, and a rerun resumes each partial file where it stopped (`Resuming …`), reuses verified complete files and downloads the rest. If the server cannot resume, the file starts again from the beginning. A transfer that brings less than 64 KiB in a minute stops; check the network and rerun the command. -- The Docker Runtime image is about 400 MB and the microsandbox files about 500 MB. On a slow link with Docker, load the image first: copy the release's `oac--linux-amd64-runtime.tar.gz` asset to the host and run `sudo docker load -i` on it. The installer then finds the exact image and skips the download. +- The microsandbox files are about 500 MB. - Interrupting the installer, or closing its terminal, stops it; run the command again to continue. ## Logs @@ -117,9 +113,9 @@ microsandbox includes the host kernel and CPU profile in its checkpoint executio 1. In Web, open **Nodes** and choose **Remove node** on the node's page, or **Remove** in its list row, then **Confirm removal**. Core refuses while the node still holds sandboxes, snapshots or pending cleanup; let them finish, or archive their Sessions. Removal is permanent: the host can come back only as a new node. Removing every node keeps the sandbox configuration. 2. Web then shows **Clean up the host** with the uninstall command. Run it on the host. **Clean up a host** on **Nodes** shows the same command at any time, for a host you removed earlier, after a deployment reset retired its node, or for a host that never registered. -Uninstall first asks Core, at the address the node enrolled with, whether the node was removed, and refuses while Core still lists it. When the node enrolled with an address other than the current public URL, the dialog also shows **Old Core address gone?**: if that address no longer responds, it gives the command with `--force`, which skips the check; remove the node on the Nodes page first. If Core is gone, so is Web: run `sudo python3 ~/.oac/node-bootstrap/.pyz --uninstall --installation-id --force` on the host, with the installer the original command left in that directory. The installation ID is in the unit name `oac-node-.service` and in `/etc/oac-node/.json`; Web shows it under **Settings** → **Service and deployment** → **Installation details**. Uninstall stops and removes the service, the node state, the records, the proxy settings and the Docker network. It deletes the `oac-node` account only if the installer created it and no node remains; an adopted account only loses the groups the installer added. The output lists what it kept: an adopted account with its home `/var/lib/oac-node`, and the downloaded installers in `~/.oac/node-bootstrap`, which `rm -f ~/.oac/node-bootstrap/*.pyz` removes. +Uninstall first asks Core, at the address the node enrolled with, whether the node was removed, and refuses while Core still lists it. When the node enrolled with an address other than the current public URL, the dialog also shows **Old Core address gone?**: if that address no longer responds, it gives the command with `--force`, which skips the check; remove the node on the Nodes page first. If Core is gone, so is Web: run `sudo python3 ~/.oac/node-bootstrap/.pyz --uninstall --installation-id --force` on the host, with the installer the original command left in that directory. The installation ID is in the unit name `oac-node-.service` and in `/etc/oac-node/.json`; Web shows it under **Settings** → **Service and deployment** → **Installation details**. Uninstall stops and removes the service, the node state, the records and the proxy settings. It deletes the `oac-node` account only if the installer created it and no node remains; an adopted account only loses the groups the installer added. The output lists what it kept: an adopted account with its home `/var/lib/oac-node`, and the downloaded installers in `~/.oac/node-bootstrap`, which `rm -f ~/.oac/node-bootstrap/*.pyz` removes. -It never deletes Docker images or volumes. It keeps the Runtime image and prints the `sudo docker image rm` command. For microsandbox, once Core has confirmed the removal, it also deletes the node's store under `/var/lib/oac-node/.oac/m/` (about 1.5 GB), and with it the created account. With `--force` Core isn't asked, so the store is kept and the output prints the command that deletes it (`sudo -u oac-node rm -rf `); microVMs may still use it, so check `pgrep -u oac-node` first, then rerun uninstall to finish removing the account. The output lists what was deleted and what was kept. Uninstall can be rerun until it completes. +Once Core has confirmed the removal, uninstall also deletes the node's store under `/var/lib/oac-node/.oac/m/` (about 1.5 GB), and with it the created account. With `--force` Core isn't asked, so the store is kept and the output prints the command that deletes it (`sudo -u oac-node rm -rf `); microVMs may still use it, so check `pgrep -u oac-node` first, then rerun uninstall to finish removing the account. The output lists what was deleted and what was kept. Uninstall can be rerun until it completes. ## When a node host fails @@ -135,12 +131,12 @@ When a node is online but its sandbox provider is not ready, **Nodes** and **Ove Each code is a Provider-neutral class; the local error behind it stays on the node. Rerun the command on the host: the installer checks the host requirements first and names what to fix ([Installer messages](./troubleshooting.md#installer-messages)). -A node reports only its first failed check, in this order: the Docker daemon or KVM, Docker's limit support, host capacity, then the installed Runtime files. An unreachable Docker daemon therefore hides a missing image. The next preparation attempt after a fix clears or replaces the code. +A node reports only its first failed check, in this order: KVM, host capacity, then the installed Runtime files. Missing KVM access therefore hides a missing image. The next preparation attempt after a fix clears or replaces the code. | Code | Help tip | Cause | Fix | | --- | --- | --- | --- | -| `provider_unavailable` | Sandbox provider unavailable | The provider's service is unreachable or fails a request, such as a stopped Docker daemon, or a failure without a class | Rerun the node's command | -| `host_unsupported` | Host unsupported | The host lacks a capability the provider requires, such as Docker CPU and memory limits (cgroup v2) or read-write access to `/dev/kvm` | Rerun the node's command | +| `provider_unavailable` | Sandbox provider unavailable | The provider's service is unreachable or fails a request, or a failure without a class | Rerun the node's command | +| `host_unsupported` | Host unsupported | The host lacks a capability the provider requires, such as read-write access to `/dev/kvm` | Rerun the node's command | | `capacity_insufficient` | Host too small | The host has fewer CPUs or less memory than one sandbox | Use a larger host, or change the sandbox size | | `runtime_image_unavailable` | Runtime image missing | The provider does not have the installation's Runtime image | Rerun the node's command to download it again; otherwise load the image from the matching release | | `artifacts_unavailable` | Provider files missing | A pinned provider file, such as the microsandbox Runtime, firmware or helper, is missing or fails its SHA-256 check | Rerun the node's command to download the missing files; otherwise restore them from the matching release | diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index 2898ea322..21caee1ec 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -61,7 +61,7 @@ docker compose -f ~/.oac/core/compose.yaml stop docker compose -f ~/.oac/core/compose.yaml start ``` -Stopping Core stops no node and no sandbox. Node services, their microVMs and Docker containers keep running; stopping is not a way to reclaim compute. A Core restart does not continue an interrupted native tool call transparently. After reconnecting, query the same Session; don't create a new Session to replay uncertain work. Session event streams are live only; recover through Session, Turn and Items reads. +Stopping Core stops no node and no sandbox. Node services and their microVMs keep running; stopping is not a way to reclaim compute. A Core restart does not continue an interrupted native tool call transparently. After reconnecting, query the same Session; don't create a new Session to replay uncertain work. Session event streams are live only; recover through Session, Turn and Items reads. A Web restart, including one caused by `oac apply`, signs everyone out of the console; see [sign-in](../web/console-server.md#sign-in). @@ -163,7 +163,7 @@ An installation with independent workspace storage or nodes needs the full proce - The installation directory, including `.env`, `compose.yaml`, Compose overrides and the management command. - Every independent workspace namespace selected by current or retained objects, even when it is outside the installation volume. Back up its entire root, including `.oac-storage-root`, object identities, staging, live data, trash and deletion markers. Preserve numeric ownership, modes, links and extended attributes, including every `user.*` attribute. Copying only `live/data` loses lifecycle evidence and can resurrect deleted identities. [Workspace storage](../workspace-provider.md#kernel-nfs-adapter) owns the layout and ownership requirements. - For a [quota mode](../workspace-provider.md#quota-mode) namespace, the entire XFS filesystem holding the export as one block-level copy: a storage or LVM snapshot, or an image of the frozen or unmounted device. Only such a copy keeps project IDs, quota limits, inode identities, the filesystem UUID and the `oac-workspace-quota` journal consistent. A file-level copy, `tar`, `rsync` or `xfsdump` loses them; the command then refuses the restored objects instead of serving them unbounded. -- Each node's state directory, `/var/lib/oac-node/.oac/nodes//`, and its provider storage: Docker volumes or microsandbox's store. Include all compute and snapshot resources still referenced by the database. See [when a node host fails](./nodes.md#when-a-node-host-fails). +- Each node's state directory, `/var/lib/oac-node/.oac/nodes//`, and its provider storage, the microsandbox store. Include all compute and snapshot resources still referenced by the database. See [when a node host fails](./nodes.md#when-a-node-host-fails). - Each private `checkpoint_root` in its entirety, including `.oac-checkpoint-store`, checkpoints, operation journals, lock files and tombstones, together with the matching Core database. The [checkpoint transfer contract](../sandbox-provider.md#checkpoint-transfer) owns its identity, ownership and lifecycle. ### Establish a stopped-write window diff --git a/docs/getting-started/sandbox-backends.md b/docs/getting-started/sandbox-backends.md index 36b81bc2a..1a1b02f04 100644 --- a/docs/getting-started/sandbox-backends.md +++ b/docs/getting-started/sandbox-backends.md @@ -2,32 +2,30 @@ title: "Choose a sandbox backend" --- -Compare microsandbox, Docker and E2B, then select one in Web. This guide is for operators who finished [Install Core and Web](./install.md) and need compute for Core-hosted Sessions. +Compare microsandbox and E2B, then select one in Web. This guide is for operators who finished [Install Core and Web](./install.md) and need compute for Core-hosted Sessions. The installer selects no backend. One installation uses one backend. To change it later, reset the deployment first; see [Change the sandbox configuration](./nodes.md#change-the-sandbox-configuration). ## Compare the backends -| | microsandbox (recommended) | Docker (compatibility) | E2B | -| --- | --- | --- | --- | -| Where sandboxes run | A microVM on your Linux nodes | A container on your Linux nodes | E2B's cloud | -| Nodes to add | Yes | Yes | None | -| You provide | Linux amd64 hosts with systemd and `/dev/kvm` | Linux amd64 hosts with systemd and rootful Docker Engine | An E2B API key and a template; a public HTTPS URL for Core | -| Isolation | Each sandbox has its own kernel | Containers share the node's kernel | E2B's | -| Idle sandboxes suspend and resume | Yes, with checkpoints | No | Yes, paused in place by E2B | -| Workspace storage | Required, on NFS | Own disk | Own disk | -| Disk limits | Root disk and workspace quota | None | Set by the template | +| | microsandbox | E2B | +| --- | --- | --- | +| Where sandboxes run | A microVM on your Linux nodes | E2B's cloud | +| Nodes to add | Yes | None | +| You provide | Linux amd64 hosts with systemd and `/dev/kvm` | An E2B API key and a template; a public HTTPS URL for Core | +| Isolation | Each sandbox has its own kernel | E2B's | +| Idle sandboxes suspend and resume | Yes, with checkpoints | Yes, paused in place by E2B | +| Workspace storage | Required, on NFS | Own disk | +| Disk limits | Root disk and workspace quota | Set by the template | The field-level differences are in the [deployment contract](../../contracts/agents-api/sandbox-deployment.md#what-each-field-means-per-sandbox-provider). Host requirements for nodes are in [Add and manage nodes](./nodes.md#host-requirements). -Docker is a compatibility backend, for trusted workloads and for hosts without `/dev/kvm`. Its nodes are [root-equivalent](./nodes.md#what-the-installer-sets-up) on their host: add them only on hosts dedicated to sandboxes. - Sandbox nodes run on Linux amd64. When Core runs on macOS or Windows, connect a Linux node or use E2B. Applications can also run [self-hosted Sessions](./self-hosted.md) on their own machines, with no backend. ## Select the backend in Web -1. Open **Settings → Sandbox and scheduling**. The setup wizard asks **Where should sandboxes run?**: **Own machines** (microsandbox or Docker) or **E2B**. -2. For **Own machines**, choose the backend and the sandbox size. Web proposes the [default size the Provider declares](../sandbox-provider.md#register-the-provider-kind) as **Standard**, and **Advanced settings** on the review page holds the exact CPU, memory and disk values. E2B declares no default size, so its review page has no **Advanced settings**. +1. Open **Settings → Sandbox and scheduling**. The setup wizard asks **Where should sandboxes run?**: **Own machines** (microsandbox) or **E2B**. +2. For **Own machines**, choose the sandbox size. Web proposes the [default size the Provider declares](../sandbox-provider.md#register-the-provider-kind) as **Standard**, and **Advanced settings** on the review page holds the exact CPU, memory and disk values. E2B declares no default size, so its review page has no **Advanced settings**. 3. Choose **Save configuration**. The choice is stored in Core's database. For **Own machines**, continue with [Add and manage nodes](./nodes.md). For microsandbox, first [Set up workspace storage](./workspace-storage.md). For checkpoint recovery across microsandbox nodes, see [Checkpoint recovery across nodes](./nodes.md#checkpoint-recovery-across-nodes). @@ -46,6 +44,6 @@ The key and template rules, including key replacement, are in [Initial setup and ## Next steps -- microsandbox or Docker: [Add and manage nodes](./nodes.md). +- microsandbox: [Add and manage nodes](./nodes.md). - Workspaces on NFS: [Set up workspace storage](./workspace-storage.md). - Models and keys: continue with [Install Core and Web](./install.md#set-a-default-model). diff --git a/docs/getting-started/troubleshooting.md b/docs/getting-started/troubleshooting.md index f631fd636..7f58a99c6 100644 --- a/docs/getting-started/troubleshooting.md +++ b/docs/getting-started/troubleshooting.md @@ -42,7 +42,7 @@ Add node shows no command: see [Before you add a node](./nodes.md#before-you-add A microsandbox sandbox cannot reach a model provider or MCP server at a private address, and the Turn fails once the Harness stops retrying. Sandbox DNS queries go to the nameservers listed in the host's `/etc/resolv.conf`, and DNS rebind protection discards any answer that contains a private address. A name those nameservers resolve to a private address therefore does not resolve in the sandbox at all, even for a public service such as a model API behind a cloud VPC resolver. -Nodes have no setting to change this. Make those nameservers return the service's public addresses. microsandbox does not read the host's `/etc/hosts`, so an entry there helps only when `/etc/resolv.conf` points at a local stub resolver that serves it, such as systemd-resolved at `127.0.0.53`. Otherwise use a Docker node, which does not filter sandbox traffic. +Nodes have no setting to change this. Make those nameservers return the service's public addresses. microsandbox does not read the host's `/etc/hosts`, so an entry there helps only when `/etc/resolv.conf` points at a local stub resolver that serves it, such as systemd-resolved at `127.0.0.53`. ### Connection failures @@ -68,8 +68,6 @@ A registered node that can't connect to Core says why in its [log](./nodes.md#lo | A sandbox provider check failed before registering, such as `This host lacks a capability …` | Fix what it names (see [Readiness codes](./nodes.md#readiness-codes)) and rerun; the token was not used | | Core's public URL changed after this command was generated | Generate a new command in Web and run it | | This host's node uses `
`, but this command uses `
` | The node was added under an older public URL. Remove it in Web, uninstall it, then add it again | -| Docker Engine is not installed, or Docker is not running | Install Docker Engine, or `sudo systemctl enable --now docker`, then rerun | -| Docker on this host does not enforce CPU and memory limits | Use cgroup v2, then rerun | | KVM is unavailable, or `/dev/kvm` must be group-accessible | Enable virtualization; your distribution's KVM package sets `root:kvm 0660` | | This host has N CPUs and M MiB of memory; each sandbox needs … | Use a larger host, or change the sandbox size | | SELinux is enforcing on this host | Use a host supported by the installer; it does not change SELinux settings | diff --git a/docs/getting-started/workspace-storage.md b/docs/getting-started/workspace-storage.md index 1f1ac793f..f00e6f6ef 100644 --- a/docs/getting-started/workspace-storage.md +++ b/docs/getting-started/workspace-storage.md @@ -4,7 +4,7 @@ title: "Set up workspace storage" Keep Session workspaces on an NFS filesystem outside the sandbox nodes, with or without per-workspace quotas. microsandbox runs every Environment on this storage, so select it before you configure the microsandbox backend. -The supported combination is microsandbox with the [kernel NFS adapter](../workspace-provider.md#kernel-nfs-adapter). Docker and E2B keep their own disks and do not support it; see [Choose a sandbox backend](./sandbox-backends.md). +The supported combination is microsandbox with the [kernel NFS adapter](../workspace-provider.md#kernel-nfs-adapter). E2B keeps its own disks and does not support it; see [Choose a sandbox backend](./sandbox-backends.md). ```mermaid flowchart LR diff --git a/docs/maintainers.md b/docs/maintainers.md index 3969fdf1c..549989e06 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -38,7 +38,7 @@ make build-core-distribution The build reuses the Core, Web, Runtime, SDK and helper builders. The manifest records the commit and its source tree, image config and OCI manifest digests, the Runtime OCI manifest digest, the microsandbox runtime and firmware hashes, and the size and SHA-256 of every Runtime and node artifact; native installers carry only their SHA-256 in the [catalog](#native-installers). Output is the control archive and its `.sha256`, the optional offline archive, and the versioned Runtime, node and native installer assets. Nothing is published. Rebuilding into a directory that already holds this commit's distribution is refused. -The ingress image carries only `oac` and four node metadata files: `manifest.json`, `SHA256SUMS`, `node-install.pyz` and `runtime/seccomp.json`. The build prepares their manifest from the same Runtime and artifact identities as the distribution, then verifies initialization with networking disabled. The node manifest omits control-plane image identities so it can be packaged before the ingress image itself exists. +The ingress image carries only `oac` and three node metadata files: `manifest.json`, `SHA256SUMS` and `node-install.pyz`. The build prepares their manifest from the same Runtime and artifact identities as the distribution, then verifies initialization with networking disabled. The node manifest omits control-plane image identities so it can be packaged before the ingress image itself exists. The control archive carries no Runtime image or node execution artifacts; the offline archive carries them. The [download contract](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md#download-contract) describes how nodes obtain them. diff --git a/docs/runtime-bootstrap.md b/docs/runtime-bootstrap.md index 5fd98fa34..68884f5f2 100644 --- a/docs/runtime-bootstrap.md +++ b/docs/runtime-bootstrap.md @@ -73,7 +73,6 @@ When `suspend_control` is true, the daemon publishes its suspend control at `run | Provider | Delivery | | --- | --- | -| Docker | Writes the file into the Runtime's owned home volume. | | microsandbox, E2B | Deliver the file before launching the same command. | ## Verification diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index 798edfd7a..a6c904eb9 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -2,12 +2,12 @@ title: "Sandbox Provider protocol" --- -The Sandbox Provider protocol connects Core to the adapter that supplies compute for Core-managed (`openai_hosted`) Environments and runs the bounded bootstrap that starts the Runtime daemon in it. This page is for adapter authors: it says what a Provider implements and what it may rely on Core to do. Docker, microsandbox and E2B implement it. +The Sandbox Provider protocol connects Core to the adapter that supplies compute for Core-managed (`openai_hosted`) Environments and runs the bounded bootstrap that starts the Runtime daemon in it. This page is for adapter authors: it says what a Provider implements and what it may rely on Core to do. microsandbox and E2B implement it. | | | | --- | --- | | Caller | Core's runtime manager and execution Worker | -| Implementations | Docker and microsandbox (node adapters) and E2B (direct adapter); see [Reference adapters](#reference-adapters) | +| Implementations | microsandbox (node adapter) and E2B (direct adapter); see [Reference adapters](#reference-adapters) | | Code | [`SandboxProvider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/sandbox_provider.go) in `sandbox_provider.go`, with the setup-time `ConfigurationAdapter` | | Transport | A direct adapter is called in process. A node adapter runs on a sandbox node and receives the same calls over the [sandbox node protocol](../contracts/agents-api/node-generation-protocol.md#provider-requests) | | Version rule | The in-process interface has no wire version. The sandbox node protocol and each helper protocol require an exact version match | @@ -50,7 +50,7 @@ Isolation belongs to the provider's infrastructure, not the daemon; see [Runtime | `Kill` | Reclaim the allocation's compute and retained resources | | `RunCommand` | Run a bounded command in the allocation's compute | -A backend without a native renewable lease, such as Docker, still keeps Core's hosted expiry and cleanup requirements. Every adapter implements `RunCommand` and its tests exercise it, but Core's orchestration does not call it; only the node transport forwards it. Confidential command input travels in `Command.Stdin`, never in arguments or logs, and the result keeps byte order, bounded output and the actual exit status. +A backend without a native renewable lease, still keeps Core's hosted expiry and cleanup requirements. Every adapter implements `RunCommand` and its tests exercise it, but Core's orchestration does not call it; only the node transport forwards it. Confidential command input travels in `Command.Stdin`, never in arguments or logs, and the result keeps byte order, bounded output and the actual exit status. `Bootstrap.Workspace` and `ResumeRequest.Workspace` carry an external workspace binding exactly when the Provider's `DeploymentPolicy.Workspace` is declared; the Provider obligations for it are in [Attachment](./workspace-provider.md#attachment). Core sets `Bootstrap.SuspendControl` exactly when the Provider declares checkpoint support, and the Provider passes it unchanged in the [Runtime bootstrap](./runtime-bootstrap.md). @@ -154,7 +154,7 @@ Preview and persistence use `providers.Normalize` and `providers.Describe`. `pro A direct adapter with a credential verifies all retained generations and allocation references before a key is replaced. The common `sandbox.CallFence` excludes native calls and waits for helper completion, including calls whose callers timed out; execution invokes the prepared verification and fencing callbacks without branching on a vendor. -Vendor deployment validation and SDK setup stay at the construction boundary, and construction never creates an Environment. For node-local adapters `sandbox.Built` returns the provider, probe, installation identity, backend fingerprint and specification digest, and a `Quiescent` check when a helper can outlive its caller; generation collection waits for it. The factory also returns its close function. `execution.RuntimeProvider` binds the adapter to its kind, installation ID, backend fingerprint, generation, mode and node ownership; the database owns the selection, and the in-memory copy is never another authority. Docker and microsandbox run on nodes, and E2B is constructed directly. The node proxy exposes checkpoint operations only for a backend whose registered declaration supports them, and common lifecycle code admits suspension through the checkpoint declaration, never through a provider name. +Vendor deployment validation and SDK setup stay at the construction boundary, and construction never creates an Environment. For node-local adapters `sandbox.Built` returns the provider, probe, installation identity, backend fingerprint and specification digest, and a `Quiescent` check when a helper can outlive its caller; generation collection waits for it. The factory also returns its close function. `execution.RuntimeProvider` binds the adapter to its kind, installation ID, backend fingerprint, generation, mode and node ownership; the database owns the selection, and the in-memory copy is never another authority. microsandbox runs on nodes, and E2B is constructed directly. The node proxy exposes checkpoint operations only for a backend whose registered declaration supports them, and common lifecycle code admits suspension through the checkpoint declaration, never through a provider name. The backend fingerprint identifies a native resource namespace, not capacity. Core keeps deployment generations so that owned allocations keep resolving to their original backend; never repoint retained allocations at a replacement backend. @@ -302,30 +302,11 @@ Run `make check-sandbox-provider-contract` while developing. It runs the shared Node tests separately cover disconnect and reconnect fencing and cleanup after a lost Create response. Helper protocols and the [sandbox node protocol](../contracts/agents-api/node-generation-protocol.md) require an exact version match. -Native acceptance proves what fixtures cannot: creation, lease behavior, owned partial cleanup, declared isolation and limits, and snapshots where supported. The opt-in Docker lifecycle and recovery tests use `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`; the SDK helpers use `make check-e2b-provider` and `make check-microsandbox-provider`. Mocked compute proves neither reclamation nor isolation. +Native acceptance proves what fixtures cannot: creation, lease behavior, owned partial cleanup, declared isolation and limits, and snapshots where supported. The SDK helpers use `make check-e2b-provider` and `make check-microsandbox-provider`. Mocked compute proves neither reclamation nor isolation. ## Reference adapters | Kind | Adapter | Helper and adapter rules | Operator guide | | --- | --- | --- | --- | -| Docker (node) | [`sandbox/docker`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/docker) | Node proxy in [`sandbox/node`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/node) | [Docker adapter](#docker-adapter) | | microsandbox (node) | [`sandbox/microsandbox`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/microsandbox) | [`tools/microsandbox-provider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/microsandbox-provider/README.md) | [Add and manage nodes](./getting-started/nodes.md) | | E2B (direct) | [`sandbox/e2b`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/e2b) | [`tools/e2b-provider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/e2b-provider/README.md) | [Sandbox deployment](../contracts/agents-api/sandbox-deployment.md#e2b-configuration); application-managed templates in [`deploy/e2b`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md) | - -## Docker adapter - -The Docker Sandbox Provider ([`sandbox/docker`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/docker)) runs every Runtime image, whichever Harness it serves, with the same container settings ([`container_options.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/container_options.go)): - -- user 1000:1000, read-only root filesystem, all capabilities dropped, `no-new-privileges`, the [seccomp profile](#seccomp-profile) and AppArmor `unconfined`; -- the node’s configured network and extra hosts ([node configuration](./configuration.md#docker-node-configuration)); -- CPU and memory from the deployment specification, a 128-process limit and a 128 MiB `/tmp` tmpfs; -- two named volumes labelled with the installation, tenant, Environment and allocation: `-home` at `/home` and `-environment` at `/environment`, whose `workspace` subdirectory is also mounted at `/workspace`. The Docker Engine must support volume subpath mounts; -- with the configured `nested_sandbox` option, Docker's `/proc` masks are lifted (`/sys/firmware` and `/sys/devices/virtual/powercap` stay masked) and the container runs an init process. - -Create refuses to reuse retained volumes that have no container. It copies the [Runtime bootstrap](./runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json` (mode 0600, UID 1000) and the `/environment` workspace, staging, initialization and package directories into the container, then starts `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json`. When the created container does not have the configured CPU, memory and exact image, Create returns the error with `CreateSettled`. Docker has no lease, so Renew only reads the container state. Kill checks the ownership labels of the container and both volumes before removing any of them, then confirms that all three are gone. - -The node uses the explicit Unix socket in its [provider configuration](./configuration.md#docker-node-configuration) and ignores `DOCKER_HOST`. No Docker socket, host home or Core credential is mounted into a Runtime. - -### Seccomp profile - -[`seccomp.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.json) is the Moby default profile at [revision 65adc7e](https://github.com/moby/profiles/blob/65adc7e022c97f55e45c054ff012988027733b87/seccomp/default.json) (Apache-2.0, see [seccomp.LICENSE](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.LICENSE); upstream file SHA-256 `785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334`) with one appended rule that allows `clone`, `unshare`, `setns`, `mount`, `umount2` and `pivot_root`. The distribution ships this file to every Docker node as `runtime/seccomp.json`. diff --git a/docs/web/console-api-usage.md b/docs/web/console-api-usage.md index 077317fb1..d30022387 100644 --- a/docs/web/console-api-usage.md +++ b/docs/web/console-api-usage.md @@ -96,14 +96,14 @@ The list carries each Harness's configuration, so the console does not read `GET | Operation | Route | Console use | | --- | --- | --- | -| Deployment | `GET`, `POST`, `PUT /core/v1/sandbox/deployment` | Read the provider, the read-only `core_url` (`OAC_PUBLIC_URL`, shown in the setup review and never sent), reset state, installation ID and specification; a 409 `sandbox_configuration_error` (E2B with a loopback `public_url`) shows the shared client's fixed safe address-configuration message in the setup wizard, with Managed in Settings leading to Settings → Service and deployment, and leaves nothing to confirm; initialize the deployment with Docker or microsandbox `resources`, or with the E2B account and no `resources` (Core adopts the template build's CPU and memory); change its settings with the expected generation. Sandbox metrics shows the size of each E2B sandbox from `specification.resources`; microsandbox's `suspension` (idle and retention seconds) shows in Settings → Sandbox and scheduling | +| Deployment | `GET`, `POST`, `PUT /core/v1/sandbox/deployment` | Read the provider, the read-only `core_url` (`OAC_PUBLIC_URL`, shown in the setup review and never sent), reset state, installation ID and specification; a 409 `sandbox_configuration_error` (E2B with a loopback `public_url`) shows the shared client's fixed safe address-configuration message in the setup wizard, with Managed in Settings leading to Settings → Service and deployment, and leaves nothing to confirm; initialize the deployment with microsandbox `resources`, or with the E2B account and no `resources` (Core adopts the template build's CPU and memory); change its settings with the expected generation. Sandbox metrics shows the size of each E2B sandbox from `specification.resources`; microsandbox's `suspension` (idle and retention seconds) shows in Settings → Sandbox and scheduling | | E2B discovery | `POST /core/v1/sandbox/providers/e2b/discovery` | The setup wizard lists the options of a discovered field with the entered credential, one level at a time. For E2B these are the key's templates, then the selected template's ready builds. The key travels only in these request bodies and the deployment write | | Reset | `POST`, `DELETE /core/v1/sandbox/deployment/reset` | Explicitly clear hosted resources, or cancel the remaining clear at the observed generation; show Core's remaining and offline projection | | Nodes | `GET /core/v1/sandbox/nodes` | Nodes page; fleet on Overview; each hosted Runtime's node on Sandbox metrics. An online node whose `provider_ready` is false is degraded; the help tip beside its status on the Nodes list and on the node's page says *Sandbox provider unavailable*, and the target preparation reason (`rollout.diagnostic`, a [readiness code](../getting-started/nodes.md#readiness-codes)) shows in its target status. A node whose `core_url` (the address it enrolled with) differs from the deployment's `core_url` is named on the Nodes page as bound to an old address, to be removed and added again, and its status there and on its page reads Old address instead of its health. **Edit node** reads the node's `recommended_capacity` from this list. **Add node** follows only the node whose `enrollment_id` equals its command's | | Node detail | `GET /core/v1/sandbox/nodes/{node_id}?range=1h\|6h\|24h` | A node's page on Nodes: the host's CPU busy share, memory and free disk from its last heartbeat, and CPU and memory history over 1 hour, 6 hours or 24 hours, refreshed every 30 s while visible. | | Allocations | `GET /core/v1/sandbox/nodes/{node_id}/allocations` | Nodes page; Sandbox metrics. A node's page shows from `compute_phase_changed_at` how long each allocation has been in its compute phase; a null time shows a dash. It does not infer snapshot expiry from the phase timestamp or the deployment's `suspension.retention_seconds` | -| Enrollment | `POST /core/v1/sandbox/enrollment-tokens` | **Add node**: the administrator keeps the recommended limits (the request body is empty and Core derives them when the node registers) or sets both (`max_active`; `max_retained` only for microsandbox, equal to `max_active` for Docker) before Core issues a single-use token inside a command that verifies the installer checksum, with the command's `enrollment_id`, which the node it registers reports. The command runs the installer with sudo (a system service) and passes the token on standard input; root runs it directly. No ordinary-user installation or removal entry is exposed, and the log hint always names the system service. The command downloads the installer from the installation's `public_url`. No token is requested until the installation is read, when it cannot be read, when it is `local_only` (or its `public_url` is not a valid HTTP or HTTPS origin), or when `/console/config` lists `node_artifacts` without the deployment's provider. The dialog reads both again on opening and when the window regains focus | -| Update node | `PATCH /core/v1/sandbox/nodes/{node_id}` | **Edit node**: the name and sandbox limits together (the retained limit only for microsandbox; under Docker, Core sets it to the active limit) | +| Enrollment | `POST /core/v1/sandbox/enrollment-tokens` | **Add node**: the administrator keeps the recommended limits (the request body is empty and Core derives them when the node registers) or sets both (`max_active` and `max_retained`) before Core issues a single-use token inside a command that verifies the installer checksum, with the command's `enrollment_id`, which the node it registers reports. The command runs the installer with sudo (a system service) and passes the token on standard input; root runs it directly. No ordinary-user installation or removal entry is exposed, and the log hint always names the system service. The command downloads the installer from the installation's `public_url`. No token is requested until the installation is read, when it cannot be read, when it is `local_only` (or its `public_url` is not a valid HTTP or HTTPS origin), or when `/console/config` lists `node_artifacts` without the deployment's provider. The dialog reads both again on opening and when the window regains focus | +| Update node | `PATCH /core/v1/sandbox/nodes/{node_id}` | **Edit node**: the name and sandbox limits together (the active and retained limits) | | Remove node | `DELETE /core/v1/sandbox/nodes/{node_id}` | Confirmed node removal; the row goes only after Core acknowledges the deletion, and a Clean up the host dialog then gives the host's uninstall command (requiring root or sudo; for a node enrolled with another address than the deployment's, also with `--force`, which skips the installer's confirmation with Core) | | Runtime observations | `GET /core/v1/sandbox/runtime-observations` | Sandbox metrics: hosted Runtimes of every project, each labelled with its project; an E2B sandbox's dialog adds its `observation.disk` as used / limit (null elsewhere) | diff --git a/docs/web/console-server.md b/docs/web/console-server.md index 758c5a11d..0fcd05cb9 100644 --- a/docs/web/console-server.md +++ b/docs/web/console-server.md @@ -92,11 +92,11 @@ Sign-in errors: 400 for a malformed body, 401 `Invalid Core key`, 405 for a meth | --- | --- | | `node_installer` | Whether the console serves a node installation payload | | `node_installer_sha256` | SHA-256 of that payload's `node-install.pyz`; Add node commands verify it before running the installer | -| `node_artifacts` | The providers (`docker`, `microsandbox`) whose node artifacts the payload holds, locally or as a release download. Read on every request, so artifacts added by rerunning the installer appear without a restart | +| `node_artifacts` | The providers whose node artifacts the payload holds (`microsandbox`), locally or as a release download. Read on every request, so artifacts added by rerunning the installer appear without a restart | ## Node installation payload -With `OAC_WEB_NODE_PAYLOAD_DIR` set, the console serves the matched distribution's node payload at `/node-install/` without sign-in: `node-install.pyz`, `manifest.json`, `SHA256SUMS`, `runtime/seccomp.json`, and the node artifacts the manifest declares under `artifacts/`. An artifact missing locally redirects (307) to its release download. Node install and uninstall commands download from `/node-install/`, so the reverse proxy must send that path to the console. Nodes verify every checksum themselves. +With `OAC_WEB_NODE_PAYLOAD_DIR` set, the console serves the matched distribution's node payload at `/node-install/` without sign-in: `node-install.pyz`, `manifest.json`, `SHA256SUMS`, and the node artifacts the manifest declares under `artifacts/`. An artifact missing locally redirects (307) to its release download. Node install and uninstall commands download from `/node-install/`, so the reverse proxy must send that path to the console. Nodes verify every checksum themselves. ## Verification diff --git a/docs/web/index.md b/docs/web/index.md index ef5e28571..2ab13806e 100644 --- a/docs/web/index.md +++ b/docs/web/index.md @@ -26,7 +26,7 @@ Signing in opens the Overview. While any step is still to do, its **Getting star | Monitor | Session log | Every Session, and each Session's read-only conversation, trace and Turns with the classified reason of a failure and its next step; a self-hosted Session's page also manages its executor credentials and gives the command that connects a host | | Resources | Agents, Environment templates, Skills, Files, Vaults | Inspection and permitted deletion, with the Project and the creating key of each resource | | Platform | Projects and keys | Create, rename and archive Projects; issue and revoke keys; each Project's usage, write history and how to call the API | -| Platform | Nodes | Add, edit and remove microsandbox or Docker nodes; each node's readiness, active and retained limits, host CPU and memory with their history, free disk, and allocations with elapsed time in each compute phase | +| Platform | Nodes | Add, edit and remove microsandbox nodes; each node's readiness, active and retained limits, host CPU and memory with their history, free disk, and allocations with elapsed time in each compute phase | | Platform | Settings | **Sandbox and scheduling**, **Workspace storage**, **Models and credentials**, and **Service and deployment**; see [configuration](../configuration.md#runtime-settings-web) | Missing data is shown as missing (—), never as zero. [Console API usage](./console-api-usage.md) lists what each page reads and how its figures are bounded. @@ -35,7 +35,7 @@ Missing data is shown as missing (—), never as zero. [Console API usage](./con | Task | Where | | --- | --- | -| Choose the sandbox backend (microsandbox, E2B or Docker), the sandbox size and Runtime, or reset the backend | **Settings → Sandbox and scheduling**; see [change the sandbox configuration](../getting-started/nodes.md#change-the-sandbox-configuration) | +| Choose the sandbox backend (microsandbox or E2B), the sandbox size and Runtime, or reset the backend | **Settings → Sandbox and scheduling**; see [change the sandbox configuration](../getting-started/nodes.md#change-the-sandbox-configuration) | | Add, resize or remove execution nodes | **Nodes**: **Add node**, **Edit node** and removal; see [Add and manage nodes](../getting-started/nodes.md) | | Set the default model of a Harness | **Settings → Models and credentials**; see [default models](../configuration.md#default-models) | | Create a Project and issue its API keys | **Projects and keys**; see [Projects and API keys](../getting-started/operations.md#projects-and-api-keys) | diff --git a/docs/workspace-provider.md b/docs/workspace-provider.md index 8e44df723..6d79cd950 100644 --- a/docs/workspace-provider.md +++ b/docs/workspace-provider.md @@ -16,7 +16,7 @@ Core selects and validates the combination of a Sandbox Provider and a filesyste | Sandbox Provider | External workspace storage | | --- | --- | | microsandbox | Required (`host_directory`, `user_xattr`) | -| Docker, E2B | Not supported; the Provider's own Environment disk | +| E2B | Not supported; the Provider's own Environment disk | ## Implement an adapter diff --git a/docs/zh/architecture.md b/docs/zh/architecture.md index 78a0388d9..99e9ba070 100644 --- a/docs/zh/architecture.md +++ b/docs/zh/architecture.md @@ -1,7 +1,7 @@ --- title: "架构" source: docs/architecture.md -source_hash: 9a52651443a4e985e05a40c1f7c6833fd6f4ecc203ab5fdfbf527566244d9222 +source_hash: 1904cebfe1774057fe73709ea6b882747a3e5860318898d8b90a847fb01ee742 --- OpenAgentCore 是 Agent 运行平台,通过可替换的协议实现统一组织 Harness、模型、工具、Session 和执行环境。它将编排、计算资源和原生执行分开。Core 负责 API 和持久状态。沙箱提供商管理计算资源。独立工作区文件系统 adapter 通过单独的协议管理 Environment 持久存储。Runtime daemon 准备 Environment 并运行选定的 Harness;Harness 的原生 SDK 或协议负责模型与工具循环。 @@ -29,7 +29,7 @@ Runtime daemon 主动向 Core 发起经过认证的 WebSocket 连接。[API 命 | Core | 认证调用方,解析并冻结配置,调度 Turn,处理取消和待处理交互,将资源与执行事实持久化到 PostgreSQL | [Core 服务](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md) | | 沙箱提供商 | 创建、观察、续期和回收计算资源;提供 Runtime 启动输入 | [沙箱提供商协议](sandbox-provider.md)、[Runtime 引导](runtime-bootstrap.md) | | Workspace filesystem adapter | 创建、观察和删除 Environment 持久存储;验证并解析计算资源所需的挂载 | [工作区文件系统协议](workspace-provider.md) | -| Sandbox node | 运行 Docker 或 microsandbox 主机,协调分配给它的 generation 与 allocation | [Sandbox node 协议](../../contracts/agents-api/zh/node-generation-protocol.md) | +| Sandbox node | 运行 microsandbox 主机,协调分配给它的 generation 与 allocation | [Sandbox node 协议](../../contracts/agents-api/zh/node-generation-protocol.md) | | Runtime | 准备工作区和能力,管理 Session Executor,执行 Turn 并报告事件与回执 | [Core–Runtime 协议](runtime-protocol.md) | | Harness adapter | 验证原生配置,调用上游 SDK 或协议,转换事件并确认原生清理完成 | [Harness 协议](../../contracts/agents-api/zh/harness-onboarding.md) | | 模型提供商 | 提供 Harness 选定的模型协议 | [模型执行](../../contracts/agents-api/zh/model-execution.md) | diff --git a/docs/zh/concepts.md b/docs/zh/concepts.md index ba320f43a..bf032f634 100644 --- a/docs/zh/concepts.md +++ b/docs/zh/concepts.md @@ -1,7 +1,7 @@ --- title: "概念" source: docs/concepts.md -source_hash: 61daa0e61271a12b44904a1074f56dbb43721bae5a3e60a03eeaee3d2e5a7b60 +source_hash: 20e710ce0826e741123fcdcb5d04df5d92b39706d82c89bc6591dae958c3f3bb --- 项目是 OpenAgentCore 的执行租户。应用使用其 API 密钥;运维人员使用独立的 Core 密钥管理安装实例。[API 命名空间和凭据](api/index.md)将每类调用方映射到对应命名空间和凭据。 @@ -28,7 +28,7 @@ source_hash: 61daa0e61271a12b44904a1074f56dbb43721bae5a3e60a03eeaee3d2e5a7b60 Runtime daemon 在 Linux、macOS 和 Windows 上运行。原生平台行为由 Runtime 及其 Harness adapter 负责;托管沙箱提供商运行 Linux 环境。 -工具以启动 daemon 的账户权限运行。daemon 不增加文件系统、权限或网络隔离。应使用外层 Environment 提供隔离:托管 Docker、E2B 或 microsandbox 环境,或在自托管机器的 Runtime 外使用容器或虚拟机。认证、私有存储、锁和进程清理保护连接与生命周期,但以同一用户运行的工具可以访问 Runtime 数据。 +工具以启动 daemon 的账户权限运行。daemon 不增加文件系统、权限或网络隔离。应使用外层 Environment 提供隔离:托管 E2B 或 microsandbox 环境,或在自托管机器的 Runtime 外使用容器或虚拟机。认证、私有存储、锁和进程清理保护连接与生命周期,但以同一用户运行的工具可以访问 Runtime 数据。 ## 秘密与审计 {#secrets-and-audit} diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 8f3bc3aee..8a22298b6 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -1,7 +1,7 @@ --- title: "配置参考" source: docs/configuration.md -source_hash: 521923265f147071ec011ab245caf859ad74f23ad9256c88f4899e9c22114d8c +source_hash: e06f34fe8629d5de462b5ee1d0f165cb8a278f39872d16a315b04fafbf9467de --- Core 安装的每项设置、其归属位置以及修改方式。安装程序写入初始值;之后请使用本参考。每项设置都恰好只有一个归属位置,分属以下三类: @@ -59,7 +59,7 @@ Core 安装的每项设置、其归属位置以及修改方式。安装程序写 | 设置 | Web 中的位置 | Core API | 注意事项 | | --- | --- | --- | --- | -| 沙箱提供商:Docker、microsandbox 或 E2B | **设置** → **沙箱与调度**:设置向导,最后点击 **保存配置** | `/core/v1/sandbox/deployment` | 每个安装只能使用一个沙箱提供商,在首次登录后选择。要改用其他提供商,必须先执行 **重置部署**;请参阅[更改沙箱配置](getting-started/nodes.md#change-the-sandbox-configuration) | +| 沙箱提供商:microsandbox 或 E2B | **设置** → **沙箱与调度**:设置向导,最后点击 **保存配置** | `/core/v1/sandbox/deployment` | 每个安装只能使用一个沙箱提供商,在首次登录后选择。要改用其他提供商,必须先执行 **重置部署**;请参阅[更改沙箱配置](getting-started/nodes.md#change-the-sandbox-configuration) | | 沙箱大小、E2B 密钥和模板构建 | **设置** → **沙箱与调度** → **修改资源** | `/core/v1/sandbox/deployment` | Web 会推荐 [提供商声明的默认大小](sandbox-provider.md#register-the-provider-kind)。现有沙箱会保留其大小。E2B 密钥仅可写入,并且已加密 | | 节点及其容量 | **节点**:**添加节点**、**编辑节点** 和移除 | `/core/v1/sandbox/enrollment-tokens`、`/core/v1/sandbox/nodes` | 请参阅[节点容量](#node-capacity)和[添加和管理节点](getting-started/nodes.md) | | 空闲挂起 | **设置** → **沙箱与调度** → **自动挂起** → **空闲多久后挂起(秒)** → **保存** | `PATCH /core/v1/sandbox/deployment` | 整个部署统一生效;参阅[挂起策略](../../contracts/agents-api/zh/sandbox-deployment.md#suspension-policy) | @@ -72,7 +72,7 @@ Core 安装的每项设置、其归属位置以及修改方式。安装程序写 ### 节点容量 {#node-capacity} -节点容量是 **同时运行的沙箱**(`max_active`),并且仅对 microsandbox 还有 **保留的沙箱**(`max_retained`),其中 `max_retained >= max_active >= 1`。**添加节点** 默认采用推荐值,即节点注册时 Core [按主机推导](../../contracts/agents-api/zh/sandbox-deployment.md#recommended-capacity)的值;也可以自行填写两个数字。Docker 从不暂停沙箱,因此 Web 不会询问此项,并且 Core 会使 `max_retained` 保持等于 `max_active`。之后可在 **节点** 使用 **编辑节点** 修改这些值,它会显示推荐值并可一键填入。预留和尚未确认的清理操作都会占用容量;retained slot 已满时可以[提前结束空闲检查点的保留期](./sandbox-provider.md#cold-replacement-after-checkpoint-retention);调低限制不会停止任何正在运行的沙箱。节点自身的文件无法更改其容量、大小或 Runtime。 +节点容量是 **同时运行的沙箱**(`max_active`),并且还有 **保留的沙箱**(`max_retained`),其中 `max_retained >= max_active >= 1`。**添加节点** 默认采用推荐值,即节点注册时 Core [按主机推导](../../contracts/agents-api/zh/sandbox-deployment.md#recommended-capacity)的值;也可以自行填写两个数字。之后可在 **节点** 使用 **编辑节点** 修改这些值,它会显示推荐值并可一键填入。预留和尚未确认的清理操作都会占用容量;retained slot 已满时可以[提前结束空闲检查点的保留期](./sandbox-provider.md#cold-replacement-after-checkpoint-retention);调低限制不会停止任何正在运行的沙箱。节点自身的文件无法更改其容量、大小或 Runtime。 `OAC_EXECUTION_CONCURRENCY` 与此无关:它限制 Core 中并发执行的工作量。 @@ -109,20 +109,6 @@ Session 创建可以在异步工作区准入之前成功。容量耗尽时,Cor 上述路径位于 Docker 命名卷 `_data` 中。所有主机平台都由 Docker 管理 Linux 文件权限;每个服务只挂载需要的子目录。数据卷必须和项目定义、公共 URL 一同保留。仅删除机密目录不会重置安装;数据库已存在时初始化会拒绝重建。Core 也会校验安装 ID 与数据库的绑定。运行时设置仍保存在 [Core 数据库](#runtime-settings-web)中。 -## Docker 节点配置 {#docker-node-configuration} - -节点安装程序会将 Docker 的主机设置写入节点配置文件的 `native` 对象,只有 Docker 适配器读取它。部署资源和容量仍存储在 [Core 的数据库](#runtime-settings-web)中。 - -| 字段 | 安装程序设置的值 | 含义 | -| --- | --- | --- | -| `host` | `unix:///var/run/docker.sock` | 显式 Docker Engine 套接字 | -| `image` | 加载后 Runtime 镜像的本地 ID | 本安装的 Runtime 镜像。主机的镜像存储决定其本地 ID 是清单中的 `image_id` 还是 `image_manifest_digest`,因此该值属于节点本地 | -| `network` | `oac-node-` | Runtime 容器网络 | -| `seccomp_file` | `/runtime/seccomp.json` | 所匹配发行版的 seccomp 配置文件 | -| `nested_sandbox` | `true` | 启用 Docker 适配器的 init 进程和 proc-mask 配置 | - -[Docker 适配器](sandbox-provider.md#docker-adapter)负责容器隔离、卷布局和生命周期行为。 - ## 安装目录 {#installation-directory} 安装目录默认为 `~/.oac/core`(Windows 为 `$HOME/.oac/core`)。其中保存进程设置和原生管理命令;服务持久数据位于 [Compose 数据卷](#compose-installations)。 diff --git a/docs/zh/getting-started/install.md b/docs/zh/getting-started/install.md index 46a9f1f17..01eb5f039 100644 --- a/docs/zh/getting-started/install.md +++ b/docs/zh/getting-started/install.md @@ -1,7 +1,7 @@ --- title: "安装 Core 和 Web" source: docs/getting-started/install.md -source_hash: 6f5dbf881566fe1be1c4ae0ae998066f8b6df43b6d489eb88a328a6868ab7706 +source_hash: a0bb75ea6ce1671d2d4bfb2880e2b12ea8555cade40218f0cc2a4f43a829508f --- 本指南带运维人员从一台空主机到可用的安装:在 Docker 下运行的 Core、Web 控制台和 PostgreSQL,一个沙箱提供商,一个默认模型和第一个项目 API 密钥。Web 的 **概览** 通过 **新手引导** 清单跟踪同样的步骤。 @@ -78,7 +78,7 @@ curl -fsSL https://openagentcore.dev/install.sh | bash -s -- --public-url https: ## 添加沙箱容量 {#add-sandbox-capacity} -安装程序不选择沙箱提供商。请[选择沙箱提供商](./sandbox-backends.md):在自有 Linux 节点上运行 microsandbox,并使用[设置工作区存储](./workspace-storage.md)中的工作区存储,是推荐的自管部署;E2B 不需要节点。应用也可以在自己的机器上运行[自托管 Session](./self-hosted.md)。 +安装程序不选择沙箱提供商。请[选择沙箱提供商](./sandbox-backends.md):在自有 Linux 节点上运行 microsandbox,并使用[设置工作区存储](./workspace-storage.md)中的工作区存储,是自管部署;E2B 不需要节点。应用也可以在自己的机器上运行[自托管 Session](./self-hosted.md)。 ## 设置默认模型 {#set-a-default-model} diff --git a/docs/zh/getting-started/nodes.md b/docs/zh/getting-started/nodes.md index 785194012..2cb00fd09 100644 --- a/docs/zh/getting-started/nodes.md +++ b/docs/zh/getting-started/nodes.md @@ -1,10 +1,10 @@ --- title: "添加和管理节点" source: docs/getting-started/nodes.md -source_hash: 8524f052c003f02834e859bda9c1e1467d7e918f3f3e98b6be726317d078308a +source_hash: 43558fa9028c8fa4fff37eada64b9d895f52800c136d3e4f35f185d45e021050 --- -节点是一台 Linux 主机,为 Core 托管 Session 运行沙箱。本指南面向在沙箱提供商(参阅[选择沙箱提供商](./sandbox-backends.md))为 microsandbox 或 Docker 时添加、修改和移除节点的运维人员;E2B 不需要节点。 +节点是一台 Linux 主机,为 Core 托管 Session 运行沙箱。本指南面向在沙箱提供商(参阅[选择沙箱提供商](./sandbox-backends.md))为 microsandbox 时添加、修改和移除节点的运维人员;E2B 不需要节点。 Core 将新 Session 分配给有空余容量的节点;节点创建沙箱,沙箱回连 Core。应用为自己的 Session 连接的机器是[自托管执行器](./self-hosted.md),而不是节点。添加节点的方法是在 Web 生成命令并在主机上执行。[沙箱部署协议](../../../contracts/agents-api/zh/sandbox-deployment.md)定义沙箱设置及其变更规则,[节点协议](../../../contracts/agents-api/zh/node-generation-protocol.md)定义节点如何准备和保留 Runtime 代次。 @@ -23,9 +23,8 @@ Core 主机与其他主机一样加入:要在它上面运行沙箱,将它添 - Linux amd64 和 systemd;Python 3.9+、`curl`、`sha256sum` 和 `flock`;root 或 sudo。 - SELinux 不处于 enforcing 模式。安装程序不支持 enforcing SELinux 主机。 -- Docker:正在运行的 rootful Docker Engine,其 `/var/run/docker.sock` 套接字属于 `docker` 组,权限为 `0660`,并强制执行 CPU 和内存限制(cgroup v2)。 -- microsandbox:`/dev/kvm` 属于 `kvm` 组(硬件或嵌套虚拟化),并具有 microsandbox 链接的库(glibc)。 -- CPU 和内存至少足以运行一个所配置规格的沙箱;安装 Runtime 镜像期间约需 3 GB 空闲磁盘:下载的归档及其解包副本在导入完成前位于 `/var/lib/oac-node`,镜像本身在 Docker 存储或 microsandbox 存储中约占 1.5 GB。安装程序在下载前检查空间,并在导入后删除归档,因此节点最终只保留镜像和不足 0.2 GB 的程序。 +- `/dev/kvm` 属于 `kvm` 组(硬件或嵌套虚拟化),并具有 microsandbox 链接的库(glibc)。 +- CPU 和内存至少足以运行一个所配置规格的沙箱;安装 Runtime 镜像期间约需 3 GB 空闲磁盘:下载的归档及其解包副本在导入完成前位于 `/var/lib/oac-node`,镜像本身在 microsandbox 存储中约占 1.5 GB。安装程序在下载前检查空间,并在导入后删除归档,因此节点最终只保留镜像和不足 0.2 GB 的程序。 - 可通过公开 URL 访问控制台和 Core;沙箱也能访问 Core。 ## 添加节点 {#add-a-node} @@ -64,18 +63,16 @@ Core 主机与其他主机一样加入:要在它上面运行沙箱,将它添 | 项目 | 详情 | | --- | --- | | 服务用户 | 系统用户 `oac-node`,UID 为 65532(与 Core 服务相同),主目录 `/var/lib/oac-node`,无登录 shell。已有账号的 UID、主目录都相同且 shell 为 nologin 时会复用;其他名为 `oac-node` 的账号会被拒绝。如果其他账号占用 UID 65532,安装程序会停止且不做任何更改 | -| 用户组 | 拥有 `/var/run/docker.sock`(Docker)或 `/dev/kvm`(microsandbox)的组:仅 `docker` 或 `kvm` | +| 用户组 | 拥有 `/dev/kvm` 的组:仅 `kvm` | | 服务 | `/etc/systemd/system/oac-node-.service`,root 所属的系统单元,使用 `User=oac-node`,开机启用。Core 无法访问时每 5 秒重启;Core 不再接受节点后永久停止 | | 节点状态 | `/var/lib/oac-node/.oac/nodes//`:身份、配置和节点文件。microsandbox 在 `/var/lib/oac-node/.oac/m/` 保存镜像和沙箱 | | 记录 | `/etc/oac-node/`:安装程序创建或修改的内容,用于重新运行和卸载,以及服务的代理设置 | -| Docker | 一次性导入 Runtime 镜像,以及网络 `oac-node-` | +| Runtime 镜像 | 一次性导入 microsandbox 存储 | | microsandbox 网络策略 | 沙箱仅能访问 Core、主机上的 DNS 和公开地址:无入站连接、不可访问私有网络 | -root 只准备账号、组和服务单元;其他操作(包括 Docker 网络)都以 `oac-node` 在没有终端的独立会话中执行。安装程序不安装 Docker、KVM 或软件包,不启动 Docker,不修改设备权限、sudoers、防火墙或 SELinux 设置,也不修改其他账号。节点向外连接 Core;Core 不需要通过 SSH 或 Docker TCP 访问主机。 +root 只准备账号、组和服务单元;其他操作都以 `oac-node` 在没有终端的独立会话中执行。安装程序不安装 KVM 或软件包,不修改设备权限、sudoers、防火墙或 SELinux 设置,也不修改其他账号。节点向外连接 Core;Core 不需要通过 SSH 访问主机。 > [!CAUTION] -> Docker 模式等同于 root 权限。加入 `docker` 组后,`oac-node` 以及任何能控制节点的主体,都可以在主机上执行 root 操作,并且容器共享主机内核。只在沙箱专用主机上添加 Docker 节点。microsandbox 节点只需要 `kvm` 组,每个沙箱运行在独立 microVM 中。 - > [!WARNING] > microsandbox 沙箱无法访问私有地址;参见[沙箱无法访问私有端点](./troubleshooting.md#sandboxes-cannot-reach-a-private-endpoint)。 @@ -89,7 +86,7 @@ root 只准备账号、组和服务单元;其他操作(包括 Docker 网络 - 节点注册前失败的运行不会在 Core 留下任何记录。下一条命令即使在配置变更之后执行,也会替换上次尝试的配置和不再匹配的已下载文件,保留匹配的文件并继续。 - 慢速下载期间命令过期时,注册失败并显示 `The enrollment command expired or was already used`。下载文件保留:在 Web 生成新命令后执行,下载从中断处继续。 - 每个文件都会显示进度。失败或中断时保留已收到的部分;重新执行时,每个未完成的文件从中断处继续(`Resuming …`),复用已校验的完整文件,其余照常下载。服务器不支持续传时,该文件从头开始。每分钟下载不足 64 KiB 时会停止,请检查网络后重新执行命令。 -- Docker Runtime 镜像约 400 MB,microsandbox 文件约 500 MB。使用 Docker 且网络较慢时,可先加载镜像:将发行资产 `oac--linux-amd64-runtime.tar.gz` 复制到主机并运行 `sudo docker load -i`。安装程序发现精确匹配的镜像后跳过下载。 +- microsandbox 文件约 500 MB。 - 中断安装程序或关闭终端会停止程序;重新执行命令继续。 ## 日志 {#logs} @@ -119,9 +116,9 @@ microsandbox 的检查点执行兼容性包含主机内核和 CPU profile。因 1. 在 Web 打开 **节点**,在节点页面选择 **移除节点**,或在列表行选择 **移除**,再选择 **确认移除**。节点仍持有沙箱、快照或待清理资源时,Core 拒绝操作;等待完成或归档对应 Session。移除是永久的:主机只能作为新节点重新加入。移除所有节点仍保留沙箱配置。 2. Web 随后展示 **清理主机** 和卸载命令,在主机执行。**节点** 页的 **清理主机** 随时展示同一条命令:主机早先已移除、部署重置使其节点失效,或主机从未注册,都可使用。 -卸载先向节点注册时使用的 Core 地址确认节点是否已移除;Core 仍列出该节点时拒绝卸载。注册地址与当前公开 URL 不同时,对话框还展示 **旧 Core 地址已失效?**:该地址不再响应时,提供带 `--force` 的命令以跳过检查;先在 **节点** 页面移除节点。Core 已不存在时 Web 也不在了:在主机用原命令留下的安装程序执行 `sudo python3 ~/.oac/node-bootstrap/.pyz --uninstall --installation-id --force`。安装 ID 在单元名 `oac-node-.service` 和 `/etc/oac-node/.json` 中;Web 在 **设置** → **服务与部署** → **安装详情** 中也会展示。卸载停止并移除服务、节点状态、记录、代理设置和 Docker 网络。只有安装程序创建了 `oac-node` 且不再有节点时,才删除该账号;复用的账号仅移除安装程序添加的组。输出会列出保留的内容:复用的账号及其主目录 `/var/lib/oac-node`,以及 `~/.oac/node-bootstrap` 中下载的安装程序,可用 `rm -f ~/.oac/node-bootstrap/*.pyz` 删除。 +卸载先向节点注册时使用的 Core 地址确认节点是否已移除;Core 仍列出该节点时拒绝卸载。注册地址与当前公开 URL 不同时,对话框还展示 **旧 Core 地址已失效?**:该地址不再响应时,提供带 `--force` 的命令以跳过检查;先在 **节点** 页面移除节点。Core 已不存在时 Web 也不在了:在主机用原命令留下的安装程序执行 `sudo python3 ~/.oac/node-bootstrap/.pyz --uninstall --installation-id --force`。安装 ID 在单元名 `oac-node-.service` 和 `/etc/oac-node/.json` 中;Web 在 **设置** → **服务与部署** → **安装详情** 中也会展示。卸载停止并移除服务、节点状态、记录、和代理设置。只有安装程序创建了 `oac-node` 且不再有节点时,才删除该账号;复用的账号仅移除安装程序添加的组。输出会列出保留的内容:复用的账号及其主目录 `/var/lib/oac-node`,以及 `~/.oac/node-bootstrap` 中下载的安装程序,可用 `rm -f ~/.oac/node-bootstrap/*.pyz` 删除。 -程序不删除 Docker 镜像或卷。保留 Runtime 镜像并输出 `sudo docker image rm` 命令。microsandbox 在 Core 确认节点已移除后,还会删除节点位于 `/var/lib/oac-node/.oac/m/` 的存储(约 1.5 GB),随后删除所创建的账号。使用 `--force` 时不询问 Core,因此保留存储,并输出删除命令(`sudo -u oac-node rm -rf `);microVM 可能仍使用存储,请先检查 `pgrep -u oac-node`,再重新卸载以完成账号移除。输出会列出已删除和已保留的内容。可以重复卸载直到完成。 +Core 确认节点已移除后,卸载程序还会删除节点位于 `/var/lib/oac-node/.oac/m/` 的存储(约 1.5 GB),随后删除所创建的账号。使用 `--force` 时不询问 Core,因此保留存储,并输出删除命令(`sudo -u oac-node rm -rf `);microVM 可能仍使用存储,请先检查 `pgrep -u oac-node`,再重新卸载以完成账号移除。输出会列出已删除和已保留的内容。可以重复卸载直到完成。 ## 节点主机故障时 {#when-a-node-host-fails} @@ -137,12 +134,12 @@ microsandbox 的检查点执行兼容性包含主机内核和 CPU profile。因 每个状态码都是与提供商无关的类别;其背后的本地错误留在节点上。请在主机上重新运行该命令:安装程序会先检查主机要求,并指出需要修复的问题([安装程序消息](./troubleshooting.md#installer-messages))。 -节点按如下顺序仅报告首个失败检查:Docker 守护进程或 KVM、Docker 限制支持、主机容量、已安装的 Runtime 文件。因此无法访问 Docker 守护进程时,会隐藏镜像缺失问题。修复后的下一次准备尝试会清除或替换状态码。 +节点按如下顺序仅报告首个失败检查:KVM、主机容量、已安装的 Runtime 文件。因此缺少 KVM 访问权限时,会隐藏镜像缺失问题。修复后的下一次准备尝试会清除或替换状态码。 | 状态码 | 帮助提示 | 原因 | 解决方法 | | --- | --- | --- | --- | -| `provider_unavailable` | 沙箱提供商不可用 | 提供商的服务不可达或请求失败(例如 Docker 守护进程已停止),或失败没有类别 | 重新运行节点的命令 | -| `host_unsupported` | 主机不满足要求 | 主机缺少提供商所需的能力,例如 Docker 的 CPU 与内存限制(cgroup v2)或对 `/dev/kvm` 的读写权限 | 重新运行节点的命令 | +| `provider_unavailable` | 沙箱提供商不可用 | 提供商的服务不可达或请求失败,或失败没有类别 | 重新运行节点的命令 | +| `host_unsupported` | 主机不满足要求 | 主机缺少提供商所需的能力,例如对 `/dev/kvm` 的读写权限 | 重新运行节点的命令 | | `capacity_insufficient` | 主机资源不足 | 主机 CPU 或内存不足以运行一个沙箱 | 使用更大主机或修改沙箱规格 | | `runtime_image_unavailable` | 缺少 Runtime 镜像 | 提供商中没有本安装的 Runtime 镜像 | 重新运行节点的命令以再次下载;否则加载匹配发行版镜像 | | `artifacts_unavailable` | 沙箱提供商文件缺失 | 固定版本的提供商文件(例如 microsandbox 的 Runtime、固件或辅助程序)缺失或 SHA-256 检查失败 | 重新运行节点的命令以下载缺失文件;否则从匹配发行版恢复 | diff --git a/docs/zh/getting-started/operations.md b/docs/zh/getting-started/operations.md index 4e446de16..7acc55a3d 100644 --- a/docs/zh/getting-started/operations.md +++ b/docs/zh/getting-started/operations.md @@ -1,7 +1,7 @@ --- title: "运维" source: docs/getting-started/operations.md -source_hash: e3e28c2904453b38465e84d955e45ffae533b202f1ce0b88bc18c4c3efe6a773 +source_hash: 7ccb5fea5da457afd341300bcd026dc20683625d73b57a494ab50b56c67b33e5 --- Core 安装运维人员的日常任务:`oac` 命令、服务健康状态、Core 密钥、备份和卸载。节点有[专门的指南](./nodes.md),[故障排查](./troubleshooting.md)列出了安装程序和访问方面的提示信息。设置见[配置参考](../configuration.md)。 @@ -63,7 +63,7 @@ docker compose -f ~/.oac/core/compose.yaml stop docker compose -f ~/.oac/core/compose.yaml start ``` -停止 Core 不会停止节点或沙箱。节点服务、microVM 和 Docker 容器继续运行;停止不是回收计算资源的方法。Core 重启不会透明地继续被中断的原生工具调用。重连后查询同一 Session;不要创建新 Session 来重放结果不确定的工作。Session 事件流仅提供实时事件;通过读取 Session、Turn 和 Items 恢复。 +停止 Core 不会停止节点或沙箱。节点服务和 microVM 继续运行;停止不是回收计算资源的方法。Core 重启不会透明地继续被中断的原生工具调用。重连后查询同一 Session;不要创建新 Session 来重放结果不确定的工作。Session 事件流仅提供实时事件;通过读取 Session、Turn 和 Items 恢复。 Web 重启(包括 `oac apply` 引起的重启)会让所有控制台用户退出登录;参阅[登录](../web/console-server.md#sign-in)。 @@ -165,7 +165,7 @@ docker run --rm -v "${project}_data:/data" -v ~/oac-backup:/backup:ro --entrypoi - 安装目录,包括 `.env`、`compose.yaml`、Compose 覆盖文件和管理命令。 - 当前或保留对象所使用的每个独立工作区命名空间,即使其位于安装卷之外。备份整个根目录,包括 `.oac-storage-root`、对象标识、staging、live 数据、trash 和删除标记。保留数值所有者、权限、链接及扩展属性,包括全部 `user.*` 属性。仅复制 `live/data` 会丢失生命周期证据,并可能使已删除标识复活。[工作区存储](../workspace-provider.md#kernel-nfs-adapter)负责定义布局与所有权要求。 - 对于[配额模式](../workspace-provider.md#quota-mode)命名空间,将承载导出的整个 XFS 文件系统作为一份块级副本备份:存储或 LVM 快照,或已冻结或已卸载设备的镜像。只有这样的副本能保持 project ID、配额限制、inode 标识、文件系统 UUID 和 `oac-workspace-quota` 日志库的一致。文件级复制、`tar`、`rsync` 或 `xfsdump` 会丢失这些信息;此时该命令会拒绝恢复后的对象,而不是在无上限状态下提供它们。 -- 各节点状态目录 `/var/lib/oac-node/.oac/nodes//` 及其提供商存储:Docker 卷或 microsandbox 存储。包含数据库仍然引用的全部计算资源和快照。参见[节点主机故障时](./nodes.md#when-a-node-host-fails)。 +- 各节点状态目录 `/var/lib/oac-node/.oac/nodes//` 及其提供商存储,即 microsandbox 存储。包含数据库仍然引用的全部计算资源和快照。参见[节点主机故障时](./nodes.md#when-a-node-host-fails)。 - 每个私有 `checkpoint_root` 的全部内容,包括 `.oac-checkpoint-store`、检查点、operation journal、lock 文件与 tombstone,并与匹配的 Core 数据库一起保留。[检查点转移契约](../sandbox-provider.md#checkpoint-transfer)定义其身份、所有权和生命周期。 ### 建立停止写入窗口 {#establish-a-stopped-write-window} diff --git a/docs/zh/getting-started/sandbox-backends.md b/docs/zh/getting-started/sandbox-backends.md index f4a5f43ff..37284c487 100644 --- a/docs/zh/getting-started/sandbox-backends.md +++ b/docs/zh/getting-started/sandbox-backends.md @@ -1,35 +1,33 @@ --- title: "选择沙箱提供商" source: docs/getting-started/sandbox-backends.md -source_hash: 8727d59c6aab61d84991aff807ab1a4a34c01ee82f7066b6ad40ea153248c344 +source_hash: ff785ba90caac12dda09f1c7560152bb9dbbf3cd4ff7427a83efc87680677403 --- -比较 microsandbox、Docker 和 E2B,然后在 Web 中选择一个。本指南面向已完成[安装 Core 和 Web](./install.md)、需要为 Core 托管的 Session 提供计算资源的运维人员。 +比较 microsandbox 和 E2B,然后在 Web 中选择一个。本指南面向已完成[安装 Core 和 Web](./install.md)、需要为 Core 托管的 Session 提供计算资源的运维人员。 安装程序不选择提供商。一个安装只使用一个提供商。以后要更改,请先重置部署;参阅[更改沙箱配置](./nodes.md#change-the-sandbox-configuration)。 ## 比较各提供商 {#compare-the-backends} -| | microsandbox(推荐) | Docker(兼容) | E2B | -| --- | --- | --- | --- | -| 沙箱运行位置 | Linux 节点上的 microVM | Linux 节点上的容器 | E2B 云端 | -| 需要添加节点 | 是 | 是 | 否 | -| 你需要提供 | 带 systemd 和 `/dev/kvm` 的 Linux amd64 主机 | 带 systemd 和 rootful Docker Engine 的 Linux amd64 主机 | E2B API 密钥和模板;Core 的公共 HTTPS URL | -| 隔离 | 每个沙箱有独立内核 | 容器共享节点内核 | 由 E2B 提供 | -| 空闲沙箱可挂起和恢复 | 是,通过检查点 | 否 | 是,由 E2B 原地暂停 | -| 工作区存储 | 必需,使用 NFS | 自有磁盘 | 自有磁盘 | -| 磁盘限制 | 根磁盘和工作区配额 | 无 | 由模板设置 | +| | microsandbox | E2B | +| --- | --- | --- | +| 沙箱运行位置 | Linux 节点上的 microVM | E2B 云端 | +| 需要添加节点 | 是 | 否 | +| 你需要提供 | 带 systemd 和 `/dev/kvm` 的 Linux amd64 主机 | E2B API 密钥和模板;Core 的公共 HTTPS URL | +| 隔离 | 每个沙箱有独立内核 | 由 E2B 提供 | +| 空闲沙箱可挂起和恢复 | 是,通过检查点 | 是,由 E2B 原地暂停 | +| 工作区存储 | 必需,使用 NFS | 自有磁盘 | +| 磁盘限制 | 根磁盘和工作区配额 | 由模板设置 | 字段级差异见[部署契约](../../../contracts/agents-api/zh/sandbox-deployment.md#what-each-field-means-per-sandbox-provider)。节点的主机要求见[添加和管理节点](./nodes.md#host-requirements)。 -Docker 是兼容选项,用于可信负载和没有 `/dev/kvm` 的主机。Docker 节点在其主机上[等同于 root](./nodes.md#what-the-installer-sets-up):仅在专用于沙箱的主机上添加。 - 沙箱节点运行在 Linux amd64 上。Core 运行在 macOS 或 Windows 上时,请连接 Linux 节点或使用 E2B。应用也可以在自己的机器上运行[自托管 Session](./self-hosted.md),无需提供商。 ## 在 Web 中选择提供商 {#select-the-backend-in-web} -1. 打开 **设置 → 沙箱与调度**。设置向导会询问 **沙箱在哪里运行?**:**自有机器**(microsandbox 或 Docker)或 **E2B**。 -2. 选择 **自有机器** 时,选择提供商和沙箱规格。Web 会把[提供商声明的默认大小](../sandbox-provider.md#register-the-provider-kind)推荐为 **标准**,审阅页的 **高级设置** 可填写确切的 CPU、内存和磁盘值。E2B 不声明默认大小,所以它的审阅页没有 **高级设置**。 +1. 打开 **设置 → 沙箱与调度**。设置向导会询问 **沙箱在哪里运行?**:**自有机器**(microsandbox)或 **E2B**。 +2. 选择 **自有机器** 时,选择沙箱规格。Web 会把[提供商声明的默认大小](../sandbox-provider.md#register-the-provider-kind)推荐为 **标准**,审阅页的 **高级设置** 可填写确切的 CPU、内存和磁盘值。E2B 不声明默认大小,所以它的审阅页没有 **高级设置**。 3. 选择 **保存配置**。该选择保存在 Core 的数据库中。 选择 **自有机器** 后,继续阅读[添加和管理节点](./nodes.md)。使用 microsandbox 时,请先[设置工作区存储](./workspace-storage.md)。microsandbox 节点间的检查点恢复,参阅[跨节点恢复检查点](./nodes.md#checkpoint-recovery-across-nodes)。 @@ -48,6 +46,6 @@ E2B 不需要节点,但其沙箱会从 E2B 云端调用 Core,因此 Core 需 ## 后续步骤 {#next-steps} -- microsandbox 或 Docker:[添加和管理节点](./nodes.md)。 +- microsandbox:[添加和管理节点](./nodes.md)。 - 使用 NFS 的工作区:[设置工作区存储](./workspace-storage.md)。 - 模型和密钥:继续阅读[安装 Core 和 Web](./install.md#set-a-default-model)。 diff --git a/docs/zh/getting-started/troubleshooting.md b/docs/zh/getting-started/troubleshooting.md index 094fc2d47..51e71d346 100644 --- a/docs/zh/getting-started/troubleshooting.md +++ b/docs/zh/getting-started/troubleshooting.md @@ -1,7 +1,7 @@ --- title: "故障排查" source: docs/getting-started/troubleshooting.md -source_hash: b57b7719fd48f61d1a10077eaca44efc89a58b7d2d68d4a5ce96bf537ba6680e +source_hash: c2d729e1f6d7a0fab71db8a8183ce84f336fa8386f02b521327afaddebfe2f5e --- 按症状查找安装、访问、节点和 Session 问题的原因与解决方法。本页面向 Core 安装的运维人员。 @@ -44,7 +44,7 @@ source_hash: b57b7719fd48f61d1a10077eaca44efc89a58b7d2d68d4a5ce96bf537ba6680e microsandbox 沙箱无法访问私有地址上的模型提供商或 MCP 服务器,Harness 停止重试后 Turn 失败。沙箱的 DNS 查询发往主机 `/etc/resolv.conf` 中列出的域名服务器,而 DNS rebind 保护会丢弃任何包含私有地址的应答。因此,这些域名服务器解析为私有地址的名称在沙箱内根本无法解析,即使是云 VPC 解析器之后的公共模型 API 这类公共服务也是如此。 -节点没有更改此行为的设置。请让这些域名服务器返回该服务的公共地址。microsandbox 不读取主机的 `/etc/hosts`,因此只有当 `/etc/resolv.conf` 指向提供该条目的本地存根解析器(例如 `127.0.0.53` 上的 systemd-resolved)时,那里的条目才有用。否则,请使用不过滤沙箱流量的 Docker 节点。 +节点没有更改此行为的设置。请让这些域名服务器返回该服务的公共地址。microsandbox 不读取主机的 `/etc/hosts`,因此只有当 `/etc/resolv.conf` 指向提供该条目的本地存根解析器(例如 `127.0.0.53` 上的 systemd-resolved)时,那里的条目才有用。 ### 连接失败 {#connection-failures} @@ -70,8 +70,6 @@ microsandbox 沙箱无法访问私有地址上的模型提供商或 MCP 服务 | 注册前的沙箱提供商检查失败,例如 `This host lacks a capability …` | 修复提示的问题(参阅[就绪状态码](./nodes.md#readiness-codes))后重试;令牌未被使用 | | Core's public URL changed after this command was generated | 在 Web 生成并执行新命令 | | This host's node uses `
`, but this command uses `
` | 节点在旧公开 URL 下添加。在 Web 移除、卸载,然后重新添加 | -| Docker Engine is not installed, or Docker is not running | 安装 Docker Engine,或运行 `sudo systemctl enable --now docker` 后重试 | -| Docker on this host does not enforce CPU and memory limits | 使用 cgroup v2 后重试 | | KVM is unavailable, or `/dev/kvm` must be group-accessible | 启用虚拟化;发行版的 KVM 包会设置 `root:kvm 0660` | | This host has N CPUs and M MiB of memory; each sandbox needs … | 使用更大主机或修改沙箱规格 | | SELinux is enforcing on this host | 使用安装程序支持的主机;程序不修改 SELinux 设置 | diff --git a/docs/zh/getting-started/workspace-storage.md b/docs/zh/getting-started/workspace-storage.md index 54dc24e86..61651171c 100644 --- a/docs/zh/getting-started/workspace-storage.md +++ b/docs/zh/getting-started/workspace-storage.md @@ -1,12 +1,12 @@ --- title: "设置工作区存储" source: docs/getting-started/workspace-storage.md -source_hash: 153136a405856b60d0bcf749521b33e54fc3f3d4f6bcdaabcfd901fb041e934f +source_hash: 4a9e08947b7e359b1bb725217ee097c3df3283054be986219be421911ee930a5 --- 将 Session 工作区保存在沙箱节点之外的 NFS 文件系统上,可选择是否设置每个工作区的配额。microsandbox 的每个 Environment 都运行在此存储上,因此请在配置 microsandbox 后端之前先选择它。 -支持的组合是 microsandbox 与[内核 NFS 适配器](../workspace-provider.md#kernel-nfs-adapter)。Docker 和 E2B 使用自有磁盘,不支持独立工作区存储;参阅[选择沙箱提供商](./sandbox-backends.md)。 +支持的组合是 microsandbox 与[内核 NFS 适配器](../workspace-provider.md#kernel-nfs-adapter)。E2B 使用自有磁盘,不支持独立工作区存储;参阅[选择沙箱提供商](./sandbox-backends.md)。 ```mermaid flowchart LR diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index 8593da678..ece31ac1a 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: 0bc83086935af877837d1d791b3ad8eafa486fcc9adbf00b119d4546dc66658c +source_hash: d222cc5fe4c258dcc4e8956c08757276dc6340511c5eaf2e0fba2698e6718761 --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请按 [安装 Core 和 Web](getting-started/install.md) 操作。安装程序代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装程序](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -40,7 +40,7 @@ make build-core-distribution 构建过程会复用 Core、Web、Runtime、SDK 和辅助程序构建器。清单会记录提交及其源代码树、镜像配置及 OCI 清单摘要、Runtime OCI 清单摘要、microsandbox 运行时和固件哈希,以及每个 Runtime 和节点构件的大小与 SHA-256;原生安装程序在[目录](#native-installers)中仅记录其 SHA-256。输出包括控制归档及其 `.sha256`、可选的离线归档,以及带版本号的 Runtime、节点和原生安装程序资源。此过程不会发布任何内容。如果目标目录中已包含此提交的分发包,重建会拒绝执行。 -ingress 镜像仅包含 `oac` 和四个节点元数据文件:`manifest.json`、`SHA256SUMS`、`node-install.pyz` 与 `runtime/seccomp.json`。构建流程根据与分发包相同的 Runtime 和构件身份生成其清单,再禁用网络验证初始化。节点清单不包含控制平面镜像身份,因此可在 ingress 镜像生成前打包。 +ingress 镜像仅包含 `oac` 和三个节点元数据文件:`manifest.json`、`SHA256SUMS` 与 `node-install.pyz`。构建流程根据与分发包相同的 Runtime 和构件身份生成其清单,再禁用网络验证初始化。节点清单不包含控制平面镜像身份,因此可在 ingress 镜像生成前打包。 控制归档不包含 Runtime 镜像或节点执行构件;离线归档包含这些内容。[下载契约](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md#download-contract)说明了节点如何获取这些内容。 diff --git a/docs/zh/runtime-bootstrap.md b/docs/zh/runtime-bootstrap.md index a5cc5f88e..95f02c013 100644 --- a/docs/zh/runtime-bootstrap.md +++ b/docs/zh/runtime-bootstrap.md @@ -1,7 +1,7 @@ --- title: "Runtime 引导" source: docs/runtime-bootstrap.md -source_hash: 68bb957984c615d36b941efae570b366b6245f08fe4c76561f8aaa92966e1a4b +source_hash: 991f76a7edfe957cb0558a490a2ec4f094148bba85980aea386b85c802db9e08 --- Runtime 引导协议连接沙箱提供商与其启动的托管 Runtime。提供商向 Runtime 交付一个引导文件,该文件是此次启动中 Runtime 唯一的认证输入,也是其 Environment 绑定的唯一来源。 @@ -75,7 +75,6 @@ oac-daemon connect --bootstrap-file /home/runtime/runtime-bootstrap.json | 提供商 | 交付方式 | | --- | --- | -| Docker | 将文件写入 Runtime 拥有的 home volume。 | | microsandbox、E2B | 在启动同一命令之前交付文件。 | ## 验证 {#verification} diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index 45dc17c42..976903dad 100644 --- a/docs/zh/sandbox-provider.md +++ b/docs/zh/sandbox-provider.md @@ -1,15 +1,15 @@ --- title: "沙箱提供商协议" source: docs/sandbox-provider.md -source_hash: 9aac833e15cf78b06e4378b7526074bdb4cbac82aef4528e17da382053bc638d +source_hash: e2577eda24eff2656820be8e5aa791b504bddc70399c4329bdd9bb6ad749b858 --- -沙箱提供商协议连接 Core 与为 Core 管理的(`openai_hosted`)Environment 提供计算资源的 adapter,并由该 adapter 运行启动 Runtime daemon 的有界引导流程。本页面向 adapter 作者:说明提供商必须实现什么,以及可以依赖 Core 做什么。Docker、microsandbox 和 E2B 实现了该协议。 +沙箱提供商协议连接 Core 与为 Core 管理的(`openai_hosted`)Environment 提供计算资源的 adapter,并由该 adapter 运行启动 Runtime daemon 的有界引导流程。本页面向 adapter 作者:说明提供商必须实现什么,以及可以依赖 Core 做什么。microsandbox 和 E2B 实现了该协议。 | | | | --- | --- | | 调用方 | Core 的 runtime manager 与 execution Worker | -| 实现 | Docker 和 microsandbox(node adapter),以及 E2B(direct adapter);见[参考 adapter](#reference-adapters) | +| 实现 | microsandbox(node adapter)和 E2B(direct adapter);见[参考 adapter](#reference-adapters) | | 代码 | `sandbox_provider.go` 中的 [`SandboxProvider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/sandbox_provider.go),以及 setup 阶段的 `ConfigurationAdapter` | | 传输 | direct adapter 在进程内被调用。node adapter 运行在沙箱节点上,通过[沙箱节点协议](../../contracts/agents-api/zh/node-generation-protocol.md#provider-requests)接收同样的调用 | | 版本规则 | 进程内接口没有 wire 版本。沙箱节点协议和各 helper 协议要求精确版本匹配 | @@ -52,7 +52,7 @@ source_hash: 9aac833e15cf78b06e4378b7526074bdb4cbac82aef4528e17da382053bc638d | `Kill` | 回收 allocation 的计算资源与保留资源 | | `RunCommand` | 在 allocation 的计算资源中运行有界命令 | -Docker 等没有原生可续期租约的 backend 仍遵守 Core 的 hosted expiry 和 cleanup 要求。每个 adapter 都实现 `RunCommand`,测试也执行它,但 Core 编排不调用它;只有 node transport 转发。机密命令输入通过 `Command.Stdin` 传输,不放在参数或日志中;结果保留字节顺序、有界输出和实际退出状态。 +没有原生可续期租约的 backend 仍遵守 Core 的 hosted expiry 和 cleanup 要求。每个 adapter 都实现 `RunCommand`,测试也执行它,但 Core 编排不调用它;只有 node transport 转发。机密命令输入通过 `Command.Stdin` 传输,不放在参数或日志中;结果保留字节顺序、有界输出和实际退出状态。 `Bootstrap.Workspace` 和 `ResumeRequest.Workspace` 仅当提供商的 `DeploymentPolicy.Workspace` 已声明时才携带外部工作区绑定;提供商对此的义务见[挂载](workspace-provider.md#attachment)。Core 仅在提供商声明支持 checkpoint 时设置 `Bootstrap.SuspendControl`,提供商将其原样放入 [Runtime bootstrap](runtime-bootstrap.md)。 @@ -156,7 +156,7 @@ configuration adapter 必须非 nil,包括其具体值。每个 `Configuration 具有凭据的 direct adapter 在替换 key 前验证全部保留 generation 与 allocation reference。公共 `sandbox.CallFence` 排除原生调用并等待 helper 完成,包括调用方已超时的调用;execution 调用已准备的 verification 和 fencing callback,不按厂商分支。 -厂商部署验证和 SDK setup 留在构造边界,构造不创建 Environment。node-local adapter 的 `sandbox.Built` 返回提供商、probe、installation identity、backend fingerprint 和 specification digest;helper 可能比调用方存活更久时,还返回 `Quiescent` 检查,generation 回收会等待它。factory 还返回 close 函数。`execution.RuntimeProvider` 将 adapter 绑定到 kind、installation ID、backend fingerprint、generation、mode 和 node ownership;选择由数据库负责,内存副本不构成另一权限来源。Docker 与 microsandbox 在 node 上运行,E2B 直接构造。node proxy 仅对注册声明支持 checkpoint 的 backend 暴露 checkpoint 操作,公共 lifecycle 通过 checkpoint 声明准入 suspension,不通过提供商名称。 +厂商部署验证和 SDK setup 留在构造边界,构造不创建 Environment。node-local adapter 的 `sandbox.Built` 返回提供商、probe、installation identity、backend fingerprint 和 specification digest;helper 可能比调用方存活更久时,还返回 `Quiescent` 检查,generation 回收会等待它。factory 还返回 close 函数。`execution.RuntimeProvider` 将 adapter 绑定到 kind、installation ID、backend fingerprint、generation、mode 和 node ownership;选择由数据库负责,内存副本不构成另一权限来源。microsandbox 在 node 上运行,E2B 直接构造。node proxy 仅对注册声明支持 checkpoint 的 backend 暴露 checkpoint 操作,公共 lifecycle 通过 checkpoint 声明准入 suspension,不通过提供商名称。 backend fingerprint 标识原生资源命名空间,不表示容量。Core 保留部署 generation,使所属 allocation 继续解析到原 backend;不要将保留 allocation 重新指向替代 backend。 @@ -304,30 +304,11 @@ Go adapter 的 E2B 模板和 endpoint validator 消费共享 [selector fixture]( node 测试单独覆盖 disconnect、reconnect fencing,以及 Create response 丢失后的 cleanup。helper protocol 和 [sandbox node 协议](../../contracts/agents-api/zh/node-generation-protocol.md)要求精确版本匹配;直接进程内接口没有独立 wire version。 -原生验收证明 fixture 无法证明的事实:creation、lease 行为、所属 partial cleanup、声明的 isolation 与 limit,以及支持时的 snapshot。显式启用的 Docker lifecycle 和 recovery 测试使用 `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`;SDK helper 使用 `make check-e2b-provider` 和 `make check-microsandbox-provider`。mock compute 不能证明 reclamation 或 isolation。 +原生验收证明 fixture 无法证明的事实:creation、lease 行为、所属 partial cleanup、声明的 isolation 与 limit,以及支持时的 snapshot。SDK helper 使用 `make check-e2b-provider` 和 `make check-microsandbox-provider`。mock compute 不能证明 reclamation 或 isolation。 ## 参考 adapter {#reference-adapters} | Kind | Adapter | Helper 与 adapter 规则 | 运维指南 | | --- | --- | --- | --- | -| Docker (node) | [`sandbox/docker`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/docker) | [`sandbox/node`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/node) 中的 node proxy | [Docker adapter](#docker-adapter) | | microsandbox (node) | [`sandbox/microsandbox`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/microsandbox) | [`tools/microsandbox-provider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/microsandbox-provider/README.md) | [Node](getting-started/nodes.md) | | E2B (direct) | [`sandbox/e2b`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/e2b) | [`tools/e2b-provider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/tools/e2b-provider/README.md) | [沙箱部署](../../contracts/agents-api/zh/sandbox-deployment.md#e2b-configuration);应用管理的模板见 [`deploy/e2b`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md) | - -## Docker adapter {#docker-adapter} - -Docker 沙箱提供商([`sandbox/docker`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/docker))对所有 Runtime image 使用相同 container setting,无论服务哪个 Harness([`container_options.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/container_options.go)): - -- user 1000:1000、只读 root filesystem、移除全部 capability、`no-new-privileges`、[seccomp profile](#seccomp-profile) 和 AppArmor `unconfined`; -- node 配置的 network 和 extra host([node 配置](configuration.md#docker-node-configuration)); -- deployment specification 中的 CPU 和 memory,128-process limit 和 128 MiB `/tmp` tmpfs; -- 两个 named volume,label 包含 installation、tenant、Environment 和 allocation:`-home` 挂载到 `/home`,`-environment` 挂载到 `/environment`,后者的 `workspace` 子目录也挂载到 `/workspace`。Docker Engine 必须支持 volume subpath mount; -- 配置 `nested_sandbox` option 时,解除 Docker `/proc` mask(`/sys/firmware` 和 `/sys/devices/virtual/powercap` 保持 mask),container 运行 init process。 - -Create 拒绝复用没有 container 的保留 volume。它将 [Runtime 引导](runtime-bootstrap.md)文件复制到 `/home/runtime/runtime-bootstrap.json`(mode 0600、UID 1000),并将 `/environment` workspace、staging、initialization 和 package directory 放入 container,然后启动 `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json`。创建的 container 不具备配置的 CPU、memory 和精确 image 时,Create 返回 error 和 `CreateSettled`。Docker 没有 lease,因此 Renew 仅读取 container state。Kill 在删除前检查 container 和两个 volume 的 ownership label,再确认三者都已不存在。 - -node 使用 [提供商配置](configuration.md#docker-node-configuration)中的明确 Unix socket,忽略 `DOCKER_HOST`。不将 Docker socket、host home 或 Core credential 挂载进 Runtime。 - -### Seccomp profile {#seccomp-profile} - -[`seccomp.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.json) 是 [revision 65adc7e](https://github.com/moby/profiles/blob/65adc7e022c97f55e45c054ff012988027733b87/seccomp/default.json) 的 Moby default profile(Apache-2.0,参见 [seccomp.LICENSE](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/docker/seccomp.LICENSE);上游文件 SHA-256 为 `785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334`),追加一条允许 `clone`、`unshare`、`setns`、`mount`、`umount2` 和 `pivot_root` 的规则。发行包将此文件作为 `runtime/seccomp.json` 交付每个 Docker node。 diff --git a/docs/zh/web/console-api-usage.md b/docs/zh/web/console-api-usage.md index 70c296880..96675e0a7 100644 --- a/docs/zh/web/console-api-usage.md +++ b/docs/zh/web/console-api-usage.md @@ -1,7 +1,7 @@ --- title: "控制台 API 使用" source: docs/web/console-api-usage.md -source_hash: 4e5430db89b1d39fa0cd3aae2599936f748c6c0059443f7678adce8910b9aa44 +source_hash: 1e058d564c992bbf7d3e924025b5f9e980d725bcc426d105b3f671b2239c30e2 --- 本页列出各控制台页面调用的 Core 路由,以及控制台如何限定读取范围。本页面向控制台开发者;运维人员请阅读 [Web 控制台](index.md)。[Core 管理 API](../../../contracts/agents-api/zh/admin-api.md) 定义了路由、响应结构、分页和审计记录;[API 命名空间和凭据](../api/index.md) 定义了本文使用的术语。 @@ -98,14 +98,14 @@ source_hash: 4e5430db89b1d39fa0cd3aae2599936f748c6c0059443f7678adce8910b9aa44 | 操作 | 路由 | 控制台用途 | | --- | --- | --- | -| 部署 | `GET`、`POST`、`PUT /core/v1/sandbox/deployment` | 读取提供商、只读 `core_url`(即 `OAC_PUBLIC_URL`,会显示在设置审核中且绝不发送)、重置状态、安装 ID 和规范;409 `sandbox_configuration_error`(E2B 搭配回环地址形式的 `public_url`)会在设置向导中显示共享客户端固定的安全地址配置消息,并通过 **在设置中管理** 前往 **设置 → 服务与部署**,且无需确认;使用 Docker 或 microsandbox 的 `resources` 初始化部署,或者使用 E2B 账户且不提供 `resources`(Core 采用模板构建的 CPU 和内存);使用预期的 generation 更改设置。**沙箱监控** 用 `specification.resources` 显示每个 E2B Sandbox 的大小;microsandbox 的 `suspension`(空闲和保留秒数)会显示在 **设置 → 沙箱与调度** 中 | +| 部署 | `GET`、`POST`、`PUT /core/v1/sandbox/deployment` | 读取提供商、只读 `core_url`(即 `OAC_PUBLIC_URL`,会显示在设置审核中且绝不发送)、重置状态、安装 ID 和规范;409 `sandbox_configuration_error`(E2B 搭配回环地址形式的 `public_url`)会在设置向导中显示共享客户端固定的安全地址配置消息,并通过 **在设置中管理** 前往 **设置 → 服务与部署**,且无需确认;使用 microsandbox 的 `resources` 初始化部署,或者使用 E2B 账户且不提供 `resources`(Core 采用模板构建的 CPU 和内存);使用预期的 generation 更改设置。**沙箱监控** 用 `specification.resources` 显示每个 E2B Sandbox 的大小;microsandbox 的 `suspension`(空闲和保留秒数)会显示在 **设置 → 沙箱与调度** 中 | | E2B 发现 | `POST /core/v1/sandbox/providers/e2b/discovery` | 设置向导用输入的凭据逐级列出 discovered 字段的选项;E2B 先列出该密钥可见的模板,再列出所选模板的可用构建。该密钥只会通过这些请求体和部署写入请求传输 | | 重置 | `POST`、`DELETE /core/v1/sandbox/deployment/reset` | 显式清除托管资源,或在观测到的 generation 处取消剩余清除;显示 Core 的剩余资源和离线预测 | | 节点 | `GET /core/v1/sandbox/nodes` | **节点** 页面;**概览** 上的机群;**沙箱监控** 中每个托管 Runtime 所在的节点。`provider_ready` 为 false 的在线节点为降级;**节点** 列表及节点页面中紧邻其状态的帮助提示显示 *沙箱提供商不可用*,目标准备原因(`rollout.diagnostic`,一个[就绪状态码](../getting-started/nodes.md#readiness-codes))显示在其目标状态中。如果节点的 `core_url`(其注册时使用的地址)与部署的 `core_url` 不同,**节点** 页面会将其标记为绑定到旧地址,需要移除后重新添加;此时它在该页面和节点页面中的状态会显示 **地址已过期**,而不是健康状态。**添加节点** 仅跟踪 `enrollment_id` 与其命令所含 `enrollment_id` 相等的节点 | | 节点详情 | `GET /core/v1/sandbox/nodes/{node_id}?range=1h\|6h\|24h` | **节点** 中的节点页面:主机最近一次心跳报告的 CPU 忙碌占比、内存使用量和可用磁盘,以及 1 小时、6 小时或 24 小时内 CPU 与内存的历史记录;页面可见时每 30 秒刷新一次。**编辑节点** 读取节点的 `recommended_capacity` 作为推荐值 | | 分配 | `GET /core/v1/sandbox/nodes/{node_id}/allocations` | **节点** 页面;**沙箱监控**。节点页面根据 `compute_phase_changed_at` 显示每个分配处于计算阶段的时间;时间为 null 时显示短横线。它不会根据阶段时间或部署的 `suspension.retention_seconds` 推算快照到期时间 | -| 注册 | `POST /core/v1/sandbox/enrollment-tokens` | **添加节点**:管理员保留推荐上限(请求体不含上限,由 Core 在节点注册时推导)或同时设置两个上限(`max_active`;`max_retained` 仅适用于 microsandbox,在 Docker 下等于 `max_active`),然后 Core 才会把一次性令牌放入命令中;该命令会验证安装程序校验和,并包含命令的 `enrollment_id`,节点注册时会报告此 ID。命令使用 sudo 运行安装程序(作为系统服务),并通过标准输入传递令牌;以 root 运行时则直接执行。界面不提供普通用户安装或移除入口,日志提示始终指明系统服务。命令从安装的 `public_url` 下载安装程序。只有成功读取安装信息后才会请求令牌;如果安装信息无法读取、安装为 `local_only`(或其 `public_url` 不是有效的 HTTP 或 HTTPS 来源),或者 `/console/config` 列出的 `node_artifacts` 不包含部署的提供商,则不会请求令牌。对话框在打开时和窗口重新获得焦点时,会再次读取这两项信息 | -| 更新节点 | `PATCH /core/v1/sandbox/nodes/{node_id}` | **编辑节点**:同时修改名称和 Sandbox 限制(保留数量限制仅适用于 microsandbox;在 Docker 下,Core 会将其设为活动数量限制) | +| 注册 | `POST /core/v1/sandbox/enrollment-tokens` | **添加节点**:管理员保留推荐上限(请求体不含上限,由 Core 在节点注册时推导)或同时设置两个上限(`max_active` 和 `max_retained`),然后 Core 才会把一次性令牌放入命令中;该命令会验证安装程序校验和,并包含命令的 `enrollment_id`,节点注册时会报告此 ID。命令使用 sudo 运行安装程序(作为系统服务),并通过标准输入传递令牌;以 root 运行时则直接执行。界面不提供普通用户安装或移除入口,日志提示始终指明系统服务。命令从安装的 `public_url` 下载安装程序。只有成功读取安装信息后才会请求令牌;如果安装信息无法读取、安装为 `local_only`(或其 `public_url` 不是有效的 HTTP 或 HTTPS 来源),或者 `/console/config` 列出的 `node_artifacts` 不包含部署的提供商,则不会请求令牌。对话框在打开时和窗口重新获得焦点时,会再次读取这两项信息 | +| 更新节点 | `PATCH /core/v1/sandbox/nodes/{node_id}` | **编辑节点**:同时修改名称和 Sandbox 限制(活动和保留数量限制) | | 移除节点 | `DELETE /core/v1/sandbox/nodes/{node_id}` | 确认后移除节点;只有 Core 确认删除后该行才会消失,随后 **清理主机** 对话框会提供主机的卸载命令(需要 root 或 sudo;对于使用不同于部署地址的地址注册的节点,还需要使用 `--force`,以跳过安装程序与 Core 的确认) | | Runtime 观测 | `GET /core/v1/sandbox/runtime-observations` | **沙箱监控**:每个项目的托管 Runtime,每项均以所属项目为标签;E2B Sandbox 对话框还会将 `observation.disk` 显示为已用量/限制值(其他位置为 null) | diff --git a/docs/zh/web/console-server.md b/docs/zh/web/console-server.md index 33105843d..a38243bca 100644 --- a/docs/zh/web/console-server.md +++ b/docs/zh/web/console-server.md @@ -1,7 +1,7 @@ --- title: "控制台服务器" source: docs/web/console-server.md -source_hash: 0c4b0c50abbeabdca19980b4e7a67552680c0cdc7a534c586b58c9fe6a509ca8 +source_hash: 2a0635be9825d1fd44091c61943224a34476a746587177128b76b29fb520445e --- 控制台服务器(`oac-web`)提供 Web,使用 Core 密钥认证管理员,并将浏览器的 `/core/v1` 请求转发到 Core。本参考面向不使用安装程序运行 Web 的运维人员,以及评审其安全边界的人员。 @@ -94,11 +94,11 @@ flowchart LR | --- | --- | | `node_installer` | 控制台是否提供节点安装文件 | | `node_installer_sha256` | 文件中 `node-install.pyz` 的 SHA-256;**添加节点** 命令执行安装程序前验证它 | -| `node_artifacts` | 文件中包含节点资产的提供商(`docker`、`microsandbox`),资产在本地或通过发行下载提供。每次请求都读取,因此重新运行安装程序新增的资产无需重启即可出现 | +| `node_artifacts` | 文件中包含节点资产的提供商(`microsandbox`),资产在本地或通过发行下载提供。每次请求都读取,因此重新运行安装程序新增的资产无需重启即可出现 | ## 节点安装文件 {#node-installation-payload} -设置 `OAC_WEB_NODE_PAYLOAD_DIR` 后,控制台在 `/node-install/` 无需登录地提供匹配发行版的节点文件:`node-install.pyz`、`manifest.json`、`SHA256SUMS`、`runtime/seccomp.json`,以及清单声明的 `artifacts/` 下节点资产。本地缺失的资产重定向(307)到其发行下载地址。节点安装和卸载命令从 `/node-install/` 下载,因此反向代理必须将该路径发给控制台。节点自行验证每个校验和。 +设置 `OAC_WEB_NODE_PAYLOAD_DIR` 后,控制台在 `/node-install/` 无需登录地提供匹配发行版的节点文件:`node-install.pyz`、`manifest.json`、`SHA256SUMS`,以及清单声明的 `artifacts/` 下节点资产。本地缺失的资产重定向(307)到其发行下载地址。节点安装和卸载命令从 `/node-install/` 下载,因此反向代理必须将该路径发给控制台。节点自行验证每个校验和。 ## 验证 {#verification} diff --git a/docs/zh/web/index.md b/docs/zh/web/index.md index 68ef0758b..d79473185 100644 --- a/docs/zh/web/index.md +++ b/docs/zh/web/index.md @@ -1,7 +1,7 @@ --- title: "Web 控制台" source: docs/web/index.md -source_hash: b7de8bf7ba82f43f9e60180a6ab544bd7327325cc86055723407c5f21dcd77e9 +source_hash: 6ea51f7e9634ae3f7433aad452b2a81302d13df0b533743d77feaafd4a53edb6 --- Web 是 OpenAgentCore 安装的管理员控制台:运维人员用 Core 密钥登录,查看健康状态和用量,检查应用创建的内容,并管理项目、密钥、节点和设置。应用使用项目 API 密钥调用 `/v1`,不使用 Web。 @@ -28,7 +28,7 @@ Web 是 OpenAgentCore 安装的管理员控制台:运维人员用 Core 密钥 | 监控 | Session 日志 | 全部 Session;各 Session 的只读对话、跟踪、Turn、失败分类原因及下一步操作;自托管 Session 页面还管理执行器凭据并提供主机连接命令 | | 资源 | Agent、Environment 模板、Skills、文件、Vault | 检查和允许的删除操作,显示各资源所属项目及创建密钥 | | 平台 | 项目与密钥 | 创建、重命名和归档项目;签发与撤销密钥;各项目的用量、写入历史和 API 调用方法 | -| 平台 | 节点 | 添加、编辑和移除 microsandbox 或 Docker 节点;各节点的就绪状态、活跃与保留上限、主机 CPU 与内存及其历史、可用磁盘,以及分配及其在各计算阶段的已用时间 | +| 平台 | 节点 | 添加、编辑和移除 microsandbox 节点;各节点的就绪状态、活跃与保留上限、主机 CPU 与内存及其历史、可用磁盘,以及分配及其在各计算阶段的已用时间 | | 平台 | 设置 | **沙箱与调度**、**工作区存储**、**模型与凭据** 和 **服务与部署**;参阅[配置](../configuration.md#runtime-settings-web) | 缺失数据展示为缺失(—),不会当作零。[控制台 API 使用](console-api-usage.md)列出各页面读取内容及统计边界。 @@ -37,7 +37,7 @@ Web 是 OpenAgentCore 安装的管理员控制台:运维人员用 Core 密钥 | 任务 | 位置 | | --- | --- | -| 选择沙箱提供商(microsandbox、E2B 或 Docker)、规格和 Runtime,或重置沙箱提供商 | **设置 → 沙箱与调度**;参阅[修改沙箱配置](../getting-started/nodes.md#change-the-sandbox-configuration) | +| 选择沙箱提供商(microsandbox 或 E2B)、规格和 Runtime,或重置沙箱提供商 | **设置 → 沙箱与调度**;参阅[修改沙箱配置](../getting-started/nodes.md#change-the-sandbox-configuration) | | 添加、调整或移除执行节点 | **节点**:**添加节点**、**编辑节点** 和移除;参阅[添加和管理节点](../getting-started/nodes.md) | | 设置 Harness 默认模型 | **设置 → 模型与凭据**;参阅[默认模型](../configuration.md#default-models) | | 创建项目并签发 API 密钥 | **项目与密钥**;参阅 [项目和 API 密钥](../getting-started/operations.md#projects-and-api-keys) | diff --git a/docs/zh/workspace-provider.md b/docs/zh/workspace-provider.md index aa8e2bcb2..0efac398c 100644 --- a/docs/zh/workspace-provider.md +++ b/docs/zh/workspace-provider.md @@ -1,7 +1,7 @@ --- title: 工作区文件系统协议 source: docs/workspace-provider.md -source_hash: b7f103947580f0a4b987b3670e78a005668797212edd316f8ed2e7d6bc5d105a +source_hash: 3f8264d8bd0232f31cb6c91c2f482ece6f713292dc4c53044a0742aeccaa8baa --- 工作区文件系统协议连接 Core 和沙箱节点与拥有 Environment 持久存储的适配器。Core 创建和删除存储对象;节点将对象解析为主机目录,由其[沙箱提供商协议](sandbox-provider.md)挂载到计算资源。本文面向文件系统适配器和沙箱提供商的作者。 @@ -18,7 +18,7 @@ Core 在分配任一资源前,选择并验证沙箱提供商与文件系统适 | 沙箱提供商 | 外部工作区存储 | | --- | --- | | microsandbox | 必需(`host_directory`、`user_xattr`) | -| Docker、E2B | 不支持;使用提供商自有的 Environment 磁盘 | +| E2B | 不支持;使用提供商自有的 Environment 磁盘 | ## 实现适配器 {#implement-an-adapter} diff --git a/go.mod b/go.mod index 6c13bc5ef..b1a3b0e34 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,10 @@ module github.com/MiniMax-AI/OpenAgentCore go 1.26.8 require ( - github.com/containerd/errdefs v1.0.0 github.com/go-chi/chi/v5 v5.3.2 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/jackc/pgx/v5 v5.10.0 - github.com/moby/moby/api v1.56.0 - github.com/moby/moby/client v0.6.0 github.com/openai/openai-go/v3 v3.61.0 github.com/pressly/goose/v3 v3.27.3 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 @@ -25,24 +22,14 @@ require ( ) require ( - github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.7.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/felixge/httpsnoop v1.1.0 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect github.com/tidwall/gjson v1.19.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect diff --git a/go.sum b/go.sum index e0f04e381..60f18aee9 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,12 @@ -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= -github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY= github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -58,22 +44,12 @@ github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyi github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= -github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= -github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= -github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/openai/openai-go/v3 v3.61.0 h1:nMLuGFdKBF0sB3qFVNwE8kpBpenVN1ucYRoKcx7E1u0= github.com/openai/openai-go/v3 v3.61.0/go.mod h1:ufI1+K+t0ijRB3gk8eztiw1crcDpsBuxRQL4sbLIrts= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pressly/goose/v3 v3.27.3 h1:pIglVHjw99r4e/hDHHwbl9vfOsDMqUokfkXo6+n/RxA= @@ -107,8 +83,6 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA= @@ -161,8 +135,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws= modernc.org/libc v1.74.3/go.mod h1:4H7h/MJ8wnjL8RAbp9v3OXgnk22X7MouHIhDbvP3gj4= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -171,5 +143,3 @@ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= -pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= -pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/providerassets/catalog.json b/internal/providerassets/catalog.json index 233ed30dd..332bbfa96 100644 --- a/internal/providerassets/catalog.json +++ b/internal/providerassets/catalog.json @@ -1,21 +1,4 @@ { - "docker": [ - { - "path": "native/bin/oac-node", - "suffix": "sandbox-node", - "role": "node" - }, - { - "path": "images/runtime.tar.gz", - "suffix": "runtime.tar.gz", - "role": "image" - }, - { - "path": "runtime/seccomp.json", - "suffix": "seccomp.json", - "role": "policy" - } - ], "microsandbox": [ { "path": "native/bin/oac-node", @@ -27,11 +10,6 @@ "suffix": "runtime.tar.gz", "role": "image" }, - { - "path": "runtime/seccomp.json", - "suffix": "seccomp.json", - "role": "policy" - }, { "path": "native/bin/oac-microsandbox-provider", "suffix": "microsandbox-provider", diff --git a/packages/agents-client/src/admin-client.test.ts b/packages/agents-client/src/admin-client.test.ts index e40ad125a..27d605d16 100644 --- a/packages/agents-client/src/admin-client.test.ts +++ b/packages/agents-client/src/admin-client.test.ts @@ -435,7 +435,7 @@ describe("AdminClient project monitoring", () => { it("retains the project label on global Runtime observations", async () => { const observation = { id: sessionId, object: "agent.runtime_observation", session_id: sessionId, environment_id: resourceId, - mode: "openai_hosted", provider_type: "docker", + mode: "openai_hosted", provider_type: "microsandbox", instance: { kind: "managed_allocation", allocation_id: resourceId, device_id: keyId, connection_generation: null }, lifecycle_state: "active", status: "observed", reason: null, allocation_created_at: 10, resolved_at: 30, observed_at: 20, started_at: 10, diff --git a/packages/agents-client/src/client.test.ts b/packages/agents-client/src/client.test.ts index 837eee9e5..3b25e1584 100644 --- a/packages/agents-client/src/client.test.ts +++ b/packages/agents-client/src/client.test.ts @@ -119,7 +119,7 @@ function runtimeObservation(overrides: Record = {}): Record { it("projects documented flat values and retains one-request semantics", async () => { - const details = { current_generation: 4, supported: ["docker", "microsandbox"], enabled: true, missing: null, label: "ready", empty: [] }; + const details = { current_generation: 4, supported: ["microsandbox", "e2b"], enabled: true, missing: null, label: "ready", empty: [] }; const { fetch, request } = requester(details); const error = await request.response("/sandbox/deployment", undefined, "PUT", { expected_generation: 3 }).catch((value: unknown) => value); expect(error).toBeInstanceOf(AgentCoreError); @@ -20,7 +20,7 @@ describe("Core error details", () => { expect(fetch).toHaveBeenCalledTimes(1); expect(fetch.mock.calls[0]).toEqual(["/core/v1/sandbox/deployment", expect.objectContaining({ method: "PUT", credentials: "same-origin", redirect: "error" })]); details.supported[0] = "mutated"; - expect((error as AgentCoreError).details?.supported).toEqual(["docker", "microsandbox"]); + expect((error as AgentCoreError).details?.supported).toEqual(["microsandbox", "e2b"]); }); it.each([ diff --git a/packages/agents-client/src/deployment-contract.ts b/packages/agents-client/src/deployment-contract.ts index d7376ad97..24fce195e 100644 --- a/packages/agents-client/src/deployment-contract.ts +++ b/packages/agents-client/src/deployment-contract.ts @@ -1,3 +1,3 @@ // Code generated by services/core/cmd/specification-contract from sandbox/deployment_contract.go; DO NOT EDIT. -export const deploymentContract = {"workspace_fields":["attachment","user_xattr","capacity_quota"],"resources":[{"name":"cpus","min":1,"max":255,"omit_zero":false},{"name":"memory_mib","min":512,"max":1048576,"omit_zero":false},{"name":"root_disk_mib","min":0,"max":4294967295,"omit_zero":true},{"name":"environment_disk_mib","min":0,"max":4294967295,"omit_zero":true}],"providers":{"docker":{"mode":"nodes","disk":false,"default_resources":{"cpus":2,"memory_mib":2048},"setup":{"label":"Docker","summary":{"en":"Runs each sandbox as a container on the host's kernel: CPU and memory limits but no disk quota, for trusted workloads or hosts without KVM.","zh-CN":"每个沙箱是一个共用主机内核的容器:能限制 CPU 和内存,但没有磁盘配额,只适合可信的工作负载或没有 KVM 的主机。"},"cautions":[{"en":"Containers share the host's kernel, so a container escape reaches the host.","zh-CN":"容器共用主机内核,一旦容器逃逸就能触及主机。"},{"en":"The node's service account joins the docker group, which is equivalent to root on that host.","zh-CN":"节点的服务账号会加入 docker 组,这在那台主机上等同于 root 权限。"}],"recommended":false,"fields":[],"presets":[],"credential":"not_required","public_origin":"not_required","discovery":false}},"e2b":{"mode":"direct","disk":false,"default_resources":null,"setup":{"label":"E2B","summary":{"en":"Runs sandboxes in a hosted E2B-compatible cloud: no machines to manage, billed by the service.","zh-CN":"沙箱运行在托管的 E2B 兼容云端:无需管理机器,由服务方计费。"},"cautions":[],"recommended":false,"fields":[{"name":"api_key","secret":true,"optional":false,"discovered":false,"label":{"en":"API key","zh-CN":"API 密钥"},"help":{"en":"","zh-CN":""}},{"name":"template","secret":false,"optional":false,"discovered":true,"label":{"en":"Template build","zh-CN":"模板构建"},"help":{"en":"Core validates the exact ready build again when you save.","zh-CN":"保存时 Core 会再次校验这个已就绪的构建。"}},{"name":"api_url","secret":false,"optional":true,"discovered":false,"label":{"en":"Sandbox API URL","zh-CN":"沙箱 API 地址"},"help":{"en":"The selected service supplies a default. You can edit it for a compatible endpoint.","zh-CN":"所选服务会提供默认值,兼容的端点可以自行修改。"}},{"name":"domain","secret":false,"optional":true,"discovered":false,"label":{"en":"Sandbox data-plane domain","zh-CN":"沙箱数据面域名"},"help":{"en":"","zh-CN":""}}],"presets":[{"label":"SandBase Sandbox","values":{"api_url":"https://sandbox.sandbase.ai","domain":"sandbox.sandbase.ai"},"credential_url":"https://www.sandbase.ai/console/keys"},{"label":"E2B","values":{"api_url":"https://api.e2b.app","domain":"e2b.app"},"credential_url":"https://e2b.dev/dashboard?tab=api-keys"}],"credential":"required","public_origin":"required","discovery":true}},"microsandbox":{"mode":"nodes","workspace":{"attachment":"host_directory","user_xattr":true},"disk":true,"default_resources":{"cpus":2,"memory_mib":4096,"root_disk_mib":8192},"setup":{"label":"microsandbox","summary":{"en":"Runs each sandbox as a lightweight virtual machine: stronger isolation and a root disk size limit, with each sandbox's /environment on workspace storage, but the host needs KVM.","zh-CN":"每个沙箱是一个轻量虚拟机:隔离更强,能限制根盘大小,每个沙箱的 /environment 位于工作区存储上,但主机需要支持 KVM。"},"cautions":[],"recommended":true,"fields":[],"presets":[],"credential":"not_required","public_origin":"not_required","discovery":false}}},"minimum_disk":1024} as const; +export const deploymentContract = {"workspace_fields":["attachment","user_xattr","capacity_quota"],"resources":[{"name":"cpus","min":1,"max":255,"omit_zero":false},{"name":"memory_mib","min":512,"max":1048576,"omit_zero":false},{"name":"root_disk_mib","min":0,"max":4294967295,"omit_zero":true},{"name":"environment_disk_mib","min":0,"max":4294967295,"omit_zero":true}],"providers":{"e2b":{"mode":"direct","disk":false,"default_resources":null,"setup":{"label":"E2B","summary":{"en":"Runs sandboxes in a hosted E2B-compatible cloud: no machines to manage, billed by the service.","zh-CN":"沙箱运行在托管的 E2B 兼容云端:无需管理机器,由服务方计费。"},"cautions":[],"recommended":false,"fields":[{"name":"api_key","secret":true,"optional":false,"discovered":false,"label":{"en":"API key","zh-CN":"API 密钥"},"help":{"en":"","zh-CN":""}},{"name":"template","secret":false,"optional":false,"discovered":true,"label":{"en":"Template build","zh-CN":"模板构建"},"help":{"en":"Core validates the exact ready build again when you save.","zh-CN":"保存时 Core 会再次校验这个已就绪的构建。"}},{"name":"api_url","secret":false,"optional":true,"discovered":false,"label":{"en":"Sandbox API URL","zh-CN":"沙箱 API 地址"},"help":{"en":"The selected service supplies a default. You can edit it for a compatible endpoint.","zh-CN":"所选服务会提供默认值,兼容的端点可以自行修改。"}},{"name":"domain","secret":false,"optional":true,"discovered":false,"label":{"en":"Sandbox data-plane domain","zh-CN":"沙箱数据面域名"},"help":{"en":"","zh-CN":""}}],"presets":[{"label":"SandBase Sandbox","values":{"api_url":"https://sandbox.sandbase.ai","domain":"sandbox.sandbase.ai"},"credential_url":"https://www.sandbase.ai/console/keys"},{"label":"E2B","values":{"api_url":"https://api.e2b.app","domain":"e2b.app"},"credential_url":"https://e2b.dev/dashboard?tab=api-keys"}],"credential":"required","public_origin":"required","discovery":true}},"microsandbox":{"mode":"nodes","workspace":{"attachment":"host_directory","user_xattr":true},"disk":true,"default_resources":{"cpus":2,"memory_mib":4096,"root_disk_mib":8192},"setup":{"label":"microsandbox","summary":{"en":"Runs each sandbox as a lightweight virtual machine: stronger isolation and a root disk size limit, with each sandbox's /environment on workspace storage, but the host needs KVM.","zh-CN":"每个沙箱是一个轻量虚拟机:隔离更强,能限制根盘大小,每个沙箱的 /environment 位于工作区存储上,但主机需要支持 KVM。"},"cautions":[],"recommended":true,"fields":[],"presets":[],"credential":"not_required","public_origin":"not_required","discovery":false}}},"minimum_disk":1024} as const; diff --git a/packages/agents-client/src/runtime-history.test.ts b/packages/agents-client/src/runtime-history.test.ts index 4e8a4d11f..6cee0f2e9 100644 --- a/packages/agents-client/src/runtime-history.test.ts +++ b/packages/agents-client/src/runtime-history.test.ts @@ -64,7 +64,7 @@ function history(overrides: Record = {}): Record Promise> = { @@ -72,8 +69,8 @@ function read(name: string, body: unknown) { return reads[name]!(new SandboxAdminClient({ fetch: vi.fn().mockResolvedValue(response(body)) })); } const { enrollment_id: _enrollment, ...unenrolled } = node; -const { suspension: _suspension, ...unsuspended } = docker; -const { specification_digest: _digest, ...undigested } = docker; +const { suspension: _suspension, ...unsuspended } = microsandbox; +const { specification_digest: _digest, ...undigested } = microsandbox; const { compute_phase_changed_at: _changed, ...unphased } = allocation; describe("strict sandbox administration projections", () => { @@ -85,7 +82,7 @@ describe("strict sandbox administration projections", () => { ["nodes", "ready and unready", { data: [node, unready] }], ["nodes", "empty", { data: [] }], ["detail", "observed", detail], ["unobserved", "never observed", unobserved], ["allocations", "known and unknown phase times", { data: [allocation, { ...allocation, compute_phase: "suspended", compute_phase_changed_at: created, diagnostic: "node_unavailable" }] }], - ["deployment", "unconfigured", unconfigured], ["deployment", "Docker", docker], ["deployment", "microsandbox", microsandbox], ["deployment", "external workspace", externalDeployment], ["deployment", "E2B", e2bDeployment], + ["deployment", "unconfigured", unconfigured], ["deployment", "microsandbox", microsandbox], ["deployment", "external workspace", externalDeployment], ["deployment", "E2B", e2bDeployment], ])("accepts %s as Core serializes it: %s", async (name, _, body) => { expect(await read(name, body)).toEqual(body); }); @@ -106,9 +103,9 @@ describe("strict sandbox administration projections", () => { ["allocations", "another node's allocation", { data: [{ ...allocation, node_id: unready.id }] }], ["deployment", "a missing suspension", unsuspended], ["deployment", "a specification without its digest", undigested], - ["deployment", "an e2b member for Docker", { ...docker, configuration: e2bDeployment.configuration }], - ["deployment", "E2B without its e2b member", { ...docker, provider: "e2b", mode: "direct" }], - ["deployment", "an unknown mode", { ...docker, mode: "hybrid" }], + ["deployment", "an e2b member for microsandbox", { ...microsandbox, configuration: e2bDeployment.configuration }], + ["deployment", "E2B without its e2b member", { ...microsandbox, provider: "e2b", mode: "direct" }], + ["deployment", "an unknown mode", { ...microsandbox, mode: "hybrid" }], ])("rejects %s with %s", async (name, _, body) => { await expect(read(name, body)).rejects.toMatchObject({ status: 502, code: "invalid_admin_response" }); }); @@ -179,7 +176,7 @@ describe("Core sandbox credential boundaries", () => { }); it("patches idle policy once through the existing deployment resource", async () => { - const fetch = vi.fn().mockResolvedValue(response({ ...docker, suspension: { idle_seconds: 60, retention_seconds: 86400 } })); + const fetch = vi.fn().mockResolvedValue(response({ ...microsandbox, suspension: { idle_seconds: 60, retention_seconds: 86400 } })); const admin = new SandboxAdminClient({ baseUrl: "/core/v1/sandbox", token: "admin-only", fetch }); expect((await admin.updateSuspension({ idle_seconds: 60 })).suspension?.idle_seconds).toBe(60); const [url, init] = fetch.mock.calls[0]!; @@ -193,15 +190,15 @@ describe("Core sandbox credential boundaries", () => { }); it("initializes using only the explicit provider with cancellation and admin credentials", async () => { - const deployment = docker; + const deployment = microsandbox; const fetch = vi.fn().mockResolvedValue(response(deployment)); const admin = new SandboxAdminClient({ baseUrl: "/core/v1/sandbox", token: "admin-only", fetch }); const controller = new AbortController(); - expect(await admin.initializeDeployment({ expected_generation: 0, provider: "docker" }, { signal: controller.signal })).toEqual(deployment); + expect(await admin.initializeDeployment({ expected_generation: 0, provider: "microsandbox" }, { signal: controller.signal })).toEqual(deployment); const [url, init] = fetch.mock.calls[0]!; expect(url).toBe("/core/v1/sandbox/deployment"); expect(init?.method).toBe("POST"); - expect(JSON.parse(String(init?.body))).toEqual({ expected_generation: 0, provider: "docker" }); + expect(JSON.parse(String(init?.body))).toEqual({ expected_generation: 0, provider: "microsandbox" }); expect(init?.signal).toBe(controller.signal); expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer admin-only"); expect(new Headers(init?.headers).has("OpenAI-Beta")).toBe(false); @@ -209,7 +206,7 @@ describe("Core sandbox credential boundaries", () => { it("forwards one specification with generation and preserves backend conflict details", async () => { const fetch = vi.fn().mockResolvedValue(response({ error: { code: "sandbox_specification_mismatch", message: "Node specification differs" } }, 409)); const admin = new SandboxAdminClient({ token: "admin-only", fetch }); - const input = { provider: "docker" as const, resources: { cpus: 2, memory_mib: 2048 }, expected_generation: 3 }; + const input = { provider: "microsandbox" as const, resources: { cpus: 2, memory_mib: 2048 }, expected_generation: 3 }; await expect(admin.updateDeployment(input)).rejects.toMatchObject({ status: 409, code: "sandbox_specification_mismatch" }); expect(fetch).toHaveBeenCalledTimes(1); expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toEqual(input); @@ -223,11 +220,11 @@ describe("Core sandbox credential boundaries", () => { it("does not retry an uncertain initialization transport failure", async () => { const fetch = vi.fn().mockRejectedValue(new TypeError("Connection lost")); const admin = new SandboxAdminClient({ baseUrl: "/core/v1/sandbox", token: "admin", fetch }); - await expect(admin.initializeDeployment({ expected_generation: 0, provider: "docker" })).rejects.toThrow("Sandbox configuration could not be confirmed."); + await expect(admin.initializeDeployment({ expected_generation: 0, provider: "microsandbox" })).rejects.toThrow("Sandbox configuration could not be confirmed."); expect(fetch).toHaveBeenCalledTimes(1); }); it("uses the explicit admin credential and admin routes without a project beta header", async () => { - const fetch = vi.fn().mockImplementation(async (url) => response(String(url).endsWith("/deployment") ? docker : { data: [] })); + const fetch = vi.fn().mockImplementation(async (url) => response(String(url).endsWith("/deployment") ? microsandbox : { data: [] })); const admin = new SandboxAdminClient({ baseUrl: "https://core.example/core/v1/sandbox", token: "admin-only", fetch }); await admin.retrieveDeployment(); await admin.listNodes(); await admin.listAllocations("node/a"); await admin.createEnrollment(); await admin.removeNode("node/a"); expect(fetch.mock.calls.map(([url]) => url)).toEqual([ @@ -312,14 +309,14 @@ describe("hosted provider configuration", () => { it("never retries uncertain switch or reset writes", async () => { const fetch = vi.fn().mockRejectedValue(new TypeError("Connection lost")); const client = new SandboxAdminClient({ fetch }); - await expect(client.updateDeployment({ provider: "docker", expected_generation: 1 })).rejects.toThrow(); + await expect(client.updateDeployment({ provider: "microsandbox", expected_generation: 1 })).rejects.toThrow(); await expect(client.startReset({ clear: "auto", expected_generation: 1 })).rejects.toThrow(); expect(fetch).toHaveBeenCalledTimes(2); }); }); const resetting = { - ...docker, + ...microsandbox, reset: { clear: "auto", requested_at: created, deadline_at: "2026-09-25T09:00:00Z", forced_at: null, remaining: { busy: 1, idle: 1, cleanup: 1, on_offline_nodes: 2, offline_nodes: [{ node_id: node.id, name: node.name, resources: 2 }] } }, @@ -375,10 +372,10 @@ it("uses the authoritative rollout signal instead of retained old Sessions for p for (const rollout of [ { ...settled.rollout, state: "preparing" }, { ...settled.rollout, previous_generation_sandboxes: 4 }, - { ...docker.rollout, state: "preparing" }, - { ...docker.rollout, nodes: { ...docker.rollout.nodes, unknown: -1 } }, + { ...microsandbox.rollout, state: "preparing" }, + { ...microsandbox.rollout, nodes: { ...microsandbox.rollout.nodes, unknown: -1 } }, ]) { - const invalid = new SandboxAdminClient({ fetch: async () => response({ ...docker, rollout }) }); + const invalid = new SandboxAdminClient({ fetch: async () => response({ ...microsandbox, rollout }) }); await expect(invalid.retrieveDeployment()).rejects.toMatchObject({ code: "invalid_admin_response" }); } }); diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index a4b67eaee..36340032b 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -104,14 +104,12 @@ if [[ "$(go env GOVERSION)" != "$required_go" ]]; then exit 1 fi go run ./services/core/cmd/provider-artifacts -for file in install_display.py node_output.py node_install.py host_docker.py host_microsandbox.py provider_assets.py node_spec.py node_generations.py node_payload.py distribution.py; do +for file in install_display.py node_output.py node_install.py host_microsandbox.py provider_assets.py node_spec.py node_generations.py node_payload.py distribution.py; do cp "deploy/node/$file" "$bundle/$file" done python3 scripts/core-distribution-manifest.py bootstraps "$bundle" "$source_epoch" "$revision" # The bundled docs (BUNDLED_DOCS); links that leave them point at this commit on GitHub. python3 scripts/core-distribution-manifest.py docs . "$bundle" "$revision" -mkdir -p "$bundle/runtime" -cp services/core/internal/sandbox/docker/seccomp.json "$bundle/runtime/" mkdir -p "$bundle/storage" install -m 0755 services/core/internal/workspacefs/nfs/quota/oac-workspace-quota "$bundle/storage/" cp LICENSE "$bundle/" diff --git a/scripts/compose-smoke.py b/scripts/compose-smoke.py index 659f6dfc4..0cf3ef170 100644 --- a/scripts/compose-smoke.py +++ b/scripts/compose-smoke.py @@ -38,14 +38,13 @@ def load(name, path): def prepare_payload(destination, revision, source_time): """Node metadata for this commit, built with the distribution's own builders.""" - (destination / 'runtime').mkdir(parents=True) + destination.mkdir(parents=True) with tempfile.TemporaryDirectory(dir=destination.parent) as sources: sources = Path(sources) for path in (ROOT / 'deploy/node').glob('*.py'): shutil.copyfile(path, sources / path.name) distribution.bootstraps(sources, source_time, revision) shutil.copyfile(sources / 'node-install.pyz', destination / 'node-install.pyz') - shutil.copyfile(ROOT / 'services/core/internal/sandbox/docker/seccomp.json', destination / 'runtime/seccomp.json') (destination / 'manifest.json').write_text(json.dumps( {'source_commit': revision, 'platform': 'linux/amd64'})) distribution.checksums(destination) diff --git a/scripts/core-distribution-manifest.py b/scripts/core-distribution-manifest.py index 7618bea85..b22f3af40 100644 --- a/scripts/core-distribution-manifest.py +++ b/scripts/core-distribution-manifest.py @@ -308,10 +308,7 @@ def package_artifacts(bundle, stage, revision): for logical, suffix in ARTIFACTS.items(): filename = f"oac-{revision}-linux-amd64-{suffix}" target = assets / filename - if logical == "runtime/seccomp.json": - shutil.copyfile(bundle / logical, target) - else: - (bundle / logical).replace(target) + (bundle / logical).replace(target) result[logical] = {"filename": filename, "sha256": sha256(target), "size": target.stat().st_size} if logical == "images/runtime.tar.gz": result[logical].update(unpacked) @@ -324,7 +321,7 @@ def bootstraps(bundle, epoch, revision): bundle = pathlib.Path(bundle) with tempfile.TemporaryDirectory(dir=bundle.parent) as directory: modules = (("node_install.py", "__main__.py"), ("distribution.py", "distribution.py"), - *((name, name) for name in ("node_spec.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_docker.py", "host_microsandbox.py"))) + *((name, name) for name in ("node_spec.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_microsandbox.py"))) for original, packaged in modules: target = pathlib.Path(directory) / packaged shutil.copyfile(bundle / original, target) @@ -366,10 +363,7 @@ def node_payload(bundle, stage, revision, source_tree, artifact_base_url="", off payload = stage / "ingress/node-payload" payload.mkdir(parents=True) (payload / "manifest.json").write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n") - for name in ("node-install.pyz", "runtime/seccomp.json"): - target = payload / name - target.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(bundle / name, target) + shutil.copyfile(bundle / "node-install.pyz", payload / "node-install.pyz") checksums(payload) diff --git a/scripts/core-distribution-manifest.test.py b/scripts/core-distribution-manifest.test.py index b488e9530..dc4df4522 100644 --- a/scripts/core-distribution-manifest.test.py +++ b/scripts/core-distribution-manifest.test.py @@ -134,7 +134,7 @@ def test_init_payload_contains_only_verified_node_metadata(self): self.manifest() payload = self.stage / "ingress/node-payload" files = sorted(p.relative_to(payload).as_posix() for p in payload.rglob("*") if p.is_file()) - self.assertEqual(files, ["SHA256SUMS", "manifest.json", "node-install.pyz", "runtime/seccomp.json"]) + self.assertEqual(files, ["SHA256SUMS", "manifest.json", "node-install.pyz"]) sums = dict(line.split(" ", 1)[::-1] for line in (payload / "SHA256SUMS").read_text().splitlines()) for name, checksum in sums.items(): self.assertEqual(distribution.sha256(payload / name), checksum) @@ -277,7 +277,7 @@ def test_thin_archive_and_detached_payload_share_one_manifest(self): self.assertEqual(set(metadata["artifacts"]), set(distribution.ARTIFACTS)) for logical, artifact in metadata["artifacts"].items(): path = self.stage / "artifacts" / artifact["filename"] - self.assertEqual((self.bundle / logical).exists(), logical == "runtime/seccomp.json") + self.assertFalse((self.bundle / logical).exists()) self.assertTrue(artifact["filename"].startswith("oac-" + REVISION + "-linux-amd64-")) self.assertEqual(artifact["size"], path.stat().st_size) self.assertEqual(artifact["sha256"], distribution.sha256(path)) @@ -312,7 +312,7 @@ def test_release_location_and_explicit_offline_requirements(self): self.assertEqual(json.loads((self.bundle / "manifest.json").read_text())["artifact_base_url"], "") def test_bootstraps_include_shared_downloader_and_are_reproducible(self): - for name in ("node_install.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_docker.py", "host_microsandbox.py", "distribution.py", "node_spec.py"): + for name in ("node_install.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_microsandbox.py", "distribution.py", "node_spec.py"): (self.bundle / name).write_text("# " + name + "\n") distribution.bootstraps(self.bundle, "1700000000", "a" * 40) first = (self.bundle / "node-install.pyz").read_bytes() @@ -320,7 +320,7 @@ def test_bootstraps_include_shared_downloader_and_are_reproducible(self): self.assertEqual(first, (self.bundle / "node-install.pyz").read_bytes()) self.assertFalse((self.bundle / "oac.pyz").exists()) with zipfile.ZipFile(self.bundle / "node-install.pyz") as contents: - self.assertEqual(set(contents.namelist()), {"__main__.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_docker.py", "host_microsandbox.py"}) + self.assertEqual(set(contents.namelist()), {"__main__.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py", "host_microsandbox.py"}) self.assertEqual(contents.read("__main__.py"), (self.bundle / "node_install.py").read_bytes()) diff --git a/services/core/cmd/oac/init.go b/services/core/cmd/oac/init.go index b56dc13d1..412f061a0 100644 --- a/services/core/cmd/oac/init.go +++ b/services/core/cmd/oac/init.go @@ -20,7 +20,7 @@ import ( ) // releaseMembers are the only files copied from the initialization image. -var releaseMembers = []string{"manifest.json", "SHA256SUMS", "node-install.pyz", "runtime/seccomp.json"} +var releaseMembers = []string{"manifest.json", "SHA256SUMS", "node-install.pyz"} var dataOwners = []struct { name string diff --git a/services/core/cmd/sandbox-node/generations_test.go b/services/core/cmd/sandbox-node/generations_test.go index d830b300c..e2847837a 100644 --- a/services/core/cmd/sandbox-node/generations_test.go +++ b/services/core/cmd/sandbox-node/generations_test.go @@ -15,7 +15,7 @@ import ( ) func TestGenerationDroppedReceiptIdentity(t *testing.T) { - config := sandbox.NodeConfig{InstallationID: "installation", Generation: 9, Provider: "docker"} + config := sandbox.NodeConfig{InstallationID: "installation", Generation: 9, Provider: "microsandbox"} stateDir := t.TempDir() directory := filepath.Join(stateDir, "generations") if err := os.Mkdir(directory, 0700); err != nil { @@ -100,7 +100,7 @@ func TestGenerationHelperKeepsCancellation(t *testing.T) { // Preparation may resolve native state, such as the digest a containerd image // store names the loaded Runtime image by; the envelope stays fixed. func TestGenerationPlanFixesOnlyTheEnvelope(t *testing.T) { - plan := sandbox.NodeConfig{InstallationID: "installation", Generation: 2, Provider: "docker", Specification: sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048}}, Native: json.RawMessage(`{"image":"sha256:` + strings.Repeat("b", 64) + `"}`)} + plan := sandbox.NodeConfig{InstallationID: "installation", Generation: 2, Provider: "microsandbox", Specification: sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048}}, Native: json.RawMessage(`{"image":"sha256:` + strings.Repeat("b", 64) + `"}`)} final := plan final.Native = json.RawMessage(`{"image":"sha256:` + strings.Repeat("c", 64) + `"}`) if !sameGenerationPlan(final, plan) { diff --git a/services/core/cmd/sandbox-node/main_test.go b/services/core/cmd/sandbox-node/main_test.go index b83e08a41..fad38d521 100644 --- a/services/core/cmd/sandbox-node/main_test.go +++ b/services/core/cmd/sandbox-node/main_test.go @@ -51,7 +51,7 @@ func refreshIdentity(t *testing.T, coreURL string) error { if err := os.Mkdir(dir, 0700); err != nil { t.Fatal(err) } - identity := node.Identity{InstallationID: uuid.NewString(), Provider: "docker", BackendFingerprint: strings.Repeat("1", 64)} + identity := node.Identity{InstallationID: uuid.NewString(), Provider: "microsandbox", BackendFingerprint: strings.Repeat("1", 64)} if _, err := node.InitIdentity(dir, coreURL, identity); err != nil { t.Fatal(err) } diff --git a/services/core/cmd/server/managed_setup_preflight_test.go b/services/core/cmd/server/managed_setup_preflight_test.go index 884b89385..e810fc2e4 100644 --- a/services/core/cmd/server/managed_setup_preflight_test.go +++ b/services/core/cmd/server/managed_setup_preflight_test.go @@ -151,10 +151,10 @@ func TestInitialE2BPublicTemplateOutsideTeamIsRejected(t *testing.T) { func TestManagedSetupRoutesProviderWithoutCredentialRequirement(t *testing.T) { s := &managedSetup{} - config := &execution.RuntimeProvider{ProviderKind: "docker"} + config := &execution.RuntimeProvider{ProviderKind: "microsandbox"} candidate, err := s.routeGenerations( execution.PreparedRuntimeDeployment{Config: config}, - deployment.Setup{Provider: "docker", Mode: "nodes"}, + deployment.Setup{Provider: "microsandbox", Mode: "nodes"}, ) if err != nil || candidate.Config != config || candidate.FenceCredential != nil || candidate.VerifyCredential != nil { t.Fatalf("explicit no-credential provider required credential routing: %v", err) diff --git a/services/core/cmd/server/managed_setup_test.go b/services/core/cmd/server/managed_setup_test.go index f866174e6..deda0f804 100644 --- a/services/core/cmd/server/managed_setup_test.go +++ b/services/core/cmd/server/managed_setup_test.go @@ -14,7 +14,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/processconfig" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers" @@ -120,11 +119,11 @@ func credentialService(t *testing.T) func(owner, candidate deployment.Setup) (de } func TestManagedSetupNeverReusesAnotherGenerationOrUnverifiedState(t *testing.T) { - value := deployment.Setup{InstallationID: "installation", Provider: "docker", Mode: "nodes", Generation: 1} + value := deployment.Setup{InstallationID: "installation", Provider: "microsandbox", Mode: "nodes", Generation: 1} var loadErr error setups := &fakeDeploymentSetups{t: t, setup: func(context.Context) (deployment.Setup, error) { return value, loadErr }} s := &managedSetup{registry: providers.Builtin(), deployment: setups, allocations: &fakeGenerationAllocations{t: t}, installationID: "installation"} - cached := &execution.RuntimeProvider{InstallationID: "installation", ProviderKind: "docker", Generation: 1} + cached := &execution.RuntimeProvider{InstallationID: "installation", ProviderKind: "microsandbox", Generation: 1} s.publish(cached) if got, err := s.load(t.Context()); err != nil || got != cached { t.Fatal("matching immutable selection was not reused") @@ -169,7 +168,7 @@ func TestManagedSetupPreparesWithoutPublishing(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() s := &managedSetup{registry: providers.Builtin(), installationID: id, hub: hub, deployment: &fakeDeploymentSetups{t: t}, allocations: &fakeGenerationAllocations{t: t}, runtimeAPI: "https://core.example/api/v1"} - previous := &execution.RuntimeProvider{InstallationID: id, Generation: 1, ProviderKind: "docker"} + previous := &execution.RuntimeProvider{InstallationID: id, Generation: 1, ProviderKind: "microsandbox"} s.publish(previous) candidate, err := s.prepare(t.Context(), deployment.Setup{InstallationID: id, Provider: "microsandbox", Mode: "nodes", Operations: microsandbox.Operations(), Suspension: &deployment.Suspension{IdleSeconds: 300, RetentionSeconds: 86400}}) if err != nil { @@ -189,7 +188,7 @@ func TestManagedSetupPreparesWithoutPublishing(t *testing.T) { func TestManagedSetupRejectedCandidateRetainsSelection(t *testing.T) { id := uuid.NewString() s := &managedSetup{processPaths: sandbox.ProcessPaths{ArtifactRoot: t.TempDir(), StateRoot: t.TempDir()}, registry: providers.Builtin(), installationID: id} - previous := &execution.RuntimeProvider{InstallationID: id, Generation: 1, ProviderKind: "docker"} + previous := &execution.RuntimeProvider{InstallationID: id, Generation: 1, ProviderKind: "microsandbox"} s.publish(previous) _, err := s.prepare(t.Context(), deployment.Setup{InstallationID: id, Provider: "e2b", Mode: "direct", UsesCredential: true, Configuration: &e2b.DeploymentConfiguration{APIKey: "synthetic-key", Template: "runtime:" + uuid.NewString()}}) @@ -200,7 +199,7 @@ func TestManagedSetupRejectedCandidateRetainsSelection(t *testing.T) { func TestManagedSetupResetTombstoneRejectsDelayedProviderLoad(t *testing.T) { id := uuid.NewString() - value := deployment.Setup{InstallationID: id, Provider: "docker", Mode: "nodes", Generation: 1} + value := deployment.Setup{InstallationID: id, Provider: "microsandbox", Mode: "nodes", Generation: 1} entered, release := make(chan struct{}), make(chan struct{}) delayed := func(ctx context.Context) (deployment.Setup, error) { close(entered) @@ -229,7 +228,7 @@ func TestManagedSetupResetTombstoneRejectsDelayedProviderLoad(t *testing.T) { if s.selected.Load().Generation != 2 || s.selected.Load().Config != nil { t.Fatal("empty publication lost its generation") } - s.publish(&execution.RuntimeProvider{Generation: 1, ProviderKind: "docker"}) + s.publish(&execution.RuntimeProvider{Generation: 1, ProviderKind: "microsandbox"}) if s.selected.Load().Config != nil { t.Fatal("late old publication resurrected provider") } @@ -266,11 +265,11 @@ func TestManagedObservationSourceKeepsSelectionAcrossReconfiguration(t *testing. if source, kind, err := setup.observationSource(t.Context()); source != nil || kind != "" || !errors.Is(err, runtimeobs.ErrUnavailable) { t.Fatal("unconfigured setup did not return typed unavailability", source, err) } - first := &docker.Provider{} - value.Provider, value.Mode, value.Generation = "docker", "nodes", 2 - setup.publish(&execution.RuntimeProvider{Generation: 2, ProviderKind: "docker", Provider: first}) + first := µsandbox.Provider{} + value.Provider, value.Mode, value.Generation = "microsandbox", "nodes", 2 + setup.publish(&execution.RuntimeProvider{Generation: 2, ProviderKind: "microsandbox", Provider: first}) source, kind, err := setup.observationSource(t.Context()) - if err != nil || source != first || kind != "docker" { + if err != nil || source != first || kind != "microsandbox" { t.Fatal(source, kind, err) } next := µsandbox.Provider{} diff --git a/services/core/deploy/claude/README.md b/services/core/deploy/claude/README.md index 2d7b89d95..a502d7970 100644 --- a/services/core/deploy/claude/README.md +++ b/services/core/deploy/claude/README.md @@ -49,4 +49,4 @@ The pinned Claude Code reports both a request that got no HTTP response, such as | Environment | `OAC_RUNTIME_HOME=/home/runtime/.oac`, `OAC_RUNTIME_CLAUDE_SDK_NODE=/usr/local/bin/node`, `OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT=/opt/claude-sdk/dist/main.js`, `OAC_RUNTIME_INITIALIZATION_DIRECTORY=/environment/initialization`, `OAC_RUNTIME_PACKAGE_DIRECTORY=/environment/packages`, `OAC_RUNTIME_STATE_DIRECTORY=/environment/runtime-state` | | Entry point | `oac-daemon connect --profile default`, working directory `/environment/workspace` | -The build runs the bundle's `runtime_check.js` against its entry point. The distribution copies `/opt/claude-sdk` into the combined Runtime image. Sandboxes run the image with the [Docker sandbox settings](../../../../docs/sandbox-provider.md#docker-adapter). +The build runs the bundle's `runtime_check.js` against its entry point. The distribution copies `/opt/claude-sdk` into the combined Runtime image. diff --git a/services/core/deploy/codex/README.md b/services/core/deploy/codex/README.md index ac2763cbf..35632cab4 100644 --- a/services/core/deploy/codex/README.md +++ b/services/core/deploy/codex/README.md @@ -75,5 +75,3 @@ Codex `0.153.4` reports a provider HTTP 400 as `other` and puts the provider's e | Entry point | `oac-daemon connect --profile default`, working directory `/environment/workspace` | The build fails unless `codex --version` reports the pinned version. The image holds no credentials, workspace data or product software. The distribution copies the Codex executables and resources into the combined Runtime image; see the [maintainer guide](../../../../docs/maintainers.md#runtime-images-and-helpers). - -The [Docker adapter](../../../../docs/sandbox-provider.md#docker-adapter) owns the shared container lifecycle and isolation settings. diff --git a/services/core/deploy/mcode/README.md b/services/core/deploy/mcode/README.md index 82a9073b9..ae8375fe9 100644 --- a/services/core/deploy/mcode/README.md +++ b/services/core/deploy/mcode/README.md @@ -51,4 +51,4 @@ In the workspace profile, the frozen installation's Skills are linked into the S | Environment | `OAC_RUNTIME_HOME=/home/runtime/.oac`, `OAC_RUNTIME_MCODE_NODE`, `OAC_RUNTIME_MCODE_BIN`, `OAC_RUNTIME_MCODE_COMPANION`, `OAC_RUNTIME_INITIALIZATION_DIRECTORY=/environment/initialization`, `OAC_RUNTIME_PACKAGE_DIRECTORY=/environment/packages`, `OAC_RUNTIME_STATE_DIRECTORY=/environment/runtime-state` | | Entry point | `oac-daemon connect --profile default`, working directory `/environment/workspace` | -The build runs the companion's `check.mjs` and the native `--version`. The combined Runtime image uses this image as its base. Sandboxes run it with the [Docker sandbox settings](../../../../docs/sandbox-provider.md#docker-adapter). +The build runs the companion's `check.mjs` and the native `--version`. The combined Runtime image uses this image as its base. diff --git a/services/core/internal/api/core_errors_test.go b/services/core/internal/api/core_errors_test.go index 0473ab004..49e27876b 100644 --- a/services/core/internal/api/core_errors_test.go +++ b/services/core/internal/api/core_errors_test.go @@ -16,7 +16,7 @@ import ( func TestCoreErrorDetailsAreScopedByRouterNotRequestPath(t *testing.T) { shared := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { writeCoreError(w, 409, "sandbox_generation_stale", "Refresh the configuration.", CoreErrorDetails{ - "current_generation": CoreErrorNumber(4), "kinds": CoreErrorStrings("docker", "microsandbox"), + "current_generation": CoreErrorNumber(4), "kinds": CoreErrorStrings("microsandbox", "microsandbox"), }, "expected_generation") }) router := chi.NewRouter() @@ -52,7 +52,7 @@ func TestCoreErrorDetailsAreScopedByRouterNotRequestPath(t *testing.T) { if err := json.Unmarshal(out.Body.Bytes(), &body); err != nil { t.Fatal(err) } - want := map[string]any{"current_generation": float64(4), "kinds": []any{"docker", "microsandbox"}} + want := map[string]any{"current_generation": float64(4), "kinds": []any{"microsandbox", "microsandbox"}} if !reflect.DeepEqual(body["error"]["details"], want) { t.Fatal(body) } @@ -74,7 +74,7 @@ func TestCoreErrorDetailsAreScopedByRouterNotRequestPath(t *testing.T) { } func TestCoreDetailsTypesOmissionAndSnapshot(t *testing.T) { - values := []string{"docker"} + values := []string{"microsandbox"} stringsDetail := CoreErrorStrings(values...) values[0] = "private-request-value" valid := CoreErrorDetails{"label": CoreErrorString("ready"), "number": CoreErrorNumber(1.5), "unset": CoreErrorNull(), "values": stringsDetail, "empty": CoreErrorStrings()} diff --git a/services/core/internal/api/core_store_validation_test.go b/services/core/internal/api/core_store_validation_test.go index ab72175a1..1ca6f5ade 100644 --- a/services/core/internal/api/core_store_validation_test.go +++ b/services/core/internal/api/core_store_validation_test.go @@ -17,6 +17,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/workspacefs" ) func TestCoreStoreValidationFieldsAndPublicFallback(t *testing.T) { @@ -32,7 +33,7 @@ func TestCoreStoreValidationFieldsAndPublicFallback(t *testing.T) { } upperCapacityErr := nodes.UpdateNode(context.Background(), managementProjectID, deployment.NodeUpdate{Name: "node", MaxActive: 1000001, MaxRetained: 8}) capacityErr := nodes.UpdateNode(context.Background(), managementProjectID, deployment.NodeUpdate{Name: "node", MaxActive: 0}) - _, resourceErr := nodes.SetupForSelection(managementProjectID, sandbox.Selection{Provider: "docker", DeploymentSpec: sandbox.DeploymentSpec{}}) + _, resourceErr := nodes.SetupForSelection(managementProjectID, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: sandbox.DeploymentSpec{Workspace: &workspacefs.Declaration{Attachment: workspacefs.AttachmentHostDirectory, UserXAttr: true}}}) for _, tc := range []struct { err error code, param string diff --git a/services/core/internal/api/installation_test.go b/services/core/internal/api/installation_test.go index 10f981f59..5b0957e4f 100644 --- a/services/core/internal/api/installation_test.go +++ b/services/core/internal/api/installation_test.go @@ -63,8 +63,8 @@ func TestDeploymentAddressIsNotInput(t *testing.T) { method, body, code string status int }{ - {http.MethodPost, `{"provider":"docker","core_url":"https://core.example","expected_generation":0}`, `"code":"invalid_request"`, http.StatusBadRequest}, - {http.MethodPut, `{"provider":"docker","core_url":"https://core.example","expected_generation":1}`, `"code":"invalid_request"`, http.StatusBadRequest}, + {http.MethodPost, `{"provider":"microsandbox","core_url":"https://core.example","expected_generation":0}`, `"code":"invalid_request"`, http.StatusBadRequest}, + {http.MethodPut, `{"provider":"microsandbox","core_url":"https://core.example","expected_generation":1}`, `"code":"invalid_request"`, http.StatusBadRequest}, {http.MethodPost, `{"provider":"e2b","expected_generation":0,"credential":{"api_key":"key"},"configuration":{"template":"runtime:build"}}`, `"code":"sandbox_configuration_error"`, http.StatusConflict}, } { request := httptest.NewRequest(test.method, "/core/v1/sandbox/deployment", strings.NewReader(test.body)) diff --git a/services/core/internal/api/runtime_history_test.go b/services/core/internal/api/runtime_history_test.go index deaa5acfd..0ac59538f 100644 --- a/services/core/internal/api/runtime_history_test.go +++ b/services/core/internal/api/runtime_history_test.go @@ -75,7 +75,7 @@ func TestRuntimeHistoryRouteBindsAuthenticatedSessionAndPreservesCoverage(t *tes Capabilities: service.capabilities, Scope: scope, Requested: requested, Resolution: 30 * time.Second, GeneratedAt: requested.End, RetainedFrom: &start, Coverage: []runtimehistory.CoveragePoint{coveragePoint}, - Series: []runtimehistory.Series{{Scope: scope, AllocationID: allocationID, StartedAt: start.Add(-time.Minute), ProviderType: "docker", Points: []runtimehistory.Point{resourcePoint}}}, + Series: []runtimehistory.Series{{Scope: scope, AllocationID: allocationID, StartedAt: start.Add(-time.Minute), ProviderType: "microsandbox", Points: []runtimehistory.Point{resourcePoint}}}, TokenUsage: []runtimehistory.TokenUsagePoint{{Start: start, End: start.Add(30 * time.Second), SampledAt: start.Add(10 * time.Second), InputTokens: 120, OutputTokens: 30}}, } } @@ -117,7 +117,7 @@ func TestRuntimeHistoryRejectsUnsafeQueriesAndFailures(t *testing.T) { handler, _, _ := adminTestHandler(t, historyWith(service)) sessionID := uuid.NewString() for _, query := range []string{ - "?range=", "?range=2h", "?range=7d", "?range=1h&range=1h", "?range=1h&provider=docker", "?range=1h&max_points=60", "?start=1&end=2", + "?range=", "?range=2h", "?range=7d", "?range=1h&range=1h", "?range=1h&provider=microsandbox", "?range=1h&max_points=60", "?start=1&end=2", } { response := runtimeObservationRequest(handler, adminSessionsPath+sessionID+"/runtime-history"+query) if response.Code != http.StatusBadRequest { diff --git a/services/core/internal/api/runtime_observations_test.go b/services/core/internal/api/runtime_observations_test.go index 1ac74c8ee..7f70543ac 100644 --- a/services/core/internal/api/runtime_observations_test.go +++ b/services/core/internal/api/runtime_observations_test.go @@ -97,7 +97,7 @@ func TestRuntimeObservationRoutesRejectQueries(t *testing.T) { sessionID: {Target: runtimeobs.Target{SessionID: sessionID, Mode: runtimeobs.ModeNone}, Status: runtimeobs.StatusUnsupported, Reason: "runtime_mode_not_observable", ResolvedAt: now}, }} handler, _, _ := adminTestHandler(t, observeWith(service)) - invalid := runtimeObservationRequest(handler, adminSessionsPath+sessionID+"/runtime-observation?provider=docker") + invalid := runtimeObservationRequest(handler, adminSessionsPath+sessionID+"/runtime-observation?provider=microsandbox") if invalid.Code != http.StatusBadRequest { t.Fatalf("unsupported query returned %d: %s", invalid.Code, invalid.Body) } @@ -109,7 +109,7 @@ func TestRuntimeObservationResponsePreservesObservedZero(t *testing.T) { sessionID, environmentID := uuid.NewString(), uuid.NewString() value, err := runtimeObservationResponse(runtimeobs.Observation{ Target: runtimeobs.Target{SessionID: sessionID, EnvironmentID: environmentID, Mode: runtimeobs.ModeManaged, Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), DeviceID: uuid.NewString(), AllocationState: "running", ComputePhase: "running", AllocationCreatedAt: now.Add(-time.Hour)}}, - Status: runtimeobs.StatusObserved, ProviderType: "docker", ResolvedAt: now, + Status: runtimeobs.StatusObserved, ProviderType: "microsandbox", ResolvedAt: now, Sample: &runtimeobs.Sample{ObservedAt: now, MemoryUsageBytes: &zeroMemory}, }) if err != nil || value.LifecycleState == nil || *value.LifecycleState != "active" || value.Memory == nil || value.Memory.UsageBytes == nil || *value.Memory.UsageBytes != 0 { diff --git a/services/core/internal/api/sandbox_deployment_changes_test.go b/services/core/internal/api/sandbox_deployment_changes_test.go index 89e20aed3..77c090ea3 100644 --- a/services/core/internal/api/sandbox_deployment_changes_test.go +++ b/services/core/internal/api/sandbox_deployment_changes_test.go @@ -81,7 +81,7 @@ func TestSandboxMutationErrorsExposeOnlyTypedCoreFacts(t *testing.T) { details string }{ {&deployment.GenerationStaleError{CurrentGeneration: 8}, "sandbox_generation_stale", 409, `"current_generation":8`}, - {&deployment.ResetRequiredError{CurrentProvider: "docker", RequestedProvider: "e2b"}, "sandbox_reset_required", 409, `"requested_provider":"e2b"`}, + {&deployment.ResetRequiredError{CurrentProvider: "microsandbox", RequestedProvider: "e2b"}, "sandbox_reset_required", 409, `"requested_provider":"e2b"`}, {&deployment.ResetRequiredError{CurrentProvider: "e2b", RequestedProvider: "e2b"}, "sandbox_reset_required", 409, `"current_provider":"e2b"`}, {&deployment.InUseError{Resources: deployment.Resources{Allocations: 2, Pending: 1}}, "sandbox_in_use", 409, `"allocations":2`}, {deployment.ErrResetInProgress, "sandbox_reset_in_progress", 409, ""}, diff --git a/services/core/internal/api/sandbox_deployment_setup_test.go b/services/core/internal/api/sandbox_deployment_setup_test.go index 26dcfb126..d7628969a 100644 --- a/services/core/internal/api/sandbox_deployment_setup_test.go +++ b/services/core/internal/api/sandbox_deployment_setup_test.go @@ -17,7 +17,7 @@ func TestSandboxDeploymentSetupRequiresAdministratorAndStrictBody(t *testing.T) calls := 0 initialize := func(_ context.Context, input sandbox.Selection) (deployment.View, error) { calls++ - if input.Provider == "microsandbox" { + if calls > 1 { return deployment.View{}, deployment.ErrConflict } return deployment.View{Provider: input.Provider}, nil @@ -29,13 +29,13 @@ func TestSandboxDeploymentSetupRequiresAdministratorAndStrictBody(t *testing.T) token, body string status, calls int }{ - {"caller", `{"provider":"docker","expected_generation":0}`, 401, 0}, - {"node-credential", `{"provider":"docker","expected_generation":0}`, 401, 0}, - {"enrollment-token", `{"provider":"docker","expected_generation":0}`, 401, 0}, - {"administrator", `{"provider":"docker","unexpected":true}`, 400, 0}, - {"administrator", `{"provider":"docker"}`, 400, 0}, - {"administrator", `{"provider":"docker","expected_generation":null}`, 400, 0}, - {"administrator", `{"provider":"docker","expected_generation":0}`, 200, 1}, + {"caller", `{"provider":"microsandbox","expected_generation":0}`, 401, 0}, + {"node-credential", `{"provider":"microsandbox","expected_generation":0}`, 401, 0}, + {"enrollment-token", `{"provider":"microsandbox","expected_generation":0}`, 401, 0}, + {"administrator", `{"provider":"microsandbox","unexpected":true}`, 400, 0}, + {"administrator", `{"provider":"microsandbox"}`, 400, 0}, + {"administrator", `{"provider":"microsandbox","expected_generation":null}`, 400, 0}, + {"administrator", `{"provider":"microsandbox","expected_generation":0}`, 200, 1}, {"administrator", `{"provider":"microsandbox","expected_generation":0}`, 409, 2}, } { request := httptest.NewRequest(http.MethodPost, "/core/v1/sandbox/deployment", strings.NewReader(test.body)) diff --git a/services/core/internal/deployment/allocations_test.go b/services/core/internal/deployment/allocations_test.go index 1d8117c9d..426d6f950 100644 --- a/services/core/internal/deployment/allocations_test.go +++ b/services/core/internal/deployment/allocations_test.go @@ -373,11 +373,11 @@ func TestReserveAllocationReplaysBeforeAdmission(t *testing.T) { func TestReserveAllocationAdmitsAndTakesTheReservedNode(t *testing.T) { key := AllocationKey{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString()} installation, node := uuid.NewString(), uuid.NewString() - specification, err := json.Marshal(testSpecification("docker")) + specification, err := json.Marshal(testSpecification("microsandbox")) if err != nil { t.Fatal(err) } - deployment := placement.Deployment{InstallationID: installation, Provider: "docker", Mode: "nodes", Generation: 7, Specification: specification} + deployment := placement.Deployment{InstallationID: installation, Provider: "microsandbox", Mode: "nodes", Generation: 7, Specification: specification} fresh := func() *fakeReservationTx { return &fakeReservationTx{t: t, loadEnvironment: hostedEnvironment(key.EnvironmentID), findAllocation: func() (Allocation, bool, error) { return Allocation{}, false, nil }, diff --git a/services/core/internal/deployment/placement/placement_test.go b/services/core/internal/deployment/placement/placement_test.go index ae9c7442f..5147a9435 100644 --- a/services/core/internal/deployment/placement/placement_test.go +++ b/services/core/internal/deployment/placement/placement_test.go @@ -64,8 +64,8 @@ func TestRegistryDeclaresPublicOrigin(t *testing.T) { if err := loopback.CheckPublicOrigin("e2b"); !errors.Is(err, ErrPublicURLUnreachable) { t.Fatalf("CheckPublicOrigin(e2b) on loopback = %v", err) } - if err := loopback.CheckPublicOrigin("docker"); err != nil { - t.Fatalf("CheckPublicOrigin(docker) on loopback = %v", err) + if err := loopback.CheckPublicOrigin("microsandbox"); err != nil { + t.Fatalf("CheckPublicOrigin(microsandbox) on loopback = %v", err) } if err := rules(t, providers.Builtin(), publicURL).CheckPublicOrigin("e2b"); err != nil { t.Fatalf("CheckPublicOrigin(e2b) on a public URL = %v", err) @@ -84,7 +84,7 @@ func TestCheckAdmission(t *testing.T) { } return nil }} - valid := Deployment{InstallationID: installation, Provider: "docker", Specification: json.RawMessage(`{"resources":{"cpus":2}}`)} + valid := Deployment{InstallationID: installation, Provider: "microsandbox", Specification: json.RawMessage(`{"resources":{"cpus":2}}`)} with := func(change func(*Deployment)) Deployment { d := valid change(&d) @@ -119,7 +119,7 @@ func TestDecidePlacement(t *testing.T) { ready := func(id string, g uint64, active int64) Node { return Node{ID: id, Online: true, ServingReady: true, ReadyGeneration: generation(g), Active: active, MaxActive: 4, Retained: active, MaxRetained: 4, CoreURL: publicURL} } - nodes := Deployment{InstallationID: "installation", Provider: "docker", Mode: "nodes"} + nodes := Deployment{InstallationID: "installation", Provider: "microsandbox", Mode: "nodes"} with := func(change func(*Deployment)) Deployment { d := nodes change(&d) diff --git a/services/core/internal/deployment/reset_test.go b/services/core/internal/deployment/reset_test.go index c7477b00c..e72c9069d 100644 --- a/services/core/internal/deployment/reset_test.go +++ b/services/core/internal/deployment/reset_test.go @@ -37,7 +37,7 @@ func resetTx(t *testing.T, stored Record, resources Resources, source adminaudit } func resetting(t *testing.T, installation string, clear ResetMode, requestedAt time.Time, deadline *time.Time) Record { - d := webDeployment(t, installation, "docker", 4) + d := webDeployment(t, installation, "microsandbox", 4) d.Reset = &ResetState{Clear: string(clear), RequestedAt: requestedAt, DeadlineAt: deadline} return d } @@ -45,7 +45,7 @@ func resetting(t *testing.T, installation string, clear ResetMode, requestedAt t func TestStartResetDecisions(t *testing.T) { installation := uuid.NewString() admin := adminaudit.WithSource(t.Context(), adminaudit.Source{CredentialID: "admin"}) - idle := webDeployment(t, installation, "docker", 4) + idle := webDeployment(t, installation, "microsandbox", 4) unconfigured := idle unconfigured.Provider = "" seconds := func(v int32) *int32 { return &v } @@ -87,7 +87,7 @@ func TestCancelResetDecisions(t *testing.T) { calls []string }{ "stale generation": {resetting(t, installation, ResetAuto, time.Now(), nil), 3, ErrConflict, nil}, - "no running reset": {webDeployment(t, installation, "docker", 4), 4, nil, nil}, + "no running reset": {webDeployment(t, installation, "microsandbox", 4), 4, nil, nil}, "cancels the reset": {resetting(t, installation, ResetAuto, time.Now(), nil), 4, nil, []string{"CancelReset", "RecordAudit reset_cancel " + installation}}, } { var calls []string @@ -108,7 +108,7 @@ func TestAdvanceResetDeadlineUsesTheStoredSource(t *testing.T) { stored Record calls []string }{ - "no reset": {webDeployment(t, installation, "docker", 4), nil}, + "no reset": {webDeployment(t, installation, "microsandbox", 4), nil}, "deadline ahead": {resetting(t, installation, ResetAuto, time.Now(), &future), nil}, "already force": {resetting(t, installation, ResetForce, time.Now(), nil), nil}, "deadline passed": {resetting(t, installation, ResetAuto, time.Now(), &past), @@ -135,7 +135,7 @@ func TestCompleteResetDecisions(t *testing.T) { resources Resources }{ "stale generation": {running, 3, requestedAt, Resources{}}, - "no running reset": {webDeployment(t, installation, "docker", 4), 4, requestedAt, Resources{}}, + "no running reset": {webDeployment(t, installation, "microsandbox", 4), 4, requestedAt, Resources{}}, "another reset": {running, 4, requestedAt.Add(time.Second), Resources{}}, "owner epoch exhausted": {exhausted, 4, requestedAt, Resources{}}, "resources still allocated": {running, 4, requestedAt, Resources{Allocations: 2, Pending: 1}}, diff --git a/services/core/internal/deployment/rules_test.go b/services/core/internal/deployment/rules_test.go index 2b5d07cd4..c54cf95d5 100644 --- a/services/core/internal/deployment/rules_test.go +++ b/services/core/internal/deployment/rules_test.go @@ -113,13 +113,13 @@ func TestCheckGeneration(t *testing.T) { t.Errorf("checkGeneration(%+v, %s) = %v, want a plain conflict", c.d, c.installation, err) } } - if initialized(Record{InstallationID: installation}) || initialized(Record{Provider: "docker"}) || !initialized(Record{InstallationID: installation, Provider: "docker"}) { + if initialized(Record{InstallationID: installation}) || initialized(Record{Provider: "microsandbox"}) || !initialized(Record{InstallationID: installation, Provider: "microsandbox"}) { t.Fatal("initialized needs both an installation and a provider") } } func TestTypedErrors(t *testing.T) { - if err := error(&ResetRequiredError{CurrentProvider: "docker", RequestedProvider: "e2b"}); !errors.Is(err, ErrConflict) { + if err := error(&ResetRequiredError{CurrentProvider: "microsandbox", RequestedProvider: "e2b"}); !errors.Is(err, ErrConflict) { t.Fatal("ResetRequiredError is not a conflict") } validation := &sandbox.ValidationError{Param: "resources.cpus", Message: "too many"} diff --git a/services/core/internal/deployment/service_test.go b/services/core/internal/deployment/service_test.go index b95b8d12c..e35d25a2c 100644 --- a/services/core/internal/deployment/service_test.go +++ b/services/core/internal/deployment/service_test.go @@ -115,9 +115,9 @@ func TestSetupForSelectionRequiresAReachablePublicURL(t *testing.T) { if err != nil || setup.Provider != "e2b" || setup.Mode != "direct" || setup.InstallationID != id || !setup.UsesCredential { t.Fatalf("E2B with a public URL = %+v, %v", setup, err) } - docker := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")} - if setup, err := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, "http://127.0.0.1:8091").SetupForSelection(id, docker); err != nil || setup.Mode != "nodes" || setup.UsesCredential { - t.Fatalf("Docker with a loopback public URL = %+v, %v", setup, err) + selection := sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")} + if setup, err := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, "http://127.0.0.1:8091").SetupForSelection(id, selection); err != nil || setup.Mode != "nodes" || setup.UsesCredential { + t.Fatalf("microsandbox with a loopback public URL = %+v, %v", setup, err) } } @@ -146,7 +146,7 @@ func TestUnknownProviderIsRejectedBeforeStorage(t *testing.T) { func TestSandboxValidationWrapperPreservesClassification(t *testing.T) { service := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, testPublicURL) installation := "00000000-0000-4000-8000-000000000001" - _, err := service.SetupForSelection(installation, sandbox.Selection{Provider: "docker"}) + _, err := service.SetupForSelection(installation, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: sandbox.DeploymentSpec{Workspace: testSpecification("microsandbox").Workspace}}) var configuration *ConfigurationError if !errors.As(err, &configuration) || !errors.Is(err, ErrInvalidInput) || configuration.Validation == nil || configuration.Validation.Param != "resources.cpus" { t.Fatalf("missing validation metadata: %#v", err) @@ -177,7 +177,7 @@ func TestRegistryLookupFailuresPropagate(t *testing.T) { // configuration the provider no longer decodes is a conflict the administrator // resolves by saving again. func TestSetupReportsDeclarationsAndStoredConfigurationConflicts(t *testing.T) { - stored := webDeployment(t, uuid.NewString(), "docker", 1) + stored := webDeployment(t, uuid.NewString(), "microsandbox", 1) reader := &fakeReader{t: t, deployment: func(context.Context) (Record, error) { return stored, nil }} service := newService(t, &fakeStorage{t: t}, reader, testPublicURL) setup, err := service.Setup(t.Context()) @@ -192,13 +192,13 @@ func TestSetupReportsDeclarationsAndStoredConfigurationConflicts(t *testing.T) { func TestSetupChangeDecisions(t *testing.T) { installation := uuid.NewString() - stored := webDeployment(t, installation, "docker", 2) + stored := webDeployment(t, installation, "microsandbox", 2) load := func() *fakeDeploymentTx { return &fakeDeploymentTx{t: t, loadDeployment: func() (Record, error) { return stored, nil }} } - docker := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker"), ExpectedGeneration: 2} + selection := sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox"), ExpectedGeneration: 2} - stale := docker + stale := selection stale.ExpectedGeneration = 1 for name, call := range map[string]func(*ExecutionOperations) error{ "CheckSetup": func(o *ExecutionOperations) error { return o.CheckSetup(t.Context(), installation, stale) }, @@ -214,7 +214,7 @@ func TestSetupChangeDecisions(t *testing.T) { } } - switched := sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox"), ExpectedGeneration: 2} + switched := sandbox.Selection{Provider: "e2b", Configuration: &e2b.DeploymentConfiguration{APIKey: "synthetic-key", Template: "runtime:" + uuid.NewString()}, ExpectedGeneration: 2} for name, call := range map[string]func(*ExecutionOperations) error{ "CheckSetup": func(o *ExecutionOperations) error { return o.CheckSetup(t.Context(), installation, switched) }, "ClassifyChange": func(o *ExecutionOperations) error { @@ -227,16 +227,16 @@ func TestSetupChangeDecisions(t *testing.T) { }, } { var reset *ResetRequiredError - if err := call(operations(t, testPublicURL, load())); !errors.As(err, &reset) || reset.CurrentProvider != "docker" || reset.RequestedProvider != "microsandbox" || !errors.Is(err, ErrConflict) { + if err := call(operations(t, testPublicURL, load())); !errors.As(err, &reset) || reset.CurrentProvider != "microsandbox" || reset.RequestedProvider != "e2b" || !errors.Is(err, ErrConflict) { t.Errorf("%s switching provider without a reset = %v", name, err) } } - resolved, unchanged, err := operations(t, testPublicURL, load()).ClassifyChange(t.Context(), installation, docker) - if err != nil || !unchanged || resolved.Provider != "docker" || resolved.ExpectedGeneration != 2 { + resolved, unchanged, err := operations(t, testPublicURL, load()).ClassifyChange(t.Context(), installation, selection) + if err != nil || !unchanged || resolved.Provider != "microsandbox" || resolved.ExpectedGeneration != 2 { t.Fatalf("ClassifyChange of the stored selection = %+v, %v, %v", resolved, unchanged, err) } - changed := docker + changed := selection changed.Resources.CPUs = 4 if _, unchanged, err := operations(t, testPublicURL, load()).ClassifyChange(t.Context(), installation, changed); err != nil || unchanged { t.Fatalf("ClassifyChange of a new specification = %v, %v", unchanged, err) @@ -247,9 +247,9 @@ func TestSetupChangeDecisions(t *testing.T) { // credential error whatever the deployment's state. func TestEnrollChecksTheTokenBeforeTheDeployment(t *testing.T) { installation, token := uuid.NewString(), "enrollment-token" - current := webDeployment(t, installation, "docker", 1) - valid := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "node", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), - DeploymentGeneration: 1, SpecificationDigest: testSpecification("docker").Digest("docker"), CoreURL: testPublicURL} + current := webDeployment(t, installation, "microsandbox", 1) + valid := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "node", Credential: strings.Repeat("n", 64), Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), + DeploymentGeneration: 1, SpecificationDigest: testSpecification("microsandbox").Digest("microsandbox"), CoreURL: testPublicURL} receipt := EnrollmentRecord{ID: uuid.NewString(), InstallationID: installation, ExpiresAt: time.Now().Add(10 * time.Minute), MaxActive: new(1), MaxRetained: new(1)} resetting := current resetting.Reset = &ResetState{Clear: "sandboxes", RequestedAt: time.Now()} @@ -348,7 +348,7 @@ func TestAuthenticateNodeChecksTheCredentialFirst(t *testing.T) { } reads := &fakeNodeReads{t: t, loadNode: func(string) (StoredNode, error) { return stored, nil }, - loadDeployment: func() (Record, error) { return webDeployment(t, uuid.NewString(), "docker", 1), nil }, + loadDeployment: func() (Record, error) { return webDeployment(t, uuid.NewString(), "microsandbox", 1), nil }, } if _, err := service(reads).AuthenticateNode(t.Context(), nodeID, credential); !errors.Is(err, ErrNodeCredential) { t.Errorf("node of another installation: %v", err) diff --git a/services/core/internal/deployment/session_archive_test.go b/services/core/internal/deployment/session_archive_test.go index e3991ba6f..9583fe32c 100644 --- a/services/core/internal/deployment/session_archive_test.go +++ b/services/core/internal/deployment/session_archive_test.go @@ -16,7 +16,7 @@ import ( ) func TestCheckArchiveDeployment(t *testing.T) { - managed := Record{InstallationID: "installation", Provider: "docker", Generation: 4} + managed := Record{InstallationID: "installation", Provider: "microsandbox", Generation: 4} for name, test := range map[string]struct { change func(*Record) want error @@ -226,7 +226,7 @@ func archiveOperations(t *testing.T, tx *fakeArchiveTx, locked sessions.LockedSe } func TestArchiveSession(t *testing.T) { - managed := Record{InstallationID: "installation", Provider: "docker", Generation: 1} + managed := Record{InstallationID: "installation", Provider: "microsandbox", Generation: 1} hosted := &sessions.Environment{ID: "environment", Status: "connected", Configuration: json.RawMessage(`{"type":"openai_hosted"}`)} expired := &sessions.Environment{ID: "environment", Status: "expired", Configuration: hosted.Configuration} live := &Allocation{ID: "allocation", DeviceID: "device", ProviderKey: "installation", State: "running"} @@ -282,7 +282,7 @@ func TestArchiveSession(t *testing.T) { func TestArchiveResetSession(t *testing.T) { requested := time.Unix(100, 0) resetting := func(clear ResetMode) Record { - return Record{InstallationID: "installation", Provider: "docker", Generation: 1, Reset: &ResetState{Clear: string(clear), RequestedAt: requested}} + return Record{InstallationID: "installation", Provider: "microsandbox", Generation: 1, Reset: &ResetState{Clear: string(clear), RequestedAt: requested}} } hosted := &sessions.Environment{ID: "environment", Status: "connected", Configuration: json.RawMessage(`{"type":"openai_hosted"}`)} failed := &sessions.Environment{ID: "environment", Status: "failed", Configuration: hosted.Configuration} diff --git a/services/core/internal/deployment/workspace_configuration_test.go b/services/core/internal/deployment/workspace_configuration_test.go index 27891db54..b67eec85f 100644 --- a/services/core/internal/deployment/workspace_configuration_test.go +++ b/services/core/internal/deployment/workspace_configuration_test.go @@ -12,7 +12,7 @@ import ( func TestWorkspaceConfigurationUsesStoredGenerationWithoutNativeProvider(t *testing.T) { declaration := workspacefs.Declaration{Attachment: workspacefs.AttachmentHostDirectory, UserXAttr: true} - for _, provider := range []string{"", "docker", "microsandbox"} { + for _, provider := range []string{"", "microsandbox"} { t.Run(provider, func(t *testing.T) { record := Record{InstallationID: uuid.NewString(), Specification: []byte(`{}`)} if provider != "" { @@ -20,11 +20,7 @@ func TestWorkspaceConfigurationUsesStoredGenerationWithoutNativeProvider(t *test } service := newService(t, &fakeStorage{t: t}, &fakeReader{t: t, deployment: func(context.Context) (Record, error) { return record, nil }}, testPublicURL) err := service.ValidateWorkspaceConfiguration(t.Context(), declaration) - if provider == "docker" { - if !errors.Is(err, workspacefs.ErrUnsupported) { - t.Fatal("incompatible provider accepted", err) - } - } else if err != nil { + if err != nil { t.Fatal(err) } }) diff --git a/services/core/internal/execution/runtime_lost_compute_test.go b/services/core/internal/execution/runtime_lost_compute_test.go index d30cf62e1..572fed5d9 100644 --- a/services/core/internal/execution/runtime_lost_compute_test.go +++ b/services/core/internal/execution/runtime_lost_compute_test.go @@ -74,12 +74,12 @@ type lostComputeFixture struct { // newLostComputeFixture commits a settled running incarnation of a Session // whose completed initialization and native identity are retained. -func newLostComputeFixture(t *testing.T, external bool, seed ...string) lostComputeFixture { +func newLostComputeFixture(t *testing.T, seed ...string) lostComputeFixture { t.Helper() provider := &lostComputeProvider{retentionProvider: retentionProvider{bootstraps: make(chan sandbox.Bootstrap, 2)}, observe: func(sandbox.Compute) (sandbox.ComputeState, error) { return sandbox.ComputeState{}, sandbox.ErrNotFound }} - f := lostComputeFixture{workspaceSettlementFixture: workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t, external)), provider: provider} + f := lostComputeFixture{workspaceSettlementFixture: workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t)), provider: provider} r, session := f.lifecycle, f.session owner, err := r.provision(t.Context(), session.TenantID, session.Environment.ID, r.config.InstallationID) if err != nil { @@ -174,7 +174,7 @@ func (f lostComputeFixture) demand(t *testing.T) { func TestConfirmedLostRunningComputeContinuesRetainedSession(t *testing.T) { for name, observe := range lostObservations { t.Run(name, func(t *testing.T) { - f := newLostComputeFixture(t, true) + f := newLostComputeFixture(t) f.provider.observe = observe r := f.lifecycle binding, err := f.storage.Get(t.Context(), f.session.TenantID, f.owner.EnvironmentID) @@ -220,7 +220,6 @@ func TestConfirmedLostRunningComputeContinuesRetainedSession(t *testing.T) { func TestUnconfirmedLostComputeKeepsOwnership(t *testing.T) { type negative struct { name string - owned bool seed []string observe func(sandbox.Compute) (sandbox.ComputeState, error) killErr error @@ -256,7 +255,6 @@ func TestUnconfirmedLostComputeKeepsOwnership(t *testing.T) { {name: "unknown_creation", seed: []string{`UPDATE runtime_allocations SET create_settled=false WHERE id=$3`}, want: sandbox.ErrNotFound}, {name: "no_compute_id", seed: []string{`UPDATE runtime_allocations SET compute_state='{"current":{"Name":"old-compute"}}' WHERE id=$3`}, want: sandbox.ErrOwnership}, {name: "restore_intent", seed: []string{`UPDATE runtime_allocations SET compute_state='{"current":{"Name":"old-compute","ID":"old-compute-id"},"target":{"Name":"next","Generation":1},"restore_id":"attempt"}' WHERE id=$3`}, want: sandbox.ErrNotFound}, - {name: "owned_workspace", owned: true, want: sandbox.ErrNotFound}, {name: "initialization_incomplete", seed: []string{`UPDATE environments SET initialization='running' WHERE id=$1`}, want: sandbox.ErrNotFound, initial: "running"}, {name: "terminal_environment", seed: []string{`UPDATE environments SET status='failed' WHERE id=$1`}, want: sandbox.ErrNotFound, status: "failed"}, {name: "native_history_undeclared", seed: []string{`UPDATE devices SET supported_agent_kinds='[{"kind":"codex","available":true,"capabilities":{}}]' WHERE id=$2`}, want: sandbox.ErrNotFound}, @@ -275,7 +273,7 @@ func TestUnconfirmedLostComputeKeepsOwnership(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - f := newLostComputeFixture(t, !test.owned, test.seed...) + f := newLostComputeFixture(t, test.seed...) r := f.lifecycle if test.observe != nil { f.provider.observe = test.observe @@ -335,7 +333,7 @@ func TestLostComputeReleaseSerializesWithConcurrentIntent(t *testing.T) { for name, observe := range lostObservations { t.Run(name, func(t *testing.T) { t.Run("restart_after_kill", func(t *testing.T) { - f := newLostComputeFixture(t, true) + f := newLostComputeFixture(t) f.provider.observe = observe r := f.lifecycle // The first owner confirms cleanup, then stops before the release commit. @@ -376,7 +374,7 @@ func TestLostComputeReleaseSerializesWithConcurrentIntent(t *testing.T) { {name: "deletion", statement: `UPDATE sessions SET deleted_at=clock_timestamp() WHERE id=(SELECT session_id FROM environments WHERE id=$1)`, want: sessions.ErrNotFound}, } { t.Run(test.name, func(t *testing.T) { - f := newLostComputeFixture(t, true) + f := newLostComputeFixture(t) f.provider.observe = observe f.provider.onKill = func() { if _, err := f.pool.Exec(t.Context(), test.statement, f.owner.EnvironmentID); err != nil { diff --git a/services/core/internal/execution/runtime_manager_test.go b/services/core/internal/execution/runtime_manager_test.go index a01528e43..1e991a193 100644 --- a/services/core/internal/execution/runtime_manager_test.go +++ b/services/core/internal/execution/runtime_manager_test.go @@ -23,7 +23,7 @@ func (heldLease) Close(context.Context) error { return nil } func testRuntimeManager(t *testing.T) *runtimeManager { t.Helper() ctx, cancel := context.WithCancel(t.Context()) - m := &runtimeManager{lease: heldLease{}, config: RuntimeProvider{ProviderKind: "docker", Mode: "nodes", Provider: &drainFixtureProvider{}}, loadDeployment: func(context.Context) (*RuntimeProvider, error) { return nil, nil }, ctx: ctx, cancel: cancel, nodes: make(map[string]*runtimeNode), failed: make(chan error, 1), inventory: make(chan struct{}, 1)} + m := &runtimeManager{lease: heldLease{}, config: RuntimeProvider{ProviderKind: "microsandbox", Mode: "nodes", Provider: &drainFixtureProvider{}}, loadDeployment: func(context.Context) (*RuntimeProvider, error) { return nil, nil }, ctx: ctx, cancel: cancel, nodes: make(map[string]*runtimeNode), failed: make(chan error, 1), inventory: make(chan struct{}, 1)} t.Cleanup(func() { m.stop(); m.drain() }) return m } diff --git a/services/core/internal/execution/runtime_observation_test.go b/services/core/internal/execution/runtime_observation_test.go index 502f33fd2..8c8670373 100644 --- a/services/core/internal/execution/runtime_observation_test.go +++ b/services/core/internal/execution/runtime_observation_test.go @@ -30,7 +30,7 @@ func TestComputeFailureObservationUsesCommittedReceipt(t *testing.T) { } else { provider.killError = sandbox.ErrComputeUnconfirmed } - fixture := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t, true)) + fixture := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t)) r, session := fixture.lifecycle, fixture.session initial, err := r.provision(t.Context(), session.TenantID, session.Environment.ID, r.config.InstallationID) if err != nil { @@ -93,7 +93,7 @@ func TestComputeFailureObservationUsesCommittedReceipt(t *testing.T) { } func TestRunningComputeWakesScheduler(t *testing.T) { - fixture := workspaceSettlementFixtureWithSetup(t, &observationCaptureProvider{}, workspaceNodeSetup(t, true)) + fixture := workspaceSettlementFixtureWithSetup(t, &observationCaptureProvider{}, workspaceNodeSetup(t)) r, session := fixture.lifecycle, fixture.session wake := make(chan struct{}, 1) r.scheduleWake = wake diff --git a/services/core/internal/execution/runtime_replacement_test.go b/services/core/internal/execution/runtime_replacement_test.go index 358d1713b..ae4edbc0f 100644 --- a/services/core/internal/execution/runtime_replacement_test.go +++ b/services/core/internal/execution/runtime_replacement_test.go @@ -198,7 +198,7 @@ func TestRetainedEnvironmentDemandRecreatesCompute(t *testing.T) { for _, demand := range []string{"input", "live_file"} { t.Run(demand, func(t *testing.T) { provider := &retentionProvider{} - fixture := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t, true)) + fixture := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t)) r, session := fixture.lifecycle, fixture.session owner, err := r.provision(t.Context(), session.TenantID, session.Environment.ID, r.config.InstallationID) if err != nil { @@ -325,26 +325,19 @@ func TestRetainedEnvironmentDemandRecreatesCompute(t *testing.T) { } } -// workspaceNodeSetup deploys microsandbox, which runs every Environment on external storage, or, without storage, docker. -func workspaceNodeSetup(t *testing.T, external bool) func(Owner, *deployment.Service, deployment.Reader) (string, string) { +// workspaceNodeSetup deploys microsandbox, which runs every Environment on external storage. +func workspaceNodeSetup(t *testing.T) func(Owner, *deployment.Service, deployment.Reader) (string, string) { return func(owner Owner, service *deployment.Service, reader deployment.Reader) (string, string) { installation := uuid.NewString() if err := owner.Deployment.Claim(t.Context(), installation); err != nil { t.Fatal(err) } specification := sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048, RootDiskMiB: 8192}, Workspace: &workspacefs.Declaration{Attachment: workspacefs.AttachmentHostDirectory, UserXAttr: true}} - provider := "microsandbox" - if !external { - provider, specification = "docker", sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048}} - } - view, err := owner.Deployment.Initialize(t.Context(), installation, sandbox.Selection{Provider: provider, DeploymentSpec: specification}) + view, err := owner.Deployment.Initialize(t.Context(), installation, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: specification}) if err != nil { t.Fatal(err) } retained, checkpoint := 2, &sandbox.CheckpointCompatibility{ArtifactDomain: "fixture-store", ExecutionClass: "fixture-runtime"} - if !external { - retained, checkpoint = 1, nil - } token, err := service.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(1), MaxRetained: &retained}) if err != nil { t.Fatal(err) @@ -368,21 +361,3 @@ func workspaceNodeSetup(t *testing.T, external bool) func(Owner, *deployment.Ser } } - -func TestProvisionRejectsRetainedBindingOnProviderWithoutStorage(t *testing.T) { - provider := &retentionProvider{bootstraps: make(chan sandbox.Bootstrap, 1)} - f := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t, false)) - r, s := f.lifecycle, f.session - if _, err := r.workspaces.Ensure(t.Context(), s.TenantID, s.Environment.ID, r.config.WorkspaceRequirements, 0); err != nil { - t.Fatal(err) - } - if _, err := r.provision(t.Context(), s.TenantID, s.Environment.ID, r.config.InstallationID); !errors.Is(err, workspacefs.ErrUnsupported) { - t.Fatal("binding silently dropped", err) - } - if provider.creates != 0 { - t.Fatal("incompatible compute was created") - } - if _, err := r.reader.EnvironmentAllocation(t.Context(), deployment.AllocationKey{TenantID: s.TenantID, EnvironmentID: s.Environment.ID}); !errors.Is(err, deployment.ErrNotFound) { - t.Fatal("incompatible compute reserved", err) - } -} diff --git a/services/core/internal/execution/runtime_restore_attempt_test.go b/services/core/internal/execution/runtime_restore_attempt_test.go index d0e42b359..b567acdca 100644 --- a/services/core/internal/execution/runtime_restore_attempt_test.go +++ b/services/core/internal/execution/runtime_restore_attempt_test.go @@ -53,7 +53,7 @@ func TestRestoreAttemptRecoveryUsesExactClosedEvidence(t *testing.T) { t.Run(outcome, func(t *testing.T) { stop := errors.New("native request captured") provider := &restoreAttemptProvider{outcome: outcome, stop: stop} - f := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t, true)) + f := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t)) r, session := f.lifecycle, f.session owner, err := r.provision(t.Context(), session.TenantID, session.Environment.ID, r.config.InstallationID) if err != nil { @@ -108,7 +108,7 @@ func TestRestoreAttemptRecoveryUsesExactClosedEvidence(t *testing.T) { func TestUnusableCheckpointEndsRetentionAndIsNeverRestoredAgain(t *testing.T) { provider := &restoreAttemptProvider{outcome: "unusable"} - f := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t, true)) + f := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t)) r, session := f.lifecycle, f.session owner, err := r.provision(t.Context(), session.TenantID, session.Environment.ID, r.config.InstallationID) if err != nil { @@ -141,7 +141,7 @@ func TestCheckpointTransferRoutesBeforeTargetIO(t *testing.T) { t.Run(operation, func(t *testing.T) { stop := errors.New("target Resume captured") provider := &restoreAttemptProvider{stop: stop} - f := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t, true)) + f := workspaceSettlementFixtureWithSetup(t, provider, workspaceNodeSetup(t)) r, session := f.lifecycle, f.session owner, err := r.provision(t.Context(), session.TenantID, session.Environment.ID, r.config.InstallationID) if err != nil { @@ -248,7 +248,7 @@ func (p *cleanupReceiptProvider) Suspend(_ context.Context, q sandbox.SuspendReq } func TestLostCaptureReceiptCanStillBeDeleted(t *testing.T) { p := &cleanupReceiptProvider{} - f := workspaceSettlementFixtureWithSetup(t, p, workspaceNodeSetup(t, true)) + f := workspaceSettlementFixtureWithSetup(t, p, workspaceNodeSetup(t)) r, s := f.lifecycle, f.session owner, err := r.provision(t.Context(), s.TenantID, s.Environment.ID, r.config.InstallationID) if err != nil { diff --git a/services/core/internal/execution/runtime_workspace_settlement_test.go b/services/core/internal/execution/runtime_workspace_settlement_test.go index 824fc5afa..2eeb97193 100644 --- a/services/core/internal/execution/runtime_workspace_settlement_test.go +++ b/services/core/internal/execution/runtime_workspace_settlement_test.go @@ -90,7 +90,7 @@ func workspaceSettlementFixtureWithSetup(t *testing.T, provider sandbox.SandboxP owner := Owner{Lease: lease, Deployment: operations, Sessions: sessionExecution(t, lease)} var installation, nodeID string if setup == nil { - installation, nodeID = workspaceNodeSetup(t, true)(owner, deployments, reader) + installation, nodeID = workspaceNodeSetup(t)(owner, deployments, reader) } else { installation, nodeID = setup(owner, deployments, reader) } diff --git a/services/core/internal/execution/sandbox_deployment_drain_test.go b/services/core/internal/execution/sandbox_deployment_drain_test.go index 4c7ff3d7d..6c38d9a90 100644 --- a/services/core/internal/execution/sandbox_deployment_drain_test.go +++ b/services/core/internal/execution/sandbox_deployment_drain_test.go @@ -14,7 +14,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/node" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -92,7 +92,7 @@ func testLifecycleCancellationPreservesLease(t *testing.T, mode string) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "microsandbox", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return configuration, nil }, unusedPreparation(t))) if err != nil { t.Fatal(err) @@ -230,7 +230,7 @@ func TestSandboxDeploymentDrainFailureCannotReactivate(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "microsandbox", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return configuration, nil }, unusedPreparation(t))) if err != nil { t.Fatal(err) @@ -256,7 +256,7 @@ func TestSandboxDeploymentDrainFailureCannotReactivate(t *testing.T) { if err := m.pauseDeployment(t.Context()); err == nil { t.Fatal("repeated drain forgot its failure") } - if err := m.activateDeployment(t.Context(), deployment.View{InstallationID: id, Generation: 1, Mode: "nodes", Provider: "docker"}); err == nil { + if err := m.activateDeployment(t.Context(), deployment.View{InstallationID: id, Generation: 1, Mode: "nodes", Provider: "microsandbox"}); err == nil { t.Fatal("failed drain reopened the provider") } if _, _, err := m.enter(t.Context()); err == nil { diff --git a/services/core/internal/execution/sandbox_deployment_setup_test.go b/services/core/internal/execution/sandbox_deployment_setup_test.go index 0337c3199..9666a1602 100644 --- a/services/core/internal/execution/sandbox_deployment_setup_test.go +++ b/services/core/internal/execution/sandbox_deployment_setup_test.go @@ -12,8 +12,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/node" "github.com/google/uuid" ) @@ -24,7 +24,7 @@ func TestDeferredSandboxDeploymentLoadsOnceBeforeNodeCreation(t *testing.T) { id := uuid.NewString() var selected atomic.Bool var loads atomic.Int32 - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "microsandbox", Mode: "nodes", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { loads.Add(1) if !selected.Load() { @@ -94,7 +94,7 @@ func TestDeferredSandboxProviderFailureKeepsRecoveryAvailable(t *testing.T) { id := uuid.NewString() available := false loadErr := ErrExecutionUnavailable - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "microsandbox", Mode: "nodes", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { if !available { return nil, loadErr @@ -125,7 +125,7 @@ func TestRejectedSandboxCandidatePreservesActiveGeneration(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "microsandbox", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} rejected := errors.New("candidate provider unavailable") m, err := newRuntimeManager(Owner{Lease: heldLease{}}, unitDeploymentService(t), nil, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return config, nil }, func(context.Context, deployment.Setup) (PreparedRuntimeDeployment, error) { @@ -199,7 +199,7 @@ func TestCommittedSandboxCandidatePublishesAfterShutdown(t *testing.T) { if err := m.pauseDeployment(t.Context()); err != nil { t.Fatal(err) } - config := &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} var published *RuntimeProvider candidate := PreparedRuntimeDeployment{Config: config, Publish: func(value *RuntimeProvider) { published = value }} m.stop() diff --git a/services/core/internal/execution/sandbox_deployment_switch_test.go b/services/core/internal/execution/sandbox_deployment_switch_test.go index 6f9a1d14d..b465a76f4 100644 --- a/services/core/internal/execution/sandbox_deployment_switch_test.go +++ b/services/core/internal/execution/sandbox_deployment_switch_test.go @@ -9,7 +9,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/node" "github.com/google/uuid" ) @@ -18,7 +18,7 @@ func TestSandboxManagerSwitchDrainsBeforeDirectActivation(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "microsandbox", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return config, nil }, unusedPreparation(t))) if err != nil { t.Fatal(err) @@ -62,7 +62,7 @@ func TestSandboxManagerSwitchDrainsBeforeDirectActivation(t *testing.T) { case <-time.After(time.Second): t.Fatal("switch drain blocked") } - config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} if err := m.activateDeployment(t.Context(), deployment.View{InstallationID: id, Generation: 2, Mode: "direct", Provider: "e2b"}); err != nil { t.Fatal(err) } @@ -100,7 +100,7 @@ func TestSandboxManagerCancelledSwitchCannotResumeBeforeDrain(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "microsandbox", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return config, nil }, unusedPreparation(t))) if err != nil { t.Fatal(err) @@ -129,7 +129,7 @@ func TestSandboxManagerCancelledSwitchCannotResumeBeforeDrain(t *testing.T) { m.mu.Lock() originalDrain := m.switchDrained m.mu.Unlock() - config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)} + config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)} expected := deployment.View{InstallationID: id, Generation: 2, Mode: "direct", Provider: "e2b"} ctx, cancel = context.WithTimeout(t.Context(), 10*time.Millisecond) if err := m.activateDeployment(ctx, expected); !errors.Is(err, context.DeadlineExceeded) { @@ -158,7 +158,7 @@ func TestSandboxActivationCannotBypassOutstandingDrain(t *testing.T) { m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return nil, nil }, func(_ context.Context, setup deployment.Setup) (PreparedRuntimeDeployment, error) { - return PreparedRuntimeDeployment{Config: &RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)}}, nil + return PreparedRuntimeDeployment{Config: &RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)}}, nil })) if err != nil { t.Fatal(err) diff --git a/services/core/internal/execution/sandbox_generations_test.go b/services/core/internal/execution/sandbox_generations_test.go index 0f4b12432..2d562c756 100644 --- a/services/core/internal/execution/sandbox_generations_test.go +++ b/services/core/internal/execution/sandbox_generations_test.go @@ -9,8 +9,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/node" "github.com/google/uuid" ) @@ -29,7 +29,7 @@ func TestE2BReplacementVerifiesTwiceAndNeverPublishesFailedCommit(t *testing.T) } hub := node.NewHub(node.HubOptions{}) defer hub.Close() - provider := hub.Proxy(uuid.NewString(), docker.Operations(), 1) + provider := hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1) verifyCalls, published, fenced, released := 0, 0, 0, 0 var rejectAt int var rejection error = sandbox.ErrCredentialOwnership diff --git a/services/core/internal/execution/sandbox_reset_test.go b/services/core/internal/execution/sandbox_reset_test.go index 0af1e93a7..5374cacb5 100644 --- a/services/core/internal/execution/sandbox_reset_test.go +++ b/services/core/internal/execution/sandbox_reset_test.go @@ -16,8 +16,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/node" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" @@ -88,7 +88,7 @@ func TestSandboxResetPageTimeoutRecoversCommittedOwner(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)}, nil + return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)}, nil }, unusedPreparation(t)) m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), config) if err != nil { @@ -202,7 +202,7 @@ func TestSandboxResetPublishesCommittedGenerationWithoutReading(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)}, nil + return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)}, nil }, unusedPreparation(t)) var published []uint64 config.PublishUnconfigured = func(generation uint64) { diff --git a/services/core/internal/execution/sandbox_snapshot_budget_test.go b/services/core/internal/execution/sandbox_snapshot_budget_test.go index eba65917a..3b2d6e894 100644 --- a/services/core/internal/execution/sandbox_snapshot_budget_test.go +++ b/services/core/internal/execution/sandbox_snapshot_budget_test.go @@ -11,7 +11,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/node" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -72,7 +72,7 @@ func TestSandboxResetSnapshotFitsPageBudget(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), docker.Operations(), 1)}, nil + return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), microsandbox.Operations(), 1)}, nil }, unusedPreparation(t)) m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), configuration) if err != nil { diff --git a/services/core/internal/persistence/postgres/deploymentpg/generations_test.go b/services/core/internal/persistence/postgres/deploymentpg/generations_test.go index e25f7e389..f1b0e713c 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/generations_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/generations_test.go @@ -94,7 +94,7 @@ func TestE2BChangeClassifierOmittedKeyAndExplicitSameKey(t *testing.T) { func TestGenerationPinRetainsOfflineZeroResourceFallback(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) node := f.enroll(t, view, deployment.Capacity{MaxActive: 4, MaxRetained: 16}) f.connect(t, node.NodeID) if _, err := f.pool.Exec(t.Context(), `UPDATE runtime_nodes SET connection_id=NULL WHERE id=$1`, node.NodeID); err != nil { @@ -148,7 +148,7 @@ func TestGenerationPinRetainsOfflineZeroResourceFallback(t *testing.T) { func TestConcurrentUpdatesAtOneGenerationHaveOneWinner(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - input := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")} + input := sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")} id, view := f.initialize(t, changes, input) type outcome struct { view deployment.View @@ -265,7 +265,7 @@ func TestE2BCredentialReplacementSealsTheNewGeneration(t *testing.T) { func TestRejectedChangeLeavesDeploymentUnchanged(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - input := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")} + input := sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")} id, view := f.initialize(t, changes, input) // The ready node pins the current generation, so a committed change also // retains it. @@ -282,9 +282,9 @@ func TestRejectedChangeLeavesDeploymentUnchanged(t *testing.T) { if after := generationsState(t, f); after != before { t.Fatal("failed audit write left the change behind", after) } - other := sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox"), ExpectedGeneration: view.Generation} + other := sandbox.Selection{Provider: "e2b", DeploymentSpec: testSpecification("e2b"), ExpectedGeneration: view.Generation, Configuration: &e2b.DeploymentConfiguration{APIKey: "synthetic-key", Template: "runtime:" + uuid.NewString()}} var reset *deployment.ResetRequiredError - if _, err := changes.Update(admin(t), id, other); !errors.As(err, &reset) || reset.CurrentProvider != "docker" || reset.RequestedProvider != "microsandbox" { + if _, err := changes.Update(admin(t), id, other); !errors.As(err, &reset) || reset.CurrentProvider != "microsandbox" || reset.RequestedProvider != "e2b" { t.Fatal("backend change without a reset", err) } if after := generationsState(t, f); after != before { diff --git a/services/core/internal/persistence/postgres/deploymentpg/host_history_test.go b/services/core/internal/persistence/postgres/deploymentpg/host_history_test.go index ed92fad46..d199aa810 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/host_history_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/host_history_test.go @@ -15,13 +15,13 @@ import ( func hostHistoryPtr[T any](value T) *T { return &value } -// hostHistoryNode initializes a Docker deployment and connects one enrolled +// hostHistoryNode initializes a microsandbox deployment and connects one enrolled // node, returning it with its connection and the owner epoch. func hostHistoryNode(t *testing.T) (fixture, string, string, uint64) { t.Helper() f := newFixture(t) changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) node := f.enroll(t, view, deployment.Capacity{MaxActive: 2, MaxRetained: 8}) connection := f.connect(t, node.NodeID) epoch, err := f.adapter.OwnerEpoch(t.Context()) diff --git a/services/core/internal/persistence/postgres/deploymentpg/idle_policy_test.go b/services/core/internal/persistence/postgres/deploymentpg/idle_policy_test.go index 1befe0ccf..52d700efa 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/idle_policy_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/idle_policy_test.go @@ -53,14 +53,9 @@ func TestIdlePolicyChangesCurrentAllocationsWithoutRollout(t *testing.T) { } } -func TestIdlePolicyRejectsUnsupportedAndUnconfiguredDeployment(t *testing.T) { +func TestIdlePolicyRejectsUnconfiguredDeployment(t *testing.T) { f := newFixture(t) if _, err := f.service.UpdateIdleSeconds(admin(t), 60); !errors.Is(err, deployment.ErrNotConfigured) { t.Fatal(err) } - changes, _ := f.execution(t) - f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) - if _, err := f.service.UpdateIdleSeconds(admin(t), 60); !errors.Is(err, deployment.ErrInvalidInput) { - t.Fatal(err) - } } diff --git a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go index a41edffb0..686d9b20f 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go @@ -114,28 +114,6 @@ func TestEnrollmentDerivesUnsetLimitsFromHost(t *testing.T) { } } -// Docker never suspends a sandbox, so every write stores its retained limit as its active limit. -func TestDockerRetainedLimitFollowsActive(t *testing.T) { - f := newFixture(t) - changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) - token, err := f.service.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(3), MaxRetained: new(1)}) - if err != nil { - t.Fatal(err) - } - current := nodesEnrollment(view, "Docker", "d") - if identity, err := f.service.Enroll(t.Context(), token.Token, current); err != nil || identity.MaxRetained != 3 { - t.Fatal("enrollment kept a separate Docker retained limit", identity, err) - } - if err := f.service.UpdateNode(t.Context(), current.NodeID, deployment.NodeUpdate{Name: "Docker", MaxActive: 5, MaxRetained: 12}); err != nil { - t.Fatal(err) - } - nodes, err := f.service.ListNodes(t.Context()) - if err != nil || len(nodes) != 1 || nodes[0].MaxActive != 5 || nodes[0].MaxRetained != 5 { - t.Fatal("node list kept a separate Docker retained limit", nodes, err) - } -} - // An unknown or consumed token and a wrong node credential are rejected before // any deployment state or enrollment input is judged, so they reveal nothing. func TestNodeCredentialPrecedesDeploymentState(t *testing.T) { @@ -143,7 +121,7 @@ func TestNodeCredentialPrecedesDeploymentState(t *testing.T) { changes, _ := f.execution(t) unknown := strings.Repeat("u", 64) // Before initialization the deployment is unavailable. - early := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "early", Credential: strings.Repeat("e", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), + early := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "early", Credential: strings.Repeat("e", 64), Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), SpecificationDigest: strings.Repeat("d", 64), DeploymentGeneration: 1, CoreURL: fixturePublicURL} if _, err := f.service.Enroll(t.Context(), unknown, early); !errors.Is(err, deployment.ErrNodeCredential) { t.Fatal("uninitialized deployment judged before the token", err) @@ -154,7 +132,7 @@ func TestNodeCredentialPrecedesDeploymentState(t *testing.T) { if _, err := f.service.AuthenticateNode(t.Context(), early.NodeID, early.Credential); !errors.Is(err, deployment.ErrNodeCredential) { t.Fatal("unknown node authenticated", err) } - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) consumed, err := f.service.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(1), MaxRetained: new(1)}) if err != nil { t.Fatal(err) @@ -172,7 +150,7 @@ func TestNodeCredentialPrecedesDeploymentState(t *testing.T) { change func(*machineapi.NodeEnrollment) want error }{ - {"provider", func(e *machineapi.NodeEnrollment) { e.Provider = "microsandbox" }, deployment.ErrInvalidInput}, + {"provider", func(e *machineapi.NodeEnrollment) { e.Provider = "e2b" }, deployment.ErrInvalidInput}, {"generation", func(e *machineapi.NodeEnrollment) { e.DeploymentGeneration++ }, deployment.ErrSpecificationMismatch}, {"specification", func(e *machineapi.NodeEnrollment) { e.SpecificationDigest = strings.Repeat("0", 64) }, deployment.ErrSpecificationMismatch}, {"address", func(e *machineapi.NodeEnrollment) { e.CoreURL = "https://other.example" }, deployment.ErrNodeAddressMismatch}, @@ -224,7 +202,7 @@ func TestNodeCredentialPrecedesDeploymentState(t *testing.T) { func TestEnrollmentAddressMismatchKeepsToken(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) token, err := f.service.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(1), MaxRetained: new(1)}) if err != nil { t.Fatal(err) @@ -265,7 +243,7 @@ func TestEnrollmentAddressMismatchKeepsToken(t *testing.T) { func TestEnrollmentReplay(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) token, err := f.service.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(1), MaxRetained: new(1)}) if err != nil { t.Fatal(err) diff --git a/services/core/internal/persistence/postgres/deploymentpg/presence_test.go b/services/core/internal/persistence/postgres/deploymentpg/presence_test.go index adba97ccd..d10084d89 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/presence_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/presence_test.go @@ -25,13 +25,13 @@ func presenceContext(t *testing.T) context.Context { return ctx } -// presenceFixture initializes a Docker deployment and enrolls two nodes, neither +// presenceFixture initializes a microsandbox deployment and enrolls two nodes, neither // connected, and returns them with the current owner epoch. func presenceFixture(t *testing.T) (fixture, string, string, uint64) { t.Helper() f := newFixture(t) changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) node := f.enroll(t, view, deployment.Capacity{MaxActive: 1, MaxRetained: 1}) other := f.enroll(t, view, deployment.Capacity{MaxActive: 1, MaxRetained: 1}) epoch, err := f.adapter.OwnerEpoch(t.Context()) @@ -229,7 +229,7 @@ func TestNodePresenceDisconnectWaitsForCommit(t *testing.T) { func TestNodeStaleEpochCannotReplaceCurrentConnection(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) node := f.enroll(t, view, deployment.Capacity{MaxActive: 1, MaxRetained: 4}) epoch, err := f.adapter.OwnerEpoch(t.Context()) if err != nil { @@ -258,7 +258,7 @@ func TestNodeStaleEpochCannotReplaceCurrentConnection(t *testing.T) { func TestNodeStatusUsesAuthenticatedFreshPresence(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) node := f.enroll(t, view, deployment.Capacity{MaxActive: 1, MaxRetained: 4}) status := func(connected, ready bool) { t.Helper() diff --git a/services/core/internal/persistence/postgres/deploymentpg/reset_test.go b/services/core/internal/persistence/postgres/deploymentpg/reset_test.go index fde3c15a0..58f40defa 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/reset_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/reset_test.go @@ -52,7 +52,7 @@ func resetCount(t *testing.T, f fixture, query string) int { func TestResetStartEscalateAndComplete(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) f.enroll(t, view, deployment.Capacity{MaxActive: 1, MaxRetained: 1}) if _, err := f.service.CreateEnrollment(admin(t), deployment.EnrollmentLimits{MaxActive: new(1), MaxRetained: new(1)}); err != nil { t.Fatal(err) @@ -106,7 +106,7 @@ func TestResetStartEscalateAndComplete(t *testing.T) { func TestResetDeadlineAndCancel(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) if err := changes.StartReset(admin(t), installation, deployment.ResetRequest{ExpectedGeneration: view.Generation, Clear: deployment.ResetAuto}); err != nil { t.Fatal(err) } @@ -138,7 +138,7 @@ func TestResetWritesNothingWithoutAuditOrLease(t *testing.T) { f := newFixture(t) closed := setupClosedExecution(t, f) changes, _ := f.execution(t) - installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + installation, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) request := deployment.ResetRequest{ExpectedGeneration: view.Generation, Clear: deployment.ResetAuto} if err := changes.StartReset(adminaudit.WithSource(t.Context(), adminaudit.Source{}), installation, request); !errors.Is(err, adminaudit.ErrInvalidSource) { t.Fatalf("StartReset with an invalid source = %v", err) @@ -161,7 +161,7 @@ func TestResetSessionsAndAddressBindings(t *testing.T) { } changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) f.enroll(t, view, deployment.Capacity{MaxActive: 1, MaxRetained: 1}) if bindings, err := f.service.AddressBindings(t.Context()); err != nil || bindings != (deployment.AddressBindings{Nodes: 1}) { t.Fatalf("AddressBindings = %+v, %v", bindings, err) diff --git a/services/core/internal/persistence/postgres/deploymentpg/setup_test.go b/services/core/internal/persistence/postgres/deploymentpg/setup_test.go index aa0ac7d53..734eaec59 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/setup_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/setup_test.go @@ -78,7 +78,7 @@ func TestSandboxDeploymentSetupConcurrentSelection(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - provider := "docker" + provider := "microsandbox" if i%2 == 1 { provider = "microsandbox" } @@ -120,7 +120,7 @@ func TestSandboxDeploymentSetupRejectsUnleasedWrites(t *testing.T) { if err := closed.Claim(t.Context(), id); !errors.Is(err, pgunit.ErrLeaseClosed) { t.Fatal("unleased claim accepted", err) } - if _, err := closed.Initialize(t.Context(), id, sandbox.Selection{DeploymentSpec: testSpecification("docker"), Provider: "docker"}); !errors.Is(err, pgunit.ErrLeaseClosed) { + if _, err := closed.Initialize(t.Context(), id, sandbox.Selection{DeploymentSpec: testSpecification("microsandbox"), Provider: "microsandbox"}); !errors.Is(err, pgunit.ErrLeaseClosed) { t.Fatal("unleased setup accepted", err) } if view, err := f.service.View(t.Context()); err != nil || view.InstallationID != "" || view.Provider != "" { diff --git a/services/core/internal/persistence/postgres/deploymentpg/specification_test.go b/services/core/internal/persistence/postgres/deploymentpg/specification_test.go index 628fd63f9..0af4983e6 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/specification_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/specification_test.go @@ -19,7 +19,7 @@ import ( ) func TestSandboxSpecificationRoundTripAndClaimStays(t *testing.T) { - for _, provider := range []string{"docker", "microsandbox", "e2b"} { + for _, provider := range []string{"microsandbox", "microsandbox", "e2b"} { t.Run(provider, func(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) @@ -77,12 +77,12 @@ func TestSandboxSpecificationInitialCredentialRemainsPrivate(t *testing.T) { func TestEnrollmentRecordsItsIDAndRefusesAnotherAddress(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - _, view := f.initialize(t, changes, sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")}) + _, view := f.initialize(t, changes, sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")}) issued, err := f.service.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(2), MaxRetained: new(2)}) if err != nil || uuid.Validate(issued.ID) != nil || issued.Token == "" { t.Fatal(issued.ID, err) } - input := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "addressed", Credential: strings.Repeat("a", 64), Provider: "docker", + input := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "addressed", Credential: strings.Repeat("a", 64), Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: "https://other.example"} if _, err := f.service.Enroll(t.Context(), issued.Token, input); !errors.Is(err, deployment.ErrNodeAddressMismatch) { t.Fatal("enrolled a node that uses another Core address", err) @@ -122,7 +122,7 @@ func TestEnrollmentRecordsItsIDAndRefusesAnotherAddress(t *testing.T) { func TestDatabaseDoesNotEnumerateProviderRegistrations(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) - input := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")} + input := sandbox.Selection{Provider: "microsandbox", DeploymentSpec: testSpecification("microsandbox")} id, view := f.initialize(t, changes, input) tx, err := f.pool.Begin(t.Context()) if err != nil { diff --git a/services/core/internal/persistence/postgres/runtimehistorypg/aggregate_test.go b/services/core/internal/persistence/postgres/runtimehistorypg/aggregate_test.go index de23739a7..1ea1582b1 100644 --- a/services/core/internal/persistence/postgres/runtimehistorypg/aggregate_test.go +++ b/services/core/internal/persistence/postgres/runtimehistorypg/aggregate_test.go @@ -18,12 +18,12 @@ func TestAggregateSeriesUsesProviderObservationOrder(t *testing.T) { samples := []*rawSample{ { resolvedAt: secondResolved, observedAt: &secondObserved, allocation: testAllocation, startedAt: &startedAt, - provider: "docker", status: runtimeobs.StatusObserved, hasSample: true, + provider: "microsandbox", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{CPUUsageName: 3, CPUCapacityName: 2, MemoryUsageName: 200}, }, { resolvedAt: firstResolved, observedAt: &firstObserved, allocation: testAllocation, startedAt: &startedAt, - provider: "docker", status: runtimeobs.StatusObserved, hasSample: true, + provider: "microsandbox", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{CPUUsageName: 1, CPUCapacityName: 2, MemoryUsageName: 100}, }, } @@ -44,17 +44,17 @@ func TestAggregateSeriesLeavesCPUUsageGapWhenEitherEndpointLacksCapacity(t *test samples := []*rawSample{ { resolvedAt: firstObserved, observedAt: &firstObserved, allocation: testAllocation, startedAt: &startedAt, - provider: "docker", status: runtimeobs.StatusObserved, hasSample: true, + provider: "microsandbox", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{CPUUsageName: 1, CPUCapacityName: 2}, }, { resolvedAt: secondObserved, observedAt: &secondObserved, allocation: testAllocation, startedAt: &startedAt, - provider: "docker", status: runtimeobs.StatusObserved, hasSample: true, + provider: "microsandbox", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{CPUUsageName: 3}, }, { resolvedAt: thirdObserved, observedAt: &thirdObserved, allocation: testAllocation, startedAt: &startedAt, - provider: "docker", status: runtimeobs.StatusObserved, hasSample: true, + provider: "microsandbox", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{CPUUsageName: 5, CPUCapacityName: 2}, }, } @@ -77,14 +77,14 @@ func TestAggregateIgnoresLookbackOnlySeriesForSeriesLimit(t *testing.T) { observedAt := start.Add(-2 * time.Second) raw = append(raw, &rawSample{ resolvedAt: start.Add(-time.Second), observedAt: &observedAt, allocation: uuid.NewString(), startedAt: &startedAt, - provider: "docker", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{}, + provider: "microsandbox", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{}, }) } startedAt := start.Add(-time.Minute) observedAt := start.Add(5 * time.Second) raw = append(raw, &rawSample{ resolvedAt: start.Add(6 * time.Second), observedAt: &observedAt, allocation: uuid.NewString(), startedAt: &startedAt, - provider: "docker", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{}, + provider: "microsandbox", status: runtimeobs.StatusObserved, hasSample: true, metrics: map[string]float64{}, }) result, err := aggregate(query, start.Add(time.Minute), raw) if err != nil { diff --git a/services/core/internal/persistence/postgres/runtimehistorypg/reader_test.go b/services/core/internal/persistence/postgres/runtimehistorypg/reader_test.go index 85bc4c916..3e7751d65 100644 --- a/services/core/internal/persistence/postgres/runtimehistorypg/reader_test.go +++ b/services/core/internal/persistence/postgres/runtimehistorypg/reader_test.go @@ -21,7 +21,7 @@ const ( func testCapabilities() runtimehistory.Capabilities { return runtimehistory.Capabilities{ SampleInterval: 30 * time.Second, - Retention: 7 * 24 * time.Hour, MinimumStep: 30 * time.Second, MaximumRange: 24 * time.Hour, + Retention: 7 * 24 * time.Hour, MinimumStep: 30 * time.Second, MaximumRange: 24 * time.Hour, MaximumPoints: 1_000, MaximumSeries: 64, MaximumTotalPoints: 10_000, Metrics: []runtimehistory.Metric{runtimehistory.MetricCPU, runtimehistory.MetricMemory}, } @@ -76,7 +76,7 @@ func observedRecord(at, started time.Time, cpu float64) runtimeobs.ExportRecord capacity := 2.0 memory := uint64(123) return runtimeobs.ExportRecord{TenantID: testTenant, SessionID: testSession, EnvironmentID: testEnvironment, AllocationID: testAllocation, - Mode: runtimeobs.ModeManaged, ProviderType: "docker", Status: runtimeobs.StatusObserved, CollectionSource: runtimeobs.CollectionSourcePeriodic, + Mode: runtimeobs.ModeManaged, ProviderType: "microsandbox", Status: runtimeobs.StatusObserved, CollectionSource: runtimeobs.CollectionSourcePeriodic, ResolvedAt: at, Sample: &runtimeobs.Sample{ObservedAt: at, StartedAt: &started, CPUUsageSecondsTotal: &cpu, CPUCapacityCores: &capacity, MemoryUsageBytes: &memory}, TokenUsage: &runtimeobs.TokenUsage{InputTokens: 12, OutputTokens: 3}} } diff --git a/services/core/internal/persistence/postgres/runtimehistorypg/samples_test.go b/services/core/internal/persistence/postgres/runtimehistorypg/samples_test.go index aae9979be..195581a7c 100644 --- a/services/core/internal/persistence/postgres/runtimehistorypg/samples_test.go +++ b/services/core/internal/persistence/postgres/runtimehistorypg/samples_test.go @@ -42,7 +42,7 @@ func historyOwner(t *testing.T, pool *pgxpool.Pool) runtimehistory.Scope { func historyRecord(scope runtimehistory.Scope, allocation string, started, at time.Time, cpu float64, input uint64) runtimeobs.ExportRecord { capacity := 2.0 memory := uint64(512) - return runtimeobs.ExportRecord{TenantID: scope.TenantID, SessionID: scope.SessionID, EnvironmentID: scope.EnvironmentID, AllocationID: allocation, Mode: runtimeobs.ModeManaged, ProviderType: "docker", Status: runtimeobs.StatusObserved, CollectionSource: runtimeobs.CollectionSourcePeriodic, ResolvedAt: at, + return runtimeobs.ExportRecord{TenantID: scope.TenantID, SessionID: scope.SessionID, EnvironmentID: scope.EnvironmentID, AllocationID: allocation, Mode: runtimeobs.ModeManaged, ProviderType: "microsandbox", Status: runtimeobs.StatusObserved, CollectionSource: runtimeobs.CollectionSourcePeriodic, ResolvedAt: at, Sample: &runtimeobs.Sample{ObservedAt: at, StartedAt: &started, CPUUsageSecondsTotal: &cpu, CPUCapacityCores: &capacity, MemoryUsageBytes: &memory}, TokenUsage: &runtimeobs.TokenUsage{InputTokens: input, OutputTokens: input / 2}} } func historyQuery(scope runtimehistory.Scope, start, end time.Time, points int) runtimehistory.Query { diff --git a/services/core/internal/persistence/postgres/sessionpg/creation_test.go b/services/core/internal/persistence/postgres/sessionpg/creation_test.go index 6467210b3..eaa2c4c65 100644 --- a/services/core/internal/persistence/postgres/sessionpg/creation_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/creation_test.go @@ -28,6 +28,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/skills" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/workspacefs" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -472,7 +473,7 @@ func TestHostedCreationAdmitsUnderDeploymentLockAndLeavesComputeUnreserved(t *te ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() tenant := uuid.NewString() - nodes, err := json.Marshal(sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048}}) + nodes, err := json.Marshal(sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048, RootDiskMiB: 1024}, Workspace: &workspacefs.Declaration{Attachment: workspacefs.AttachmentHostDirectory, UserXAttr: true}}) if err != nil { t.Fatal(err) } @@ -501,7 +502,7 @@ func TestHostedCreationAdmitsUnderDeploymentLockAndLeavesComputeUnreserved(t *te done <- err }() awaitBlocked(ctx, t, pool, holder) - if _, err := tx.Exec(ctx, `UPDATE runtime_deployment SET provider_kind='docker', mode='nodes', generation=2, specification=$1, + if _, err := tx.Exec(ctx, `UPDATE runtime_deployment SET provider_kind='microsandbox', mode='nodes', generation=2, specification=$1, reset_clear='force', reset_requested_at=now(), reset_forced_at=now(), reset_audit='{}'`, string(nodes)); err != nil { t.Fatal(err) } @@ -514,7 +515,7 @@ func TestHostedCreationAdmitsUnderDeploymentLockAndLeavesComputeUnreserved(t *te if retry, err := service.CreateSession(ctx, tenant, hosted("existing")); err != nil || retry.Created || retry.Session.ID != existing.Session.ID { t.Fatal("retry ran admission", retry, err) } - exec(t, pool, "UPDATE runtime_deployment SET reset_clear=NULL, reset_requested_at=NULL, reset_forced_at=NULL, reset_audit=NULL") + exec(t, pool, `UPDATE runtime_deployment SET provider_kind='e2b', mode='direct', generation=3, specification='{"resources":{"cpus":2,"memory_mib":2048}}', reset_clear=NULL, reset_requested_at=NULL, reset_forced_at=NULL, reset_audit=NULL`) queued, err := service.CreateSession(ctx, tenant, hosted("unplaced")) if err != nil || !queued.Created { t.Fatal("creation without available compute was not accepted", queued, err) diff --git a/services/core/internal/runtimehistory/service_test.go b/services/core/internal/runtimehistory/service_test.go index cf0047ae6..390b1323c 100644 --- a/services/core/internal/runtimehistory/service_test.go +++ b/services/core/internal/runtimehistory/service_test.go @@ -99,7 +99,7 @@ func TestServiceAuthorizesAndBoundsBackendQuery(t *testing.T) { reader.result = Result{GeneratedAt: now, RetainedFrom: &start, TokenUsage: []TokenUsagePoint{{ Start: start, End: start.Add(30 * time.Second), SampledAt: start.Add(20 * time.Second), InputTokens: 120, OutputTokens: 30, }}, Series: []Series{{ - Scope: scope, AllocationID: allocationID, StartedAt: startedAt, ProviderType: "docker", + Scope: scope, AllocationID: allocationID, StartedAt: startedAt, ProviderType: "microsandbox", Points: []Point{{ Start: start, End: start.Add(30 * time.Second), FirstObservedAt: start.Add(time.Second), LastObservedAt: start.Add(20 * time.Second), ObservationCount: 2, ObservedCount: 2, CPUContributorCount: 2, MemoryContributorCount: 2, @@ -212,7 +212,7 @@ func TestServiceRejectsMalformedBackendResults(t *testing.T) { now := time.Date(2026, 9, 23, 8, 0, 0, 0, time.UTC) start := now.Add(-time.Hour) scope := Scope{TenantID: tenantID, SessionID: sessionID, EnvironmentID: environmentID} - base := Series{Scope: scope, AllocationID: allocationID, StartedAt: start.Add(-time.Minute), ProviderType: "docker"} + base := Series{Scope: scope, AllocationID: allocationID, StartedAt: start.Add(-time.Minute), ProviderType: "microsandbox"} for name, mutate := range map[string]func(*Result){ "cross tenant": func(result *Result) { result.Series[0].TenantID = "55555555-5555-4555-8555-555555555555" }, "noncanonical allocation": func(result *Result) { result.Series[0].AllocationID = "urn:uuid:" + allocationID }, diff --git a/services/core/internal/runtimeobs/operations_fixture_test.go b/services/core/internal/runtimeobs/operations_fixture_test.go index 8364c7ad0..54a806350 100644 --- a/services/core/internal/runtimeobs/operations_fixture_test.go +++ b/services/core/internal/runtimeobs/operations_fixture_test.go @@ -13,7 +13,7 @@ func (*fixedSource) ProviderOperations() providercontract.Operations { return func (blockingSource) ProviderOperations() providercontract.Operations { return observedOperations() } func (*countingSource) ProviderOperations() providercontract.Operations { return observedOperations() } -// sourceOf selects one fixed docker source for every page. +// sourceOf selects one fixed microsandbox source for every page. func sourceOf(source Source) func(context.Context) (Source, string, error) { - return func(context.Context) (Source, string, error) { return source, "docker", nil } + return func(context.Context) (Source, string, error) { return source, "microsandbox", nil } } diff --git a/services/core/internal/runtimeobs/otlpexporter/exporter_test.go b/services/core/internal/runtimeobs/otlpexporter/exporter_test.go index 0974de22c..3b6dd0e45 100644 --- a/services/core/internal/runtimeobs/otlpexporter/exporter_test.go +++ b/services/core/internal/runtimeobs/otlpexporter/exporter_test.go @@ -41,7 +41,7 @@ func observedRecord() runtimeobs.ExportRecord { memoryUsage, memoryLimit := uint64(1024), uint64(2048) return runtimeobs.ExportRecord{ TenantID: "tenant", SessionID: "session", EnvironmentID: "environment", AllocationID: "allocation", - Mode: runtimeobs.ModeManaged, ProviderType: "docker", Status: runtimeobs.StatusObserved, + Mode: runtimeobs.ModeManaged, ProviderType: "microsandbox", Status: runtimeobs.StatusObserved, CollectionSource: runtimeobs.CollectionSourceOnRead, ResolvedAt: observedAt, SourceDuration: 50 * time.Millisecond, Sample: &runtimeobs.Sample{ @@ -88,7 +88,7 @@ func TestExporterBuildsFencedProviderNeutralMetrics(t *testing.T) { for key, want := range map[string]string{ "agents.tenant.id": "tenant", "agents.session.id": "session", "agents.environment.id": "environment", "agents.runtime.allocation.id": "allocation", "agents.runtime.mode": "openai_hosted", - "agents.runtime.provider.type": "docker", "agents.runtime.status": "observed", + "agents.runtime.provider.type": "microsandbox", "agents.runtime.status": "observed", "agents.runtime.collection.source": "on_read", "agents.runtime.resolved_at_unix_nano": "1790132400000000000", "agents.runtime.observed_at_unix_nano": "1790132400000000000", @@ -171,7 +171,7 @@ func TestExporterRejectsUnsafeRecordsBeforeTransport(t *testing.T) { client := &captureClient{} exporter := newWithClient(client) record := observedRecord() - record.ProviderType = "docker native=secret" + record.ProviderType = "microsandbox native=secret" if err := exporter.Export(t.Context(), record); err == nil || client.calls != 0 { t.Fatalf("unsafe record reached transport: err=%v calls=%d", err, client.calls) } diff --git a/services/core/internal/runtimeobs/service_test.go b/services/core/internal/runtimeobs/service_test.go index 78b924c73..595a416e8 100644 --- a/services/core/internal/runtimeobs/service_test.go +++ b/services/core/internal/runtimeobs/service_test.go @@ -181,7 +181,7 @@ func TestServiceExportsOnlySanitizedValidatedRecords(t *testing.T) { if record.TenantID != "tenant" || record.SessionID != "session" || record.EnvironmentID != "environment" || record.AllocationID != "allocation" { t.Fatalf("exported identity mismatch: %+v", record) } - if record.ProviderType != "docker" || record.Mode != ModeManaged || record.Status != StatusObserved || record.Reason != "" || record.CollectionSource != CollectionSourceOnRead { + if record.ProviderType != "microsandbox" || record.Mode != ModeManaged || record.Status != StatusObserved || record.Reason != "" || record.CollectionSource != CollectionSourceOnRead { t.Fatalf("exported classification mismatch: %+v", record) } if record.Sample == nil || record.Sample.CPUUsageSecondsTotal == nil || *record.Sample.CPUUsageSecondsTotal != 12.5 || record.Sample.MemoryUsageBytes == nil || *record.Sample.MemoryUsageBytes != 1024 { @@ -410,7 +410,7 @@ func TestServiceMapsOnlyDeclaredUnavailability(t *testing.T) { {err: ErrUnavailable, wantReason: "sample_unavailable"}, {err: ErrNotRunning, wantReason: "runtime_not_running"}, {err: context.DeadlineExceeded, wantReason: "sample_timeout"}, - {err: errors.New("Docker permission denied"), wantError: true}, + {err: errors.New("provider permission denied"), wantError: true}, } { service, err := NewService(fixedResolver{target: target}, sourceOf(&fixedSource{err: tc.err})) if err != nil { @@ -420,7 +420,7 @@ func TestServiceMapsOnlyDeclaredUnavailability(t *testing.T) { if (err != nil) != tc.wantError { t.Fatalf("wrong error classification: %+v %v", observation, err) } - if !tc.wantError && (observation.Status != StatusUnavailable || observation.Reason != tc.wantReason || observation.ProviderType != "docker") { + if !tc.wantError && (observation.Status != StatusUnavailable || observation.Reason != tc.wantReason || observation.ProviderType != "microsandbox") { t.Fatalf("declared unavailability was not mapped: %+v", observation) } } @@ -438,7 +438,7 @@ func TestServiceMapsAnActualSourceDeadlineWithoutLeakingIt(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) defer cancel() observation, err := service.ObserveSession(ctx, "tenant", "session") - if err != nil || observation.Status != StatusUnavailable || observation.Reason != "sample_timeout" || observation.ProviderType != "docker" { + if err != nil || observation.Status != StatusUnavailable || observation.Reason != "sample_timeout" || observation.ProviderType != "microsandbox" { t.Fatalf("source deadline was not safely classified: %+v %v", observation, err) } } diff --git a/services/core/internal/sandbox/docker/bootstrap.go b/services/core/internal/sandbox/docker/bootstrap.go deleted file mode 100644 index 3fee133a4..000000000 --- a/services/core/internal/sandbox/docker/bootstrap.go +++ /dev/null @@ -1,54 +0,0 @@ -package docker - -import ( - "archive/tar" - "bytes" - "context" - "io" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/moby/moby/client" -) - -type entry struct { - name string - content []byte - directory bool -} - -func (p *Provider) bootstrap(ctx context.Context, id string, b sandbox.Bootstrap) error { - // Deliver the Runtime-owned connection contract before native work can start. - auth, e := b.RuntimeConnection().Marshal() - if e != nil { - return e - } - if e = copyRuntimeFiles(ctx, p.client, id, "/home", []entry{ - {name: "runtime", directory: true}, {name: "runtime/.oac", directory: true}, - {name: "runtime/runtime-bootstrap.json", content: auth}, - }); e != nil { - return e - } - return copyRuntimeFiles(ctx, p.client, id, "/environment", []entry{{name: "workspace", directory: true}, {name: "staging", directory: true}, {name: "initialization", directory: true}, {name: "packages", directory: true}}) -} -func copyRuntimeFiles(ctx context.Context, c *client.Client, id, path string, entries []entry) error { - var content bytes.Buffer - writer := tar.NewWriter(&content) - for _, file := range entries { - header := &tar.Header{Name: file.name, Uid: 1000, Gid: 1000, Mode: 0600, Size: int64(len(file.content)), Typeflag: tar.TypeReg} - if file.directory { - header.Mode = 0700 - header.Typeflag = tar.TypeDir - } - if e := writer.WriteHeader(header); e != nil { - return e - } - if _, e := writer.Write(file.content); e != nil { - return e - } - } - if e := writer.Close(); e != nil { - return e - } - _, e := c.CopyToContainer(ctx, id, client.CopyToContainerOptions{DestinationPath: path, Content: io.Reader(&content), CopyUIDGID: true}) - return e -} diff --git a/services/core/internal/sandbox/docker/bootstrap_test.go b/services/core/internal/sandbox/docker/bootstrap_test.go deleted file mode 100644 index 2e8138b41..000000000 --- a/services/core/internal/sandbox/docker/bootstrap_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package docker - -import ( - "archive/tar" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/internal/runtimebootstrap" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/moby/moby/client" -) - -func TestBootstrapDeliversOnlyPublicConnectionInput(t *testing.T) { - b := sandbox.Bootstrap{Reference: sandbox.Reference{EnvironmentID: "0d1f4f1c-6c3e-4e0b-9a52-3e3d8b0e7a11"}, SessionID: "6a1b7c0e-2f4d-4c55-8f6e-9d2a1b3c4d5e", CoreURL: "https://core.example/api/v1", DeviceID: "da912024-1543-4242-a2c1-5f4f7ebbc6c7", Credential: "test-secret"} - found := false - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "PUT" || !strings.HasSuffix(r.URL.Path, "/containers/test/archive") { - t.Errorf("unexpected Docker operation %s", r.URL.Path) - } - tr := tar.NewReader(r.Body) - for { - h, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - t.Error(err) - break - } - if strings.Contains(h.Name, "auth.json") { - t.Error("provider wrote Runtime private storage") - } - if h.Name == "runtime/runtime-bootstrap.json" { - found = true - raw, err := io.ReadAll(tr) - if err != nil { - t.Error(err) - } - c, err := runtimebootstrap.Decode(raw) - if err != nil || c != b.RuntimeConnection() || h.Mode != 0600 || h.Uid != 1000 || h.Gid != 1000 { - t.Error("invalid launch input or permissions") - } - } - } - w.WriteHeader(200) - })) - defer server.Close() - c, err := client.New(client.WithHost(server.URL), client.WithAPIVersion("1.52")) - if err != nil { - t.Fatal(err) - } - defer c.Close() - if err := (&Provider{client: c}).bootstrap(t.Context(), "test", b); err != nil { - t.Fatal(err) - } - if !found { - t.Fatal("missing Runtime input") - } -} diff --git a/services/core/internal/sandbox/docker/command.go b/services/core/internal/sandbox/docker/command.go deleted file mode 100644 index 546109160..000000000 --- a/services/core/internal/sandbox/docker/command.go +++ /dev/null @@ -1,91 +0,0 @@ -package docker - -import ( - "bytes" - "context" - "errors" - "io" - "path" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/moby/moby/api/pkg/stdcopy" - "github.com/moby/moby/client" -) - -// RunCommand is for trusted initialization only. Closing an exec attachment does -// not kill its process. On any uncertain outcome, the caller must reclaim the -// allocation with Kill instead of admitting native work or replaying the command. -func (p *Provider) RunCommand(ctx context.Context, r sandbox.Reference, command sandbox.Command) (sandbox.CommandResult, error) { - var result sandbox.CommandResult - if len(command.Args) == 0 || len(command.Stdin) > sandbox.MaxCommandInputBytes || (command.Directory != "" && !path.IsAbs(command.Directory)) { - return result, sandbox.ErrInvalid - } - c, e := p.inspect(ctx, r) - if e != nil { - return result, e - } - if _, ok := ctx.Deadline(); !ok { - return result, sandbox.ErrInvalid - } - exec, e := p.client.ExecCreate(ctx, c.Container.ID, client.ExecCreateOptions{User: "1000:1000", Cmd: command.Args, WorkingDir: command.Directory, AttachStdin: command.Stdin != nil, AttachStdout: true, AttachStderr: true}) - if e != nil { - return result, e - } - attached, e := p.client.ExecAttach(ctx, exec.ID, client.ExecAttachOptions{}) - if e != nil { - return result, errors.Join(sandbox.ErrCommandUnconfirmed, e) - } - defer attached.Close() - written := make(chan error, 1) - if command.Stdin != nil { - go func() { - _, err := io.Copy(attached.Conn, bytes.NewReader(command.Stdin)) - if err == nil { - err = attached.CloseWrite() - } - written <- err - }() - } else { - written <- nil - } - stdout, stderr := &boundedBuffer{}, &boundedBuffer{} - done := make(chan error, 1) - go func() { _, e := stdcopy.StdCopy(stdout, stderr, attached.Reader); done <- e }() - select { - case e = <-done: - case <-ctx.Done(): - attached.Close() - <-done - e = ctx.Err() - } - if e != nil { - attached.Close() - <-written - return result, errors.Join(sandbox.ErrCommandUnconfirmed, e) - } - select { - case e = <-written: - case <-ctx.Done(): - attached.Close() - <-written - e = ctx.Err() - } - if e != nil { - return result, errors.Join(sandbox.ErrCommandUnconfirmed, e) - } - status, e := p.client.ExecInspect(ctx, exec.ID, client.ExecInspectOptions{}) - if e != nil || status.Running { - return result, errors.Join(sandbox.ErrCommandUnconfirmed, e) - } - return sandbox.CommandResult{Stdout: stdout.String(), Stderr: stderr.String(), ExitCode: status.ExitCode}, nil -} - -type boundedBuffer struct{ bytes.Buffer } - -func (b *boundedBuffer) Write(data []byte) (int, error) { - const max = 1024 * 1024 - if b.Len()+len(data) > max { - return 0, errors.New("initialization output exceeds 1 MiB") - } - return b.Buffer.Write(data) -} diff --git a/services/core/internal/sandbox/docker/container_options.go b/services/core/internal/sandbox/docker/container_options.go deleted file mode 100644 index a6d050e14..000000000 --- a/services/core/internal/sandbox/docker/container_options.go +++ /dev/null @@ -1,44 +0,0 @@ -package docker - -import ( - "github.com/MiniMax-AI/OpenAgentCore/internal/runtimebootstrap" - "github.com/moby/moby/api/types/container" - "github.com/moby/moby/api/types/mount" - "github.com/moby/moby/client" -) - -// runtimeContainerOptions is shared by managed and user-owned V1 Runtime launch. -// Keep isolation and volume layout identical; only bootstrap authority differs. -func runtimeContainerOptions(config Config, name string, labels map[string]string) client.ContainerCreateOptions { - limit := int64(128) - memory, cpus := int64(2*1024*1024*1024), int64(2*1000000000) - if config.Resources != nil { - memory = int64(config.Resources.MemoryMiB) * 1024 * 1024 - cpus = int64(config.Resources.CPUs) * 1000000000 - } - // A nested native sandbox must mount its own procfs. Keep sysfs secrets - // masked; only this qualified image profile opts out of Docker's proc masks. - var masked, readonly []string - var init *bool - if config.NestedSandbox { - masked = []string{"/sys/firmware", "/sys/devices/virtual/powercap"} - readonly = []string{} - enabled := true - init = &enabled - } - return client.ContainerCreateOptions{Name: name, Image: config.Image, - Config: &container.Config{User: "1000:1000", WorkingDir: runtimebootstrap.RuntimeWorkspace, Labels: labels}, - HostConfig: &container.HostConfig{ReadonlyRootfs: true, CapDrop: []string{"ALL"}, SecurityOpt: []string{"no-new-privileges", "seccomp=" + config.Seccomp, "apparmor=unconfined"}, NetworkMode: container.NetworkMode(config.Network), - MaskedPaths: masked, ReadonlyPaths: readonly, Init: init, - Resources: container.Resources{PidsLimit: &limit, Memory: memory, NanoCPUs: cpus}, Tmpfs: map[string]string{"/tmp": "rw,nosuid,nodev,size=128m"}, - Mounts: []mount.Mount{ - {Type: mount.TypeVolume, Source: name + "-home", Target: "/home"}, - {Type: mount.TypeVolume, Source: name + "-environment", Target: "/environment"}, - // The native sandbox mounts canonical roots, omitting symlink aliases. - // Expose the same workspace at its public path; trusted atomic staging - // remains entirely on the original /environment mount. - {Type: mount.TypeVolume, Source: name + "-environment", Target: runtimebootstrap.PublicWorkspace, VolumeOptions: &mount.VolumeOptions{Subpath: "workspace", NoCopy: true}}, - }, - }, - } -} diff --git a/services/core/internal/sandbox/docker/contract_test.go b/services/core/internal/sandbox/docker/contract_test.go deleted file mode 100644 index 2f25adc2c..000000000 --- a/services/core/internal/sandbox/docker/contract_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package docker - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" - "github.com/google/uuid" - "github.com/moby/moby/client" -) - -// The script covers only native calls before a fault. Full Docker creation and -// bootstrap remain covered by the existing opt-in lifecycle and recovery tests. -type contractStep struct { - method, path string - status int - body any - fault contracttest.Fault -} - -func dockerContractFixture(t *testing.T, cancel context.CancelFunc, script func(*Provider, sandbox.Reference) []contractStep) contracttest.Fixture { - t.Helper() - b := sandbox.Bootstrap{Reference: sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "synthetic"} - var mu sync.Mutex - var calls []string - var steps []contractStep - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := strings.TrimPrefix(r.URL.Path, "/v1.52") - call := r.Method + " " + path - mu.Lock() - index := len(calls) - calls = append(calls, call) - mu.Unlock() - if index >= len(steps) || call != steps[index].method+" "+steps[index].path { - t.Errorf("unexpected native request: %s", call) - http.Error(w, "unexpected request", 500) - return - } - step := steps[index] - switch step.fault { - case contracttest.Canceled: - cancel() - return - case contracttest.TransportFailure: - connection, _, err := w.(http.Hijacker).Hijack() - if err != nil { - t.Error(err) - return - } - _ = connection.Close() - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(step.status) - if step.body != nil { - _ = json.NewEncoder(w).Encode(step.body) - } - })) - t.Cleanup(server.Close) - c, err := client.New(client.WithHost(server.URL), client.WithAPIVersion("1.52")) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = c.Close() }) - p, err := New(c, Config{InstallationID: uuid.NewString(), Image: "sha256:" + strings.Repeat("a", 64), Network: "bridge", Seccomp: "{}"}) - if err != nil { - t.Fatal(err) - } - steps = script(p, b.Reference) - want := make([]string, len(steps)) - for i, step := range steps { - want[i] = step.method + " " + step.path - } - return contracttest.Fixture{Provider: p, Bootstrap: b, Calls: func() []string { - mu.Lock() - defer mu.Unlock() - return append([]string(nil), calls...) - }, WantCalls: want} -} - -func TestProviderContract(t *testing.T) { - contracttest.RunFailures(t, func(t *testing.T, s contracttest.Scenario, cancel context.CancelFunc) contracttest.Fixture { - return dockerContractFixture(t, cancel, func(p *Provider, r sandbox.Reference) []contractStep { - inspect := "/containers/" + p.name(r) + "/json" - home, environment := "/volumes/"+p.name(r)+"-home", "/volumes/"+p.name(r)+"-environment" - missing := map[string]string{"message": "not found"} - owned := map[string]any{"Id": "native-owned", "Config": map[string]any{"Labels": p.labels(r)}, "State": map[string]string{"Status": "running"}} - foreign := map[string]any{"Labels": map[string]string{"io.oac.tenant": uuid.NewString()}} - if s.Fault == contracttest.ForeignOwnership { - if s.Operation == "kill" { - // Verify every ownership check precedes any deletion, including volumes. - return []contractStep{{http.MethodGet, inspect, 200, owned, ""}, {http.MethodGet, home, 200, foreign, ""}} - } - return []contractStep{{http.MethodGet, inspect, 200, map[string]any{"Id": "foreign", "Config": foreign}, ""}} - } - switch s.Operation { - case "create": - return []contractStep{ - {http.MethodGet, inspect, 404, missing, ""}, - {http.MethodGet, home, 404, missing, ""}, - {http.MethodGet, environment, 404, missing, ""}, - {http.MethodPost, "/volumes/create", 0, nil, s.Fault}, - } - case "kill": - volume := map[string]any{"Labels": p.labels(r)} - steps := []contractStep{{http.MethodGet, inspect, 200, owned, ""}, {http.MethodGet, home, 200, volume, ""}, {http.MethodGet, environment, 200, volume, ""}} - if s.Fault == contracttest.CleanupFailure { - return append(steps, contractStep{http.MethodDelete, "/containers/native-owned", 204, nil, ""}, contractStep{http.MethodDelete, home, 500, map[string]string{"message": "cleanup failed"}, ""}) - } - return append(steps, contractStep{http.MethodDelete, "/containers/native-owned", 0, nil, s.Fault}) - default: - return []contractStep{{http.MethodGet, inspect, 0, nil, s.Fault}} - } - }) - }) -} - -func TestProviderContractObservation(t *testing.T) { - for _, operation := range []string{"inspect", "renew"} { - t.Run(operation, func(t *testing.T) { - f := dockerContractFixture(t, func() {}, func(p *Provider, r sandbox.Reference) []contractStep { - return []contractStep{{http.MethodGet, "/containers/" + p.name(r) + "/json", 200, map[string]any{"Id": "native-owned", "Config": map[string]any{"Labels": p.labels(r)}, "State": map[string]string{"Status": "exited"}}, ""}} - }) - var got sandbox.Info - var err error - if operation == "renew" { - got, err = f.Provider.Renew(t.Context(), f.Bootstrap.Reference) - } else { - got, err = f.Provider.GetInfo(t.Context(), f.Bootstrap.Reference) - } - contracttest.AssertObservation(t, got, err, f.Bootstrap.Reference, "native-owned", "exited") - }) - } -} diff --git a/services/core/internal/sandbox/docker/deployment.go b/services/core/internal/sandbox/docker/deployment.go deleted file mode 100644 index 16b5e77ae..000000000 --- a/services/core/internal/sandbox/docker/deployment.go +++ /dev/null @@ -1,30 +0,0 @@ -package docker - -import ( - "context" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/moby/moby/client" -) - -// Resource drift blocks managed readiness, but never ownership-based cleanup. -// Resolve the immutable selector through Docker because classic and containerd -// stores may identify the same exported image by different digests. -func (p *Provider) verifyConfiguration(ctx context.Context, actual client.ContainerInspectResult) error { - if p.config.Resources == nil { - return nil - } - resources := p.config.Resources - config := actual.Container.HostConfig - if config == nil || config.NanoCPUs != int64(resources.CPUs)*1000000000 || config.Memory != int64(resources.MemoryMiB)*1024*1024 { - return sandbox.ErrInvalid - } - image, err := p.client.ImageInspect(ctx, p.config.Image) - if err != nil { - return err - } - if image.ID == "" || actual.Container.Image != image.ID { - return sandbox.ErrInvalid - } - return nil -} diff --git a/services/core/internal/sandbox/docker/deployment_test.go b/services/core/internal/sandbox/docker/deployment_test.go deleted file mode 100644 index 7055c25ce..000000000 --- a/services/core/internal/sandbox/docker/deployment_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package docker - -import ( - "encoding/json" - "errors" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/google/uuid" - "github.com/moby/moby/api/types/container" - "github.com/moby/moby/client" -) - -func TestManagedDriftRejectsBeforeBootstrapAndRetainsCleanup(t *testing.T) { - for _, drift := range []string{"cpu", "memory", "image"} { - t.Run(drift, func(t *testing.T) { - imageID := "sha256:" + strings.Repeat("a", 64) - present := false - volumes := map[string]map[string]any{} - var created struct { - container.Config - HostConfig container.HostConfig - } - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := strings.TrimPrefix(r.URL.Path, "/v1.52") - w.Header().Set("Content-Type", "application/json") - switch { - case path == "/containers/create": - _ = json.NewDecoder(r.Body).Decode(&created) - if created.HostConfig.NanoCPUs != 3e9 || created.HostConfig.Memory != 4096*1024*1024 { - t.Error("create did not apply managed limits") - } - present = true - w.WriteHeader(201) - _, _ = io.WriteString(w, `{"Id":"runtime-id"}`) - case strings.HasPrefix(path, "/containers/") && strings.HasSuffix(path, "/json") && present: - host := created.HostConfig - actualImage := imageID - switch drift { - case "cpu": - host.NanoCPUs = 1e9 - case "memory": - host.Memory = 1024 * 1024 * 1024 - case "image": - actualImage = "sha256:" + strings.Repeat("b", 64) - } - _ = json.NewEncoder(w).Encode(map[string]any{"Id": "runtime-id", "Image": actualImage, "Config": created.Config, "HostConfig": host, "State": map[string]any{"Status": "created"}}) - case strings.HasPrefix(path, "/images/"): - _ = json.NewEncoder(w).Encode(map[string]string{"Id": imageID}) - case path == "/volumes/create": - var value map[string]any - _ = json.NewDecoder(r.Body).Decode(&value) - volumes[value["Name"].(string)] = value - _ = json.NewEncoder(w).Encode(value) - case strings.HasPrefix(path, "/volumes/") && volumes[strings.TrimPrefix(path, "/volumes/")] != nil: - name := strings.TrimPrefix(path, "/volumes/") - if r.Method == http.MethodDelete { - delete(volumes, name) - w.WriteHeader(204) - } else { - _ = json.NewEncoder(w).Encode(volumes[name]) - } - case r.Method == http.MethodDelete && strings.HasPrefix(path, "/containers/"): - present = false - w.WriteHeader(204) - case r.Method == http.MethodGet: - w.WriteHeader(404) - _, _ = io.WriteString(w, `{"message":"not found"}`) - default: - t.Errorf("unexpected bootstrap or mutation: %s %s", r.Method, path) - w.WriteHeader(500) - } - })) - defer server.Close() - c, err := client.New(client.WithHost(server.URL), client.WithAPIVersion("1.52")) - if err != nil { - t.Fatal(err) - } - defer c.Close() - resources := sandbox.Resources{CPUs: 3, MemoryMiB: 4096} - p, err := New(c, Config{InstallationID: uuid.NewString(), Image: imageID, Network: "bridge", Seccomp: `{}`, Resources: &resources}) - if err != nil { - t.Fatal(err) - } - resources.CPUs = 9 - b := sandbox.Bootstrap{Reference: sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "secret"} - info, err := p.Create(t.Context(), b) - if !errors.Is(err, sandbox.ErrInvalid) { - t.Fatal("drift accepted", err) - } - if info.Reference != b.Reference || !info.CreateSettled || info.BootstrapComplete || info.ProviderID == "" || info.State == "absent" { - t.Fatal("pre-bootstrap rejection lost its creation settlement", info) - } - if _, err = p.GetInfo(t.Context(), b.Reference); !errors.Is(err, sandbox.ErrInvalid) { - t.Fatal("drift readiness accepted", err) - } - if err = p.Kill(t.Context(), b.Reference); err != nil { - t.Fatal("drift prevented cleanup", err) - } - if present || len(volumes) != 0 { - t.Fatal("cleanup retained resources") - } - }) - } -} - -func TestNilManagedLimitsKeepCallerManagedDefaults(t *testing.T) { - options := runtimeContainerOptions(Config{}, "fixture", nil) - if options.HostConfig.NanoCPUs != 2e9 || options.HostConfig.Memory != 2*1024*1024*1024 { - t.Fatal("caller-managed defaults changed") - } -} diff --git a/services/core/internal/sandbox/docker/node.go b/services/core/internal/sandbox/docker/node.go deleted file mode 100644 index 4b3fcff0e..000000000 --- a/services/core/internal/sandbox/docker/node.go +++ /dev/null @@ -1,103 +0,0 @@ -package docker - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/url" - "os" - "path/filepath" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/containerd/errdefs" - "github.com/moby/moby/client" -) - -// Native is the node configuration's native object for Docker. Image is the -// local ID of the installation's Runtime image, as this host's image store -// resolves it. -type Native struct { - Host string `json:"host"` - Image string `json:"image"` - Network string `json:"network"` - SeccompFile string `json:"seccomp_file"` - NestedSandbox bool `json:"nested_sandbox"` -} - -func decodeNative(config sandbox.NodeConfig) (Native, error) { - var entry Native - if sandbox.DecodeConfigurationObject(config.Native, &entry, "host", "image", "network", "seccomp_file", "nested_sandbox") != nil || entry.Image == "" { - return entry, errors.New("invalid managed Docker node configuration") - } - return entry, nil -} - -// BuildNode constructs the node-local Docker adapter from a validated node configuration. -func BuildNode(config sandbox.NodeConfig, _ sandbox.LocalOptions, result *sandbox.Built) (func(), error) { - closeProvider := func() {} - entry, err := decodeNative(config) - if err != nil { - return closeProvider, err - } - host, err := url.Parse(entry.Host) - if err != nil || host.Scheme != "unix" || host.Host != "" || host.User != nil || host.RawQuery != "" || host.Fragment != "" || host.RawPath != "" || host.Path == "/" || !filepath.IsAbs(host.Path) || filepath.Clean(host.Path) != host.Path || entry.Host != "unix://"+host.Path { - return closeProvider, errors.New("managed Docker host must be an explicit canonical unix socket") - } - seccomp, err := os.ReadFile(entry.SeccompFile) - if err != nil { - return closeProvider, fmt.Errorf("cannot read managed Docker seccomp JSON: %w", err) - } - if !json.Valid(seccomp) { - return closeProvider, errors.New("invalid managed Docker seccomp JSON") - } - c, err := client.New(client.WithHost(entry.Host)) - if err != nil { - return closeProvider, errors.New("invalid managed Docker endpoint") - } - closeProvider = func() { _ = c.Close() } - provider, err := New(c, Config{InstallationID: config.InstallationID, Image: entry.Image, Network: entry.Network, Seccomp: string(seccomp), NestedSandbox: entry.NestedSandbox, Resources: &config.Specification.Resources}) - if err != nil { - closeProvider() - return func() {}, errors.New("invalid managed Docker provider configuration") - } - result.Provider = provider - result.Probe = func(ctx context.Context) (*sandbox.CheckpointCompatibility, error) { - return nil, dockerProbe(c, entry.Image, config.Specification.Resources)(ctx) - } - result.BackendFingerprint = sandbox.BackendFingerprint(config.Provider, entry.Host) - return closeProvider, nil -} - -// The probe reports its first failed check as its readiness class: the Docker -// daemon, then limit support, then host capacity for one sandbox of the -// deployment specification, then the Runtime image. Unclassified -// failures stay provider_unavailable. The returned text is local; only its -// class is reported. -func dockerProbe(c *client.Client, image string, resources sandbox.Resources) func(context.Context) error { - return func(ctx context.Context) error { - if _, err := c.Ping(ctx, client.PingOptions{}); err != nil { - return fmt.Errorf("%w: Docker daemon is unreachable", sandbox.ErrProviderUnavailable) - } - host, err := c.Info(ctx, client.InfoOptions{}) - if err != nil { - return fmt.Errorf("%w: cannot inspect Docker host resource support", sandbox.ErrProviderUnavailable) - } - if !host.Info.MemoryLimit || !host.Info.CPUCfsQuota { - return fmt.Errorf("%w: Docker does not enforce CPU and memory limits", sandbox.ErrHostUnsupported) - } - if host.Info.MemTotal <= 0 { - return errors.New("Docker host memory capacity is unavailable") - } - if err := sandbox.CheckCapacity(resources, host.Info.NCPU, uint64(host.Info.MemTotal)); err != nil { - return err - } - if _, err = c.ImageInspect(ctx, image); errdefs.IsNotFound(err) { - return sandbox.ErrRuntimeImageUnavailable - } else if err != nil { - // The daemon did not answer; the image may still be present. - return fmt.Errorf("%w: cannot inspect the Runtime image", sandbox.ErrProviderUnavailable) - } - return nil - } -} diff --git a/services/core/internal/sandbox/docker/node_test.go b/services/core/internal/sandbox/docker/node_test.go deleted file mode 100644 index 61667bb14..000000000 --- a/services/core/internal/sandbox/docker/node_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package docker - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "path/filepath" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/moby/moby/client" -) - -func TestNativeRequiresTheInstalledImage(t *testing.T) { - image := "sha256:" + strings.Repeat("b", 64) - config := sandbox.NodeConfig{Native: json.RawMessage(`{"image":"` + image + `"}`)} - if entry, err := decodeNative(config); err != nil || entry.Image != image { - t.Fatalf("rejected the installed image: %v", err) - } - for _, native := range []string{`{"image":"` + image + `","cpus":2}`, `{"image":null}`, `{}`} { - config.Native = json.RawMessage(native) - if _, err := decodeNative(config); err == nil { - t.Fatalf("accepted native %s", native) - } - } -} - -func TestDockerProbeDiagnostics(t *testing.T) { - image := "sha256:" + strings.Repeat("c", 64) - for _, tc := range []struct { - name string - limits bool - cpus int - imageStatus int - want string - unreachable, infoFails bool - }{ - {name: "unreachable", unreachable: true, want: "provider_unavailable"}, - {name: "info", infoFails: true, want: "provider_unavailable"}, - // The pinned image is also missing below; earlier checks take precedence. - {name: "limits", cpus: 8, imageStatus: 404, want: "host_unsupported"}, - {name: "capacity", limits: true, cpus: 1, imageStatus: 404, want: "capacity_insufficient"}, - {name: "image", limits: true, cpus: 8, imageStatus: 404, want: "runtime_image_unavailable"}, - {name: "image_inspect_fails", limits: true, cpus: 8, imageStatus: 500, want: "provider_unavailable"}, - {name: "ready", limits: true, cpus: 8, imageStatus: 200}, - } { - t.Run(tc.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - switch path := strings.TrimPrefix(r.URL.Path, "/v1.52"); { - case path == "/_ping": - _, _ = w.Write([]byte("OK")) - case path == "/info" && !tc.infoFails: - _ = json.NewEncoder(w).Encode(map[string]any{"MemoryLimit": tc.limits, "CpuCfsQuota": tc.limits, "NCPU": tc.cpus, "MemTotal": int64(64) << 30}) - case path == "/images/"+image+"/json" && tc.imageStatus == 200: - _ = json.NewEncoder(w).Encode(map[string]string{"Id": image}) - case path == "/images/"+image+"/json": - w.WriteHeader(tc.imageStatus) - _, _ = w.Write([]byte(`{"message":"private daemon detail"}`)) - default: - w.WriteHeader(500) - _, _ = w.Write([]byte(`{"message":"private daemon detail"}`)) - } - })) - defer server.Close() - host := server.URL - if tc.unreachable { - host = "unix://" + filepath.Join(t.TempDir(), "missing.sock") - } - c, err := client.New(client.WithHost(host), client.WithAPIVersion("1.52")) - if err != nil { - t.Fatal(err) - } - defer c.Close() - if got := sandbox.NodeDiagnostic(dockerProbe(c, image, sandbox.Resources{CPUs: 2, MemoryMiB: 1024})(t.Context())); got != tc.want { - t.Fatalf("diagnostic = %q, want %q", got, tc.want) - } - }) - } -} diff --git a/services/core/internal/sandbox/docker/operations.go b/services/core/internal/sandbox/docker/operations.go deleted file mode 100644 index 784193167..000000000 --- a/services/core/internal/sandbox/docker/operations.go +++ /dev/null @@ -1,68 +0,0 @@ -package docker - -import ( - "context" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" -) - -// Operations is this adapter's complete authored resource contract. -func Operations() providercontract.Operations { - return providercontract.Operations{ - "Create": {State: providercontract.Supported}, - "GetInfo": {State: providercontract.Supported}, - "Renew": {State: providercontract.Supported}, - "Kill": {State: providercontract.Supported}, - "RunCommand": {State: providercontract.Supported}, - "Initial": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "NewCompute": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "GetCompute": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "Suspend": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "Resume": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "KillCompute": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "DeleteSnapshot": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "RunCommandCompute": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "ResumeCompute": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "RenewCompute": {State: providercontract.Unsupported, Reason: "docker_does_not_support_checkpoints"}, - "RestorePreparation": {State: providercontract.Unsupported, Reason: "docker_does_not_support_preparation_cache"}, - "SavePreparation": {State: providercontract.Unsupported, Reason: "docker_does_not_support_preparation_cache"}, - "Observe": {State: providercontract.Supported}, - } -} -func (*Provider) ProviderOperations() providercontract.Operations { return Operations() } -func (p *Provider) Initial(context.Context, sandbox.Reference) (sandbox.Compute, error) { - return sandbox.Compute{}, &providercontract.UnsupportedError{Operation: "Initial", Reason: Operations()["Initial"].Reason} -} -func (p *Provider) NewCompute(context.Context, sandbox.Reference, uint64, *sandbox.SnapshotIdentity) (sandbox.Compute, error) { - return sandbox.Compute{}, &providercontract.UnsupportedError{Operation: "NewCompute", Reason: Operations()["NewCompute"].Reason} -} -func (p *Provider) GetCompute(context.Context, sandbox.Reference, sandbox.Compute) (sandbox.ComputeState, error) { - return sandbox.ComputeState{}, &providercontract.UnsupportedError{Operation: "GetCompute", Reason: Operations()["GetCompute"].Reason} -} -func (p *Provider) Suspend(context.Context, sandbox.SuspendRequest) (sandbox.ComputeState, error) { - return sandbox.ComputeState{}, &providercontract.UnsupportedError{Operation: "Suspend", Reason: Operations()["Suspend"].Reason} -} -func (p *Provider) Resume(context.Context, sandbox.ResumeRequest) (sandbox.ComputeState, error) { - return sandbox.ComputeState{}, &providercontract.UnsupportedError{Operation: "Resume", Reason: Operations()["Resume"].Reason} -} -func (p *Provider) KillCompute(context.Context, sandbox.Reference, sandbox.Compute) error { - return &providercontract.UnsupportedError{Operation: "KillCompute", Reason: Operations()["KillCompute"].Reason} -} -func (p *Provider) DeleteSnapshot(context.Context, sandbox.Reference, sandbox.SnapshotIdentity) error { - return &providercontract.UnsupportedError{Operation: "DeleteSnapshot", Reason: Operations()["DeleteSnapshot"].Reason} -} -func (p *Provider) RunCommandCompute(context.Context, sandbox.Reference, sandbox.Compute, sandbox.Command) (sandbox.CommandResult, error) { - return sandbox.CommandResult{}, &providercontract.UnsupportedError{Operation: "RunCommandCompute", Reason: Operations()["RunCommandCompute"].Reason} -} -func (p *Provider) ResumeCompute(context.Context, sandbox.Reference, sandbox.Compute) (sandbox.ComputeState, error) { - return sandbox.ComputeState{}, &providercontract.UnsupportedError{Operation: "ResumeCompute", Reason: Operations()["ResumeCompute"].Reason} -} -func (p *Provider) RenewCompute(context.Context, sandbox.Reference, sandbox.Compute) (sandbox.ComputeState, error) { - return sandbox.ComputeState{}, &providercontract.UnsupportedError{Operation: "RenewCompute", Reason: Operations()["RenewCompute"].Reason} -} -func (p *Provider) RestorePreparation(context.Context, sandbox.Reference, string) (bool, error) { - return false, &providercontract.UnsupportedError{Operation: "RestorePreparation", Reason: Operations()["RestorePreparation"].Reason} -} -func (p *Provider) SavePreparation(context.Context, sandbox.Reference, string) error { - return &providercontract.UnsupportedError{Operation: "SavePreparation", Reason: Operations()["SavePreparation"].Reason} -} diff --git a/services/core/internal/sandbox/docker/provider.go b/services/core/internal/sandbox/docker/provider.go deleted file mode 100644 index f97c8070e..000000000 --- a/services/core/internal/sandbox/docker/provider.go +++ /dev/null @@ -1,233 +0,0 @@ -// Package docker is a thin SDK adapter for the dedicated, colocated Runtime. -package docker - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "fmt" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "strings" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/containerd/errdefs" - "github.com/google/uuid" - "github.com/moby/moby/client" -) - -const labelPrefix = "io.oac." - -// Config is trusted operator configuration, never public Session input. The -// immutable image contains the qualified native profile and all Runtime binaries. -// Seccomp is JSON content, not a path on the Docker host. Network must provide -// trusted daemon/model connectivity; native tool network policy is in the image. -type Config struct { - InstallationID, Image, Network, Seccomp string - NestedSandbox bool - Resources *sandbox.Resources -} -type Provider struct { - client *client.Client - config Config -} - -var _ sandbox.SandboxProvider = (*Provider)(nil) - -func New(c *client.Client, config Config) (*Provider, error) { - if c == nil || !validID(config.InstallationID) || (!strings.HasPrefix(config.Image, "sha256:") && !strings.Contains(config.Image, "@sha256:")) || config.Seccomp == "" || config.Network == "" || config.Network == "host" || strings.HasPrefix(config.Network, "container:") { - return nil, sandbox.ErrInvalid - } - if config.Resources != nil { - if config.Resources.ValidatePolicy("docker", Policy()) != nil { - return nil, sandbox.ErrInvalid - } - resources := *config.Resources - config.Resources = &resources - } - return &Provider{client: c, config: config}, nil -} -func validID(v string) bool { - u, e := uuid.Parse(v) - return e == nil && u != uuid.Nil && u.String() == v -} -func validReference(r sandbox.Reference) bool { - return validID(r.TenantID) && validID(r.EnvironmentID) && validID(r.AllocationID) -} -func (p *Provider) name(r sandbox.Reference) string { - h := sha256.Sum256([]byte(p.config.InstallationID + ":" + r.TenantID + ":" + r.EnvironmentID + ":" + r.AllocationID)) - return "oac-runtime-" + hex.EncodeToString(h[:16]) -} -func (p *Provider) labels(r sandbox.Reference) map[string]string { - return map[string]string{ - labelPrefix + "installation": p.config.InstallationID, labelPrefix + "tenant": r.TenantID, labelPrefix + "environment": r.EnvironmentID, labelPrefix + "allocation": r.AllocationID, - } -} -func (p *Provider) owns(labels map[string]string, r sandbox.Reference) bool { - for k, v := range p.labels(r) { - if labels[k] != v { - return false - } - } - return true -} -func (p *Provider) inspect(ctx context.Context, r sandbox.Reference) (client.ContainerInspectResult, error) { - if !validReference(r) { - return client.ContainerInspectResult{}, sandbox.ErrInvalid - } - v, e := p.client.ContainerInspect(ctx, p.name(r), client.ContainerInspectOptions{}) - if errdefs.IsNotFound(e) { - return v, sandbox.ErrNotFound - } - if e != nil { - return v, e - } - if v.Container.Config == nil || !p.owns(v.Container.Config.Labels, r) { - return v, sandbox.ErrOwnership - } - return v, nil -} -func (p *Provider) GetInfo(ctx context.Context, r sandbox.Reference) (sandbox.Info, error) { - info := sandbox.Info{Reference: r} - v, e := p.inspect(ctx, r) - if e != nil { - return info, e - } - if e = p.verifyConfiguration(ctx, v); e != nil { - return info, e - } - info.ProviderID = v.Container.ID - info.State = string(v.Container.State.Status) - info.BootstrapComplete = info.State == "running" - return info, nil -} - -// Renew observes Docker state. Docker has no lease; service-owned expiry must be -// managed durably by Core. Never synthesize an expiry or revive a stopped Runtime. -func (p *Provider) Renew(ctx context.Context, r sandbox.Reference) (sandbox.Info, error) { - return p.GetInfo(ctx, r) -} - -func (p *Provider) Create(ctx context.Context, b sandbox.Bootstrap) (sandbox.Info, error) { - if b.Workspace != nil { - return sandbox.Info{Reference: b.Reference}, &providercontract.UnsupportedError{Operation: "Create", Reason: "external_workspace_unsupported"} - } - info := sandbox.Info{Reference: b.Reference} - if !validReference(b.Reference) || !validID(b.SessionID) || !validID(b.DeviceID) || b.RuntimeConnection().Validate() != nil { - return info, sandbox.ErrInvalid - } - if existing, e := p.GetInfo(ctx, b.Reference); e == nil { - return existing, sandbox.ErrExists - } else if !errors.Is(e, sandbox.ErrNotFound) { - return info, e - } - name := p.name(b.Reference) - // Retained volumes without a container are partial or lost state, not an - // invitation to overwrite native history with a new bootstrap identity. - for _, suffix := range []string{"-home", "-environment"} { - v, err := p.client.VolumeInspect(ctx, name+suffix, client.VolumeInspectOptions{}) - if err == nil { - if !p.owns(v.Volume.Labels, b.Reference) { - return info, sandbox.ErrOwnership - } - return info, sandbox.ErrExists - } - if !errdefs.IsNotFound(err) { - return info, err - } - } - for _, suffix := range []string{"-home", "-environment"} { - v, e := p.client.VolumeCreate(ctx, client.VolumeCreateOptions{Name: name + suffix, Labels: p.labels(b.Reference)}) - if e != nil { - return info, e - } - if !p.owns(v.Volume.Labels, b.Reference) { - return info, sandbox.ErrOwnership - } - } - options := runtimeContainerOptions(p.config, name, p.labels(b.Reference)) - options.Config.Cmd = []string{"connect", "--profile", "default", "--bootstrap-file", "/home/runtime/runtime-bootstrap.json"} - v, e := p.client.ContainerCreate(ctx, options) - if errdefs.IsConflict(e) { - return info, sandbox.ErrExists - } - if e != nil { - return info, e - } - info.ProviderID = v.ID - if p.config.Resources != nil { - actual, err := p.inspect(ctx, b.Reference) - if err != nil { - info.CreateSettled = true - return info, err - } - if err = p.verifyConfiguration(ctx, actual); err != nil { - // Container creation completed and bootstrap has not started. No - // outstanding mutation can recreate resources after owned cleanup. - info.CreateSettled = true - return info, err - } - } - // Any failure returns the retained allocation reference. The caller must Kill - // it, including on a lost acknowledgement. Never erase uncertain owner state. - if e = p.bootstrap(ctx, v.ID, b); e != nil { - return info, fmt.Errorf("runtime bootstrap: %w", e) - } - if _, e = p.client.ContainerStart(ctx, v.ID, client.ContainerStartOptions{}); e != nil { - return info, e - } - return p.GetInfo(ctx, b.Reference) -} - -// Kill is idempotent only for absence, not for errors or foreign ownership. It -// checks all resources before removing any and confirms removal of named volumes. -func (p *Provider) Kill(ctx context.Context, r sandbox.Reference) error { - if !validReference(r) { - return sandbox.ErrInvalid - } - c, e := p.inspect(ctx, r) - if e != nil && !errors.Is(e, sandbox.ErrNotFound) { - return e - } - present := e == nil - name := p.name(r) - volumes := []string{} - for _, suffix := range []string{"-home", "-environment"} { - v, e := p.client.VolumeInspect(ctx, name+suffix, client.VolumeInspectOptions{}) - if errdefs.IsNotFound(e) { - continue - } - if e != nil { - return e - } - if !p.owns(v.Volume.Labels, r) { - return sandbox.ErrOwnership - } - volumes = append(volumes, name+suffix) - } - if present { - if _, e = p.client.ContainerRemove(ctx, c.Container.ID, client.ContainerRemoveOptions{Force: true}); e != nil && !errdefs.IsNotFound(e) { - return e - } - } - for _, v := range volumes { - if _, e = p.client.VolumeRemove(ctx, v, client.VolumeRemoveOptions{}); e != nil && !errdefs.IsNotFound(e) { - return e - } - } - if _, e = p.inspect(ctx, r); !errors.Is(e, sandbox.ErrNotFound) { - if e == nil { - return errors.New("container removal unconfirmed") - } - return e - } - for _, v := range volumes { - if _, e = p.client.VolumeInspect(ctx, v, client.VolumeInspectOptions{}); !errdefs.IsNotFound(e) { - if e == nil { - return errors.New("volume removal unconfirmed") - } - return e - } - } - return nil -} diff --git a/services/core/internal/sandbox/docker/provider_test.go b/services/core/internal/sandbox/docker/provider_test.go deleted file mode 100644 index 6a91295ae..000000000 --- a/services/core/internal/sandbox/docker/provider_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package docker - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "errors" - "os" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/runtimebootstrap" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" - "github.com/containerd/errdefs" - "github.com/google/uuid" - "github.com/moby/moby/client" -) - -func TestProviderRejectsUnsafeOperatorConfiguration(t *testing.T) { - c, e := client.New() - if e != nil { - t.Fatal(e) - } - defer c.Close() - base := Config{InstallationID: uuid.NewString(), Image: "test@sha256:" + strings.Repeat("a", 64), Network: "bridge", Seccomp: `{}`} - for _, change := range []func(*Config){func(c *Config) { c.Image = "mutable:latest" }, func(c *Config) { c.InstallationID = "" }, func(c *Config) { c.Network = "host" }, func(c *Config) { c.Network = "container:other" }, func(c *Config) { c.Seccomp = "" }} { - v := base - change(&v) - if _, e := New(c, v); !errors.Is(e, sandbox.ErrInvalid) { - t.Fatalf("accepted invalid configuration: %v", e) - } - } -} - -// This optional Docker mechanism test uses a pinned fixture image whose entrypoint -// is sleep. It is not native/model acceptance; the real Runtime has separate checks. -func TestDockerProviderLifecycle(t *testing.T) { - image := os.Getenv("AGENTS_RUNTIME_DOCKER_TEST_IMAGE") - if image == "" { - t.Skip("explicit Docker fixture image required") - } - seccomp, e := os.ReadFile("seccomp.json") - if e != nil { - t.Fatal(e) - } - c, e := client.New(client.FromEnv) - if e != nil { - t.Fatal(e) - } - defer c.Close() - installationID := uuid.NewString() - p, e := New(c, Config{InstallationID: installationID, Image: image, Network: "bridge", Seccomp: string(seccomp)}) - if e != nil { - t.Fatal(e) - } - ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) - defer cancel() - bootstrap := func() sandbox.Bootstrap { - return sandbox.Bootstrap{Reference: sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "http://core.invalid/api/v1", Credential: "synthetic-test-credential"} - } - b := bootstrap() - t.Cleanup(func() { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - if e := p.Kill(ctx, b.Reference); e != nil { - t.Error(e) - } - }) - t.Run("stdin concurrent output and EOF", func(t *testing.T) { - inputOwner := bootstrap() - if _, err := p.Create(ctx, inputOwner); err != nil { - t.Fatal(err) - } - defer func() { - cleanup, stop := context.WithTimeout(context.Background(), 20*time.Second) - defer stop() - if err := p.Kill(cleanup, inputOwner.Reference); err != nil { - t.Error(err) - } - }() - for _, data := range [][]byte{{}, bytes.Repeat([]byte{0, 255, 10, 1, 42}, 900000)} { - result, err := p.RunCommand(ctx, inputOwner.Reference, sandbox.Command{Args: []string{"/bin/sh", "-c", "head -c 131072 /dev/zero; sha256sum"}, Stdin: data}) - digest := sha256.Sum256(data) - if err != nil || result.ExitCode != 0 || !strings.HasSuffix(result.Stdout, hex.EncodeToString(digest[:])+" -\n") || len(result.Stdout) != 131072+68 { - t.Fatalf("stdin/EOF failure: input=%d stdout=%d exit=%d error=%v", len(data), len(result.Stdout), result.ExitCode, err) - } - } - }) - info, e := p.Create(ctx, b) - contracttest.AssertObservation(t, info, e, b.Reference, "", "running") - resources, e := p.Observe(ctx, runtimeobs.Target{ - TenantID: b.TenantID, SessionID: b.SessionID, EnvironmentID: b.EnvironmentID, Mode: runtimeobs.ModeManaged, - Instance: runtimeobs.Instance{AllocationID: b.AllocationID, ProviderKey: installationID, DeviceID: b.DeviceID}, - }) - if e != nil || resources.StartedAt == nil || resources.CPUUsageSecondsTotal == nil || resources.MemoryUsageBytes == nil || resources.CPUCapacityCores == nil || resources.MemoryLimitBytes == nil { - t.Fatalf("bad resource observation: %+v %v", resources, e) - } - inspected, e := p.inspect(ctx, b.Reference) - if e != nil { - t.Fatal(e) - } - if strings.Contains(string(inspected.Raw), b.Credential) || inspected.Container.Config.User != "1000:1000" || !inspected.Container.HostConfig.ReadonlyRootfs || inspected.Container.HostConfig.Privileged { - t.Fatal("unsafe Docker configuration") - } - changed := b - changed.Credential = "must-not-replace-existing" - if _, e = p.Create(ctx, changed); !errors.Is(e, sandbox.ErrExists) { - t.Fatalf("duplicate not rejected: %v", e) - } - r, e := p.RunCommand(ctx, b.Reference, sandbox.Command{Args: []string{"cat", "/home/runtime/runtime-bootstrap.json"}}) - if e != nil { - t.Fatal(e) - } - auth, decodeErr := runtimebootstrap.Decode([]byte(r.Stdout)) - if decodeErr != nil || auth.Credential != b.Credential || auth.DeviceID != b.DeviceID { - t.Fatal("bootstrap changed or malformed") - } - r, e = p.RunCommand(ctx, b.Reference, sandbox.Command{Args: []string{"sh", "-c", "printf retained > /environment/workspace/history; printf failed >&2; exit 7"}}) - if e != nil || r.ExitCode != 7 || r.Stderr != "failed" { - t.Fatalf("lost command status: %+v %v", r, e) - } - r, e = p.RunCommand(ctx, b.Reference, sandbox.Command{Args: []string{"sh", "-c", "set -eu; test \"$(cat /workspace/history)\" = retained; printf replaced > /environment/staging/replacement; mv /environment/staging/replacement /environment/workspace/history; cat /workspace/history"}}) - if e != nil || r.ExitCode != 0 || r.Stdout != "replaced" { - t.Fatal("public workspace view or atomic staging failed", e) - } - wrong := b.Reference - wrong.TenantID = uuid.NewString() - if _, e = p.GetInfo(ctx, wrong); !errors.Is(e, sandbox.ErrNotFound) { - t.Fatal("foreign allocation visible") - } - if e = p.Kill(ctx, wrong); e != nil { - t.Fatal(e) - } - if _, e = p.Renew(ctx, b.Reference); e != nil { - t.Fatal("wrong tenant removed the owner") - } - timeout := 1 - if _, e = c.ContainerRestart(ctx, info.ProviderID, client.ContainerRestartOptions{Timeout: &timeout}); e != nil { - t.Fatal(e) - } - r, e = p.RunCommand(ctx, b.Reference, sandbox.Command{Args: []string{"cat", "/environment/workspace/history"}}) - if e != nil || r.Stdout != "replaced" { - t.Fatal("restart lost workspace") - } - r, e = p.RunCommand(ctx, b.Reference, sandbox.Command{Args: []string{"sh", "-c", "touch /cannot-write-root"}}) - if e != nil || r.ExitCode == 0 { - t.Fatal("root filesystem writable") - } - // Container loss must not trigger credential overwrite or state replacement. - if _, e = c.ContainerRemove(ctx, info.ProviderID, client.ContainerRemoveOptions{Force: true}); e != nil { - t.Fatal(e) - } - if _, e = p.Create(ctx, b); !errors.Is(e, sandbox.ErrExists) { - t.Fatalf("retained volumes reused: %v", e) - } - if e = p.Kill(ctx, b.Reference); e != nil { - t.Fatal(e) - } - for _, suffix := range []string{"-home", "-environment"} { - if _, e = c.VolumeInspect(ctx, p.name(b.Reference)+suffix, client.VolumeInspectOptions{}); !errdefs.IsNotFound(e) { - t.Fatal("named volume remains") - } - } - // A colliding resource with different ownership cannot be deleted, including - // when its container is absent after a partial creation. - foreign := bootstrap() - volume := p.name(foreign.Reference) + "-home" - if _, e = c.VolumeCreate(ctx, client.VolumeCreateOptions{Name: volume, Labels: map[string]string{"fixture": "foreign"}}); e != nil { - t.Fatal(e) - } - t.Cleanup(func() { - _, e := c.VolumeRemove(context.Background(), volume, client.VolumeRemoveOptions{}) - if e != nil { - t.Error(e) - } - }) - if e = p.Kill(ctx, foreign.Reference); !errors.Is(e, sandbox.ErrOwnership) { - t.Fatal("foreign volume cleanup accepted") - } - if _, e = p.Create(ctx, foreign); !errors.Is(e, sandbox.ErrOwnership) { - t.Fatal("foreign volume bootstrap accepted") - } - // Closing initialization output is not process termination. Require explicit - // reclamation, without returning partial output as a successful command. - next := bootstrap() - defer p.Kill(context.Background(), next.Reference) - if _, e = p.Create(ctx, next); e != nil { - t.Fatal(e) - } - short, stop := context.WithTimeout(ctx, 100*time.Millisecond) - _, e = p.RunCommand(short, next.Reference, sandbox.Command{Args: []string{"sleep", "30"}}) - stop() - if !errors.Is(e, sandbox.ErrCommandUnconfirmed) { - t.Fatalf("timeout classified as certain: %v", e) - } - if e = p.Kill(ctx, next.Reference); e != nil { - t.Fatal(e) - } -} diff --git a/services/core/internal/sandbox/docker/recovery_test.go b/services/core/internal/sandbox/docker/recovery_test.go deleted file mode 100644 index ebb888fdb..000000000 --- a/services/core/internal/sandbox/docker/recovery_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package docker - -import ( - "context" - "errors" - "io" - "net" - "net/http" - "net/http/httptest" - "os" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/containerd/errdefs" - "github.com/google/uuid" - "github.com/moby/moby/client" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" -) - -// These controlled transport faults use real Docker compute and volumes. They -// establish Provider recovery observations, not native or model acceptance. -func TestDockerProviderRecoveryObservations(t *testing.T) { - image := os.Getenv("AGENTS_RUNTIME_DOCKER_TEST_IMAGE") - if image == "" { - t.Skip("explicit Docker fixture image required") - } - seccomp, err := os.ReadFile("seccomp.json") - if err != nil { - t.Fatal(err) - } - for _, fault := range []string{"lost-start-response", "partial-volumes"} { - t.Run(fault, func(t *testing.T) { - ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second) - defer cancel() - transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - return (&net.Dialer{}).DialContext(ctx, "unix", "/var/run/docker.sock") - }} - defer transport.CloseIdleConnections() - var fired atomic.Bool - var creates atomic.Int32 - proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/containers/create") { - creates.Add(1) - if fault == "partial-volumes" { - fired.Store(true) - http.Error(w, `{"message":"injected before container create"}`, 500) - return - } - } - request := r.Clone(r.Context()) - request.RequestURI = "" - request.URL.Scheme = "http" - request.URL.Host = "docker" - request.Host = "docker" - response, e := transport.RoundTrip(request) - if e != nil { - http.Error(w, "Docker transport failed", 502) - return - } - defer response.Body.Close() - if fault == "lost-start-response" && r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/start") && response.StatusCode == 204 && fired.CompareAndSwap(false, true) { - connection, _, e := w.(http.Hijacker).Hijack() - if e == nil { - _ = connection.Close() - } - return - } - for k, values := range response.Header { - for _, v := range values { - w.Header().Add(k, v) - } - } - w.WriteHeader(response.StatusCode) - _, _ = io.Copy(w, response.Body) - })) - defer proxy.Close() - c, e := client.New(client.WithHost(proxy.URL)) - if e != nil { - t.Fatal(e) - } - defer c.Close() - config := Config{InstallationID: uuid.NewString(), Image: image, Network: "bridge", Seccomp: string(seccomp)} - p, e := New(c, config) - if e != nil { - t.Fatal(e) - } - b := sandbox.Bootstrap{Reference: sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "http://core.invalid/api/v1", Credential: "synthetic-recovery-token"} - direct, e := client.New(client.WithHost("unix:///var/run/docker.sock")) - if e != nil { - t.Fatal(e) - } - defer direct.Close() - recovered, e := New(direct, config) - if e != nil { - t.Fatal(e) - } - defer func() { - cleanup, stop := context.WithTimeout(context.Background(), 15*time.Second) - defer stop() - if e := recovered.Kill(cleanup, b.Reference); e != nil { - t.Error(e) - } - }() - if _, e := p.Create(ctx, b); e == nil || !fired.Load() { - t.Fatal("Create did not expose the injected uncertainty") - } - info, e := recovered.GetInfo(ctx, b.Reference) - if fault == "lost-start-response" { - if e != nil || info.State != "running" || !info.BootstrapComplete { - t.Fatalf("original allocation not recoverable: %+v %v", info, e) - } - } else { - if !errors.Is(e, sandbox.ErrNotFound) { - t.Fatalf("partial allocation observation: %v", e) - } - for _, suffix := range []string{"-home", "-environment"} { - if _, e := direct.VolumeInspect(ctx, recovered.name(b.Reference)+suffix, client.VolumeInspectOptions{}); e != nil { - t.Fatal("missing container concealed missing volume fixture", e) - } - } - } - if creates.Load() != 1 { - t.Fatalf("Create was replayed %d times", creates.Load()) - } - if e := recovered.Kill(ctx, b.Reference); e != nil { - t.Fatal(e) - } - for _, suffix := range []string{"-home", "-environment"} { - if _, e := direct.VolumeInspect(ctx, recovered.name(b.Reference)+suffix, client.VolumeInspectOptions{}); !errdefs.IsNotFound(e) { - t.Fatal("owned volume remains", e) - } - } - }) - } -} diff --git a/services/core/internal/sandbox/docker/resources.go b/services/core/internal/sandbox/docker/resources.go deleted file mode 100644 index 863af750a..000000000 --- a/services/core/internal/sandbox/docker/resources.go +++ /dev/null @@ -1,89 +0,0 @@ -package docker - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/moby/moby/api/types/container" - "github.com/moby/moby/client" -) - -// Observe is read-only. Inspect verifies allocation ownership before Docker -// statistics are requested; it never renews or changes the container. -func (p *Provider) Observe(ctx context.Context, target runtimeobs.Target) (runtimeobs.Sample, error) { - if target.Mode != runtimeobs.ModeManaged || target.Instance.AllocationID == "" { - return runtimeobs.Sample{}, sandbox.ErrInvalid - } - if target.Instance.ProviderKey != p.config.InstallationID { - return runtimeobs.Sample{}, sandbox.ErrOwnership - } - reference := sandbox.Reference{TenantID: target.TenantID, EnvironmentID: target.EnvironmentID, AllocationID: target.Instance.AllocationID} - inspected, err := p.inspect(ctx, reference) - if errors.Is(err, sandbox.ErrNotFound) { - return runtimeobs.Sample{}, runtimeobs.ErrNotRunning - } - if err != nil { - return runtimeobs.Sample{}, err - } - if inspected.Container.State == nil || !inspected.Container.State.Running { - return runtimeobs.Sample{}, runtimeobs.ErrNotRunning - } - result, err := p.client.ContainerStats(ctx, inspected.Container.ID, client.ContainerStatsOptions{Stream: false, IncludePreviousSample: false}) - if err != nil { - return runtimeobs.Sample{}, fmt.Errorf("read Docker Runtime statistics: %w", err) - } - defer result.Body.Close() - var stats dockerStatsResponse - if err := json.NewDecoder(result.Body).Decode(&stats); err != nil { - return runtimeobs.Sample{}, fmt.Errorf("decode Docker Runtime statistics: %w", err) - } - return sampleFromDocker(inspected.Container, stats) -} - -type dockerStatsResponse struct { - Read time.Time `json:"read"` - CPUStats *struct { - CPUUsage *struct { - TotalUsage *uint64 `json:"total_usage"` - } `json:"cpu_usage"` - } `json:"cpu_stats"` - MemoryStats *struct { - Usage *uint64 `json:"usage"` - } `json:"memory_stats"` -} - -func sampleFromDocker(inspected container.InspectResponse, stats dockerStatsResponse) (runtimeobs.Sample, error) { - if inspected.State == nil || inspected.HostConfig == nil || !inspected.State.Running || stats.Read.IsZero() { - return runtimeobs.Sample{}, errors.New("incomplete Docker Runtime observation") - } - startedAt, err := time.Parse(time.RFC3339Nano, inspected.State.StartedAt) - if err != nil || startedAt.IsZero() || startedAt.After(stats.Read) { - return runtimeobs.Sample{}, errors.New("invalid Docker Runtime start time") - } - sample := runtimeobs.Sample{ - ObservedAt: stats.Read, - StartedAt: &startedAt, - } - if stats.CPUStats != nil && stats.CPUStats.CPUUsage != nil && stats.CPUStats.CPUUsage.TotalUsage != nil { - cpuUsage := float64(*stats.CPUStats.CPUUsage.TotalUsage) / float64(time.Second) - sample.CPUUsageSecondsTotal = &cpuUsage - } - if stats.MemoryStats != nil && stats.MemoryStats.Usage != nil { - memoryUsage := *stats.MemoryStats.Usage - sample.MemoryUsageBytes = &memoryUsage - } - if inspected.HostConfig.NanoCPUs > 0 { - capacity := float64(inspected.HostConfig.NanoCPUs) / 1_000_000_000 - sample.CPUCapacityCores = &capacity - } - if inspected.HostConfig.Memory > 0 { - limit := uint64(inspected.HostConfig.Memory) - sample.MemoryLimitBytes = &limit - } - return sample, nil -} diff --git a/services/core/internal/sandbox/docker/resources_test.go b/services/core/internal/sandbox/docker/resources_test.go deleted file mode 100644 index d31d14f4c..000000000 --- a/services/core/internal/sandbox/docker/resources_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package docker - -import ( - "encoding/json" - "errors" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" - "github.com/google/uuid" - "github.com/moby/moby/api/types/container" - "github.com/moby/moby/client" -) - -func TestObserveVerifiesOwnershipThenReadsOneShotStats(t *testing.T) { - installationID := uuid.NewString() - target := runtimeobs.Target{ - TenantID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), Mode: runtimeobs.ModeManaged, - Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), ProviderKey: installationID, DeviceID: uuid.NewString()}, - } - observed := time.Now().UTC().Truncate(time.Microsecond) - started := observed.Add(-time.Minute) - statsRead := false - omitMeasurements := false - running := true - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - switch { - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/json"): - _ = json.NewEncoder(w).Encode(map[string]any{ - "Id": "container-id", - "State": map[string]any{"Status": "running", "Running": running, "StartedAt": started.Format(time.RFC3339Nano)}, - "HostConfig": map[string]any{"NanoCpus": 2_000_000_000, "Memory": 2048}, - "Config": map[string]any{"Labels": map[string]string{ - labelPrefix + "installation": installationID, - labelPrefix + "tenant": target.TenantID, - labelPrefix + "environment": target.EnvironmentID, - labelPrefix + "allocation": target.Instance.AllocationID, - }}, - }) - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/containers/container-id/stats"): - if r.URL.Query().Get("stream") != "false" || r.URL.Query().Get("one-shot") != "true" { - t.Errorf("stats request was not one-shot: %s", r.URL.RawQuery) - } - statsRead = true - if omitMeasurements { - _ = json.NewEncoder(w).Encode(map[string]any{"read": observed}) - return - } - _ = json.NewEncoder(w).Encode(container.StatsResponse{ - Read: observed, - CPUStats: container.CPUStats{CPUUsage: container.CPUUsage{TotalUsage: 1_500_000_000}}, - MemoryStats: container.MemoryStats{Usage: 1024}, - }) - default: - http.NotFound(w, r) - } - })) - defer server.Close() - c, err := client.New(client.WithHost(server.URL)) - if err != nil { - t.Fatal(err) - } - defer c.Close() - p, err := New(c, Config{InstallationID: installationID, Image: "fixture@sha256:" + strings.Repeat("a", 64), Network: "bridge", Seccomp: `{}`}) - if err != nil { - t.Fatal(err) - } - sample, err := p.Observe(t.Context(), target) - if err != nil || !statsRead || sample.CPUUsageSecondsTotal == nil || *sample.CPUUsageSecondsTotal != 1.5 || sample.MemoryUsageBytes == nil || *sample.MemoryUsageBytes != 1024 { - t.Fatalf("bad one-shot observation: %+v %v stats=%v", sample, err, statsRead) - } - omitMeasurements = true - missing, err := p.Observe(t.Context(), target) - if err != nil || missing.CPUUsageSecondsTotal != nil || missing.MemoryUsageBytes != nil || missing.CPUCapacityCores == nil || missing.MemoryLimitBytes == nil { - t.Fatalf("missing Docker measurements became zero: %+v %v", missing, err) - } - running = false - statsRead = false - if _, err := p.Observe(t.Context(), target); !errors.Is(err, runtimeobs.ErrNotRunning) || statsRead { - t.Fatalf("stopped Runtime was not classified before stats: %v stats=%v", err, statsRead) - } - running = true - foreign := target - foreign.Instance.ProviderKey = uuid.NewString() - statsRead = false - if _, err := p.Observe(t.Context(), foreign); err == nil || statsRead { - t.Fatal("foreign provider identity reached Docker stats") - } -} - -func TestSampleFromDockerPreservesObservedZeroAndConfiguredCapacity(t *testing.T) { - observed := time.Date(2026, 9, 22, 1, 2, 3, 0, time.UTC) - started := observed.Add(-5 * time.Minute) - zero := uint64(0) - sample, err := sampleFromDocker(container.InspectResponse{ - State: &container.State{Running: true, StartedAt: started.Format(time.RFC3339Nano)}, - HostConfig: &container.HostConfig{Resources: container.Resources{NanoCPUs: 2_000_000_000, Memory: 2 * 1024 * 1024 * 1024}}, - }, testDockerStats(observed, &zero, &zero)) - if err != nil { - t.Fatal(err) - } - if sample.ObservedAt != observed || sample.StartedAt == nil || !sample.StartedAt.Equal(started) { - t.Fatalf("lost Docker observation time: %+v", sample) - } - if sample.CPUUsageSecondsTotal == nil || *sample.CPUUsageSecondsTotal != 0 || sample.MemoryUsageBytes == nil || *sample.MemoryUsageBytes != 0 { - t.Fatalf("observed zero became unavailable: %+v", sample) - } - if sample.CPUCapacityCores == nil || *sample.CPUCapacityCores != 2 || sample.MemoryLimitBytes == nil || *sample.MemoryLimitBytes != 2*1024*1024*1024 { - t.Fatalf("lost configured capacity: %+v", sample) - } -} - -func TestSampleFromDockerNormalizesCumulativeCPU(t *testing.T) { - observed := time.Now().UTC() - started := observed.Add(-time.Hour) - cpu, memory := uint64(2_500_000_000), uint64(4096) - sample, err := sampleFromDocker(container.InspectResponse{ - State: &container.State{Running: true, StartedAt: started.Format(time.RFC3339Nano)}, - HostConfig: &container.HostConfig{}, - }, testDockerStats(observed, &cpu, &memory)) - if err != nil { - t.Fatal(err) - } - if sample.CPUUsageSecondsTotal == nil || *sample.CPUUsageSecondsTotal != 2.5 || sample.MemoryUsageBytes == nil || *sample.MemoryUsageBytes != 4096 { - t.Fatalf("bad Docker normalization: %+v", sample) - } - if sample.CPUCapacityCores != nil || sample.MemoryLimitBytes != nil { - t.Fatalf("invented unconfigured capacity: %+v", sample) - } -} - -func TestSampleFromDockerRejectsIncompleteState(t *testing.T) { - observed := time.Now().UTC() - for _, inspected := range []container.InspectResponse{ - {}, - {State: &container.State{Running: false}, HostConfig: &container.HostConfig{}}, - {State: &container.State{Running: true, StartedAt: "invalid"}, HostConfig: &container.HostConfig{}}, - } { - if _, err := sampleFromDocker(inspected, dockerStatsResponse{Read: observed}); err == nil { - t.Fatal("accepted incomplete Docker state") - } - } -} - -func testDockerStats(observed time.Time, cpu, memory *uint64) dockerStatsResponse { - stats := dockerStatsResponse{Read: observed} - if cpu != nil { - stats.CPUStats = &struct { - CPUUsage *struct { - TotalUsage *uint64 `json:"total_usage"` - } `json:"cpu_usage"` - }{CPUUsage: &struct { - TotalUsage *uint64 `json:"total_usage"` - }{TotalUsage: cpu}} - } - if memory != nil { - stats.MemoryStats = &struct { - Usage *uint64 `json:"usage"` - }{Usage: memory} - } - return stats -} diff --git a/services/core/internal/sandbox/docker/seccomp.LICENSE b/services/core/internal/sandbox/docker/seccomp.LICENSE deleted file mode 100644 index d64569567..000000000 --- a/services/core/internal/sandbox/docker/seccomp.LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/services/core/internal/sandbox/docker/seccomp.json b/services/core/internal/sandbox/docker/seccomp.json deleted file mode 100644 index a8d83b161..000000000 --- a/services/core/internal/sandbox/docker/seccomp.json +++ /dev/null @@ -1,938 +0,0 @@ -{ - "defaultAction": "SCMP_ACT_ERRNO", - "defaultErrnoRet": 1, - "archMap": [ - { - "architecture": "SCMP_ARCH_X86_64", - "subArchitectures": [ - "SCMP_ARCH_X86", - "SCMP_ARCH_X32" - ] - }, - { - "architecture": "SCMP_ARCH_AARCH64", - "subArchitectures": [ - "SCMP_ARCH_ARM" - ] - }, - { - "architecture": "SCMP_ARCH_MIPS64", - "subArchitectures": [ - "SCMP_ARCH_MIPS", - "SCMP_ARCH_MIPS64N32" - ] - }, - { - "architecture": "SCMP_ARCH_MIPS64N32", - "subArchitectures": [ - "SCMP_ARCH_MIPS", - "SCMP_ARCH_MIPS64" - ] - }, - { - "architecture": "SCMP_ARCH_MIPSEL64", - "subArchitectures": [ - "SCMP_ARCH_MIPSEL", - "SCMP_ARCH_MIPSEL64N32" - ] - }, - { - "architecture": "SCMP_ARCH_MIPSEL64N32", - "subArchitectures": [ - "SCMP_ARCH_MIPSEL", - "SCMP_ARCH_MIPSEL64" - ] - }, - { - "architecture": "SCMP_ARCH_S390X", - "subArchitectures": [ - "SCMP_ARCH_S390" - ] - }, - { - "architecture": "SCMP_ARCH_RISCV64", - "subArchitectures": null - }, - { - "architecture": "SCMP_ARCH_LOONGARCH64", - "subArchitectures": null - } - ], - "syscalls": [ - { - "names": [ - "accept", - "accept4", - "access", - "adjtimex", - "alarm", - "bind", - "brk", - "cachestat", - "capget", - "capset", - "chdir", - "chmod", - "chown", - "chown32", - "clock_adjtime", - "clock_adjtime64", - "clock_getres", - "clock_getres_time64", - "clock_gettime", - "clock_gettime64", - "clock_nanosleep", - "clock_nanosleep_time64", - "close", - "close_range", - "connect", - "copy_file_range", - "creat", - "dup", - "dup2", - "dup3", - "epoll_create", - "epoll_create1", - "epoll_ctl", - "epoll_ctl_old", - "epoll_pwait", - "epoll_pwait2", - "epoll_wait", - "epoll_wait_old", - "eventfd", - "eventfd2", - "execve", - "execveat", - "exit", - "exit_group", - "faccessat", - "faccessat2", - "fadvise64", - "fadvise64_64", - "fallocate", - "fanotify_mark", - "fchdir", - "fchmod", - "fchmodat", - "fchmodat2", - "fchown", - "fchown32", - "fchownat", - "fcntl", - "fcntl64", - "fdatasync", - "fgetxattr", - "flistxattr", - "flock", - "fork", - "fremovexattr", - "fsetxattr", - "fstat", - "fstat64", - "fstatat64", - "fstatfs", - "fstatfs64", - "fsync", - "ftruncate", - "ftruncate64", - "futex", - "futex_requeue", - "futex_time64", - "futex_wait", - "futex_waitv", - "futex_wake", - "futimesat", - "getcpu", - "getcwd", - "getdents", - "getdents64", - "getegid", - "getegid32", - "geteuid", - "geteuid32", - "getgid", - "getgid32", - "getgroups", - "getgroups32", - "getitimer", - "getpeername", - "getpgid", - "getpgrp", - "getpid", - "getppid", - "getpriority", - "getrandom", - "getresgid", - "getresgid32", - "getresuid", - "getresuid32", - "getrlimit", - "get_robust_list", - "getrusage", - "getsid", - "getsockname", - "getsockopt", - "get_thread_area", - "gettid", - "gettimeofday", - "getuid", - "getuid32", - "getxattr", - "getxattrat", - "inotify_add_watch", - "inotify_init", - "inotify_init1", - "inotify_rm_watch", - "io_cancel", - "ioctl", - "io_destroy", - "io_getevents", - "io_pgetevents", - "io_pgetevents_time64", - "ioprio_get", - "ioprio_set", - "io_setup", - "io_submit", - "ipc", - "kill", - "landlock_add_rule", - "landlock_create_ruleset", - "landlock_restrict_self", - "lchown", - "lchown32", - "lgetxattr", - "link", - "linkat", - "listen", - "listmount", - "listxattr", - "listxattrat", - "llistxattr", - "_llseek", - "lremovexattr", - "lseek", - "lsetxattr", - "lstat", - "lstat64", - "madvise", - "map_shadow_stack", - "membarrier", - "memfd_create", - "memfd_secret", - "mincore", - "mkdir", - "mkdirat", - "mknod", - "mknodat", - "mlock", - "mlock2", - "mlockall", - "mmap", - "mmap2", - "mprotect", - "mq_getsetattr", - "mq_notify", - "mq_open", - "mq_timedreceive", - "mq_timedreceive_time64", - "mq_timedsend", - "mq_timedsend_time64", - "mq_unlink", - "mremap", - "mseal", - "msgctl", - "msgget", - "msgrcv", - "msgsnd", - "msync", - "munlock", - "munlockall", - "munmap", - "name_to_handle_at", - "nanosleep", - "newfstatat", - "_newselect", - "open", - "openat", - "openat2", - "pause", - "pidfd_open", - "pidfd_send_signal", - "pipe", - "pipe2", - "pkey_alloc", - "pkey_free", - "pkey_mprotect", - "poll", - "ppoll", - "ppoll_time64", - "prctl", - "pread64", - "preadv", - "preadv2", - "prlimit64", - "process_mrelease", - "pselect6", - "pselect6_time64", - "pwrite64", - "pwritev", - "pwritev2", - "read", - "readahead", - "readlink", - "readlinkat", - "readv", - "recv", - "recvfrom", - "recvmmsg", - "recvmmsg_time64", - "recvmsg", - "remap_file_pages", - "removexattr", - "removexattrat", - "rename", - "renameat", - "renameat2", - "restart_syscall", - "riscv_hwprobe", - "rmdir", - "rseq", - "rt_sigaction", - "rt_sigpending", - "rt_sigprocmask", - "rt_sigqueueinfo", - "rt_sigreturn", - "rt_sigsuspend", - "rt_sigtimedwait", - "rt_sigtimedwait_time64", - "rt_tgsigqueueinfo", - "sched_getaffinity", - "sched_getattr", - "sched_getparam", - "sched_get_priority_max", - "sched_get_priority_min", - "sched_getscheduler", - "sched_rr_get_interval", - "sched_rr_get_interval_time64", - "sched_setaffinity", - "sched_setattr", - "sched_setparam", - "sched_setscheduler", - "sched_yield", - "seccomp", - "select", - "semctl", - "semget", - "semop", - "semtimedop", - "semtimedop_time64", - "send", - "sendfile", - "sendfile64", - "sendmmsg", - "sendmsg", - "sendto", - "setfsgid", - "setfsgid32", - "setfsuid", - "setfsuid32", - "setgid", - "setgid32", - "setgroups", - "setgroups32", - "setitimer", - "setpgid", - "setpriority", - "setregid", - "setregid32", - "setresgid", - "setresgid32", - "setresuid", - "setresuid32", - "setreuid", - "setreuid32", - "setrlimit", - "set_robust_list", - "setsid", - "setsockopt", - "set_thread_area", - "set_tid_address", - "setuid", - "setuid32", - "setxattr", - "setxattrat", - "shmat", - "shmctl", - "shmdt", - "shmget", - "shutdown", - "sigaltstack", - "signalfd", - "signalfd4", - "sigprocmask", - "sigreturn", - "socketcall", - "socketpair", - "splice", - "stat", - "stat64", - "statfs", - "statfs64", - "statmount", - "statx", - "symlink", - "symlinkat", - "sync", - "sync_file_range", - "syncfs", - "sysinfo", - "tee", - "tgkill", - "time", - "timer_create", - "timer_delete", - "timer_getoverrun", - "timer_gettime", - "timer_gettime64", - "timer_settime", - "timer_settime64", - "timerfd_create", - "timerfd_gettime", - "timerfd_gettime64", - "timerfd_settime", - "timerfd_settime64", - "times", - "tkill", - "truncate", - "truncate64", - "ugetrlimit", - "umask", - "uname", - "unlink", - "unlinkat", - "uretprobe", - "utime", - "utimensat", - "utimensat_time64", - "utimes", - "vfork", - "vmsplice", - "wait4", - "waitid", - "waitpid", - "write", - "writev" - ], - "action": "SCMP_ACT_ALLOW" - }, - { - "names": [ - "process_vm_readv", - "process_vm_writev", - "ptrace" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "minKernel": "4.8" - } - }, - { - "names": [ - "socket" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 38, - "op": "SCMP_CMP_LT" - } - ] - }, - { - "names": [ - "socket" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 39, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "socket" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 41, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "socket" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 42, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "socket" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 43, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "socket" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 44, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "socket" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 45, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "personality" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 0, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "personality" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 8, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "personality" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 131072, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "personality" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 131080, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "personality" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 4294967295, - "op": "SCMP_CMP_EQ" - } - ] - }, - { - "names": [ - "sync_file_range2", - "swapcontext" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "arches": [ - "ppc64le" - ] - } - }, - { - "names": [ - "arm_fadvise64_64", - "arm_sync_file_range", - "sync_file_range2", - "breakpoint", - "cacheflush", - "set_tls" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "arches": [ - "arm", - "arm64" - ] - } - }, - { - "names": [ - "arch_prctl" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "arches": [ - "amd64", - "x32" - ] - } - }, - { - "names": [ - "modify_ldt" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "arches": [ - "amd64", - "x32", - "x86" - ] - } - }, - { - "names": [ - "s390_pci_mmio_read", - "s390_pci_mmio_write", - "s390_runtime_instr" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "arches": [ - "s390", - "s390x" - ] - } - }, - { - "names": [ - "riscv_flush_icache" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "arches": [ - "riscv64" - ] - } - }, - { - "names": [ - "open_by_handle_at" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_DAC_READ_SEARCH" - ] - } - }, - { - "names": [ - "bpf", - "clone", - "clone3", - "fanotify_init", - "fsconfig", - "fsmount", - "fsopen", - "fspick", - "lookup_dcookie", - "lsm_get_self_attr", - "lsm_list_modules", - "lsm_set_self_attr", - "mount", - "mount_setattr", - "move_mount", - "open_tree", - "perf_event_open", - "quotactl", - "quotactl_fd", - "setdomainname", - "sethostname", - "setns", - "syslog", - "umount", - "umount2", - "unshare" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_ADMIN" - ] - } - }, - { - "names": [ - "clone" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 0, - "value": 2114060288, - "op": "SCMP_CMP_MASKED_EQ" - } - ], - "excludes": { - "caps": [ - "CAP_SYS_ADMIN" - ], - "arches": [ - "s390", - "s390x" - ] - } - }, - { - "names": [ - "clone" - ], - "action": "SCMP_ACT_ALLOW", - "args": [ - { - "index": 1, - "value": 2114060288, - "op": "SCMP_CMP_MASKED_EQ" - } - ], - "comment": "s390 parameter ordering for clone is different", - "includes": { - "arches": [ - "s390", - "s390x" - ] - }, - "excludes": { - "caps": [ - "CAP_SYS_ADMIN" - ] - } - }, - { - "names": [ - "clone3" - ], - "action": "SCMP_ACT_ERRNO", - "errnoRet": 38, - "excludes": { - "caps": [ - "CAP_SYS_ADMIN" - ] - } - }, - { - "names": [ - "reboot" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_BOOT" - ] - } - }, - { - "names": [ - "chroot" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_CHROOT" - ] - } - }, - { - "names": [ - "delete_module", - "init_module", - "finit_module" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_MODULE" - ] - } - }, - { - "names": [ - "acct" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_PACCT" - ] - } - }, - { - "names": [ - "kcmp", - "pidfd_getfd", - "process_madvise", - "process_vm_readv", - "process_vm_writev", - "ptrace" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_PTRACE" - ] - } - }, - { - "names": [ - "iopl", - "ioperm" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_RAWIO" - ] - } - }, - { - "names": [ - "settimeofday", - "stime", - "clock_settime", - "clock_settime64" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_TIME" - ] - } - }, - { - "names": [ - "vhangup" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_TTY_CONFIG" - ] - } - }, - { - "names": [ - "get_mempolicy", - "mbind", - "set_mempolicy", - "set_mempolicy_home_node" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYS_NICE" - ] - } - }, - { - "names": [ - "syslog" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_SYSLOG" - ] - } - }, - { - "names": [ - "bpf" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_BPF" - ] - } - }, - { - "names": [ - "perf_event_open" - ], - "action": "SCMP_ACT_ALLOW", - "includes": { - "caps": [ - "CAP_PERFMON" - ] - } - }, - { - "names": [ - "clone", - "unshare", - "setns", - "mount", - "umount2", - "pivot_root" - ], - "action": "SCMP_ACT_ALLOW" - } - ] -} diff --git a/services/core/internal/sandbox/docker/selection.go b/services/core/internal/sandbox/docker/selection.go deleted file mode 100644 index 12a29c724..000000000 --- a/services/core/internal/sandbox/docker/selection.go +++ /dev/null @@ -1,24 +0,0 @@ -package docker - -import ( - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" -) - -func Policy() sandbox.DeploymentPolicy { - return sandbox.DeploymentPolicy{DefaultResources: &sandbox.Resources{CPUs: 2, MemoryMiB: 2048}} -} - -// Setup declares how the console connects Docker nodes. -func Setup() sandbox.SetupDeclaration { - return sandbox.SetupDeclaration{ - Label: "Docker", - Summary: sandbox.Text{ - EN: "Runs each sandbox as a container on the host's kernel: CPU and memory limits but no disk quota, for trusted workloads or hosts without KVM.", - ZHCN: "每个沙箱是一个共用主机内核的容器:能限制 CPU 和内存,但没有磁盘配额,只适合可信的工作负载或没有 KVM 的主机。", - }, - Cautions: []sandbox.Text{ - {EN: "Containers share the host's kernel, so a container escape reaches the host.", ZHCN: "容器共用主机内核,一旦容器逃逸就能触及主机。"}, - {EN: "The node's service account joins the docker group, which is equivalent to root on that host.", ZHCN: "节点的服务账号会加入 docker 组,这在那台主机上等同于 root 权限。"}, - }, - } -} diff --git a/services/core/internal/sandbox/docker/workspace_test.go b/services/core/internal/sandbox/docker/workspace_test.go deleted file mode 100644 index 14772bec7..000000000 --- a/services/core/internal/sandbox/docker/workspace_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package docker - -import ( - "context" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/workspacefs" - "testing" -) - -func TestCreateExplicitlyRejectsExternalWorkspace(t *testing.T) { - provider := new(Provider) - _, err := provider.Create(context.Background(), sandbox.Bootstrap{Workspace: &workspacefs.Binding{}}) - if reason, ok := providercontract.UnsupportedReason(err, "Create"); !ok || reason != "external_workspace_unsupported" { - t.Fatal("external workspace not rejected before native access", err) - } -} diff --git a/services/core/internal/sandbox/node/connection_test.go b/services/core/internal/sandbox/node/connection_test.go index b88a00792..1664ab50f 100644 --- a/services/core/internal/sandbox/node/connection_test.go +++ b/services/core/internal/sandbox/node/connection_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/google/uuid" @@ -178,7 +178,7 @@ func TestCopiedIdentityCannotReplaceNodeWithInflightCreate(t *testing.T) { hub.mu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - proxy := hub.Proxy(id.NodeID, docker.Operations(), 1) + proxy := hub.Proxy(id.NodeID, microsandbox.Operations(), 1) r := reference() created := make(chan error, 1) go func() { _, err := proxy.Create(ctx, sandbox.Bootstrap{Reference: r}); created <- err }() diff --git a/services/core/internal/sandbox/node/creation_settlement_test.go b/services/core/internal/sandbox/node/creation_settlement_test.go index 4f5641ac6..64ab07e1d 100644 --- a/services/core/internal/sandbox/node/creation_settlement_test.go +++ b/services/core/internal/sandbox/node/creation_settlement_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) @@ -73,7 +73,7 @@ func TestNodeCarriesCreationSettlementWithoutConvertingFailureToSuccess(t *testi } }() wait(t, func() bool { return hub.Online(id.NodeID) }) - proxy := hub.Proxy(id.NodeID, docker.Operations(), 1) + proxy := hub.Proxy(id.NodeID, microsandbox.Operations(), 1) for _, operation := range []func(context.Context) (sandbox.Info, error){ func(ctx context.Context) (sandbox.Info, error) { return proxy.Create(ctx, sandbox.Bootstrap{Reference: ref}) diff --git a/services/core/internal/sandbox/node/docker_live_test.go b/services/core/internal/sandbox/node/docker_live_test.go deleted file mode 100644 index e9d2c3168..000000000 --- a/services/core/internal/sandbox/node/docker_live_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package node - -import ( - "context" - "errors" - "net/http/httptest" - "os" - "strings" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" - sandboxdocker "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" - "github.com/google/uuid" - "github.com/moby/moby/client" -) - -// This uses the same pinned sleep-entrypoint image as the Docker mechanism -// tests. It exercises real Docker resources through the node transport, not a -// native harness/model workflow. No provider credentials are required. -func TestDockerNodeTransportLifecycle(t *testing.T) { - image := os.Getenv("AGENTS_RUNTIME_DOCKER_TEST_IMAGE") - if image == "" { - t.Skip("explicit Docker fixture image required") - } - seccomp, err := os.ReadFile("../docker/seccomp.json") - if err != nil { - t.Fatal(err) - } - dockerClient, err := client.New(client.FromEnv) - if err != nil { - t.Fatal(err) - } - defer dockerClient.Close() - id := identity() - provider, err := sandboxdocker.New(dockerClient, sandboxdocker.Config{InstallationID: id.InstallationID, Image: image, Network: "bridge", Seccomp: string(seccomp)}) - if err != nil { - t.Fatal(err) - } - var credential string - hub := newTestHub(HubOptions{Authenticate: func(ctx context.Context, node, token string) (Identity, error) { - if node != id.NodeID || token != credential { - return Identity{}, ErrAuthentication - } - return id, nil - }, OwnerEpoch: func(context.Context) (uint64, error) { return 1, nil }}) - server := httptest.NewServer(hub) - defer server.Close() - defer hub.Close() - dir := stateDir(t) - stored, err := InitIdentity(dir, server.URL, id) - if err != nil { - t.Fatal(err) - } - credential = stored.Credential - start := func() (context.CancelFunc, chan error) { - ctx, cancel := context.WithCancel(context.Background()) - done := make(chan error, 1) - go func() { - done <- runProvider(ctx, AgentConfig{CoreURL: server.URL, StateDirectory: dir, Identity: id, Credential: credential}, provider, func(ctx context.Context) (*sandbox.CheckpointCompatibility, error) { - _, err := dockerClient.Ping(ctx, client.PingOptions{}) - return nil, err - }) - }() - return cancel, done - } - stop, done := start() - running := true - defer func() { - if running { - stop() - if err := <-done; err != nil { - t.Error(err) - } - } - }() - wait(t, func() bool { return hub.Online(id.NodeID) }) - proxy := hub.Proxy(id.NodeID, docker.Operations(), 1) - r := reference() - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - defer cancel() - if err := provider.Kill(ctx, r); err != nil { - t.Error("owned fallback cleanup", err) - } - }() - ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) - defer cancel() - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "http://core.invalid/api/v1", Credential: "synthetic-node-transport-credential"} - callCtx, callCancel := context.WithTimeout(ctx, 25*time.Second) - info, err := proxy.Create(callCtx, b) - callCancel() - if err != nil { - t.Fatal(err) - } - if !info.BootstrapComplete || info.State != "running" { - t.Fatalf("create compute state: %+v", info) - } - command := func(args ...string) sandbox.CommandResult { - t.Helper() - commandCtx, stop := context.WithTimeout(ctx, 15*time.Second) - defer stop() - out, err := proxy.RunCommand(commandCtx, r, sandbox.Command{Args: args, Directory: "/workspace"}) - if err != nil || out.ExitCode != 0 { - t.Fatalf("command failed: err=%v exit=%d", err, out.ExitCode) - } - return out - } - command("/bin/sh", "-c", "printf node-transport-persisted > /workspace/node-transport-proof") - hub.mu.Lock() - previous := hub.peers[id.NodeID] - hub.mu.Unlock() - assertDuplicateRejected(t, server.URL, id.NodeID, credential) - hub.mu.Lock() - retained := hub.peers[id.NodeID] == previous - hub.mu.Unlock() - if !retained { - t.Fatal("duplicate connection replaced real Docker node") - } - if command("/bin/cat", "/workspace/node-transport-proof").Stdout != "node-transport-persisted" { - t.Fatal("duplicate disrupted live node") - } - hub.Disconnect(id.NodeID) - wait(t, func() bool { - hub.mu.Lock() - defer hub.mu.Unlock() - return hub.peers[id.NodeID] != nil && hub.peers[id.NodeID] != previous - }) - observed, err := proxy.GetInfo(ctx, r) - if err != nil || observed.ProviderID != info.ProviderID { - t.Fatal("connection loss changed compute", err) - } - if command("/bin/cat", "/workspace/node-transport-proof").Stdout != "node-transport-persisted" { - t.Fatal("workspace changed after disconnect") - } - stop() - if err = <-done; err != nil { - t.Fatal(err) - } - running = false - wait(t, func() bool { return !hub.Online(id.NodeID) }) - persisted, err := readIdentity(dir) - if err != nil || persisted.Credential != credential || persisted.Identity.NodeID != id.NodeID { - t.Fatal("node restart changed identity", err) - } - stop, done = start() - running = true - wait(t, func() bool { return hub.Online(id.NodeID) }) - observed, err = proxy.GetInfo(ctx, r) - if err != nil || observed.ProviderID != info.ProviderID { - t.Fatal("node restart changed compute", err) - } - if strings.TrimSpace(command("/bin/cat", "/workspace/node-transport-proof").Stdout) != "node-transport-persisted" { - t.Fatal("workspace changed after node restart") - } - if err = proxy.Kill(ctx, r); err != nil { - t.Fatal("node cleanup", err) - } - if _, err = provider.GetInfo(ctx, r); !errors.Is(err, sandbox.ErrNotFound) { - t.Fatal("container retained after cleanup", err) - } - // Kill verifies removal of both named Runtime volumes before returning. - t.Logf("real Docker node transport passed: installation=%s allocation=%s compute=%s; duplicate connection rejected; reconnect and node restart retained identity/file; owned container and volumes removed", id.InstallationID, r.AllocationID, info.ProviderID) -} diff --git a/services/core/internal/sandbox/node/enrollment_status_test.go b/services/core/internal/sandbox/node/enrollment_status_test.go index 86091d0ef..e016dc79a 100644 --- a/services/core/internal/sandbox/node/enrollment_status_test.go +++ b/services/core/internal/sandbox/node/enrollment_status_test.go @@ -9,9 +9,9 @@ import ( func TestEnrollmentRecoveryAcceptsCoreReadiness(t *testing.T) { for _, fields := range []string{"", `,"connected":true,"provider_ready":false`} { - response := &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"node_id":"node","installation_id":"installation","provider":"docker"` + fields + `}`))} + response := &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"node_id":"node","installation_id":"installation","provider":"microsandbox"` + fields + `}`))} value, err := readEnrollment(response) - if err != nil || value.NodeID != "node" || value.InstallationID != "installation" || value.Provider != "docker" { + if err != nil || value.NodeID != "node" || value.InstallationID != "installation" || value.Provider != "microsandbox" { t.Fatal(value, err) } } diff --git a/services/core/internal/sandbox/node/generation_connection_test.go b/services/core/internal/sandbox/node/generation_connection_test.go index 2052f0913..a6c87a9bb 100644 --- a/services/core/internal/sandbox/node/generation_connection_test.go +++ b/services/core/internal/sandbox/node/generation_connection_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) @@ -63,7 +63,7 @@ func TestGenerationWireRoutesOldOwnershipAndCurrentTargetSeparately(t *testing.T }() wait(t, func() bool { return hub.Online(id.NodeID) }) for _, generation := range []uint64{1, 17, 9} { - proxy := hub.GenerationProvider(docker.Operations(), func(context.Context, sandbox.Reference) (string, uint64, error) { return id.NodeID, generation, nil }) + proxy := hub.GenerationProvider(microsandbox.Operations(), func(context.Context, sandbox.Reference) (string, uint64, error) { return id.NodeID, generation, nil }) ref := reference() if _, err := proxy.GetInfo(ctx, ref); err != nil { t.Fatal("retained generation info failed", generation, err) diff --git a/services/core/internal/sandbox/node/hub_lifetime_test.go b/services/core/internal/sandbox/node/hub_lifetime_test.go index 26850904e..a99786566 100644 --- a/services/core/internal/sandbox/node/hub_lifetime_test.go +++ b/services/core/internal/sandbox/node/hub_lifetime_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/gorilla/websocket" @@ -46,7 +46,7 @@ func assertInfoResponsive(t *testing.T, hub *Hub, id Identity) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - info, err := hub.Proxy(id.NodeID, docker.Operations(), 1).GetInfo(ctx, reference()) + info, err := hub.Proxy(id.NodeID, microsandbox.Operations(), 1).GetInfo(ctx, reference()) if err != nil || info.ProviderID != "retained" { t.Fatalf("unrelated node RPC blocked: info=%+v err=%v", info, err) } @@ -287,7 +287,7 @@ func TestHubSendQueueRespectsCallerCancellation(t *testing.T) { defer cancel() done := make(chan error, 1) go func() { - _, err := hub.Proxy(id.NodeID, docker.Operations(), 1).GetInfo(ctx, reference()) + _, err := hub.Proxy(id.NodeID, microsandbox.Operations(), 1).GetInfo(ctx, reference()) done <- err }() select { diff --git a/services/core/internal/sandbox/node/node_test.go b/services/core/internal/sandbox/node/node_test.go index 5c360e01d..595b8be2d 100644 --- a/services/core/internal/sandbox/node/node_test.go +++ b/services/core/internal/sandbox/node/node_test.go @@ -15,7 +15,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/machineapi" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/google/uuid" @@ -63,7 +63,7 @@ func reference() sandbox.Reference { return sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()} } func identity() Identity { - return Identity{DeploymentGeneration: 1, SpecificationDigest: strings.Repeat("a", 64), NodeID: uuid.NewString(), InstallationID: uuid.NewString(), Provider: "docker", BackendFingerprint: strings.Repeat("1", 64), MaxActive: 4, MaxRetained: 16} + return Identity{DeploymentGeneration: 1, SpecificationDigest: strings.Repeat("a", 64), NodeID: uuid.NewString(), InstallationID: uuid.NewString(), Provider: "microsandbox", BackendFingerprint: strings.Repeat("1", 64), MaxActive: 4, MaxRetained: 16} } func stateDir(t *testing.T) string { t.Helper() @@ -125,7 +125,7 @@ func TestLostCreateResponseDoesNotReplayAndReconnectSerializesCleanup(t *testing t.Fatal("running node did not retain lifetime identity lock") } r := reference() - proxy := hub.Proxy(id.NodeID, docker.Operations(), 1) + proxy := hub.Proxy(id.NodeID, microsandbox.Operations(), 1) createCtx, stopCreate := context.WithTimeout(ctx, 150*time.Millisecond) defer stopCreate() createDone := make(chan error, 1) @@ -166,12 +166,9 @@ func TestLostCreateResponseDoesNotReplayAndReconnectSerializesCleanup(t *testing } } -func TestOfflineIsUnknownAndDockerDoesNotAdvertiseCheckpoint(t *testing.T) { +func TestOfflineIsUnknown(t *testing.T) { h := newTestHub(HubOptions{OwnerEpoch: func(context.Context) (uint64, error) { return 1, nil }}) - p := h.Proxy(uuid.NewString(), docker.Operations(), 1) - if sandbox.SupportsCheckpoint(p.ProviderOperations()) { - t.Fatal("docker advertised checkpoint") - } + p := h.Proxy(uuid.NewString(), microsandbox.Operations(), 1) _, err := p.GetInfo(context.Background(), reference()) if !errors.Is(err, sandbox.ErrComputeUnconfirmed) || errors.Is(err, sandbox.ErrNotFound) { t.Fatalf("offline = %v", err) diff --git a/services/core/internal/sandbox/node/observations_test.go b/services/core/internal/sandbox/node/observations_test.go index 9f389f11a..31aa5b575 100644 --- a/services/core/internal/sandbox/node/observations_test.go +++ b/services/core/internal/sandbox/node/observations_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" @@ -91,7 +91,7 @@ func TestObservationsRouteThroughAssignedNodeWithoutLifecycleCalls(t *testing.T) stopSecond := runObservationNode(t, hub, server.URL, second, b) ra, rb := reference(), reference() assignments := map[sandbox.Reference]string{ra: first.NodeID, rb: second.NodeID} - source := hub.GenerationProvider(docker.Operations(), func(_ context.Context, r sandbox.Reference) (string, uint64, error) { + source := hub.GenerationProvider(microsandbox.Operations(), func(_ context.Context, r sandbox.Reference) (string, uint64, error) { if id, ok := assignments[r]; ok { return id, 1, nil } diff --git a/services/core/internal/sandbox/node/operations_test.go b/services/core/internal/sandbox/node/operations_test.go index 5cda07c72..d6769a0fe 100644 --- a/services/core/internal/sandbox/node/operations_test.go +++ b/services/core/internal/sandbox/node/operations_test.go @@ -8,7 +8,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" "github.com/google/uuid" ) @@ -37,14 +37,14 @@ func TestUnsupportedWireIsExplicitAndDoesNotInvokeProvider(t *testing.T) { } } func TestUnsupportedProxyRejectsBeforeNodeResolution(t *testing.T) { - p := (&Hub{}).GenerationProvider(docker.Operations(), func(context.Context, sandbox.Reference) (string, uint64, error) { + p := (&Hub{}).GenerationProvider(e2b.Operations(), func(context.Context, sandbox.Reference) (string, uint64, error) { t.Fatal("unsupported call resolved a node") return "", 0, nil }).(*provider) if err := sandbox.ValidateProvider(p); err != nil { t.Fatal(err) } - if _, err := p.Initial(t.Context(), reference()); !errors.Is(err, providercontract.ErrUnsupported) { + if err := p.SavePreparation(t.Context(), reference(), "key"); !errors.Is(err, providercontract.ErrUnsupported) { t.Fatal(err) } } diff --git a/services/core/internal/sandbox/node/recovery_test.go b/services/core/internal/sandbox/node/recovery_test.go index ad62151d7..b35130470 100644 --- a/services/core/internal/sandbox/node/recovery_test.go +++ b/services/core/internal/sandbox/node/recovery_test.go @@ -13,7 +13,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/google/uuid" "github.com/gorilla/websocket" ) @@ -57,10 +57,10 @@ func TestCoreRestartFencesOldConnectionAndNodeRestartKeepsIdentity(t *testing.T) if first.Online(id.NodeID) { t.Fatal("old owner remained online") } - if _, err = first.Proxy(id.NodeID, docker.Operations(), 1).GetInfo(context.Background(), reference()); !errors.Is(err, sandbox.ErrComputeUnconfirmed) { + if _, err = first.Proxy(id.NodeID, microsandbox.Operations(), 1).GetInfo(context.Background(), reference()); !errors.Is(err, sandbox.ErrComputeUnconfirmed) { t.Fatalf("old owner request = %v", err) } - if _, err = second.Proxy(id.NodeID, docker.Operations(), 1).GetInfo(context.Background(), reference()); err != nil { + if _, err = second.Proxy(id.NodeID, microsandbox.Operations(), 1).GetInfo(context.Background(), reference()); err != nil { t.Fatal(err) } stop() @@ -156,7 +156,7 @@ func TestHeartbeatAcknowledgementKeepsIdleConnectionAlive(t *testing.T) { if !hub.Online(id.NodeID) { t.Fatal("idle node disconnected") } - if _, err = hub.Proxy(id.NodeID, docker.Operations(), 1).GetInfo(ctx, reference()); err != nil { + if _, err = hub.Proxy(id.NodeID, microsandbox.Operations(), 1).GetInfo(ctx, reference()); err != nil { t.Fatal(err) } } @@ -236,7 +236,7 @@ func TestDegradedNodeRetainsObservationAndCleanup(t *testing.T) { case <-time.After(time.Second): t.Fatal("missing health") } - proxy := hub.Proxy(id.NodeID, docker.Operations(), 1) + proxy := hub.Proxy(id.NodeID, microsandbox.Operations(), 1) r := reference() if _, err = proxy.Create(ctx, sandbox.Bootstrap{Reference: r}); !errors.Is(err, sandbox.ErrComputeUnconfirmed) { t.Fatalf("create = %v", err) diff --git a/services/core/internal/sandbox/node/timeout_test.go b/services/core/internal/sandbox/node/timeout_test.go index ef859d0b0..cfea53997 100644 --- a/services/core/internal/sandbox/node/timeout_test.go +++ b/services/core/internal/sandbox/node/timeout_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/google/uuid" @@ -103,7 +103,7 @@ func TestQueuedMutationExpiresWithoutExecution(t *testing.T) { } }() wait(t, func() bool { return hub.Online(id.NodeID) }) - proxy := hub.Proxy(id.NodeID, docker.Operations(), 1) + proxy := hub.Proxy(id.NodeID, microsandbox.Operations(), 1) r := reference() createCtx, stopCreate := context.WithTimeout(ctx, 3*time.Second) defer stopCreate() diff --git a/services/core/internal/sandbox/operations_test.go b/services/core/internal/sandbox/operations_test.go index 62d2c5b3f..d8bca197b 100644 --- a/services/core/internal/sandbox/operations_test.go +++ b/services/core/internal/sandbox/operations_test.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "reflect" @@ -13,7 +12,7 @@ import ( ) type changedDeclaration struct { - *docker.Provider + *microsandbox.Provider operations providercontract.Operations } @@ -33,7 +32,7 @@ func TestDeclarationsRejectMissingUnknownAndContradictoryOperations(t *testing.T o["Renew"] = providercontract.Support{State: providercontract.Unsupported, Reason: "no_native_lease"} }}, {"partial checkpoint", func(o providercontract.Operations) { - o["Initial"] = providercontract.Support{State: providercontract.Supported} + o["Initial"] = providercontract.Support{State: providercontract.Unsupported, Reason: "no_checkpoints"} }}, {"unsafe reason", func(o providercontract.Operations) { o["Observe"] = providercontract.Support{State: providercontract.Unsupported, Reason: "https://private:key@host"} @@ -43,14 +42,14 @@ func TestDeclarationsRejectMissingUnknownAndContradictoryOperations(t *testing.T }}, } { t.Run(mutate.name, func(t *testing.T) { - o := docker.Operations() + o := microsandbox.Operations() mutate.change(o) - if err := sandbox.ValidateProvider(&changedDeclaration{Provider: &docker.Provider{}, operations: o}); !errors.Is(err, providercontract.ErrContract) { + if err := sandbox.ValidateProvider(&changedDeclaration{Provider: µsandbox.Provider{}, operations: o}); !errors.Is(err, providercontract.ErrContract) { t.Fatal(err) } }) } - var nilProvider *docker.Provider + var nilProvider *microsandbox.Provider if err := sandbox.ValidateProvider(nilProvider); !errors.Is(err, providercontract.ErrContract) { t.Fatal("typed nil accepted", err) } @@ -59,7 +58,7 @@ func TestDeclarationsRejectMissingUnknownAndContradictoryOperations(t *testing.T // Unsupported methods must be callable safely even without native clients or // configuration: any attempt at native I/O would panic on these zero providers. func TestEveryUnsupportedNativeOperationRejectsWithoutSideEffects(t *testing.T) { - for _, provider := range []sandbox.SandboxProvider{&docker.Provider{}, &e2b.Provider{}, µsandbox.Provider{}} { + for _, provider := range []sandbox.SandboxProvider{µsandbox.Provider{}, &e2b.Provider{}, µsandbox.Provider{}} { if err := sandbox.ValidateProvider(provider); err != nil { t.Fatal(err) } diff --git a/services/core/internal/sandbox/providers/artifacts.go b/services/core/internal/sandbox/providers/artifacts.go index d92cc90a0..600bee23e 100644 --- a/services/core/internal/sandbox/providers/artifacts.go +++ b/services/core/internal/sandbox/providers/artifacts.go @@ -12,7 +12,6 @@ import ( var nodeProgram = providerassets.Artifact{Path: "native/bin/oac-node", Suffix: "sandbox-node", Role: "node"} var runtimeImage = providerassets.Artifact{Path: "images/runtime.tar.gz", Suffix: "runtime.tar.gz", Role: "image"} -var runtimePolicy = providerassets.Artifact{Path: "runtime/seccomp.json", Suffix: "seccomp.json", Role: "policy"} // ArtifactCatalog projects registered node requirements for Web and installers. func (r *Registry) ArtifactCatalog() (map[string][]providerassets.Artifact, error) { @@ -56,7 +55,7 @@ func validateNodeArtifacts(items []providerassets.Artifact) error { switch item.Role { case "node": nodes++ - case "runtime", "policy", "image": + case "runtime", "image": default: return invalid } diff --git a/services/core/internal/sandbox/providers/artifacts_test.go b/services/core/internal/sandbox/providers/artifacts_test.go index 5e29329af..f447af3a2 100644 --- a/services/core/internal/sandbox/providers/artifacts_test.go +++ b/services/core/internal/sandbox/providers/artifacts_test.go @@ -57,7 +57,7 @@ func TestNodeArtifactRegistrationRejectsInvalidDeclarations(t *testing.T) { func(a *Adapter) { a.NodeArtifacts[0].Role = "unknown" }, func(a *Adapter) { a.NodeArtifacts = append(a.NodeArtifacts, a.NodeArtifacts[0]) }, } { - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] a.NodeArtifacts = append([]providerassets.Artifact(nil), a.NodeArtifacts...) mutate(&a) if err := ValidateRegistration(a); !errors.Is(err, providercontract.ErrContract) { @@ -65,7 +65,7 @@ func TestNodeArtifactRegistrationRejectsInvalidDeclarations(t *testing.T) { } } const kind = "another-node-provider" - registry.adapters[kind] = registry.adapters["docker"] + registry.adapters[kind] = registry.adapters["microsandbox"] catalog, err := registry.ArtifactCatalog() if err != nil || !reflect.DeepEqual(catalog[kind], registry.adapters[kind].NodeArtifacts) { t.Fatalf("additional registration not projected: %v", err) @@ -79,7 +79,7 @@ func TestArtifactCatalogRejectsConflictingSharedFiles(t *testing.T) { func(item *providerassets.Artifact) { item.Suffix = "different" }, func(item *providerassets.Artifact) { item.Path = "native/bin/other-node" }, } { - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] a.NodeArtifacts = append([]providerassets.Artifact(nil), a.NodeArtifacts...) mutate(&a.NodeArtifacts[0]) registry.adapters[kind] = a diff --git a/services/core/internal/sandbox/providers/config_test.go b/services/core/internal/sandbox/providers/config_test.go index 33695ac33..92e97e123 100644 --- a/services/core/internal/sandbox/providers/config_test.go +++ b/services/core/internal/sandbox/providers/config_test.go @@ -10,16 +10,16 @@ import ( ) func TestNodeRejectsCoreConfigurationAndUnknownProvider(t *testing.T) { - for _, field := range []string{`"nodes":{"local":false}`, `"maintenance":true`, `"docker":{}`} { + for _, field := range []string{`"nodes":{"local":false}`, `"maintenance":true`, `"microsandbox":{}`} { path := filepath.Join(t.TempDir(), "config.json") - if err := os.WriteFile(path, []byte(`{"provider":"docker",`+field+`}`), 0600); err != nil { + if err := os.WriteFile(path, []byte(`{"provider":"microsandbox",`+field+`}`), 0600); err != nil { t.Fatal(err) } if _, err := Load(path); err == nil { t.Fatal("accepted a field outside the node configuration") } } - if sandbox.BackendFingerprint("docker", "socket") == sandbox.BackendFingerprint("microsandbox", "socket") { + if sandbox.BackendFingerprint("e2b", "socket") == sandbox.BackendFingerprint("microsandbox", "socket") { t.Fatal("provider namespaces collide") } } @@ -30,7 +30,7 @@ func TestNodeBuildRequiresNodeProviderAndGeneration(t *testing.T) { if _, _, err := registry.Build(sandbox.NodeConfig{Generation: 1, Provider: "e2b", InstallationID: uuid.NewString(), Specification: sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048}}}, options); err == nil { t.Fatal("accepted a cloud provider on a node") } - if _, _, err := registry.Build(sandbox.NodeConfig{Provider: "docker", InstallationID: uuid.NewString(), Specification: validRegistrationSpec()}, options); err == nil { + if _, _, err := registry.Build(sandbox.NodeConfig{Provider: "microsandbox", InstallationID: uuid.NewString(), Specification: validRegistrationSpec()}, options); err == nil { t.Fatal("accepted unbound node") } } diff --git a/services/core/internal/sandbox/providers/configuration_flow_test.go b/services/core/internal/sandbox/providers/configuration_flow_test.go index 037daac32..b5ca45f72 100644 --- a/services/core/internal/sandbox/providers/configuration_flow_test.go +++ b/services/core/internal/sandbox/providers/configuration_flow_test.go @@ -90,10 +90,12 @@ func TestAdditionalConfigurationProviderUsesCommonAPIAndStore(t *testing.T) { // The deployment identity and execution lease are database-wide. pool := pgtest.OpenIsolated(t, nil) kind := "regional-fixture" - adapter, err := providers.Builtin().Lookup("docker") + adapter, err := providers.Builtin().Lookup("microsandbox") if err != nil { t.Fatal(err) } + // The fixture runs without workspace storage or disks. + adapter.Policy = sandbox.DeploymentPolicy{} adapter.Configuration = regionalCodec{} registry := providers.FixtureRegistry(t, kind, adapter) // The deployment reaches the registered configuration only through the diff --git a/services/core/internal/sandbox/providers/configuration_test.go b/services/core/internal/sandbox/providers/configuration_test.go index cfabbadeb..ad6a4a194 100644 --- a/services/core/internal/sandbox/providers/configuration_test.go +++ b/services/core/internal/sandbox/providers/configuration_test.go @@ -10,7 +10,7 @@ import ( func TestNodeConfigurationExplicitUnsupportedAndStrictEmptyInput(t *testing.T) { registry := Builtin() - for _, kind := range []string{"docker", "microsandbox"} { + for _, kind := range []string{"microsandbox", "microsandbox"} { a, err := registry.Lookup(kind) if err != nil { t.Fatal(err) @@ -40,7 +40,7 @@ func TestConfigurationRequirementsDoNotTurnLookupFailuresIntoFalse(t *testing.T) if _, err := check("missing-configuration-provider"); err == nil { t.Fatal("unknown provider treated as not required") } - if yes, err := check("docker"); err != nil || yes { + if yes, err := check("microsandbox"); err != nil || yes { t.Fatal("explicit not-required rejected", err) } if yes, err := check("e2b"); err != nil || !yes { diff --git a/services/core/internal/sandbox/providers/operations_test.go b/services/core/internal/sandbox/providers/operations_test.go index 9e2659573..6a50a1e31 100644 --- a/services/core/internal/sandbox/providers/operations_test.go +++ b/services/core/internal/sandbox/providers/operations_test.go @@ -3,7 +3,7 @@ package providers import ( "errors" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "testing" ) @@ -16,7 +16,7 @@ func TestRegistrationRejectsMissingAndMismatchedDeclarations(t *testing.T) { } } delete(registry.adapters, "invalid-contract-fixture") - if err := ValidateBinding(registry.adapters["e2b"], &docker.Provider{}); !errors.Is(err, providercontract.ErrContract) { + if err := ValidateBinding(registry.adapters["e2b"], µsandbox.Provider{}); !errors.Is(err, providercontract.ErrContract) { t.Fatal("registration differs from instance", err) } } diff --git a/services/core/internal/sandbox/providers/registration_configuration_test.go b/services/core/internal/sandbox/providers/registration_configuration_test.go index 61677895a..2cc059e22 100644 --- a/services/core/internal/sandbox/providers/registration_configuration_test.go +++ b/services/core/internal/sandbox/providers/registration_configuration_test.go @@ -23,7 +23,7 @@ func (a registrationConfiguration) Requirements() sandbox.ConfigurationRequireme func TestConfigurationRegistrationRejectsNil(t *testing.T) { registry := Builtin() - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] var typedNil *registrationConfiguration for _, configuration := range []sandbox.ConfigurationAdapter{nil, typedNil} { a.Configuration = configuration @@ -35,7 +35,7 @@ func TestConfigurationRegistrationRejectsNil(t *testing.T) { func TestConfigurationRequirementsRejectEachOmission(t *testing.T) { registry := Builtin() - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] original := a.Configuration.Requirements() typ := reflect.TypeOf(original) for i := 0; i < typ.NumField(); i++ { @@ -60,7 +60,7 @@ func TestConfigurationRequirementsRejectInvalidDeclarations(t *testing.T) { func(r *sandbox.ConfigurationRequirements) { r.Discovery.Reason = "https://private:key@host" }, func(r *sandbox.ConfigurationRequirements) { r.Discovery.State = providercontract.Supported }, } { - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] requirements := a.Configuration.Requirements() change(&requirements) a.Configuration = registrationConfiguration{requirements: requirements} @@ -77,7 +77,7 @@ func TestConfigurationRequirementsDoNotInventDependencies(t *testing.T) { // resource operation that may be Unsupported for this provider. for _, credential := range []sandbox.Requirement{sandbox.Required, sandbox.NotRequired} { for _, public := range []sandbox.Requirement{sandbox.Required, sandbox.NotRequired} { - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] requirements := a.Configuration.Requirements() requirements.Credential, requirements.PublicOrigin = credential, public a.Configuration = registrationConfiguration{requirements: requirements} @@ -102,7 +102,7 @@ func TestConfigurationRequirementsDoNotInventDependencies(t *testing.T) { func TestFutureConfigurationRequirementNeedsExplicitHandling(t *testing.T) { registry := Builtin() - original := registry.adapters["docker"].Configuration.Requirements() + original := registry.adapters["microsandbox"].Configuration.Requirements() fields := make([]reflect.StructField, 0, 4) typ := reflect.TypeOf(original) for i := 0; i < typ.NumField(); i++ { @@ -184,14 +184,14 @@ func (contradictingConfiguration) VerifyCredential(context.Context, sandbox.Dire func TestSupportedSetupOperationsCannotReportUnsupported(t *testing.T) { registry := Builtin() - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] requirements := a.Configuration.Requirements() supported := providercontract.Support{State: providercontract.Supported} requirements.Discovery, requirements.SelectionDiscovery, requirements.CredentialVerification = supported, supported, supported a.Configuration = contradictingConfiguration{registrationConfiguration{requirements: requirements}} - registry.adapters["docker"] = a - direct := sandbox.DirectConfig{Selection: sandbox.Selection{Provider: "docker"}} - _, discoverErr := registry.DiscoverConfiguration(t.Context(), "docker", sandbox.ConfigurationDiscoveryInput{}, sandbox.ProcessPaths{}) + registry.adapters["microsandbox"] = a + direct := sandbox.DirectConfig{Selection: sandbox.Selection{Provider: "microsandbox"}} + _, discoverErr := registry.DiscoverConfiguration(t.Context(), "microsandbox", sandbox.ConfigurationDiscoveryInput{}, sandbox.ProcessPaths{}) _, selectionErr := registry.DiscoverSelection(t.Context(), direct) for _, err := range []error{discoverErr, selectionErr, registry.VerifyCredential(t.Context(), direct, nil)} { if !errors.Is(err, providercontract.ErrContract) || errors.Is(err, providercontract.ErrUnsupported) { diff --git a/services/core/internal/sandbox/providers/registration_test.go b/services/core/internal/sandbox/providers/registration_test.go index 4d7c10cf2..17fb3f831 100644 --- a/services/core/internal/sandbox/providers/registration_test.go +++ b/services/core/internal/sandbox/providers/registration_test.go @@ -7,13 +7,15 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/workspacefs" "github.com/google/uuid" ) func validRegistrationSpec() sandbox.DeploymentSpec { return sandbox.DeploymentSpec{ - Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048}, + Resources: sandbox.Resources{CPUs: 2, MemoryMiB: 2048, RootDiskMiB: 1024}, + Workspace: &workspacefs.Declaration{Attachment: workspacefs.AttachmentHostDirectory, UserXAttr: true}, } } @@ -54,7 +56,7 @@ func TestRegistrationRejectsBeforeCallbacksOrConstruction(t *testing.T) { {"incomplete operations", func(a *Adapter) { a.Operations = func() providercontract.Operations { return nil } }}, } { t.Run(tc.name, func(t *testing.T) { - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] a.BuildLocal = func(sandbox.NodeConfig, sandbox.LocalOptions, *sandbox.Built) (func(), error) { t.Fatal("called local constructor") return nil, nil @@ -89,7 +91,7 @@ func TestRegistrationRejectsBeforeCallbacksOrConstruction(t *testing.T) { return err }}, {"direct build", func() error { _, err := registry.BuildDirect(sandbox.DirectConfig{Selection: selection}); return err }}, - {"binding", func() error { return ValidateBinding(a, &docker.Provider{}) }}, + {"binding", func() error { return ValidateBinding(a, µsandbox.Provider{}) }}, {"projection", func() error { python, typescript, err := registry.DeploymentContract() if python != "" || typescript != "" { @@ -118,14 +120,14 @@ func TestCompleteRegistrationsPreserveConstruction(t *testing.T) { } const kind = "new-test-provider" calls, closes := 0, 0 - options := sandbox.LocalOptions{GenerationStateDirectory: t.TempDir()} - a := registry.adapters["docker"] + options := sandbox.LocalOptions{GenerationStateDirectory: t.TempDir(), Workspace: unreachableWorkspaceResolver{}} + a := registry.adapters["microsandbox"] a.BuildLocal = func(_ sandbox.NodeConfig, got sandbox.LocalOptions, built *sandbox.Built) (func(), error) { calls++ if got != options { t.Fatalf("construction options = %+v, want %+v", got, options) } - built.Provider = &docker.Provider{} + built.Provider = µsandbox.Provider{} return func() { closes++ }, nil } registry.adapters[kind] = a @@ -142,7 +144,7 @@ func TestCompleteRegistrationsPreserveConstruction(t *testing.T) { a.Mode, a.BuildLocal, a.NodeArtifacts = "direct", nil, nil a.BuildDirect = func(sandbox.DirectConfig) (sandbox.SandboxProvider, error) { calls++ - return &docker.Provider{}, nil + return µsandbox.Provider{}, nil } registry.adapters[kind] = a p, err := registry.BuildDirect(sandbox.DirectConfig{Selection: sandbox.Selection{Provider: kind}}) @@ -155,8 +157,8 @@ func TestCompleteRegistrationsPreserveConstruction(t *testing.T) { } func TestRegistrationRejectsInvalidDefaultResources(t *testing.T) { - a := Builtin().adapters["docker"] - a.Policy.DefaultResources = &sandbox.Resources{CPUs: 2, MemoryMiB: 2048, RootDiskMiB: 1024} + a := Builtin().adapters["microsandbox"] + a.Policy.DefaultResources = &sandbox.Resources{CPUs: 2, MemoryMiB: 2048, RootDiskMiB: 1} if err := ValidateRegistration(a); !errors.Is(err, providercontract.ErrContract) { t.Fatal(err) } @@ -183,7 +185,7 @@ func TestSetupDeclarationMustMatchRequirements(t *testing.T) { a.Setup.Presets = []sandbox.SetupPreset{{Label: "x", Values: map[string]string{"unknown": "x"}}} }, } { - a := registry.adapters["docker"] + a := registry.adapters["microsandbox"] change(&a) if err := ValidateRegistration(a); !errors.Is(err, providercontract.ErrContract) { t.Fatalf("%s: %v", name, err) diff --git a/services/core/internal/sandbox/providers/registry.go b/services/core/internal/sandbox/providers/registry.go index d797896ed..8dac6b760 100644 --- a/services/core/internal/sandbox/providers/registry.go +++ b/services/core/internal/sandbox/providers/registry.go @@ -8,7 +8,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/providerassets" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" ) @@ -36,13 +35,8 @@ var ErrUnknownProvider = fmt.Errorf("%w: unsupported sandbox provider", sandbox. // Builtin returns a registry of the adapters built into this program. func Builtin() *Registry { return &Registry{adapters: map[string]Adapter{ - "docker": { - NodeArtifacts: []providerassets.Artifact{nodeProgram, runtimeImage, runtimePolicy}, - Policy: docker.Policy(), Setup: docker.Setup(), Operations: docker.Operations, Mode: sandbox.DeploymentNodes, BuildLocal: docker.BuildNode, - Configuration: nodeConfigurationAdapter{"docker", docker.Policy()}, - }, "microsandbox": { - NodeArtifacts: append([]providerassets.Artifact{nodeProgram, runtimeImage, runtimePolicy}, microsandbox.NodeArtifacts...), + NodeArtifacts: append([]providerassets.Artifact{nodeProgram, runtimeImage}, microsandbox.NodeArtifacts...), Policy: microsandbox.Policy(), Setup: microsandbox.Setup(), Operations: microsandbox.Operations, Mode: sandbox.DeploymentNodes, BuildLocal: microsandbox.BuildNode, Configuration: nodeConfigurationAdapter{"microsandbox", microsandbox.Policy()}, }, diff --git a/services/core/internal/sandbox/providers/registry_test.go b/services/core/internal/sandbox/providers/registry_test.go index 10f5be9ba..ec8b25040 100644 --- a/services/core/internal/sandbox/providers/registry_test.go +++ b/services/core/internal/sandbox/providers/registry_test.go @@ -17,7 +17,6 @@ func TestRegistrationOwnsDeploymentPolicy(t *testing.T) { kind, mode, namespace string checkpoint bool }{ - {"docker", "nodes", "nodes", false}, {"microsandbox", "nodes", "nodes", true}, {"e2b", "direct", "e2b", true}, } { @@ -68,10 +67,10 @@ func TestNewRegistrationDoesNotNeedCoreDispatchChanges(t *testing.T) { registry := Builtin() const kind = "contract-test-provider" // Registration is test-local: production registrations are fixed, never plugins. - registry.adapters[kind] = registry.adapters["docker"] + registry.adapters[kind] = registry.adapters["microsandbox"] s, err := registry.Normalize(sandbox.Selection{Provider: kind, DeploymentSpec: validRegistrationSpec()}) checkpoint, checkpointErr := registry.SupportsCheckpoint(kind) - if err != nil || checkpointErr != nil || s.Provider != kind || checkpoint { + if err != nil || checkpointErr != nil || s.Provider != kind || !checkpoint { t.Fatal("new entry did not follow shared boundary", err, checkpointErr) } d, err := registry.Describe(kind, uuid.NewString()) @@ -85,21 +84,9 @@ func TestNewRegistrationDoesNotNeedCoreDispatchChanges(t *testing.T) { func TestRetainedLimitUsesRegisteredCapabilities(t *testing.T) { registry := Builtin() - const kind = "capacity-test-provider" - for _, checkpoint := range []bool{false, true} { - adapter := registry.adapters["docker"] - if checkpoint { - adapter = registry.adapters["microsandbox"] - } - registry.adapters[kind] = adapter - for _, retained := range []int{0, 20} { - want := 10 - if checkpoint { - want = retained - } - if got, err := registry.RetainedLimit(kind, 10, retained); err != nil || got != want { - t.Fatalf("checkpoint=%v retained=%d: got %d, want %d (%v)", checkpoint, retained, got, want, err) - } + for _, retained := range []int{0, 20} { + if got, err := registry.RetainedLimit("microsandbox", 10, retained); err != nil || got != retained { + t.Fatalf("retained=%d: got %d (%v)", retained, got, err) } } } @@ -107,7 +94,7 @@ func TestRetainedLimitUsesRegisteredCapabilities(t *testing.T) { // Capability questions report lookup failures instead of answering false. func TestCapabilityLookupsReportFailures(t *testing.T) { registry := Builtin() - invalid := registry.adapters["docker"] + invalid := registry.adapters["microsandbox"] invalid.Operations = nil registry.adapters["invalid-registration"] = invalid for kind, want := range map[string]error{"unregistered": ErrUnknownProvider, "invalid-registration": providercontract.ErrContract} { diff --git a/services/core/internal/sandbox/providers/validation_test.go b/services/core/internal/sandbox/providers/validation_test.go index 31a078923..a98dd5f8f 100644 --- a/services/core/internal/sandbox/providers/validation_test.go +++ b/services/core/internal/sandbox/providers/validation_test.go @@ -16,11 +16,10 @@ func TestDeploymentValidationFieldsPreserveMessages(t *testing.T) { param, message string min, max *uint32 }{ - {"docker", sandbox.Resources{}, "resources.cpus", "cpus must be 1..255 and memory_mib must be 512..1048576", validationBound(1), validationBound(255)}, - {"docker", sandbox.Resources{CPUs: 1, MemoryMiB: 511}, "resources.memory_mib", "cpus must be 1..255 and memory_mib must be 512..1048576", validationBound(512), validationBound(1048576)}, + {"microsandbox", sandbox.Resources{}, "resources.cpus", "cpus must be 1..255 and memory_mib must be 512..1048576", validationBound(1), validationBound(255)}, + {"microsandbox", sandbox.Resources{CPUs: 1, MemoryMiB: 511}, "resources.memory_mib", "cpus must be 1..255 and memory_mib must be 512..1048576", validationBound(512), validationBound(1048576)}, {"microsandbox", sandbox.Resources{CPUs: 1, MemoryMiB: 512}, "resources.root_disk_mib", "microsandbox requires root_disk_mib of at least 1024 MiB", validationBound(1024), nil}, {"microsandbox", sandbox.Resources{CPUs: 1, MemoryMiB: 512, RootDiskMiB: 1024, EnvironmentDiskMiB: 1}, "resources.environment_disk_mib", "microsandbox requires environment_disk_mib of 0 or at least 1024 MiB", validationBound(1024), nil}, - {"docker", sandbox.Resources{CPUs: 1, MemoryMiB: 512, RootDiskMiB: 1}, "resources.root_disk_mib", "docker does not support independent disk capacity limits", validationBound(0), validationBound(0)}, {"e2b", sandbox.Resources{CPUs: 1, MemoryMiB: 512, EnvironmentDiskMiB: 1}, "resources.environment_disk_mib", "e2b does not support independent disk capacity limits", validationBound(0), validationBound(0)}, } { spec := sandbox.DeploymentSpec{Resources: tc.resources} @@ -40,7 +39,7 @@ func TestDeploymentValidationFieldsPreserveMessages(t *testing.T) { } } } - if err := registry.ValidateSpecification("docker", sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 255, MemoryMiB: 1048576}}); err != nil { + if err := registry.ValidateSpecification("microsandbox", sandbox.DeploymentSpec{Resources: sandbox.Resources{CPUs: 255, MemoryMiB: 1048576, RootDiskMiB: 1024}, Workspace: &workspacefs.Declaration{Attachment: workspacefs.AttachmentHostDirectory, UserXAttr: true}}); err != nil { t.Fatal(err) } var field *sandbox.ValidationError diff --git a/services/core/internal/sandbox/testdata/deployment-contract.json b/services/core/internal/sandbox/testdata/deployment-contract.json index f9b24c5d1..ec4376b90 100644 --- a/services/core/internal/sandbox/testdata/deployment-contract.json +++ b/services/core/internal/sandbox/testdata/deployment-contract.json @@ -1,32 +1,4 @@ [ - { - "name": "docker", - "provider": "docker", - "specification": { - "resources": { - "cpus": 2, - "memory_mib": 4096 - } - }, - "valid": true, - "canonical": "{\"provider\":\"docker\",\"resources\":{\"cpus\":2,\"memory_mib\":4096}}", - "digest": "6931ff71e3c92bccfbc422fb11d0e69fdf591e7bdd1aed76ac47fa4f144baca1" - }, - { - "name": "docker-zero-disks", - "provider": "docker", - "specification": { - "resources": { - "cpus": 2, - "memory_mib": 4096, - "root_disk_mib": 0, - "environment_disk_mib": 0 - } - }, - "valid": true, - "canonical": "{\"provider\":\"docker\",\"resources\":{\"cpus\":2,\"memory_mib\":4096}}", - "digest": "6931ff71e3c92bccfbc422fb11d0e69fdf591e7bdd1aed76ac47fa4f144baca1" - }, { "name": "microsandbox", "provider": "microsandbox", @@ -62,68 +34,81 @@ }, { "name": "cpu-zero", - "provider": "docker", + "provider": "microsandbox", "specification": { "resources": { "cpus": 0, - "memory_mib": 4096 + "memory_mib": 4096, + "root_disk_mib": 1024 + }, + "workspace": { + "attachment": "host_directory", + "user_xattr": true } }, "valid": false }, { "name": "cpu-overflow", - "provider": "docker", + "provider": "microsandbox", "specification": { "resources": { "cpus": 256, - "memory_mib": 4096 + "memory_mib": 4096, + "root_disk_mib": 1024 + }, + "workspace": { + "attachment": "host_directory", + "user_xattr": true } }, "valid": false }, { "name": "cpu-bool", - "provider": "docker", + "provider": "microsandbox", "specification": { "resources": { "cpus": true, - "memory_mib": 4096 + "memory_mib": 4096, + "root_disk_mib": 1024 + }, + "workspace": { + "attachment": "host_directory", + "user_xattr": true } }, "valid": false }, { "name": "memory-low", - "provider": "docker", + "provider": "microsandbox", "specification": { "resources": { "cpus": 2, - "memory_mib": 511 + "memory_mib": 511, + "root_disk_mib": 1024 + }, + "workspace": { + "attachment": "host_directory", + "user_xattr": true } }, "valid": false }, { "name": "unknown-resource", - "provider": "docker", - "specification": { - "resources": { - "cpus": 2, - "memory_mib": 4096, - "other": 1 - } - }, - "valid": false - }, - { - "name": "docker-disk", - "provider": "docker", + "provider": "microsandbox", "specification": { "resources": { "cpus": 2, "memory_mib": 4096, + "other": 1, "root_disk_mib": 1024 + }, + "workspace": { + "attachment": "host_directory", + "user_xattr": true } }, "valid": false @@ -148,14 +133,19 @@ }, { "name": "runtime-rejected", - "provider": "docker", + "provider": "microsandbox", "specification": { "resources": { "cpus": 2, - "memory_mib": 4096 + "memory_mib": 4096, + "root_disk_mib": 1024 }, "runtime": { "source_commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "workspace": { + "attachment": "host_directory", + "user_xattr": true } }, "valid": false @@ -243,22 +233,6 @@ }, "valid": false }, - { - "name": "docker-external-rejected", - "provider": "docker", - "specification": { - "resources": { - "cpus": 255, - "memory_mib": 1048576 - }, - "workspace": { - "attachment": "host_directory", - "user_xattr": true, - "capacity_quota": false - } - }, - "valid": false - }, { "name": "microsandbox-external-omitted-false-quota", "provider": "microsandbox", diff --git a/services/core/internal/sessions/creation_test.go b/services/core/internal/sessions/creation_test.go index 7cbc81e34..b40fa412c 100644 --- a/services/core/internal/sessions/creation_test.go +++ b/services/core/internal/sessions/creation_test.go @@ -440,7 +440,7 @@ func TestCreateSession(t *testing.T) { if external { spec = json.RawMessage(`{"workspace":{}}`) } - tx.lockDeployment = returns(placement.Deployment{InstallationID: "installation", Provider: "docker", Mode: string(sandbox.DeploymentNodes), Generation: 5, Specification: spec}) + tx.lockDeployment = returns(placement.Deployment{InstallationID: "installation", Provider: "microsandbox", Mode: string(sandbox.DeploymentNodes), Generation: 5, Specification: spec}) tx.createEnvironment = returns("environment") _, err, _ := runCreation(t, rules, tx, creationInput("openai_hosted")) if external != errors.Is(err, ErrInvalidInput) { @@ -457,13 +457,13 @@ func TestCreateSession(t *testing.T) { calls []string }{ {"a reset closes hosted admission", rules, func(tx *fakeCreationTx) { - tx.lockDeployment = returns(placement.Deployment{InstallationID: "installation", Provider: "docker", Resetting: true}) + tx.lockDeployment = returns(placement.Deployment{InstallationID: "installation", Provider: "microsandbox", Resetting: true}) }, placement.ErrResetAdmission, []string{"UpsertSession create", "LockDeployment"}}, {"an unconfigured deployment still rejects admission", rules, func(tx *fakeCreationTx) { tx.lockDeployment = returns(placement.Deployment{Mode: string(sandbox.DeploymentNodes)}) }, placement.ErrNodeUnavailable, []string{"UpsertSession create", "LockDeployment"}}, {"an invalid deployment still rejects admission", rules, func(tx *fakeCreationTx) { - tx.lockDeployment = returns(placement.Deployment{InstallationID: "installation", Provider: "docker", Mode: string(sandbox.DeploymentNodes), Specification: json.RawMessage(`[]`)}) + tx.lockDeployment = returns(placement.Deployment{InstallationID: "installation", Provider: "microsandbox", Mode: string(sandbox.DeploymentNodes), Specification: json.RawMessage(`[]`)}) }, placement.ErrAdmissionClosed, []string{"UpsertSession create", "LockDeployment"}}, {"hosted creation needs rules", nil, func(*fakeCreationTx) {}, nil, []string{"UpsertSession create"}}, } { diff --git a/services/core/internal/sessions/environment_test.go b/services/core/internal/sessions/environment_test.go index 986c5a9ca..c84c99561 100644 --- a/services/core/internal/sessions/environment_test.go +++ b/services/core/internal/sessions/environment_test.go @@ -17,7 +17,7 @@ func TestCreatesEnvironment(t *testing.T) { t.Fatalf("%s: %v, %v", configuration, got, err) } } - for _, configuration := range []string{`not json`, `{"environment":{"type":"docker"}}`, `{"environment":{}}`} { + for _, configuration := range []string{`not json`, `{"environment":{"type":"microsandbox"}}`, `{"environment":{}}`} { if _, err := createsEnvironment([]byte(configuration)); !errors.Is(err, ErrInvalidInput) { t.Fatalf("%s: %v", configuration, err) } diff --git a/services/core/tests/integration/admin_session_archive_race_test.go b/services/core/tests/integration/admin_session_archive_race_test.go index d8244af31..f2a2f30d7 100644 --- a/services/core/tests/integration/admin_session_archive_race_test.go +++ b/services/core/tests/integration/admin_session_archive_race_test.go @@ -23,7 +23,7 @@ func TestManagedSessionArchiveReleasesPendingNodePlacement(t *testing.T) { if err := changes.Claim(t.Context(), installation); err != nil { t.Fatal(err) } - if _, err := changes.Initialize(t.Context(), installation, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err != nil { + if _, err := changes.Initialize(t.Context(), installation, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err != nil { t.Fatal(err) } nodes := deploymentService(t, s) @@ -32,7 +32,7 @@ func TestManagedSessionArchiveReleasesPendingNodePlacement(t *testing.T) { t.Fatal(err) } nodeID := uuid.NewString() - if _, err := nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Archive fixture", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { + if _, err := nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: nodeID, Name: "Archive fixture", Provider: "microsandbox", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { t.Fatal(err) } onlineManagerNode(t, s, nodeID) diff --git a/services/core/tests/integration/credential_matrix_http_test.go b/services/core/tests/integration/credential_matrix_http_test.go index 4ee0c36f0..270c91cb7 100644 --- a/services/core/tests/integration/credential_matrix_http_test.go +++ b/services/core/tests/integration/credential_matrix_http_test.go @@ -31,6 +31,7 @@ import ( // own /api/v1 machine connection routes. func TestCredentialNamespaceMatrix(t *testing.T) { s, _ := newManagedTestStore(t) + selectStoreWorkspaceStorage(t, s) s.SetPlacement(placementRules(t, "https://core.example")) ctx := t.Context() coreKey := uuid.NewString() @@ -88,7 +89,7 @@ func TestCredentialNamespaceMatrix(t *testing.T) { var executor sessions.IssuedExecutorCredential created("POST", "/core/v1/projects/"+project.ID+"/environments/"+environment.ID+"/executor-credentials", coreKey, `{"key_id":"`+uuid.NewString()+`"}`, &executor) - // A node credential: a Docker deployment, an enrollment token issued with the Core key, and an enrolled node. + // A node credential: a microsandbox deployment, an enrollment token issued with the Core key, and an enrolled node. deployments := deploymentService(t, s) installation := uuid.NewString() provider := &lifecycleProvider{resources: map[string]sandbox.Info{}} @@ -110,15 +111,15 @@ func TestCredentialNamespaceMatrix(t *testing.T) { _ = worker.Run(cancelled) }) }) - specification := SandboxDeploymentTestSpec("docker") - if _, err := worker.InitializeSandboxDeployment(ctx, sandbox.Selection{DeploymentSpec: specification, Provider: "docker"}); err != nil { + specification := SandboxDeploymentTestSpec("microsandbox") + if _, err := worker.InitializeSandboxDeployment(ctx, sandbox.Selection{DeploymentSpec: specification, Provider: "microsandbox"}); err != nil { t.Fatal(err) } var enrollment api.SandboxEnrollmentToken created("POST", "/core/v1/sandbox/enrollment-tokens", coreKey, `{"max_active":2,"max_retained":2}`, &enrollment) nodeID, nodeCredential := uuid.NewString(), strings.Repeat("n", 64) - enroll, _ := json.Marshal(machineapi.NodeEnrollment{NodeID: nodeID, Credential: nodeCredential, Name: "Matrix node", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), - DeploymentGeneration: 1, SpecificationDigest: specification.Digest("docker"), CoreURL: "https://core.example"}) + enroll, _ := json.Marshal(machineapi.NodeEnrollment{NodeID: nodeID, Credential: nodeCredential, Name: "Matrix node", Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), + DeploymentGeneration: 1, SpecificationDigest: specification.Digest("microsandbox"), CoreURL: "https://core.example"}) var node deployment.NodeIdentity created("POST", "/api/v1/sandbox-node/enroll", enrollment.Token, string(enroll), &node) diff --git a/services/core/tests/integration/device_bootstrap_binding_test.go b/services/core/tests/integration/device_bootstrap_binding_test.go index 153b083b8..474c9452a 100644 --- a/services/core/tests/integration/device_bootstrap_binding_test.go +++ b/services/core/tests/integration/device_bootstrap_binding_test.go @@ -21,8 +21,8 @@ func TestDeviceCredentialCarriesPersistedAllocationNode(t *testing.T) { t.Fatal(err) } remote := uuid.NewString() - _, err = nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: remote, Credential: strings.Repeat("x", 64), - Name: "remote", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) + _, err = nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: remote, Credential: strings.Repeat("x", 64), + Name: "remote", Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_lifecycle_nodes_test.go b/services/core/tests/integration/runtime_lifecycle_nodes_test.go index 0bdb9326e..3c73655f9 100644 --- a/services/core/tests/integration/runtime_lifecycle_nodes_test.go +++ b/services/core/tests/integration/runtime_lifecycle_nodes_test.go @@ -23,7 +23,7 @@ func lifecycleTestNode(t *testing.T, s *Store) string { t.Fatal(err) } id := uuid.NewString() - _, err = nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: id, Credential: strings.Repeat("n", 64), Name: "second", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) + _, err = nodes.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: id, Credential: strings.Repeat("n", 64), Name: "second", Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_node_generations_test.go b/services/core/tests/integration/runtime_node_generations_test.go index 73af2fa97..28cdd091c 100644 --- a/services/core/tests/integration/runtime_node_generations_test.go +++ b/services/core/tests/integration/runtime_node_generations_test.go @@ -55,7 +55,7 @@ func placedGeneration(t *testing.T, s, w *Store, session sessions.Session) (stri } func TestNodeGenerationsCapacityFallbackAndImmutablePending(t *testing.T) { - for _, provider := range []string{"microsandbox", "docker"} { + for _, provider := range []string{"microsandbox", "microsandbox"} { t.Run(provider, func(t *testing.T) { s, w, first, input := webSpecificationFixture(t, provider) nodes := deploymentService(t, s) @@ -149,7 +149,7 @@ func TestNodeGenerationsCapacityFallbackAndImmutablePending(t *testing.T) { } func TestNodeGenerationsReconnectKeepsServingPin(t *testing.T) { - s, w, first, input := webSpecificationFixture(t, "docker") + s, w, first, input := webSpecificationFixture(t, "microsandbox") service := deploymentService(t, s) node := specificationNode(t, s, first) old := onlineManagerNode(t, s, node.NodeID) @@ -188,7 +188,7 @@ func TestNodeGenerationsReconnectKeepsServingPin(t *testing.T) { } func TestNodeGenerationPreparationRefusalCreatesNoProvisionalOwnership(t *testing.T) { - s, w, first, _ := webSpecificationFixture(t, "docker") + s, w, first, _ := webSpecificationFixture(t, "microsandbox") node := specificationNode(t, s, first) connection := uuid.NewString() if err := deploymentService(t, s).ConnectNode(t.Context(), node.NodeID, connection, first.OwnerEpoch); err != nil { diff --git a/services/core/tests/integration/runtime_nodes_test.go b/services/core/tests/integration/runtime_nodes_test.go index 3dbdce6a2..00ce1533b 100644 --- a/services/core/tests/integration/runtime_nodes_test.go +++ b/services/core/tests/integration/runtime_nodes_test.go @@ -20,12 +20,12 @@ import ( ) // managerNode is the deployment managerFixture sets up: installation -// InstallationID runs Docker on the one online node NodeID. +// InstallationID runs microsandbox on the one online node NodeID. type managerNode struct{ InstallationID, NodeID string } func managerFixture(t *testing.T, active, retained int) (*Store, *Store, managerNode) { t.Helper() - s, w, view, _ := webSpecificationFixture(t, "docker") + s, w, view, _ := webSpecificationFixture(t, "microsandbox") node := enrollNode(t, s, view, deployment.Capacity{MaxActive: active, MaxRetained: retained}) return s, w, managerNode{InstallationID: view.InstallationID, NodeID: node.NodeID} } @@ -235,11 +235,11 @@ func TestRuntimeNodesEnrollmentAndEpoch(t *testing.T) { if err != nil { t.Fatal(err) } - input := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Credential: strings.Repeat("x", 64), Name: "remote", Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} + input := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: uuid.NewString(), Credential: strings.Repeat("x", 64), Name: "remote", Provider: "e2b", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, input); !errors.Is(err, deployment.ErrInvalidInput) { t.Fatal("mixed provider accepted", err) } - input.Provider = "docker" + input.Provider = "microsandbox" enrolled, err := nodes.Enroll(t.Context(), token, input) if err != nil || enrolled.InstallationID != d.InstallationID { t.Fatal(enrolled, err) diff --git a/services/core/tests/integration/sandbox_deployment_audit_http_test.go b/services/core/tests/integration/sandbox_deployment_audit_http_test.go index 09890735f..34d84f6f6 100644 --- a/services/core/tests/integration/sandbox_deployment_audit_http_test.go +++ b/services/core/tests/integration/sandbox_deployment_audit_http_test.go @@ -24,11 +24,12 @@ type auditedDeploymentChanges struct { } func (d auditedDeploymentChanges) UpdateSandboxDeployment(ctx context.Context, input sandbox.Selection) (deployment.View, error) { + input.Workspace = SandboxDeploymentTestSpec("microsandbox").Workspace return d.operations.Update(ctx, d.installation, input) } func TestSandboxDeploymentUpdateHTTPCommitsWithAdministratorAudit(t *testing.T) { - store, writer, initial, input := webSpecificationFixture(t, "docker") + store, writer, initial, input := webSpecificationFixture(t, "microsandbox") changes := auditedDeploymentChanges{strictStandIn{t}, deploymentExecution(t, writer), initial.InstallationID} handler, err := publicHandler(t, store, newTestAuthenticator(t, nil), "codex", func(d *api.Dependencies) { d.Sandboxes.DeploymentChanges = changes }) if err != nil { @@ -38,7 +39,7 @@ func TestSandboxDeploymentUpdateHTTPCommitsWithAdministratorAudit(t *testing.T) t.Helper() resources := input.Resources resources.CPUs = cpus - body, err := json.Marshal(map[string]any{"expected_generation": generation, "provider": "docker", "configuration": map[string]any{}, "resources": resources}) + body, err := json.Marshal(map[string]any{"expected_generation": generation, "provider": "microsandbox", "configuration": map[string]any{}, "resources": resources}) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_deployment_setup_test.go b/services/core/tests/integration/sandbox_deployment_setup_test.go index 3b0a655c2..d066964d3 100644 --- a/services/core/tests/integration/sandbox_deployment_setup_test.go +++ b/services/core/tests/integration/sandbox_deployment_setup_test.go @@ -39,7 +39,7 @@ func TestSandboxDeploymentSetupPersistsWithoutExecution(t *testing.T) { if err != nil || !reflect.DeepEqual(replay, selected) { t.Fatal("identical retry changed selection", replay, err) } - for _, changed := range []sandbox.Selection{{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}} { + for _, changed := range []sandbox.Selection{{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}} { if _, err := deploymentExecution(t, w).Initialize(t.Context(), id, changed); !errors.Is(err, deployment.ErrConflict) { t.Fatal("changed selection accepted", err) } diff --git a/services/core/tests/integration/sandbox_deployment_switch_test.go b/services/core/tests/integration/sandbox_deployment_switch_test.go index 5beeb5598..f37840198 100644 --- a/services/core/tests/integration/sandbox_deployment_switch_test.go +++ b/services/core/tests/integration/sandbox_deployment_switch_test.go @@ -117,7 +117,7 @@ func TestSandboxDirectDeploymentOwnershipAndCleanSwitch(t *testing.T) { if err := deploymentExecution(t, w).StartReset(SandboxResetTestContext(t.Context()), id, deployment.ResetRequest{Clear: "auto", ExpectedGeneration: 1}); err != nil { t.Fatal(err) } - update := sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker", ExpectedGeneration: 1} + update := sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox", ExpectedGeneration: 1} if _, err := changes.Update(SandboxResetTestContext(t.Context()), id, update); !errors.Is(err, deployment.ErrResetInProgress) { t.Fatal("reset allowed switch", err) } @@ -154,14 +154,14 @@ func TestSandboxSwitchRetiresNodesAndEnrollment(t *testing.T) { if err := changes.Claim(t.Context(), id); err != nil { t.Fatal(err) } - if _, err := changes.Initialize(t.Context(), id, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err != nil { + if _, err := changes.Initialize(t.Context(), id, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err != nil { t.Fatal(err) } token, err := EnrollmentTestToken(nodes.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(2), MaxRetained: new(4)})) if err != nil { t.Fatal(err) } - node := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Name: "Machine", Provider: "docker", Credential: strings.Repeat("c", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} + node := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: uuid.NewString(), Name: "Machine", Provider: "microsandbox", Credential: strings.Repeat("c", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, node); err != nil { t.Fatal(err) } @@ -187,7 +187,7 @@ func TestSandboxSwitchRetiresNodesAndEnrollment(t *testing.T) { if _, err := nodes.Enroll(t.Context(), strings.Repeat("invalid", 8), spareNode); !errors.Is(err, deployment.ErrNodeCredential) { t.Fatal("invalid token disclosed deployment validation", err) } - spareNode.Provider = "docker" + spareNode.Provider = "microsandbox" input := e2bSelection() if _, err := resetAndSelect(t, w, id, 1, input); err != nil { t.Fatal(err) @@ -202,7 +202,7 @@ func TestSandboxSwitchRetiresNodesAndEnrollment(t *testing.T) { if err := deploymentExecution(t, w).StartReset(SandboxResetTestContext(t.Context()), id, deployment.ResetRequest{Clear: "auto", ExpectedGeneration: 3}); err != nil { t.Fatal(err) } - if _, err := resetAndSelect(t, w, id, 3, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err != nil { + if _, err := resetAndSelect(t, w, id, 3, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err != nil { t.Fatal(err) } if err := deploymentExecution(t, w).CancelReset(SandboxResetTestContext(t.Context()), id, 5); err != nil { @@ -254,7 +254,7 @@ func TestSandboxResetSerializesFreshDirectSessions(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = changes.Update(SandboxResetTestContext(t.Context()), id, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker", ExpectedGeneration: 1}) + _, err = changes.Update(SandboxResetTestContext(t.Context()), id, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox", ExpectedGeneration: 1}) if view.Resources.Pending > 0 && !errors.Is(err, deployment.ErrResetInProgress) { t.Fatal("committed pending Session bypassed switch guard", err) } @@ -314,7 +314,7 @@ func TestSandboxSwitchPreservesReleasedAllocationAndItemHistory(t *testing.T) { if err := deploymentExecution(t, w).StartReset(SandboxResetTestContext(t.Context()), installation, deployment.ResetRequest{Clear: "auto", ExpectedGeneration: 1}); err != nil { t.Fatal(err) } - if _, err := resetAndSelect(t, w, installation, 1, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err != nil { + if _, err := resetAndSelect(t, w, installation, 1, sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err != nil { t.Fatal(err) } items, err = sessionAdapter(s).ListItems(t.Context(), tenant, history.ID, "", 100, true) @@ -352,8 +352,8 @@ func TestUnspecifiedNodeDeploymentRejectedWithoutMutation(t *testing.T) { if err := changes.Claim(t.Context(), id); err != nil { t.Fatal(err) } - spec := SandboxDeploymentTestSpec("docker") - selection := sandbox.Selection{DeploymentSpec: spec, Provider: "docker"} + spec := SandboxDeploymentTestSpec("microsandbox") + selection := sandbox.Selection{DeploymentSpec: spec, Provider: "microsandbox"} if _, err := changes.Initialize(t.Context(), id, selection); err != nil { t.Fatal(err) } @@ -361,7 +361,7 @@ func TestUnspecifiedNodeDeploymentRejectedWithoutMutation(t *testing.T) { if err != nil { t.Fatal(err) } - node := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: spec.Digest("docker"), NodeID: uuid.NewString(), Name: "Legacy", Provider: "docker", Credential: strings.Repeat("l", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} + node := machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: spec.Digest("microsandbox"), NodeID: uuid.NewString(), Name: "Legacy", Provider: "microsandbox", Credential: strings.Repeat("l", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, node); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_deployment_switch_worker_test.go b/services/core/tests/integration/sandbox_deployment_switch_worker_test.go index c219cb02a..8c1ef6ed0 100644 --- a/services/core/tests/integration/sandbox_deployment_switch_worker_test.go +++ b/services/core/tests/integration/sandbox_deployment_switch_worker_test.go @@ -3,6 +3,7 @@ package integration import ( "context" "errors" + "github.com/jackc/pgx/v5/pgxpool" "reflect" "strings" "sync/atomic" @@ -21,8 +22,24 @@ import ( "github.com/google/uuid" ) -func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { +// switchWorker is a running Worker over a deferred fixture provider. Workspace +// storage is selected only for node deployments: a cloud deployment has none. +type switchWorker struct { + s *Store + pool *pgxpool.Pool + w *execution.Worker + deployments *deployment.Service + id string + done chan error + fail *atomic.Bool + preparations *atomic.Int32 +} + +func startSwitchWorker(t *testing.T, storage bool) switchWorker { s, pool := newManagedTestStore(t) + if storage { + selectStoreWorkspaceStorage(t, s) + } deployments := deploymentService(t, s) id := uuid.NewString() p := &lifecycleProvider{resources: map[string]sandbox.Info{}} @@ -53,8 +70,14 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { t.Error("worker shutdown blocked") } }) + return switchWorker{s, pool, w, deployments, id, done, &fail, &preparations} +} + +func TestSandboxWorkerRecoversFailedNodeActivation(t *testing.T) { + f := startSwitchWorker(t, true) + s, pool, w, deployments, fail, preparations := f.s, f.pool, f.w, f.deployments, f.fail, f.preparations fail.Store(true) - if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err == nil { + if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err == nil { t.Fatal("rejected initial provider configuration was committed") } empty, err := deployments.View(t.Context()) @@ -62,18 +85,18 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { t.Fatal("failed initial candidate changed the deployment", err) } fail.Store(false) - if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err != nil { + if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err != nil { t.Fatal(err) } prepared := preparations.Load() - if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err == nil || preparations.Load() != prepared { + if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err == nil || preparations.Load() != prepared { t.Fatal("stale identical POST reached provider preparation", err) } enrollment, err := EnrollmentTestToken(deployments.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(2), MaxRetained: new(4)})) if err != nil { t.Fatal(err) } - node := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "retained candidate fixture", Provider: "docker", Credential: strings.Repeat("n", 64), BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), CoreURL: s.placement.PublicURL()} + node := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "retained candidate fixture", Provider: "microsandbox", Credential: strings.Repeat("n", 64), BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), CoreURL: s.placement.PublicURL()} if _, err := deployments.Enroll(t.Context(), enrollment, node); err != nil { t.Fatal(err) } @@ -81,7 +104,7 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { if err != nil { t.Fatal(err) } - input := sandbox.Selection{ExpectedGeneration: 1, DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"} + input := sandbox.Selection{ExpectedGeneration: 1, DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"} input.Resources.CPUs++ fail.Store(true) if _, err := w.UpdateSandboxDeployment(SandboxResetTestContext(t.Context()), input); err == nil { @@ -109,12 +132,21 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { if err := w.ReconcileManagedRuntimes(t.Context()); err != nil { t.Fatal("failed commit left manager barrier closed", err) } - if view, err := deployments.View(t.Context()); err != nil || view.Generation != 1 || view.Provider != "docker" { + if view, err := deployments.View(t.Context()); err != nil || view.Generation != 1 || view.Provider != "microsandbox" { t.Fatal("failed commit replaced provider", view, err) } if _, err := w.UpdateSandboxDeployment(SandboxResetTestContext(t.Context()), input); err != nil { t.Fatal(err) } +} + +func TestSandboxWorkerResetsCloudDeployment(t *testing.T) { + f := startSwitchWorker(t, false) + s, w, deployments, id, done := f.s, f.w, f.deployments, f.id, f.done + empty, err := deployments.View(t.Context()) + if err != nil { + t.Fatal(err) + } reset := func(generation uint64) deployment.View { t.Helper() if _, err := w.StartSandboxReset(SandboxResetTestContext(t.Context()), deployment.ResetRequest{ExpectedGeneration: generation, Clear: "force"}); err != nil { @@ -138,32 +170,27 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { t.Fatal("reset completion blocked (including possible manager self-drain)") return deployment.View{} } - empty = reset(2) cloud := sandbox.Selection{ExpectedGeneration: empty.Generation, DeploymentSpec: SandboxDeploymentTestSpec("e2b"), Provider: "e2b", Configuration: &e2b.DeploymentConfiguration{APIKey: "fixture-api-key", Template: "runtime:" + uuid.NewString()}} if _, err := w.InitializeSandboxDeployment(t.Context(), cloud); err != nil { - t.Fatal("setup after unconfigured publication", err) + t.Fatal("initial cloud setup", err) } tenant, session, environment := managedSession(t, s) allocation, err := w.ProvisionEnvironment(t.Context(), tenant, environment.ID, id) if err != nil || allocation.NodeID != "" || allocation.State != "running" { t.Fatal("direct provider not available after resume", allocation, err) } - next := sandbox.Selection{ExpectedGeneration: 4, DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"} - if _, err := w.UpdateSandboxDeployment(SandboxResetTestContext(t.Context()), next); !errors.Is(err, deployment.ErrConflict) { - t.Fatal("dirty switch accepted", err) - } if err := sessionService(t, s).DeleteSession(t.Context(), sessions.DeleteSessionCommand{TenantID: tenant, SessionID: session.ID}); err != nil { t.Fatal(err) } reconcileManagedState(t, w, s, tenant, environment.ID, "released") - empty = reset(4) - next.ExpectedGeneration = empty.Generation - if _, err := w.InitializeSandboxDeployment(t.Context(), next); err != nil { + empty = reset(1) + cloud.ExpectedGeneration = empty.Generation + if _, err := w.InitializeSandboxDeployment(t.Context(), cloud); err != nil { t.Fatal("clean setup after reset", err) } select { case err := <-done: - t.Fatalf("provider switch stopped Worker: %v", err) + t.Fatalf("reset stopped Worker: %v", err) default: } } diff --git a/services/core/tests/integration/sandbox_deployment_worker_test.go b/services/core/tests/integration/sandbox_deployment_worker_test.go index dafe4e91e..8211ee575 100644 --- a/services/core/tests/integration/sandbox_deployment_worker_test.go +++ b/services/core/tests/integration/sandbox_deployment_worker_test.go @@ -15,12 +15,14 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" ) func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { s, _ := newManagedTestStore(t) + selectStoreWorkspaceStorage(t, s) deployments := deploymentService(t, s) id := uuid.NewString() p := &lifecycleProvider{resources: map[string]sandbox.Info{}} @@ -29,10 +31,12 @@ func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, BackendFingerprint: setup.BackendFingerprint, CoreURL: "https://core.example/api/v1", Provider: p}, nil + return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, BackendFingerprint: setup.BackendFingerprint, CoreURL: "https://core.example/api/v1", Provider: p, + Generation: setup.Generation, Resources: setup.Specification.Resources, WorkspaceRequirements: microsandbox.Policy().Workspace, Workspace: setup.Specification.Workspace}, nil }, func(ctx context.Context, setup deployment.Setup) (execution.PreparedRuntimeDeployment, error) { - return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: p}}, nil + return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: p, + Resources: setup.Specification.Resources, WorkspaceRequirements: microsandbox.Policy().Workspace, Workspace: setup.Specification.Workspace}}, nil }) start := func() (*execution.Worker, func()) { t.Helper() @@ -49,7 +53,7 @@ func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { if _, err := w.CreateSession(t.Context(), uuid.NewString(), input); !errors.Is(err, execution.ErrExecutionUnavailable) { t.Fatal("uninitialized worker admitted hosted Session", err) } - if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("docker"), Provider: "docker"}); err != nil { + if _, err := w.InitializeSandboxDeployment(t.Context(), sandbox.Selection{DeploymentSpec: SandboxDeploymentTestSpec("microsandbox"), Provider: "microsandbox"}); err != nil { t.Fatal(err) } queued, err := s.CreateSession(t.Context(), uuid.NewString(), input) @@ -64,7 +68,7 @@ func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { t.Fatal(err) } nodeID := uuid.NewString() - if _, err := deployments.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Remote", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { + if _, err := deployments.Enroll(t.Context(), token, machineapi.NodeEnrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: nodeID, Name: "Remote", Provider: "microsandbox", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { t.Fatal(err) } connect := func() { diff --git a/services/core/tests/integration/sandbox_generations_test.go b/services/core/tests/integration/sandbox_generations_test.go index 8ee2ea69a..6fac7d6b8 100644 --- a/services/core/tests/integration/sandbox_generations_test.go +++ b/services/core/tests/integration/sandbox_generations_test.go @@ -103,7 +103,7 @@ func TestE2BRetainedCustomEndpointAfterOnlineSwitch(t *testing.T) { } func TestPendingPlacementGenerationSurvivesRepeatedUpdates(t *testing.T) { - s, w, view, _ := webSpecificationFixture(t, "docker") + s, w, view, _ := webSpecificationFixture(t, "microsandbox") node := specificationNode(t, s, view) tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) if err := reserveSessionPlacement(t, s, w, session); err != nil { @@ -178,7 +178,7 @@ func TestGenerationUpdateSerializesWithAllocationAdmission(t *testing.T) { } func TestSandboxSnapshotRolloutEquivalence(t *testing.T) { - s, _, view, _ := webSpecificationFixture(t, "docker") + s, _, view, _ := webSpecificationFixture(t, "microsandbox") node := specificationNode(t, s, view) deployments := deploymentService(t, s) heartbeat := func(connection, state string) { diff --git a/services/core/tests/integration/sandbox_node_auth_order_http_test.go b/services/core/tests/integration/sandbox_node_auth_order_http_test.go index 3ec1663f5..67705bba4 100644 --- a/services/core/tests/integration/sandbox_node_auth_order_http_test.go +++ b/services/core/tests/integration/sandbox_node_auth_order_http_test.go @@ -37,7 +37,7 @@ func TestSandboxNodeRoutesAuthenticateBeforeDeploymentState(t *testing.T) { token, claimedToken, claimed := strings.Repeat("e", 64), strings.Repeat("c", 64), uuid.NewString() enrollment(token, uuid.NewString()) enrollment(claimedToken, claimed) - enroll, _ := json.Marshal(machineapi.NodeEnrollment{NodeID: uuid.NewString(), Credential: strings.Repeat("n", 64), Name: "Early node", Provider: "docker", + enroll, _ := json.Marshal(machineapi.NodeEnrollment{NodeID: uuid.NewString(), Credential: strings.Repeat("n", 64), Name: "Early node", Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: strings.Repeat("d", 64), CoreURL: "https://core.example"}) nodeID := uuid.NewString() type check struct { diff --git a/services/core/tests/integration/sandbox_specification_lifecycle_test.go b/services/core/tests/integration/sandbox_specification_lifecycle_test.go index 68ea7f21a..4173aa642 100644 --- a/services/core/tests/integration/sandbox_specification_lifecycle_test.go +++ b/services/core/tests/integration/sandbox_specification_lifecycle_test.go @@ -64,7 +64,7 @@ func enrollNode(t *testing.T, s *Store, view deployment.View, capacity deploymen } func TestSandboxSpecificationBootstrapReadDoesNotConsumeEnrollment(t *testing.T) { - s, w, view, input := webSpecificationFixture(t, "docker") + s, w, view, input := webSpecificationFixture(t, "microsandbox") nodes := deploymentService(t, s) token, err := EnrollmentTestToken(nodes.CreateEnrollment(t.Context(), deployment.EnrollmentLimits{MaxActive: new(2), MaxRetained: new(4)})) if err != nil { @@ -79,7 +79,7 @@ func TestSandboxSpecificationBootstrapReadDoesNotConsumeEnrollment(t *testing.T) if _, err := nodes.NodeConfiguration(t.Context(), "", "invalid-token", 0); !errors.Is(err, deployment.ErrNodeCredential) { t.Fatal("unauthenticated configuration read", err) } - node := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "bootstrap", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: s.placement.PublicURL()} + node := machineapi.NodeEnrollment{NodeID: uuid.NewString(), Name: "bootstrap", Credential: strings.Repeat("n", 64), Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: s.placement.PublicURL()} for _, change := range []func(*machineapi.NodeEnrollment){ func(n *machineapi.NodeEnrollment) { n.DeploymentGeneration++ }, func(n *machineapi.NodeEnrollment) { n.SpecificationDigest = strings.Repeat("c", 64) }, @@ -304,7 +304,7 @@ func TestSandboxSpecificationAllocationRaceWithMaintenance(t *testing.T) { // A node keeps the public URL it enrolled with. After the public URL changes it // receives no new sandboxes until it is re-added. func TestNodeBoundToAnotherPublicURLGetsNoNewSandboxes(t *testing.T) { - s, w, view, _ := webSpecificationFixture(t, "docker") + s, w, view, _ := webSpecificationFixture(t, "microsandbox") s.SetPlacement(placementRules(t, "https://old.example")) w.SetPlacement(s.placement) node := specificationNode(t, s, view) diff --git a/services/core/tests/integration/worker_fixture_test.go b/services/core/tests/integration/worker_fixture_test.go index e3c7f3679..877899150 100644 --- a/services/core/tests/integration/worker_fixture_test.go +++ b/services/core/tests/integration/worker_fixture_test.go @@ -153,6 +153,22 @@ func fixtureOwner(s *Store, lease *pgunit.Lease) (execution.Owner, error) { }, nil } +// selectStoreWorkspaceStorage selects the fixture workspace storage on s before a +// Worker takes its execution lease. +func selectStoreWorkspaceStorage(t *testing.T, s *Store) { + t.Helper() + lease, err := pgunit.AcquireLease(t.Context(), s.pool) + if err != nil { + t.Fatal(err) + } + defer lease.Close(context.Background()) + owner, err := fixtureOwner(s, lease) + if err != nil { + t.Fatal(err) + } + selectWorkspaceStorage(t, owner.Workspaces) +} + // webDeployment claims s's deployment for a new installation and selects // provider on it as Web setup does, then closes its execution lease so a // Worker can start. It returns the installation. diff --git a/services/web/distribution_test.go b/services/web/distribution_test.go index 3a540af8a..ac6555193 100644 --- a/services/web/distribution_test.go +++ b/services/web/distribution_test.go @@ -84,7 +84,7 @@ func TestConsoleReportsServableNodeProviders(t *testing.T) { } } artifacts := map[string]any{} - for _, logical := range []string{"native/bin/oac-node", "images/runtime.tar.gz", "native/microsandbox/msb", "runtime/seccomp.json"} { + for _, logical := range []string{"native/bin/oac-node", "images/runtime.tar.gz", "native/microsandbox/msb"} { artifacts[logical] = map[string]any{"filename": strings.ReplaceAll(logical, "/", "-"), "size": len("runtime-bytes")} } release := publishedPayload(t, payload, map[string]any{"artifacts": artifacts}) @@ -100,7 +100,7 @@ func TestConsoleReportsServableNodeProviders(t *testing.T) { } defer h.Close() server := serveSignedIn(t, h) - if _, body := responseBody(t, server, consoleRequest(t, server, "GET", "/console/config")); !strings.Contains(body, `"node_artifacts":["docker"]`) { + if _, body := responseBody(t, server, consoleRequest(t, server, "GET", "/console/config")); !strings.Contains(body, `"node_artifacts":[]`) { t.Fatal("microsandbox reported without its helper and firmware:", body) } request := consoleRequest(t, server, "GET", "/node-install/artifacts/images-runtime.tar.gz") diff --git a/services/web/node_artifacts_test.go b/services/web/node_artifacts_test.go index 6fdc01150..993dcef5e 100644 --- a/services/web/node_artifacts_test.go +++ b/services/web/node_artifacts_test.go @@ -37,7 +37,7 @@ func TestOnlineNodeArtifactsRedirectWithoutLocalPayload(t *testing.T) { } defer payload.Close() h := &console{nodePayload: payload} - if got := strings.Join(h.nodeArtifacts(), ","); got != "docker,microsandbox" { + if got := strings.Join(h.nodeArtifacts(), ","); got != "microsandbox" { t.Fatal(got) } name := manifest.Artifacts["images/runtime.tar.gz"].Filename diff --git a/services/web/node_installation.go b/services/web/node_installation.go index 65dd5135b..e09f6589e 100644 --- a/services/web/node_installation.go +++ b/services/web/node_installation.go @@ -18,7 +18,7 @@ import ( // Serving the fixed list avoids a package registry or an arbitrary file endpoint. var nodePayloadFiles = map[string]bool{ "node-install.pyz": true, - "manifest.json": true, "SHA256SUMS": true, "runtime/seccomp.json": true, + "manifest.json": true, "SHA256SUMS": true, } // The generated adapter declarations bound the distribution payload endpoint. diff --git a/website/.vitepress/theme/assets/logos/LICENSE-svgl.txt b/website/.vitepress/theme/assets/logos/LICENSE-svgl.txt deleted file mode 100644 index 5a24d8eaa..000000000 --- a/website/.vitepress/theme/assets/logos/LICENSE-svgl.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Pablo Hdez - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/website/.vitepress/theme/assets/logos/docker.svg b/website/.vitepress/theme/assets/logos/docker.svg deleted file mode 100644 index 2e0703dc2..000000000 --- a/website/.vitepress/theme/assets/logos/docker.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/website/.vitepress/theme/ecosystem-logos.ts b/website/.vitepress/theme/ecosystem-logos.ts index 3d1f0ea4a..166d7ccb4 100644 --- a/website/.vitepress/theme/ecosystem-logos.ts +++ b/website/.vitepress/theme/ecosystem-logos.ts @@ -23,7 +23,6 @@ import volcengine from './assets/logos/volcengine.svg' import baiducloud from './assets/logos/baiducloud.svg' import digitalocean from './assets/logos/digitalocean.svg' import hetzner from './assets/logos/hetzner.svg' -import docker from './assets/logos/docker.svg' import e2b from './assets/logos/e2b.svg' export const agentLogos = [ @@ -54,6 +53,5 @@ export const computeLogos = [ { name: 'Baidu AI Cloud', src: baiducloud }, { name: 'DigitalOcean', src: digitalocean }, { name: 'Hetzner', src: hetzner }, - { name: 'Docker', src: docker }, { name: 'E2B', src: e2b }, ] as const diff --git a/website/README.md b/website/README.md index 02a7952e0..dc4dce982 100644 --- a/website/README.md +++ b/website/README.md @@ -40,7 +40,6 @@ The hero holds the title, the lede, the install command and the interactive exam SVG files are bundled under `.vitepress/theme/assets/logos/` and tinted with CSS (dark in the light theme, white in the dark theme), preserving their proportions. Their sources are: - AI brands, AWS, Azure, Google Cloud, Alibaba Cloud, Tencent Cloud, Huawei Cloud, Volcengine, Baidu AI Cloud and DigitalOcean: [Lobe Icons](https://github.com/lobehub/lobe-icons), from `@lobehub/icons-static-svg` version `1.95.1`, using the matching filename in its `icons/` directory. MIT license: `LICENSE-lobe-icons.txt` beside the assets. -- Docker: [SVGL's Docker SVG](https://github.com/pheralb/svgl/blob/main/static/library/docker.svg). MIT license: `LICENSE-svgl.txt` beside the assets. - Hetzner: [Simple Icons' Hetzner SVG](https://github.com/simple-icons/simple-icons/blob/develop/icons/hetzner.svg). CC0 notice: `LICENSE-simple-icons.txt` beside the assets. - E2B: the medium white symbol from [E2B's official brand assets](https://changelog.e2b.dev/brand), file `e2b-symbol-white-m.svg`. Preserve its proportions and clear space according to that source's usage guidance.