From 9814f7ff43ac29fe5e42779a2e87ecea7d0d1379 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Tue, 20 Oct 2026 04:54:15 +0000 Subject: [PATCH] Size sandboxes for the selected workspace storage --- .../src/features/sandbox/SandboxSetupWizard.tsx | 16 ++++++++++------ .../sandbox/deployment-specification.test.ts | 13 ++++++++++++- .../features/sandbox/deployment-specification.ts | 8 +++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx index 7644af628..b3a0ff0da 100644 --- a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx +++ b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx @@ -15,7 +15,8 @@ import { formatBytes } from "../../lib/format"; import { installationQuery } from "../../lib/installation"; import type { ParseKeys } from "i18next"; import { sandboxConfigurationRejection, sandboxProviderLabel } from "../../lib/sandbox-labels"; -import { defaultSandboxResources, isRuntimeRelease, isRuntimeReleaseField, RUNTIME_RELEASE_FIELDS, savedSpecification, validSandboxResources } from "./deployment-specification"; +import { defaultSandboxResources, isRuntimeRelease, isRuntimeReleaseField, RUNTIME_RELEASE_FIELDS, savedSpecification, selectedWorkspace, validSandboxResources } from "./deployment-specification"; +import { workspaceStorageQuery } from "../settings/workspace-storage"; import { e2bKeyReady, e2bUpdateSelection } from "./sandbox-update"; import { distributionRuntimeQuery, sandboxAdmin } from "./sandbox-queries"; import "./sandbox-wizard.css"; @@ -134,6 +135,9 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab const [provider, setProvider] = useState(current?.provider ?? null); const policy = provider ? deploymentContract.providers[provider] : null; const saved = provider && current ? savedSpecification(provider, current.provider, current.specification) : null; + // The selected storage sizes a first initialization; a saved specification takes precedence. + const storage = useQuery(workspaceStorageQuery).data; + const workspace = saved?.workspace ?? (provider ? selectedWorkspace(provider, storage) : undefined); // Choosing a Provider that declares a default size replaces this placeholder. const [resources, setResources] = useState(current?.specification?.resources ?? { cpus: 0, memory_mib: 0 }); const [size, setSize] = useState(current?.specification ? presetOf(current.provider, current.specification.resources, current.specification.workspace) ?? "current" : "standard"); @@ -208,7 +212,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab const e2bReady = provider !== "e2b" || (keyReady && validTemplate(template.trim()) && validEndpoint(apiURL.trim(), domain.trim()) && (!editing || !connectionChanged || apiKey.trim().length > 0)); // A Provider without a declared default size takes it from its configuration, so the selection sends no resources. const sized = Boolean(policy?.default_resources); - const sizeReady = provider !== null && (!sized || validSandboxResources(provider, resources, saved?.workspace)); + const sizeReady = provider !== null && (!sized || validSandboxResources(provider, resources, workspace)); const ready = provider !== null && runtimeReady && e2bReady && sizeReady && !disabled && !busy; const order: Step[] = editing ? where === "direct" ? ["e2b", "review"] : ["size", "review"] : where === "direct" ? ["where", "e2b", "review"] : ["where", "backend", "size", "review"]; @@ -220,7 +224,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab if (next !== provider) { setRejection(null); const kept = current ? savedSpecification(next, current.provider, current.specification) : null; - const proposed = kept?.resources ?? defaultSandboxResources(next); + const proposed = kept?.resources ?? defaultSandboxResources(next, selectedWorkspace(next, storage)); if (proposed) setResources(proposed); setSize(kept ? presetOf(next, kept.resources, kept.workspace) ?? "current" : "standard"); setRuntime({}); @@ -266,7 +270,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab } const sizeLabel = (value: SandboxResources) => t("{{cpus}} CPU · {{memory}}", { cpus: value.cpus, memory: formatBytes(value.memory_mib * MIB) }); - const diskLabel = (value: SandboxResources) => saved?.workspace ? t("Root disk {{root}} · external workspace", { root: formatBytes((value.root_disk_mib ?? 0) * MIB) }) : t("Root disk {{root}} · data disk {{data}}", { root: formatBytes((value.root_disk_mib ?? 0) * MIB), data: formatBytes((value.environment_disk_mib ?? 0) * MIB) }); + const diskLabel = (value: SandboxResources) => workspace ? t("Root disk {{root}} · external workspace", { root: formatBytes((value.root_disk_mib ?? 0) * MIB) }) : t("Root disk {{root}} · data disk {{data}}", { root: formatBytes((value.root_disk_mib ?? 0) * MIB), data: formatBytes((value.environment_disk_mib ?? 0) * MIB) }); let page: ReactNode; if (step === "where") { @@ -344,7 +348,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab ); } else if (step === "size") { - const options = provider ? presets(provider, saved?.workspace) : null; + const options = provider ? presets(provider, workspace) : null; // A saved size outside the presets stays on offer as the current one. const kept = saved && provider && presetOf(provider, saved.resources, saved.workspace) === null ? saved.resources : null; const disks = (value: SandboxResources) => (policy?.disk ? diskLabel(value) : undefined); @@ -431,7 +435,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab { setSize("custom"); setResources({ ...resources, memory_mib }); setFieldRejection(null); }} /> {policy?.disk ? <> { setResources({ ...resources, root_disk_mib }); setFieldRejection(null); }} /> - {saved?.workspace && !saved.workspace.capacity_quota ?

