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
31 changes: 29 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,33 @@ does not qualify its isolation or enable public creation.

### Hosted sandbox nodes and optional suspension

Default installation includes Core, Web and PostgreSQL but no execution node.
It always creates a separate deployment administrator credential. Core receives
only its digest; the paired console server receives the private token and injects
it only on approved management routes after console login and same-origin checks.
The browser never receives that token. Node/daemon transport routes instead
forward their own credentials unchanged to Core. Zero-node Core receives neither the Docker socket nor KVM.
The Web's first setup selects one provider and public Core origin through the
admin-only deployment endpoint. The paired console serves only an explicit list
of non-secret matched distribution artifacts for its node installation command;
never serve private installation files or arbitrary paths. Node installation
reuses the existing node process and Provider configuration, verifies downloaded
files, retains private identity and uses a user service. It performs no SSH
installation, Session creation or model call. PostgreSQL owns this immutable selection under
the existing execution lease and deployment lock. Exact retries are idempotent;
a changed selection conflicts. No provider migration or hot reload is implied.

Web-managed startup claims the stable installation identity and a new owner epoch
even before provider selection. The existing runtime manager stays present and
loads one immutable configuration when selection becomes available, before any
node lifecycle is created. Admission refuses uninitialized hosted work without
creating Session state. File-managed and Web-managed configuration are mutually
exclusive. Node registration, observation and daemon bootstrap reuse existing
contracts. Derive Runtime bootstrap and daemon WebSocket addresses from the saved
validated origin; never infer them from inbound Host headers. Keep the startup
configuration API a startup snapshot; use the live deployment endpoint in setup.


A Core deployment may run without a sandbox provider. When enabled, exactly one
sandbox provider is selected at setup: Docker or microsandbox. Keep both adapters but reject multiple provider entries,
legacy default-provider maps and engine-based placement. Harness selection is
Expand All @@ -618,8 +645,8 @@ migrate an existing Session to another provider or recreate a released allocatio
Fresh adoption of a deployment with unverified retained allocations fails closed.

The [Hosted Sandbox Manager](services/agents-api/HOSTED-SANDBOX-MANAGER.md) is a
deployment-level admin surface, separate from project credentials. Its Web token
stays in memory. Node enrollment credentials authorize only registration; durable
deployment-level admin surface, separate from project credentials. A direct-Core
Web token stays in memory; the paired console token stays on its server. Node enrollment credentials authorize only registration; durable
node credentials authorize only node transport. Project keys can read a narrow
node directory and their own Session placement, never global allocations.

Expand Down
23 changes: 16 additions & 7 deletions apps/web/e2e/agents-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3030,17 +3030,26 @@ test("publishes Dashboard counts only after every top-level Agent and Session pa
});
});

await openSessionsFromHome(page);
await page.getByRole("button", { name: "Dashboard", exact: true }).click();
await page.goto("/");
const dashboard = page.locator(".dashboard-page");
// The synthetic second Session has no Runtime observation. Let that initial
// snapshot finish before navigation, so requests cannot be aborted mid-chain.
await expect(dashboard.locator(".dashboard-source-badge").filter({ hasText: "Runtime" })).toContainText("Unavailable");
expect(sessionAfters).toEqual([null, "session_snapshot", null, "session_snapshot"]);
await page.getByRole("button", { name: "Sessions", exact: true }).click();
await expect.poll(() => sessionAfters.length).toBe(6);
await page.getByRole("button", { name: "Dashboard", exact: true }).click();
await expect(dashboard.locator(".dashboard-summary > div").filter({ hasText: "Agents" })).toContainText("3");
await expect(dashboard.locator(".dashboard-summary > div").filter({ hasText: "Sessions" })).toContainText("2");
expect(agentAfters).toEqual([null, "agent_b"]);
// Session collection loads once for the page and once per Runtime snapshot.
// Returning to Dashboard refreshes Runtime immediately instead of waiting 30 seconds.
await expect.poll(() => sessionAfters.length).toBe(6);
expect(sessionAfters.filter((after) => after === null)).toHaveLength(3);
expect(sessionAfters.filter((after) => after === "session_snapshot")).toHaveLength(3);
// Session collection loads once; the unavailable Runtime snapshot is retried
// on entry to Sessions and again on return to Dashboard. Each reads both pages.
await expect.poll(() => sessionAfters).toEqual([
null, "session_snapshot",
null, "session_snapshot",
null, "session_snapshot",
null, "session_snapshot",
]);
});

