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
175 changes: 155 additions & 20 deletions CONTRIBUTING.md

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,24 @@ check-database:
sqlc-generate:
cd services/agents-api && $(SQLC) generate

SWAG ?= go run github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION)

.PHONY: openapi
openapi:
@set -e; root="$${PARSAR_HOME:-$$HOME/.parsar}/build"; mkdir -p "$$root"; \
output=$$(mktemp -d "$$root/core-openapi.XXXXXX"); trap 'rm -rf "$$output"' EXIT; \
go run github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION) init \
$(SWAG) init \
-g cmd/server/main.go --dir ./services/agents-api,./contracts/agents-api/v1 \
--output "$$output" \
--outputTypes yaml --parseInternal; \
python3 scripts/patch-agents-openapi.py "$$output/swagger.yaml"; \
mv "$$output/swagger.yaml" contracts/agents-api/openapi.yaml
go run ./scripts/openapi-split "$$output/swagger.yaml" contracts/agents-api/openapi.yaml contracts/agents-api/sandbox-manager.openapi.yaml

check-sqlc:
python3 scripts/check-sqlc.py

check-go:
go test ./apps/parsar-daemon/... ./internal/... ./contracts/agents-api/... -count=1
go test ./apps/parsar-daemon/... ./internal/... ./contracts/agents-api/... ./scripts/openapi-split -count=1

build-daemon:
@set -e; output="$${PARSAR_HOME:-$$HOME/.parsar}/build/daemon"; \
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ enable microsandbox or Docker explicitly when installing. With a provider enable
Core creates each required sandbox from the colocated Runtime image. Model
credentials are supplied through the existing write-only API extension.

Hosted deployments use one selected provider across local or remote nodes. The
Hosted Sandbox Manager shows node health, capacity and Session placement. New
Sessions use automatic placement by default or an explicitly selected node;
existing Sessions retain their node across disconnects and resume.

## Start here

