import { spawnSync } from "node:child_process"; import http from "node:http"; import net from "node:net"; import { resolveSupervisorToken } from "@rakazo/core"; import { describe, expect, it } from "vitest"; import { resolveDockerSocketPath, supervisorApp, waitForScreenReady } from "./index.js"; import { assertRequestIdentity, attemptComputerControl, ComputerControlUnavailableError, clearComputerScreenRegistry, completeReleasedScreen, computerControlTimeoutMs, containerActionStep, containerActionSteps, DOCKER_BROWSER_ALIASES, demuxDockerStream, ensureScreenCommand, hasComputerIdentity, hasValidBearerToken, interactiveScreenCommand, isComputerControlUnavailable, nextScreenIndex, normalizeWorkspaceRelative, parseObservation, preferComputerControl, releaseAssignedScreen, type ScreenAssignment, sandboxCommandTimedOut, sandboxTimeoutCommand, shouldReplayComputerActions, stopExtraScreenCommand, } from "./supervisor-logic.js"; const token = resolveSupervisorToken(process.env); describe("computer screen readiness", () => { it("waits for the server to actually answer HTTP requests before succeeding", async () => { const server = http.createServer((_req, res) => res.end("ok")); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("expected a TCP address"); try { await expect(waitForScreenReady("127.0.0.1", address.port, 2_000)).resolves.toBe(true); } finally { server.close(); } }); it("does not treat an open TCP port as ready when nothing is serving HTTP on it yet", async () => { // Regression test: a bare TCP accept (e.g. the Docker port mapping coming // up before websockify inside the container does) must not be mistaken // for the screen being ready — that gap is exactly what caused // "socket hang up" in the browser. const server = net.createServer((socket) => socket.destroy()); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("expected a TCP address"); try { await expect(waitForScreenReady("127.0.0.1", address.port, 700)).resolves.toBe(false); } finally { server.close(); } }); it("does not treat HTTP error responses as ready", async () => { const server = http.createServer((_req, res) => { res.statusCode = 503; res.end("starting"); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("expected a TCP address"); try { await expect(waitForScreenReady("127.0.0.1", address.port, 700)).resolves.toBe(false); } finally { server.close(); } }); it("does not treat redirect responses as ready", async () => { const server = http.createServer((_req, res) => { res.statusCode = 302; res.setHeader("Location", "/vnc.html"); res.end(); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("expected a TCP address"); try { await expect(waitForScreenReady("127.0.0.1", address.port, 700)).resolves.toBe(false); } finally { server.close(); } }); it("times out instead of hanging when nothing is listening", async () => { const server = net.createServer(); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("expected a TCP address"); const closedPort = address.port; await new Promise((resolve) => server.close(() => resolve())); await expect(waitForScreenReady("127.0.0.1", closedPort, 700)).resolves.toBe(false); }); }); describe("sandbox supervisor Docker endpoint", () => { it("respects Docker host and socket overrides before platform defaults", () => { expect(resolveDockerSocketPath({ DOCKER_HOST: "tcp://docker.test:2375" }, "win32")).toBe( undefined, ); expect(resolveDockerSocketPath({ DOCKER_SOCKET: "/tmp/docker.sock" }, "win32")).toBe( "/tmp/docker.sock", ); expect(resolveDockerSocketPath({}, "win32")).toBe("//./pipe/docker_engine"); expect(resolveDockerSocketPath({}, "linux")).toBe("/var/run/docker.sock"); }); }); describe("sandbox supervisor HTTP boundary", () => { it("keeps health public while every computer route requires the service token", async () => { const health = await supervisorApp.request("/health"); expect(health.status).toBe(200); await expect(health.json()).resolves.toMatchObject({ ok: true }); const protectedRequests: Array<[string, string]> = [ ["POST", "/computers"], ["GET", "/computers/id"], ["POST", "/computers/id/exec"], ["POST", "/computers/id/observe"], ["POST", "/computers/id/actions"], ["GET", "/computers/id/files"], ["POST", "/computers/id/files"], ["GET", "/computers/id/screen"], ["POST", "/computers/id/screen-mode"], ["DELETE", "/computers/id/screen"], ["POST", "/computers/id/input"], ["POST", "/computers/id/stop"], ["DELETE", "/computers/id"], ]; for (const [method, pathname] of protectedRequests) { const response = await supervisorApp.request(pathname, { method }); expect(response.status, `${method} ${pathname}`).toBe(401); await expect(response.json()).resolves.toEqual({ error: "unauthorized" }); } }); it("rejects malformed and incorrect bearer credentials", () => { expect(hasValidBearerToken(undefined, token)).toBe(false); expect(hasValidBearerToken(token, token)).toBe(false); expect(hasValidBearerToken("Basic credentials", token)).toBe(false); expect(hasValidBearerToken(`Bearer ${"x".repeat(token.length)}`, token)).toBe(false); expect(hasValidBearerToken(`Bearer ${token}`, token)).toBe(true); }); it("rejects a provision request whose identity headers do not match its body", async () => { const response = await supervisorApp.request("/computers", { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "x-rakazo-bot-id": "other-bot", "x-rakazo-space-id": "workspace", }, body: JSON.stringify({ botId: "bot", spaceId: "workspace", homePath: "/tmp/never-used", }), }); expect(response.status).toBe(500); await expect(response.json()).resolves.toEqual({ error: "computer identity mismatch" }); }); }); describe("sandbox supervisor input containment", () => { it("normalizes portable paths and rejects lexical traversal", () => { expect(normalizeWorkspaceRelative("/notes\\result.txt")).toBe("notes/result.txt"); expect(normalizeWorkspaceRelative("//notes///result.txt")).toBe("notes/result.txt"); expect(() => normalizeWorkspaceRelative("../outside")).toThrow(/escapes/); expect(() => normalizeWorkspaceRelative("notes/./result.txt")).toThrow(/escapes/); expect(() => normalizeWorkspaceRelative("notes/../outside")).toThrow(/escapes/); }); it("requires both bot and workspace identities to match", () => { expect(() => assertRequestIdentity("bot", "workspace", { botId: "bot", spaceId: "workspace" }), ).not.toThrow(); expect(() => assertRequestIdentity(undefined, "workspace", { botId: "bot", spaceId: "workspace" }), ).toThrow(/identity mismatch/); expect(() => assertRequestIdentity("bot", "other", { botId: "bot", spaceId: "workspace" }), ).toThrow(/identity mismatch/); }); it("accepts the legacy workspace label without weakening container identity", () => { expect( hasComputerIdentity({ "rakazo.botId": "bot", "rakazo.workspaceId": "space" }, "bot", "space"), ).toBe(true); expect( hasComputerIdentity( { "rakazo.botId": "bot", "rakazo.workspaceId": "other-space" }, "bot", "space", ), ).toBe(false); expect( hasComputerIdentity( { "rakazo.botId": "bot", "rakazo.spaceId": "space", "rakazo.workspaceId": "other-space", }, "bot", "space", ), ).toBe(true); }); it("bounds scroll and wait actions before sending them to the computer", () => { expect(containerActionStep({ kind: "wait", ms: 99_999 })).toEqual({ waitMs: 5_000 }); expect(containerActionStep({ kind: "wait", ms: -1 })).toEqual({ waitMs: 0 }); expect(containerActionStep({ kind: "scroll", direction: "up", amount: 99 })).toEqual({ argv: ["env", "DISPLAY=:1", "xdotool", "click", "--repeat", "20", "4"], }); }); it("sends Unicode text through the sandbox clipboard helper", () => { expect(containerActionStep({ kind: "clipboard", text: "中文輸入🙂" })).toEqual({ argv: ["env", "DISPLAY=:1", "rakazo-paste", "中文輸入🙂"], }); }); it("routes Docker browser aliases through the safe wrapper on every display", () => { for (const application of DOCKER_BROWSER_ALIASES) { expect( containerActionStep({ kind: "launch", application, uri: "https://example.com" }, ":2"), ).toEqual({ argv: ["env", "DISPLAY=:2", "rakazo-browser", "https://example.com"], }); } expect(containerActionStep({ kind: "launch", application: "xterm" }, ":3")).toEqual({ argv: ["env", "DISPLAY=:3", "rakazo-terminal"], }); expect(containerActionStep({ kind: "open", path: "https://example.com" }, ":3")).toEqual({ argv: ["env", "DISPLAY=:3", "xdg-open", "https://example.com"], }); }); it("routes mixed-case Docker browser aliases through the safe wrapper", () => { for (const application of ["Chrome", "Firefox", "Chromium", "Google-Chrome"]) { expect( containerActionStep({ kind: "launch", application, uri: "https://example.com" }, ":2"), ).toEqual({ argv: ["env", "DISPLAY=:2", "rakazo-browser", "https://example.com"], }); } expect(containerActionStep({ kind: "launch", application: "XTerm" }, ":3")).toEqual({ argv: ["env", "DISPLAY=:3", "rakazo-terminal"], }); }); it("keeps browser routing argv identical for control and Docker exec fallback", () => { const action = { kind: "launch" as const, application: "chromium", uri: "https://example.com" }; expect(containerActionSteps([action], ":2")).toEqual([ { argv: ["env", "DISPLAY=:2", "rakazo-browser", "https://example.com"] }, ]); }); it("falls back to docker-exec when computer control fails", async () => { await expect( preferComputerControl( async () => { throw new Error("connection refused"); }, async () => "docker-exec", ), ).resolves.toBe("docker-exec"); await expect(preferComputerControl(undefined, async () => "docker-exec")).resolves.toBe( "docker-exec", ); await expect( preferComputerControl( async () => "fast-path", async () => "docker-exec", ), ).resolves.toBe("fast-path"); }); it("replays actions only when control was never reached", async () => { await expect(attemptComputerControl(undefined)).resolves.toEqual({ status: "unavailable" }); await expect( attemptComputerControl(async () => { throw new ComputerControlUnavailableError("fetch failed"); }), ).resolves.toEqual({ status: "unavailable" }); await expect( attemptComputerControl(async () => { throw new Error("computer action failed"); }), ).resolves.toMatchObject({ status: "failed" }); const timeout = Object.assign(new Error("The operation was aborted due to timeout"), { name: "TimeoutError", }); await expect( attemptComputerControl(async () => { throw timeout; }), ).resolves.toMatchObject({ status: "failed" }); expect(isComputerControlUnavailable(new TypeError("fetch failed"))).toBe(false); expect( isComputerControlUnavailable( Object.assign(new TypeError("fetch failed"), { cause: new Error("connect ECONNREFUSED 127.0.0.1:7070"), }), ), ).toBe(true); expect( isComputerControlUnavailable( Object.assign(new TypeError("fetch failed"), { cause: new Error("connect ENETUNREACH 172.18.0.4:7070"), }), ), ).toBe(true); expect( isComputerControlUnavailable( Object.assign(new TypeError("fetch failed"), { cause: new Error("read ECONNRESET"), }), ), ).toBe(false); expect(isComputerControlUnavailable(timeout)).toBe(false); await expect(attemptComputerControl(async () => ({ completed: 2 }))).resolves.toEqual({ status: "ok", value: { completed: 2 }, }); }); it("falls back on connection refused but does not replay after a request-sent failure", async () => { const refused = await attemptComputerControl(async () => { throw Object.assign(new TypeError("fetch failed"), { cause: new Error("connect ECONNREFUSED 172.18.0.4:7070"), }); }); expect(refused).toEqual({ status: "unavailable" }); expect(shouldReplayComputerActions(refused)).toBe(true); const afterWrite = await attemptComputerControl(async () => { throw new Error("computer control failed"); }); expect(afterWrite).toMatchObject({ status: "failed" }); expect(shouldReplayComputerActions(afterWrite)).toBe(false); const timedOut = await attemptComputerControl(async () => { throw Object.assign(new Error("The operation was aborted due to timeout"), { name: "TimeoutError", }); }); expect(timedOut).toMatchObject({ status: "failed" }); expect(shouldReplayComputerActions(timedOut)).toBe(false); const reset = await attemptComputerControl(async () => { throw Object.assign(new TypeError("fetch failed"), { cause: new Error("read ECONNRESET"), }); }); expect(reset).toMatchObject({ status: "failed" }); expect(shouldReplayComputerActions(reset)).toBe(false); }); it("extends the computer control deadline for mapped waits", () => { expect(computerControlTimeoutMs([])).toBe(15_000); expect(computerControlTimeoutMs([{ kind: "wait", ms: 5_000 }], 5_000)).toBe(25_000); expect( computerControlTimeoutMs( [ { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, { kind: "wait", ms: 5_000 }, ], 5_000, ), ).toBe(60_000); }); it("wraps sandbox commands in a process-tree timeout", () => { expect(sandboxTimeoutCommand(["bash", "-lc", "sleep 10"], 2_500, "/tmp/completed-124")).toEqual( [ "timeout", "--kill-after=1s", "2.5s", "sh", "-c", '"$@"; status=$?; if [ "$status" -eq 124 ]; then : > "$0"; fi; exit "$status"', "/tmp/completed-124", "bash", "-lc", "sleep 10", ], ); expect(sandboxCommandTimedOut(124, false)).toBe(true); expect(sandboxCommandTimedOut(124, true)).toBe(false); expect(sandboxCommandTimedOut(1, false)).toBe(false); }); it("keeps the viewer read-only and uses a separate process for takeover control", () => { expect(interactiveScreenCommand(false)).toMatch(/pkill .*5901/); expect(interactiveScreenCommand(false)).not.toMatch(/x11vnc -display/); expect(interactiveScreenCommand(true, "lease-new")).toMatch(/x11vnc -display .* -rfbport 5901/); expect(interactiveScreenCommand(true, "lease-new")).toMatch(/6081/); expect(interactiveScreenCommand(true, "lease-new")).not.toMatch(/-rfbport 5900/); expect(interactiveScreenCommand(false, "lease-old")).toContain("!= 'lease-old'"); }); it("assigns distinct screen indexes per Team bot and starts extra displays", () => { const assigned = new Map(); expect(nextScreenIndex(assigned, "writer")).toBe(0); expect(nextScreenIndex(assigned, "researcher")).toBe(1); expect(nextScreenIndex(assigned, "writer")).toBe(0); expect(ensureScreenCommand(0)).toContain("-display :1"); expect(ensureScreenCommand(0)).toContain("seq 1 100"); expect(ensureScreenCommand(1)).toContain("Xvfb :2"); expect(ensureScreenCommand(1)).toContain("rfbport 5902"); expect(ensureScreenCommand(1)).toContain("0.0.0.0:6082"); expect(() => nextScreenIndex(assigned, "overflow", undefined, 1)).toThrow( /cannot allocate another screen/, ); }); it("generates syntactically valid shell to start an extra display", () => { const result = spawnSync("bash", ["-n"], { input: ensureScreenCommand(1) }); expect(result.status).toBe(0); expect(result.stderr.toString()).toBe(""); }); it("frees a released screen slot so a ninth Team bot can reuse it", () => { const assigned = new Map(); for (let index = 0; index < 8; index += 1) { expect(nextScreenIndex(assigned, `bot-${index}`)).toBe(index); } expect(() => nextScreenIndex(assigned, "bot-8")).toThrow(/cannot allocate another screen/); expect(releaseAssignedScreen(assigned, "bot-3")).toBe(3); expect(assigned.get("bot-0")?.index).toBe(0); expect(assigned.get("bot-3")?.releasing).toBe(true); expect(() => nextScreenIndex(assigned, "bot-3")).toThrow(/still being released/); expect(() => nextScreenIndex(assigned, "bot-8")).toThrow(/cannot allocate another screen/); completeReleasedScreen(assigned, "bot-3", 3); expect(assigned.get("bot-3")).toBeUndefined(); expect(nextScreenIndex(assigned, "bot-8")).toBe(3); expect(nextScreenIndex(assigned, "bot-0")).toBe(0); expect(releaseAssignedScreen(assigned, "missing")).toBeUndefined(); expect(() => nextScreenIndex(assigned, "bot-9")).toThrow(/cannot allocate another screen/); }); it("clears all screen assignments when a container stops so slots can be reused", () => { const registry = new Map>(); const containerId = "container-1"; const assigned = new Map(); registry.set(containerId, assigned); for (let index = 0; index < 8; index += 1) { nextScreenIndex(assigned, `bot-${index}`); } expect(() => nextScreenIndex(assigned, "bot-8")).toThrow(/cannot allocate another screen/); clearComputerScreenRegistry(registry, containerId); expect(registry.has(containerId)).toBe(false); const fresh = new Map(); registry.set(containerId, fresh); for (let index = 0; index < 8; index += 1) { expect(nextScreenIndex(fresh, `bot-fresh-${index}`)).toBe(index); } }); it("does not release a screen reclaimed by a newer execution fence", () => { const assigned = new Map(); expect(nextScreenIndex(assigned, "writer", "run-1:1")).toBe(0); expect(nextScreenIndex(assigned, "writer", "run-2:2")).toBe(0); expect(releaseAssignedScreen(assigned, "writer", "run-1:1")).toBeUndefined(); expect(nextScreenIndex(assigned, "researcher")).toBe(1); expect(releaseAssignedScreen(assigned, "writer", "run-2:2")).toBe(0); completeReleasedScreen(assigned, "writer", 0); }); it("releases a retained screen after the same run resumes", () => { const assigned = new Map(); expect(nextScreenIndex(assigned, "writer", "run-1:1")).toBe(0); expect(releaseAssignedScreen(assigned, "writer", "run-1:8")).toBe(0); completeReleasedScreen(assigned, "writer", 0); expect(nextScreenIndex(assigned, "researcher")).toBe(0); }); it("does not let a delayed request restore an older lease", () => { const assigned = new Map(); expect(nextScreenIndex(assigned, "writer", "run-1:8")).toBe(0); expect(() => nextScreenIndex(assigned, "writer", "run-1:1")).toThrow( /owned by a newer execution/, ); expect(releaseAssignedScreen(assigned, "writer", "run-1:1")).toBeUndefined(); expect(releaseAssignedScreen(assigned, "writer", "run-1:8")).toBe(0); }); it("lets a new run take a leftover screen from a stopped run", () => { const assigned = new Map(); expect(nextScreenIndex(assigned, "writer", "run-old:8")).toBe(0); expect(nextScreenIndex(assigned, "writer", "run-new:1")).toBe(0); expect(releaseAssignedScreen(assigned, "writer", "run-old:8")).toBeUndefined(); expect(releaseAssignedScreen(assigned, "writer", "run-new:1")).toBe(0); }); it("stops extra displays without touching the primary desktop", () => { expect(stopExtraScreenCommand(0)).toBe(""); expect(stopExtraScreenCommand(1)).toContain("Xvfb :2 -screen"); expect(stopExtraScreenCommand(1)).toContain("rfbport 5902"); expect(stopExtraScreenCommand(1)).toContain("websockify.*6082"); expect(stopExtraScreenCommand(1)).not.toMatch(/Xvfb :1 /); expect(stopExtraScreenCommand(1)).not.toMatch(/6080/); }); it("parses a captured frame without trusting optional desktop metadata", () => { expect( parseObservation( [ "GEOM 1280 800", "CURSOR X=12 Y=34 SCREEN=0 WINDOW=99", "WINDOW 99", "TITLE Browser", "IMAGE AQID", ].join("\n"), ), ).toEqual({ image: "AQID", mimeType: "image/png", width: 1280, height: 800, cursor: { x: 12, y: 34 }, activeWindow: { id: "99", title: "Browser" }, }); expect(() => parseObservation("GEOM 1280 800\nIMAGE ")).toThrow(/no image/); }); }); describe("docker exec stream demux", () => { const frame = (type: number, text: string) => { const payload = Buffer.from(text, "utf8"); const header = Buffer.alloc(8); header[0] = type; header.writeUInt32BE(payload.length, 4); return Buffer.concat([header, payload]); }; it("keeps stderr frames out of stdout", () => { const stream = Buffer.concat([ frame(1, "aGVsbG8="), frame(2, "python: DeprecationWarning\n"), frame(1, "\n"), ]); expect(demuxDockerStream(stream)).toEqual({ stdout: "aGVsbG8=\n", stderr: "python: DeprecationWarning\n", }); }); it("treats a raw tty stream as stdout", () => { expect(demuxDockerStream(Buffer.from("plain output\n"))).toEqual({ stdout: "plain output\n", stderr: "", }); expect(demuxDockerStream(Buffer.alloc(0))).toEqual({ stdout: "", stderr: "" }); }); it("falls back to raw stdout when the stream is not a complete multiplexed sequence", () => { const cut = Buffer.concat([frame(1, "kept"), frame(2, "truncated stderr")]).subarray( 0, 12 + 8 + 6, ); expect(demuxDockerStream(cut)).toEqual({ stdout: cut.toString("utf8"), stderr: "" }); const dangling = Buffer.concat([frame(1, "ok"), Buffer.from([1, 0, 0])]); expect(demuxDockerStream(dangling)).toEqual({ stdout: dangling.toString("utf8"), stderr: "", }); }); it("treats raw payloads that begin with 0x01 or 0x02 as stdout when not valid frames", () => { // size 0xffffffff does not fit remaining bytes → not a complete multiplexed stream const raw01 = Buffer.from([0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x41, 0x42]); expect(demuxDockerStream(raw01)).toEqual({ stdout: raw01.toString("utf8"), stderr: "", }); const raw02 = Buffer.from([0x02, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x43, 0x44]); expect(demuxDockerStream(raw02)).toEqual({ stdout: raw02.toString("utf8"), stderr: "", }); }); it("rejects frames with nonzero reserved header padding as raw stdout", () => { // type 1, nonzero padding, size 0 — would look like an empty stdout frame without the check const padded = Buffer.from([0x01, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00]); expect(demuxDockerStream(padded)).toEqual({ stdout: padded.toString("utf8"), stderr: "", }); }); });