test("keeps the previous Dashboard result when pagination exceeds the safety limit", async ({ page, request }) => {
Expand Down
24 changes: 21 additions & 3 deletions apps/web/e2e/fixture-sandbox.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ let nodes = [];
let calls = [];
let provider = "docker";
let diagnostic = "";
let coreUrl = "";
export function resetSandboxFixture() {
nodes = [node("node-local", "Core server"), node("node-offline", "Offline host", false)]; calls = []; provider = "docker"; diagnostic = "";
nodes = [node("node-local", "Core server"), node("node-offline", "Offline host", false)]; calls = []; provider = "docker"; diagnostic = ""; coreUrl = "";
}
resetSandboxFixture();
export function handleSandboxFixture(request, response, url, sendJson, sendError) {
Expand All @@ -21,8 +22,10 @@ export function handleSandboxFixture(request, response, url, sendJson, sendError
nodes = nodes.map((entry) => entry.id === "node-local" ? { ...entry, online: value !== "node_unavailable", provider_ready: value !== "provider_unavailable", diagnostic: value === "provider_unavailable" ? value : "" } : entry);
sendJson(response, {}); return true;
}
if (path === "/__fixture/sandbox") { sendJson(response, { nodes, calls }); return true; }
if (path === "/__fixture/sandbox") { sendJson(response, { nodes, calls, provider, core_url: coreUrl }); return true; }
if (path === "/__fixture/sandbox-microsandbox") { provider = "microsandbox"; sendJson(response, {}); return true; }
if (path === "/__fixture/sandbox-uninitialized") { provider = ""; coreUrl = ""; nodes = []; sendJson(response, {}); return true; }
if (path === "/__fixture/sandbox-add-node") { nodes.push({ ...node("node-enrolled", "Enrolled host"), provider }); sendJson(response, {}); return true; }
const projectRoute = path === "/v1/sandbox/nodes" || /^\/v1\/agents\/sessions\/[^/]+\/sandbox-placement$/.test(path);
if (projectRoute && request.headers["openai-beta"] !== "agents=v1") {
sendError(response, 400, "OpenAI-Beta: agents=v1 is required.", "invalid_beta"); return true;
Expand All @@ -34,7 +37,22 @@ export function handleSandboxFixture(request, response, url, sendJson, sendError
if (!path.startsWith("/core/v1/sandbox/")) return false;
calls.push({ path, method: request.method, authorized: request.headers.authorization === "Bearer fixture-admin-key" });
if (request.headers.authorization !== "Bearer fixture-admin-key") { sendError(response, 401, "A deployment admin key is required.", "invalid_admin_key"); return true; }
if (path.endsWith("/deployment")) sendJson(response, { installation_id: "fixture-installation", provider, maintenance: false, owner_epoch: 1 });
const deployment = () => ({ installation_id: "fixture-installation", provider, core_url: coreUrl, maintenance: false, owner_epoch: 1 });
if (path.endsWith("/deployment") && request.method === "POST") {
let body = "";
request.on("data", (chunk) => { body += chunk; });
request.on("end", () => {
try {
const input = JSON.parse(body);
if (provider && (provider !== input.provider || coreUrl !== input.core_url)) {
sendError(response, 409, "Sandbox deployment is already configured.", "sandbox_deployment_conflict"); return;
}
provider = input.provider; coreUrl = input.core_url;
sendJson(response, deployment());
} catch { sendError(response, 400, "Invalid setup request."); }
});
}
else if (path.endsWith("/deployment")) sendJson(response, deployment());
else if (path.endsWith("/enrollment-tokens")) sendJson(response, { token: "fixture-once-token", expires_at: "2026-09-23T09:00:00Z" });
else if (path.endsWith("/allocations")) sendJson(response, { data: path.includes("node-local") ? [{ id: "allocation-1", node_id: "node-local", session_id: "session_snapshot", tenant_id: "fixture-project", environment_id: "environment-1", state: "active", compute_phase: "running", initialization: "ready", diagnostic, created_at: now }] : [] });
else if (request.method === "DELETE") {
Expand Down
Loading
Loading