{t("Workspace storage is configured by the operator; no capacity quota is enforced.")}

: { setResources({ ...resources, environment_disk_mib }); setFieldRejection(null); }} />} + {workspace && !workspace.capacity_quota ?

{t("Workspace storage is configured by the operator; no capacity quota is enforced.")}

: { setResources({ ...resources, environment_disk_mib }); setFieldRejection(null); }} />} : null} : null} diff --git a/apps/web/src/features/sandbox/deployment-specification.test.ts b/apps/web/src/features/sandbox/deployment-specification.test.ts index a55925899..fcfb05cf7 100644 --- a/apps/web/src/features/sandbox/deployment-specification.test.ts +++ b/apps/web/src/features/sandbox/deployment-specification.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { defaultSandboxResources, distributionRuntime, isRuntimeReleaseField, sandboxesThatFit, savedSpecification, validSandboxResources } from "./deployment-specification"; +import { defaultSandboxResources, distributionRuntime, isRuntimeReleaseField, sandboxesThatFit, savedSpecification, selectedWorkspace, validSandboxResources } from "./deployment-specification"; import { deploymentContract, type SandboxProvider, type SandboxSpecification } from "@oac/agents-client"; const manifest = { platform: "linux/amd64", source_commit: "0".repeat(40), images: { runtime: `sha256:${"a".repeat(64)}` }, @@ -57,6 +57,17 @@ describe("deployment resources and Runtime", () => { 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("sizes a first initialization from the selected workspace storage", () => { + const sized = (capacity: number | null) => { + const workspace = selectedWorkspace("microsandbox", capacity === null ? null : { tenant_capacity_mib: capacity }); + const resources = defaultSandboxResources("microsandbox", workspace)!; + return { workspace, resources, valid: validSandboxResources("microsandbox", resources, workspace) }; + }; + expect(sized(0)).toMatchObject({ workspace: { capacity_quota: false }, resources: { environment_disk_mib: 0 }, valid: true }); + expect(sized(1024)).toMatchObject({ workspace: { capacity_quota: true }, resources: { environment_disk_mib: 8192 }, valid: true }); + expect(sized(null).workspace).toBeUndefined(); + expect(selectedWorkspace("docker", { tenant_capacity_mib: 0 })).toBeUndefined(); + }); it("accepts only Core's Runtime image in the Runtime reference", async () => { const digest = "b".repeat(64); const runtime_ref = `oac-runtime@sha256:${digest}`; diff --git a/apps/web/src/features/sandbox/deployment-specification.ts b/apps/web/src/features/sandbox/deployment-specification.ts index 2beb67bb4..93ef7ae13 100644 --- a/apps/web/src/features/sandbox/deployment-specification.ts +++ b/apps/web/src/features/sandbox/deployment-specification.ts @@ -12,7 +12,13 @@ interface Manifest { /** The size the Provider declares for setup to propose; null when its configuration selects the size. */ export function defaultSandboxResources(provider: SandboxProvider, workspace?: SandboxSpecification["workspace"]): SandboxResources | null { const size: SandboxResources | null = deploymentContract.providers[provider].default_resources; - return size && { ...size, ...(workspace ? { environment_disk_mib: 0 } : {}) }; + return size && { ...size, ...(workspace && !workspace.capacity_quota ? { environment_disk_mib: 0 } : {}) }; +} + +/** The workspace requirement a Provider takes from the selected storage; none when it owns its workspaces or no storage is selected. */ +export function selectedWorkspace(provider: SandboxProvider, storage: { tenant_capacity_mib: number } | null | undefined): SandboxSpecification["workspace"] { + const policy = deploymentContract.providers[provider]; + return storage && "workspace" in policy ? { ...policy.workspace, capacity_quota: storage.tenant_capacity_mib > 0 } : undefined; } /** Core keeps owned disk limits; external workspace capacity is optional and requires real quota support. */