- [Install Core and Web](docs/getting-started/install.md)
- [Make your first API request](docs/getting-started/quickstart.md)
- [Service health, data and operations](docs/getting-started/operations.md)
- [Hosted Sandbox Manager](services/agents-api/HOSTED-SANDBOX-MANAGER.md)
- [Protocol coverage and native differences](contracts/agents-api/README.md)
- [Add or select a harness](contracts/agents-api/harness-selection.md)
- [Public landing page source](site/index.html)
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/agents-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1776,7 +1776,7 @@ test("streams initial Session creation, captures early events, then hands off to

for (const failure of [
{ label: "response loss", control: { sessionCreateResponseLoss: 1 }, message: "Agent core request failed (502)." },
{ label: "creation-stream EOF before identity", control: { sessionCreateStreamMissingIdentity: 1 }, message: "Agent core returned an empty event stream." },
{ label: "creation-stream EOF before identity", control: { sessionCreateStreamMissingIdentity: 1 }, message: "Agent Core already recorded this Session creation, so its stream sends no events." },
]) {
test(`keeps one Session create attempt across ${failure.label} and an unchanged manual retry`, async ({ page, request }) => {
await openAgents(page, request);
Expand Down
4 changes: 4 additions & 0 deletions apps/web/e2e/fixture-core.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import http from "node:http";
import { handleSandboxFixture, resetSandboxFixture } from "./fixture-sandbox.mjs";

const host = "127.0.0.1";
const port = Number(process.env.AGENTS_FIXTURE_PORT ?? 18092);
Expand Down Expand Up @@ -759,13 +760,16 @@ const server = http.createServer(async (request, response) => {
try {
const url = new URL(request.url ?? "/", `http://${host}:${port}`);

if (handleSandboxFixture(request, response, url, sendJson, sendError)) return;

if (request.method === "GET" && url.pathname === "/__fixture/health") {
return sendJson(response, { ready: true });
}
if (request.method === "POST" && url.pathname === "/__fixture/reset") {
for (const stream of streamResponses.keys()) stream.end();
streamResponses.clear();
state = initialState();
resetSandboxFixture();
return sendJson(response, { reset: true });
}
if (request.method === "POST" && url.pathname === "/__fixture/control") {
Expand Down
47 changes: 47 additions & 0 deletions apps/web/e2e/fixture-sandbox.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const now = "2026-09-23T08:00:00Z";
const node = (id, name, online = true) => ({ id, name, provider: "docker", online, provider_ready: online, diagnostic: "",
last_seen_at: now, max_active: 4, max_retained: 16, active: id === "node-local" ? 1 : 0, reserved: 0,
retained: 0, cleanup_pending: 0, created_at: now, running: id === "node-local" ? 1 : 0, snapshots: 0,
cpu_count: online ? 8 : null, available_memory_bytes: online ? 8589934592 : null, available_disk_bytes: online ? 34359738368 : null,
});
let nodes = [];
let calls = [];
let provider = "docker";
let diagnostic = "";
export function resetSandboxFixture() {
nodes = [node("node-local", "Core server"), node("node-offline", "Offline host", false)]; calls = []; provider = "docker"; diagnostic = "";
}
resetSandboxFixture();
export function handleSandboxFixture(request, response, url, sendJson, sendError) {
const path = url.pathname;
if (path === "/__fixture/sandbox-diagnostic") {
const value = url.searchParams.get("value") ?? "";
if (!["", "node_unavailable", "resource_missing", "compute_unconfirmed", "ownership_mismatch", "provider_unavailable"].includes(value)) { sendError(response, 400, "Unknown diagnostic"); return true; }
diagnostic = value;
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-microsandbox") { provider = "microsandbox"; 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;
}
if (path === "/v1/sandbox/nodes") { sendJson(response, { data: nodes.map(({ id, name, online }) => ({ id, name, available: online })) }); return true; }
if (/^\/v1\/agents\/sessions\/[^/]+\/sandbox-placement$/.test(path)) {
sendJson(response, { node_id: "node-local", node_name: "Core server", available: !diagnostic, state: "active", compute_phase: "running", diagnostic }); return true;
}
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 });
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") {
const id = path.split("/").at(-1);
if (id === "node-local") sendError(response, 409, "Node has active allocations or retained resources.", "runtime_node_in_use");
else { nodes = nodes.filter((entry) => entry.id !== id); sendJson(response, { id, deleted: true }); }
} else if (path.endsWith("/nodes")) sendJson(response, { data: nodes });
else sendError(response, 404, "Unknown sandbox fixture route.");
return true;
}
152 changes: 152 additions & 0 deletions apps/web/e2e/sandbox-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { expect, test, type Page } from "@playwright/test";
const fixture = `http://127.0.0.1:${process.env.AGENTS_FIXTURE_PORT ?? 18092}`;
async function connectAdmin(page: Page) {
await page.getByRole("button", { name: "Hosted Sandbox Manager", exact: true }).click();
await page.getByLabel("Deployment admin key").fill("fixture-admin-key");
await page.getByRole("button", { name: "Connect admin", exact: true }).click();
await expect(page.getByRole("heading", { name: "Nodes", exact: true })).toBeVisible();
}
test.beforeEach(async ({ page, request }) => {
await request.post(`${fixture}/__fixture/reset`);
await page.goto("/");
await expect(page.getByRole("button", { name: "Sessions", exact: true })).toBeVisible();
});
test("admin access, node health, guarded removal and enrollment remain separate from project credentials", async ({ page, request }) => {
await page.getByRole("button", { name: "Hosted Sandbox Manager", exact: true }).click();
expect((await (await request.get(`${fixture}/__fixture/sandbox`)).json()).calls).toHaveLength(0);
await page.getByLabel("Deployment admin key").fill("project-key");
await page.getByRole("button", { name: "Connect admin", exact: true }).click();
await expect(page.getByRole("alert")).toContainText("deployment admin key is required");
await page.getByRole("button", { name: "Disconnect admin" }).click();
await connectAdmin(page);
await expect(page.getByRole("region", { name: "Sandbox nodes", exact: true })).toContainText("Provider ready");
await expect(page.getByRole("region", { name: "Sandbox nodes", exact: true })).toContainText("Host metrics unavailable");
await expect(page.getByRole("region", { name: "Sandbox allocations", exact: true })).toContainText("session_snapshot");
await page.getByRole("button", { name: "Remove Core server", exact: true }).click();
await page.getByRole("button", { name: "Confirm removal" }).click();
await expect(page.getByRole("alert")).toContainText("active allocations or retained resources");
await expect(page.getByRole("button", { name: "Remove Core server", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Cancel removal" }).click();
await page.getByRole("button", { name: "Remove Offline host", exact: true }).click();
await page.getByRole("button", { name: "Confirm removal" }).click();
await expect(page.getByRole("button", { name: "Remove Offline host", exact: true })).toHaveCount(0);
await page.getByLabel("Core URL reachable from the node").fill("https://core.example");
await page.getByRole("button", { name: "Generate enrollment command" }).click();
await expect(page.getByLabel("One-time enrollment command")).toHaveValue(/fixture-once-token/);
await expect(page.getByLabel("One-time enrollment command")).toHaveValue(/--enrollment-token-file/);
const storage = await page.evaluate(() => JSON.stringify({ local: { ...localStorage }, session: { ...sessionStorage } }));
expect(storage).not.toContain("fixture-admin-key"); expect(storage).not.toContain("fixture-once-token");
expect(page.url()).not.toContain("fixture-admin-key");
await page.reload();
await expect(page.getByLabel("Deployment admin key")).toHaveValue("");
await expect(page.getByLabel("One-time enrollment command")).toHaveCount(0);
});
test("microsandbox shares the manager and mobile tables stay contained", async ({ page, request }) => {
await request.post(`${fixture}/__fixture/sandbox-microsandbox`);
await page.setViewportSize({ width: 390, height: 844 });
await connectAdmin(page);
await expect(page.locator(".sandbox-summary")).toContainText("microsandbox");
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
const table = page.getByRole("region", { name: "Sandbox nodes", exact: true });
await expect(table).toBeVisible();
const bounds = await table.boundingBox();
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(390);
await expect(page.getByRole("button", { name: "Disconnect admin" })).toBeInViewport();
await page.getByLabel("Core URL reachable from the node").scrollIntoViewIfNeeded();
await expect(page.getByLabel("Core URL reachable from the node")).toBeInViewport();
});
test("hosted creation defaults to automatic and an explicit unavailable node is never replaced", async ({ page }) => {
await page.getByRole("button", { name: "Sessions", exact: true }).click();
await page.getByRole("button", { name: "New Session", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "Create a Session" });
await dialog.getByLabel("Saved Agent", { exact: true }).selectOption("agent_b");
await dialog.getByRole("radio", { name: /Managed hosted/ }).check();
const advanced = dialog.getByRole("button", { name: /Advanced settings/ });
if (await advanced.getAttribute("aria-expanded") !== "true") await advanced.click();
await expect(dialog.getByLabel("Sandbox node", { exact: true })).toHaveValue("");
await dialog.getByLabel("Sandbox node", { exact: true }).selectOption("node-local");
const creates: Array<Record<string, unknown>> = [];
await page.route("**/v1/agents/sessions", async (route) => {
if (route.request().method() !== "POST") return route.continue();
creates.push(route.request().postDataJSON());
await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: { code: "runtime_node_unavailable", message: "Selected sandbox node is unavailable.", type: "server_error" } }) });
});
await dialog.getByRole("button", { name: "Create Session", exact: true }).click();
await expect(dialog.getByRole("alert")).toContainText("Selected sandbox node is unavailable");
await expect(dialog.getByLabel("Sandbox node", { exact: true })).toHaveValue("node-local");
expect(creates).toHaveLength(1);
expect(creates[0]?.x_agents_core).toEqual({ sandbox_node_id: "node-local" });
});
test("Session details show the actual Core placement", async ({ page }) => {
await page.getByRole("button", { name: "Sessions", exact: true }).click();
await page.locator(".conversation-session-action").click();
const dialog = page.getByRole("dialog");
await expect(dialog).toContainText("Core server");
await expect(dialog).toContainText("node-local");
});
test("empty nodes and a failed refresh have distinct states", async ({ page }) => {
await page.route("**/core/v1/sandbox/nodes", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify({ data: [] }) }));
await connectAdmin(page);
await expect(page.getByText("No nodes registered. Add a node to provide hosted capacity.")).toBeVisible();
await expect(page.getByText("No sandbox allocations.")).toBeVisible();
await page.route("**/core/v1/sandbox/deployment", (route) => route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: { message: "Deployment unavailable." } }) }));
await page.getByRole("button", { name: "Refresh sandbox state" }).click();
await expect(page.getByRole("alert")).toContainText("Previously loaded state is shown below");
});
test("late placement reads cannot replace another Session's placement", async ({ page, request }) => {
const second = await request.post(`${fixture}/v1/agents/sessions`, {
headers: { "OpenAI-Beta": "agents=v1", "Idempotency-Key": "placement-second" },
data: { agent_id: "agent_b", environment: { type: "none" }, input: "Read placement", metadata: { title: "Placement second" }, stream: false },
});
expect(second.status()).toBe(201);
const secondId = (await second.json()).id;
let releaseOld: () => void = () => {};
const oldReleased = new Promise<void>((resolve) => { releaseOld = resolve; });
let oldRequested = false;
await page.route("**/v1/agents/sessions/*/sandbox-placement", async (route) => {
const isSecond = route.request().url().includes(secondId);
if (!isSecond) { oldRequested = true; await oldReleased; }
await route.fulfill({ contentType: "application/json", body: JSON.stringify({ node_id: isSecond ? "new-node" : "old-node", node_name: isSecond ? "Second placement" : "Old placement", available: true, state: "active", compute_phase: "running" }) }).catch(() => {});
});
await page.reload();
await page.getByRole("button", { name: "Sessions", exact: true }).click();
await page.locator('.session-row-action[aria-label="Manage Lifecycle Agent"]').click();
await expect.poll(() => oldRequested).toBe(true);
await page.getByRole("button", { name: "Close dialog", exact: true }).click();
await page.locator('.session-row-action[aria-label="Manage Placement second"]').click();
await expect(page.getByRole("dialog")).toContainText("Second placement");
releaseOld();
await expect(page.getByRole("dialog")).not.toContainText("Old placement");
});
test("disconnect diagnostics clear after reconnection in manager and Session details", async ({ page, request }) => {
await request.post(`${fixture}/__fixture/sandbox-diagnostic?value=node_unavailable`);
await connectAdmin(page);
await expect(page.getByRole("region", { name: "Sandbox allocations", exact: true })).toContainText("Node disconnected");
await expect(page.getByRole("region", { name: "Sandbox allocations", exact: true })).toContainText("Existing resources stay assigned");
await page.getByRole("button", { name: "Sessions", exact: true }).click();
await page.locator(".conversation-session-action").click();
const dialog = page.getByRole("dialog");
await expect(dialog).toContainText("Node disconnected");
await request.post(`${fixture}/__fixture/sandbox-diagnostic?value=`);
await dialog.getByRole("button", { name: "Refresh placement" }).click();
await expect(dialog).toContainText("Available · Recorded allocation");
await expect(dialog).not.toContainText("Node disconnected");
await page.getByRole("button", { name: "Close dialog", exact: true }).click();
await connectAdmin(page);
await expect(page.getByRole("region", { name: "Sandbox allocations", exact: true })).toContainText("No reported issue");
await expect(page.getByRole("region", { name: "Sandbox allocations", exact: true })).not.toContainText("Node disconnected");
});
test("a missing resource preserves ownership and offers inspection without replacement", async ({ page, request }) => {
await request.post(`${fixture}/__fixture/sandbox-diagnostic?value=resource_missing`);
await connectAdmin(page);
const allocations = page.getByRole("region", { name: "Sandbox allocations", exact: true });
await expect(allocations).toContainText("Sandbox resource missing");
await expect(allocations).toContainText("retains the ownership record");
await expect(allocations).toContainText("does not create a replacement automatically");
await page.getByRole("button", { name: "Sessions", exact: true }).click();
await page.locator(".conversation-session-action").click();
await expect(page.getByRole("dialog")).toContainText("Sandbox resource missing");
await expect(page.getByRole("dialog")).toContainText("Check the provider resource on the assigned node");
const requests = await (await request.get(`${fixture}/__fixture/requests`)).json();
expect(requests.filter((entry: { method: string; path: string }) => entry.method === "POST" && entry.path === "/v1/agents/sessions")).toHaveLength(0);
});
Loading
Loading