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
7 changes: 4 additions & 3 deletions apps/web/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,13 +594,14 @@ still frame, the request is a plain block and no transition runs.
Setting up hosted sandboxes, and changing the provider or resources in maintenance,
is a set of pages inside the Nodes page, one decision each: where sandboxes run (own machines
or E2B), then the backend (Docker or microsandbox, compared behind a help tip) or
the E2B account, then the size of each sandbox (three presets), then a review.
the E2B account, 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 review states where sandboxes run,
the size, the Runtime (taken from this console's distribution manifest) and the
Core address, asking for it only when the console's own address cannot serve.
Advanced settings, one link away, hold the complete form: resources, the Core
address, the Runtime release and the E2B template. A change keeps the saved size
Advanced settings, one link away, hold the complete form: resources (not for
E2B), the Core address, the Runtime release and the E2B template. A change keeps the saved size
and Runtime while the backend stays the same (a saved size outside the presets is
offered as Current); another backend starts from its standard size and this
console's Runtime, and E2B always needs its key again. Rules sit behind help tips.
Expand Down
12 changes: 8 additions & 4 deletions apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,20 @@ workbench.
and offer Delete.
- **Platform**: Projects and keys (projects, their assets and usage, named keys,
write history), Nodes (sandbox setup as pages — where sandboxes run, the
backend or E2B account, the size of each sandbox, a review, and advanced settings
backend or E2B account, the size of each sandbox (own machines only; E2B takes the
template build's), a review, and advanced settings
with the complete form — then the node list with each node's capacity, host
figures and allocations, enrollment, renaming, sandbox limits and removal; Add node
asks for the node's sandbox limits before it issues the one-time command), System (only the sandbox
configuration every project shares: where sandboxes run, each sandbox's size, the
Runtime or E2B template, the Core address and maintenance).
Runtime or E2B template build, microsandbox's idle suspension, the Core address and maintenance).
- A node whose provider is not ready names the reason (Docker unreachable, no Docker
limits, missing Runtime image, no KVM, missing microsandbox components, a host too
small) and its fix in the help tip beside its status, wherever that status shows.
- **E2B deployments** have no machines: the Nodes entry becomes Sandbox backend,
and Overview and Sandbox metrics show the sandboxes Core holds in E2B's cloud
(running, starting, size, template) instead of node capacity, with no node column
or Add node action.
(running, starting, size, template build) instead of node capacity, with no node column
or Add node action; a sandbox's dialog adds its disk use.
- **microsandbox** suspends idle sandboxes into snapshots, so its nodes show how
many sleep (Core's retained minus active) on the Nodes list, a node's page, Sandbox
metrics and Overview; Docker never suspends and shows none of it.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/data/routes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function buildDemo(now = Math.floor(Date.now() / 1000)) {
sessions.sort((a, b) => b.created_at - a.created_at);
const nodes = [
{ id: "node-local", name: "core-01", provider: "docker", online: true, provider_ready: true, diagnostic: "", 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, active: 5, reserved: 1, retained: 2, cleanup_pending: 0, created_at: new Date((now - 86400 * 30) * 1000).toISOString() },
{ id: "node-gpu", name: "gpu-worker-02", provider: "docker", online: true, provider_ready: true, diagnostic: "", 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, active: 7, reserved: 0, retained: 5, cleanup_pending: 1, created_at: new Date((now - 86400 * 12) * 1000).toISOString() },
{ id: "node-gpu", name: "gpu-worker-02", provider: "docker", online: true, provider_ready: false, diagnostic: "docker_limits_unsupported", 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, active: 7, reserved: 0, retained: 5, cleanup_pending: 1, created_at: new Date((now - 86400 * 12) * 1000).toISOString() },
{ id: "node-edge", name: "edge-03", provider: "docker", online: false, provider_ready: false, diagnostic: "", 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, 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");
Expand Down
24 changes: 18 additions & 6 deletions apps/web/e2e/fixture-console.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ const release = { source_commit: "c0ffee".padEnd(40, "0"), image_id: `sha256:${h
const manifest = { platform: "linux/amd64", source_commit: release.source_commit, images: { runtime: release.image_id }, image_manifest_digests: { runtime: release.image_manifest_digest }, runtime_ref: release.microsandbox_ref, microsandbox: { runtime_sha256: release.runtime_sha256, firmware_sha256: release.firmware_sha256 }, artifacts: {} };

function configuredDeployment() {
return { installation_id: "7f3c2a90-fixture", provider: "docker", core_url: `http://127.0.0.1:${port}`, maintenance: false, owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 }, runtime: release }, specification_digest: "fixture" };
return { installation_id: "7f3c2a90-fixture", provider: "docker", core_url: `http://127.0.0.1:${port}`, maintenance: false, owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 }, runtime: release }, specification_digest: "fixture", suspension: null };
}
/** 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", resources: { allocations: 3, pending: 1 }, specification: { resources: { cpus: 2, memory_mib: 2048 } }, e2b: { template: "parsar-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b", credential_configured: true } };
return { ...configuredDeployment(), provider: "e2b", mode: "direct", resources: { allocations: 3, pending: 1 }, specification: { resources: { cpus: 2, memory_mib: 2048 } }, e2b: { template: "parsar-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b", credential_configured: true, template_build: templateBuild } };
}

function reset(mode = "setup", fresh = false, sandbox = "configured") {
Expand Down Expand Up @@ -146,7 +148,9 @@ function adminRead(response, path, url) {
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 === "/runtime-observations") {
const data = a.runtimeObservations();
// Only E2B reports a sandbox's disk.
const e2b = state.deployment?.provider === "e2b";
const data = a.runtimeObservations().map((entry) => ({ ...entry, observation: { ...entry.observation, disk: e2b && entry.observation.status === "observed" ? { usage_bytes: 3 * 2 ** 30, limit_bytes: 10 * 2 ** 30 } : null } }));
return send(response, 200, { object: "list", data, has_more: false, first_id: data[0]?.observation.id ?? null, last_id: data.at(-1)?.observation.id ?? null });
}
if (path === "/core-metrics") return send(response, 200, coreMetrics(url.searchParams.get("range") ?? "1h"));
Expand Down Expand Up @@ -206,12 +210,20 @@ async function sandboxRoute(request, response, path) {
if (path === "/deployment" && request.method === "POST") {
const input = await body(request);
if (state.deployment) return error(response, 409, "The sandbox deployment is already configured.", "sandbox_deployment_conflict");
if (!input.resources || (input.provider !== "e2b" && !input.runtime)) return error(response, 400, "resources and runtime are required.", "invalid_sandbox_configuration");
state.deployment = { ...configuredDeployment(), provider: input.provider, core_url: input.core_url, mode: input.provider === "e2b" ? "direct" : "nodes", specification: { resources: input.resources, ...(input.runtime ? { runtime: input.runtime } : {}) } };
const e2b = input.provider === "e2b";
if (!e2b && (!input.resources || !input.runtime)) return error(response, 400, "resources and runtime are required.", "invalid_sandbox_configuration");
// As Core: E2B may omit resources and adopt its template build's CPU and memory; only microsandbox suspends.
const resources = input.resources ?? { cpus: templateBuild.resources.cpus, memory_mib: templateBuild.resources.memory_mib };
state.deployment = {
...configuredDeployment(), provider: input.provider, core_url: input.core_url, mode: e2b ? "direct" : "nodes",
specification: { resources, ...(input.runtime ? { runtime: input.runtime } : {}) },
...(e2b ? { e2b: { template: input.e2b?.template ?? "", credential_configured: true, template_build: templateBuild } } : {}),
suspension: input.provider === "microsandbox" ? { idle_seconds: 300, retention_seconds: 86400 } : null,
};
return send(response, 201, state.deployment);
}
if (path === "/deployment") {
return send(response, 200, state.deployment ?? { installation_id: "7f3c2a90-fixture", provider: "", core_url: `http://127.0.0.1:${port}`, maintenance: false, owner_epoch: 3, generation: 0, mode: "", resources: { allocations: 0, pending: 0 } });
return send(response, 200, state.deployment ?? { installation_id: "7f3c2a90-fixture", provider: "", core_url: `http://127.0.0.1:${port}`, maintenance: false, owner_epoch: 3, generation: 0, mode: "", resources: { allocations: 0, pending: 0 }, suspension: null });
}
if (path === "/nodes") return send(response, 200, { data: state.nodes });
if (path === "/enrollment-tokens" && request.method === "POST") {
Expand Down
4 changes: 4 additions & 0 deletions apps/web/e2e/monitoring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ test("shows the deployment's health on Overview and each monitor page", async ({
await expect(page.getByLabel("Agent run summary")).toContainText("Requests");
await page.getByRole("button", { name: "Sandbox metrics" }).click();
await expect(page.getByRole("table").first()).toContainText("core-01");
// A degraded node names why its provider is not ready.
await expect(page.getByRole("button", { name: "Docker limits unsupported" })).toBeVisible();
});

test("opens a Session's conversation from the Session log, read-only", async ({ page, request }) => {
Expand Down Expand Up @@ -50,6 +52,8 @@ test("opens a node and a sandbox in dialogs from Sandbox metrics", async ({ page
test("shows E2B's cloud instead of machines", async ({ page, request }) => {
await openConsole(page, request, "sandbox-metrics", { sandbox: "e2b" });
await expect(page.getByRole("heading", { name: "E2B cloud" })).toBeVisible();
// Each sandbox takes the template build's size, disk included.
await expect(page.getByText("10 GiB disk")).toBeVisible();
await expect(page.getByRole("button", { name: "Add node" })).toHaveCount(0);
await expect(page.getByRole("columnheader", { name: "Node", exact: true })).toHaveCount(0);

Expand Down
5 changes: 2 additions & 3 deletions apps/web/e2e/nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,17 @@ test("keeps the saved size and Runtime for the same backend, and starts another
expect(submitted?.resources).toEqual({ cpus: 2, memory_mib: 4096, root_disk_mib: 8192, environment_disk_mib: 8192 });
expect(submitted?.runtime).toMatchObject({ source_commit: "c0ffee".padEnd(40, "0") });

// E2B needs its key again and takes no Runtime or disks.
// E2B needs its key again and takes its size from the template build: no size, Runtime or disks.
await back.click();
await back.click();
await back.click();
await page.getByRole("button", { name: "E2B cloud" }).click();
await page.getByLabel("E2B API key").fill("fixture-private-key");
await page.getByLabel("Template build").fill("template:94be54a1-138c-4f30-bc87-b13686272dbe");
await page.getByRole("button", { name: "Next" }).click();
await page.getByRole("button", { name: /^Standard/ }).click();
await save.click();
await expect.poll(() => submitted?.provider).toBe("e2b");
expect(submitted?.resources).toEqual({ cpus: 2, memory_mib: 2048 });
expect(submitted?.resources).toBeUndefined();
expect(submitted?.runtime).toBeUndefined();
});

Expand Down
16 changes: 16 additions & 0 deletions apps/web/src/features/fleet/DiagnosticTip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useTranslation } from "react-i18next";

import { HelpTip } from "../../components/console-ui";
import { sandboxDiagnosticMessage } from "../../lib/sandbox-diagnostic";

/** The specific reason behind a node's status, in the help tip beside it: what is wrong, then how to fix it. */
export function DiagnosticTip({ code }: { code: string }) {
const { i18n } = useTranslation();
const message = sandboxDiagnosticMessage(code, i18n.resolvedLanguage?.startsWith("zh") ? "zh" : "en");
if (!message) return null;
return (
<HelpTip label={message.label}>
<strong className="help-tip-title">{message.label}</strong> {message.advice}
</HelpTip>
);
}
Loading