Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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 with independent workspace storage, idle sandboxes release compute and resume on a compatible node with their files intact. E2B, 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, Docker and your own Linux, macOS or Windows machine also run 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).

![Deployment overview](docs/assets/console-overview-en.webp)
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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、Docker 以及你自己的 Linux、macOS 或 Windows 机器同样可以运行 Session。详见[沙箱提供商协议](https://openagentcore.dev/zh/docs/sandbox-provider)。
- **部件可替换。** 沙箱提供商、工作区文件系统、Harness 和模型提供商都通过[既定协议](AGENTS.md#protocols-at-every-boundary)接入。

![部署概览](docs/assets/console-overview-zh.webp)
Expand Down
3 changes: 3 additions & 0 deletions apps/web/e2e/fixture-console.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const LOCKOUT_SECONDS = 30;

let state;
const SOURCE_COMMIT = "c0ffee".padEnd(40, "0");
// microsandbox runs every sandbox on workspace storage, so the installation has it selected.
const WORKSPACE_STORAGE = { id: "5d0c2a4e-8f6b-4c1d-9e3a-7b2f1a0c6d48", adapter: "nfs", parameters: { root: "/srv/oac-workspaces", namespace_id: "22222222-2222-4222-8222-222222222222", uid: 65532 }, tenant_capacity_mib: 0 };

/**
* config.json's public_url. "public": an HTTPS address, so applications get an API base URL;
Expand Down Expand Up @@ -259,6 +261,7 @@ async function adminWrite(request, response, path) {
function adminRead(response, path, url) {
const a = state.admin;
if (path === "/installation") return send(response, 200, installation());
if (path === "/workspace-storage") return send(response, 200, WORKSPACE_STORAGE);
if (path === "/projects") return send(response, 200, { data: a.projects.map(a.publicProject), has_more: false });
if (path === "/summary") return send(response, 200, a.summary(url));
if (path === "/request-logs") return send(response, 200, requestLog(url));
Expand Down
4 changes: 2 additions & 2 deletions apps/web/e2e/sandbox-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ test("shows each node's retained limit and elapsed phase time without inventing
await page.route("**/core/v1/sandbox/deployment", async (route) => {
const response = await route.fetch();
const deployment = await response.json();
await route.fulfill({ response, json: { ...deployment, provider: "microsandbox", suspension: { idle_seconds: 300, retention_seconds: 86400 }, specification: { ...deployment.specification, resources: { ...deployment.specification.resources, root_disk_mib: 8192, environment_disk_mib: 8192 } } } });
await route.fulfill({ response, json: { ...deployment, provider: "microsandbox", suspension: { idle_seconds: 300, retention_seconds: 86400 }, specification: { ...deployment.specification, resources: { ...deployment.specification.resources, root_disk_mib: 8192 } } } });
});
await page.route("**/core/v1/sandbox/nodes", async (route) => {
const response = await route.fetch();
Expand Down Expand Up @@ -50,7 +50,7 @@ test("edits deployment idle time without changing generation", async ({ page, re
page.on("request", (sent) => { if (new URL(sent.url()).pathname.startsWith("/core/v1/sandbox/nodes")) nodeReads.push(sent.url()); });
await openConsole(page, request, "settings?id=sandbox");
const deployment = await setDeployment(request, { suspension: { idle_seconds: 300, retention_seconds: 86400 } });
await setDeployment(request, { provider: "microsandbox", specification: { ...deployment.specification!, resources: { ...deployment.specification!.resources, root_disk_mib: 8192, environment_disk_mib: 8192 } } });
await setDeployment(request, { provider: "microsandbox", specification: { ...deployment.specification!, resources: { ...deployment.specification!.resources, root_disk_mib: 8192 } } });
await page.getByRole("button", { name: "Refresh sandbox state" }).click();
const area = page.getByRole("region", { name: "Automatic suspension" });
const idle = area.getByLabel("Idle before suspension (seconds)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function SandboxDeploymentSettings({ deployment, disabled, fresh, onUpdat
</header>
<div className="settings-rows">
<Fact label={t("Provider")}>{sandboxProviderLabel(deployment.provider, locale)}</Fact>
<Fact label={t("Each sandbox")} help={spec?.workspace && !spec.workspace.capacity_quota ? t("Workspace storage is configured by the operator; no capacity quota is enforced.") : undefined}>{size ? `${sizeLabel(size)}${size.root_disk_mib ? ` · ${spec?.workspace ? t("Root disk {{root}} · external workspace", { root: formatBytes(size.root_disk_mib * MIB) }) : t("Root disk {{root}} · data disk {{data}}", { root: formatBytes(size.root_disk_mib * MIB), data: formatBytes((size.environment_disk_mib ?? 0) * MIB) })}` : ""}` : MISSING}</Fact>
<Fact label={t("Each sandbox")} help={spec?.workspace && !spec.workspace.capacity_quota ? t("Workspace storage is configured by the operator; no capacity quota is enforced.") : undefined}>{size ? `${sizeLabel(size)}${size.root_disk_mib ? ` · ${t("Root disk {{root}} · external workspace", { root: formatBytes(size.root_disk_mib * MIB) })}` : ""}` : MISSING}</Fact>
{deployment.provider === "e2b" ? <>
<Fact label={t("Sandbox API URL")}><code className="system-code">{deployment.configuration?.api_url}</code></Fact>
<Fact label={t("Sandbox data-plane domain")}><code className="system-code">{deployment.configuration?.domain}</code></Fact>
Expand Down
27 changes: 14 additions & 13 deletions apps/web/src/features/sandbox/SandboxSetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,21 @@ export function validEndpoint(apiURL: string, domain: string): boolean {
* double of it, disks included where the default declares them. A Provider
* whose configuration selects the size declares none and has no presets.
*/
function presets(provider: SandboxProvider, workspace?: SandboxSpecification["workspace"]): Record<Preset, SandboxResources> | null {
const standard = defaultSandboxResources(provider, workspace);
function presets(provider: SandboxProvider): Record<Preset, SandboxResources> | null {
const standard = defaultSandboxResources(provider);
if (!standard) return null;
const scale = (factor: number): SandboxResources => ({
cpus: Math.max(1, standard.cpus * factor),
memory_mib: standard.memory_mib * factor,
...(standard.root_disk_mib ? { root_disk_mib: standard.root_disk_mib * factor, environment_disk_mib: standard.environment_disk_mib! * factor } : {}),
...(standard.root_disk_mib ? { root_disk_mib: standard.root_disk_mib * factor } : {}),
});
return { small: scale(0.5), standard, large: scale(2) };
}

function presetOf(provider: SandboxProvider, resources: SandboxResources, workspace?: SandboxSpecification["workspace"]): Preset | null {
function presetOf(provider: SandboxProvider, resources: SandboxResources): Preset | null {
const same = (a: SandboxResources, b: SandboxResources) => a.cpus === b.cpus && a.memory_mib === b.memory_mib
&& (a.root_disk_mib ?? 0) === (b.root_disk_mib ?? 0) && (a.environment_disk_mib ?? 0) === (b.environment_disk_mib ?? 0);
const all = presets(provider, workspace);
const all = presets(provider);
return all ? (Object.keys(all) as Preset[]).find((key) => same(all[key], resources)) ?? null : null;
}

Expand Down Expand Up @@ -128,7 +128,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
const workspace = saved?.workspace ?? (provider ? selectedWorkspace(provider, storage) : undefined);
// Choosing a Provider that declares a default size replaces this placeholder.
const [resources, setResources] = useState<SandboxResources>(current?.specification?.resources ?? { cpus: 0, memory_mib: 0 });
const [size, setSize] = useState<Size>(current?.specification ? presetOf(current.provider, current.specification.resources, current.specification.workspace) ?? "current" : "standard");
const [size, setSize] = useState<Size>(current?.specification ? presetOf(current.provider, current.specification.resources) ?? "current" : "standard");
const [apiKey, setApiKey] = useState("");
const [replacementRequested, setReplacementRequested] = useState(false);
const [template, setTemplate] = useState(current?.e2bTemplate ?? "");
Expand Down Expand Up @@ -204,9 +204,9 @@ 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, selectedWorkspace(next, storage));
const proposed = kept?.resources ?? defaultSandboxResources(next);
if (proposed) setResources(proposed);
setSize(kept ? presetOf(next, kept.resources, kept.workspace) ?? "current" : "standard");
setSize(kept ? presetOf(next, kept.resources) ?? "current" : "standard");
}
setProvider(next);
}
Expand Down Expand Up @@ -248,7 +248,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) => 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) => t("Root disk {{root}} · external workspace", { root: formatBytes((value.root_disk_mib ?? 0) * MIB) });

let page: ReactNode;
if (step === "where") {
Expand All @@ -262,7 +262,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
);
} else if (step === "backend") {
page = (
<Question title={t("Which sandbox backend?")} help={t("microsandbox, the recommended default, runs each sandbox as a lightweight virtual machine: stronger isolation and its own root and data disks with size limits, but the host needs KVM. Docker 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.")}>
<Question title={t("Which sandbox backend?")} help={t("microsandbox, the recommended default, 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. Docker 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.")}>
<div className="wizard-choices">
<Choice icon={Cpu} title="microsandbox" badge={t("Recommended")} selected={provider === "microsandbox"} onClick={() => { choose("microsandbox"); setStep("size"); }} />
<Choice icon={Box} title="Docker" selected={provider === "docker"} onClick={() => { if (dockerConfirmed) selectDocker(); else setConfirmingDocker(true); }} />
Expand Down Expand Up @@ -326,9 +326,9 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
</Question>
);
} else if (step === "size") {
const options = provider ? presets(provider, workspace) : null;
const options = provider ? presets(provider) : 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 kept = saved && provider && presetOf(provider, saved.resources) === null ? saved.resources : null;
const disks = (value: SandboxResources) => (policy?.disk ? diskLabel(value) : undefined);
page = (
<Question title={t("How big is each sandbox?")} help={t("These limits apply to the selected configuration generation. Existing sandboxes keep their limits. Concurrency is set per node.")}>
Expand All @@ -345,6 +345,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
/>
))}
</div>
{policy && "workspace" in policy && !workspace ? <p>{t("This backend runs every sandbox on workspace storage. Select it under Settings, Workspace storage first.")}</p> : null}
<button className="wizard-link" type="button" onClick={() => { setSize("custom"); setStep("advanced"); }}>
<SlidersHorizontal size={14} aria-hidden="true" />{t("Custom size in advanced settings")}
</button>
Expand Down Expand Up @@ -407,7 +408,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
<NumberField error={fieldError("resources.memory_mib")} id={`${id}-memory`} label={t("Memory (MiB)")} value={resources.memory_mib} onChange={(memory_mib) => { setSize("custom"); setResources({ ...resources, memory_mib }); setFieldRejection(null); }} />
{policy?.disk ? <>
<NumberField error={fieldError("resources.root_disk_mib")} id={`${id}-root`} label={t("Root disk (MiB)")} value={resources.root_disk_mib ?? 0} onChange={(root_disk_mib) => { setResources({ ...resources, root_disk_mib }); setFieldRejection(null); }} />
{workspace && !workspace.capacity_quota ? <p>{t("Workspace storage is configured by the operator; no capacity quota is enforced.")}</p> : <NumberField error={fieldError("resources.environment_disk_mib")} id={`${id}-data`} label={t("Data disk at /environment (MiB)")} value={resources.environment_disk_mib ?? 0} onChange={(environment_disk_mib) => { setResources({ ...resources, environment_disk_mib }); setFieldRejection(null); }} />}
{workspace?.capacity_quota ? <NumberField error={fieldError("resources.environment_disk_mib")} id={`${id}-data`} label={t("Capacity quota at /environment (MiB)")} value={resources.environment_disk_mib ?? 0} onChange={(environment_disk_mib) => { setResources({ ...resources, environment_disk_mib }); setFieldRejection(null); }} /> : workspace ? <p>{t("Workspace storage is configured by the operator; no capacity quota is enforced.")}</p> : null}
</> : null}
</div>
</fieldset> : null}
Expand Down
23 changes: 9 additions & 14 deletions apps/web/src/features/sandbox/deployment-specification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("deployment resources", () => {
expect(resources).toEqual(declared);
if (resources) {
expect(resources).not.toBe(declared);
expect(validSandboxResources(provider, resources)).toBe(true);
expect(validSandboxResources(provider, resources, selectedWorkspace(provider, { tenant_capacity_mib: 0 }))).toBe(true);
}
}
});
Expand All @@ -28,12 +28,12 @@ describe("deployment resources", () => {
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: 2048, root_disk_mib: 1024 })).toBe(false);
expect(validSandboxResources("microsandbox", { cpus: 2, memory_mib: 2048, root_disk_mib: 1023, environment_disk_mib: 8192 })).toBe(false);
expect(validSandboxResources("microsandbox", { cpus: 2, memory_mib: 2048, root_disk_mib: 1023 }, { ...deploymentContract.providers.microsandbox.workspace, capacity_quota: false })).toBe(false);
});
it("validates external storage without fabricating a disk quota or weakening root disk bounds", () => {
it("runs microsandbox only on selected storage, where the environment disk is a quota request", () => {
const workspace = { ...deploymentContract.providers.microsandbox.workspace, capacity_quota: false };
const resources = defaultSandboxResources("microsandbox", workspace)!;
expect(resources.environment_disk_mib).toBe(0);
const resources = defaultSandboxResources("microsandbox")!;
expect(resources.environment_disk_mib).toBeUndefined();
expect(validSandboxResources("microsandbox", resources, workspace)).toBe(true);
expect(validSandboxResources("microsandbox", resources)).toBe(false);
expect(validSandboxResources("microsandbox", { ...resources, root_disk_mib: 0 }, workspace)).toBe(false);
Expand All @@ -42,15 +42,10 @@ describe("deployment resources", () => {
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();
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();
});
});
Loading
Loading