Vili/scripts/test-gpu-gate-http.mjs
Meta Vens cb36beb09a Refuse cross-site POSTs to the state-changing routes and grant no CORS preflight
POST /gpu-permission and POST /motion/generate now require Content-Type application/json and
refuse a request whose Origin is present and not the daemon's own loopback origin, 403 cross-site,
before the body is read. The blanket OPTIONS handler that approved any preflight is deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 02:29:43 +02:00

209 lines
10 KiB
JavaScript

// Boots the real daemon and observes the gate where it is decided: the HTTP edge. Needs the
// daemon's own dependencies (ws, cultcache-ts), so it runs on a machine that has them. Every boot
// sets VILI_GPU_MIN_FREE_MIB=999999 so an allowed request ends in a VRAM refusal and never starts
// the worker: no test here may start a GPU job.
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { mkdtemp, readdir, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
const daemonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "vili-daemon.mjs");
let nextPort = 18000 + Math.floor(Math.random() * 1000);
async function bootDaemon(stateRoot, { host = "127.0.0.1", env = { VILI_GPU_MIN_FREE_MIB: "999999" } } = {}) {
const port = nextPort++;
const child = spawn(process.execPath, [daemonPath, "--host", host, "--port", String(port), "--state-root", stateRoot], {
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
let output = "";
child.stdout.on("data", (chunk) => { output += chunk; });
child.stderr.on("data", (chunk) => { output += chunk; });
const deadline = Date.now() + 30000;
while (!output.includes("Vili listening")) {
if (child.exitCode !== null || Date.now() > deadline) throw new Error(`daemon did not start: ${output.slice(-500)}`);
await new Promise((resolve) => setTimeout(resolve, 100));
}
const stop = () => new Promise((resolve) => {
if (child.exitCode !== null) resolve();
else { child.once("exit", resolve); child.kill(); }
});
return { child, port, stop };
}
const url = (d, route, host = "127.0.0.1") => `http://${host}:${d.port}${route}`;
const generate = (d, body) => fetch(url(d, "/motion/generate"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const setPermission = (d, body, host = "127.0.0.1") => fetch(url(d, "/gpu-permission", host), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
const readPermission = async (d) => (await fetch(url(d, "/gpu-permission"))).json();
test("http: refusal at the daemon edge", async () => {
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "vili-gate-"));
const d = await bootDaemon(stateRoot);
try {
assert.deepEqual(await readPermission(d), { allowed: false, changedAt: null, jobRunning: false, lastRefusal: null });
assert.equal((await generate(d, {})).status, 400, "request-shape errors come ahead of the gate");
const refused = await generate(d, { prompt: "a person waves hello, never seen before" });
assert.equal(refused.status, 503);
assert.equal(refused.headers.get("retry-after"), "60");
assert.deepEqual(await refused.json(), { ok: false, code: "gpu-busy", source: "household", gpu: { allowed: false, changedAt: null } });
assert.deepEqual(await readdir(path.join(stateRoot, "jobs")), [], "no job file");
assert.equal((await readdir(stateRoot)).includes("artifacts"), false, "no artifact directory");
const state = await (await fetch(url(d, "/operator-state"))).json();
assert.equal(state.gpu.lastRefusal.source, "household");
assert.equal(state.gpu.allowed, false);
} finally {
await d.stop();
await rm(stateRoot, { recursive: true, force: true });
}
});
test("http: the permission persists across a daemon restart", async () => {
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "vili-gate-persist-"));
let d = await bootDaemon(stateRoot);
try {
const on = await setPermission(d, { allowed: true });
assert.equal(on.status, 200);
const onBody = await on.json();
assert.equal(onBody.ok, true);
assert.equal(onBody.gpu.allowed, true);
const before = await readPermission(d);
assert.equal(before.allowed, true);
assert.ok(before.changedAt);
assert.equal((await setPermission(d, { allowed: "yes" })).status, 400);
assert.equal((await setPermission(d, {})).status, 400);
assert.deepEqual(await readPermission(d), before, "a refused write changes nothing");
await d.stop();
d = await bootDaemon(stateRoot);
const after = await readPermission(d);
assert.equal(after.allowed, true);
assert.equal(after.changedAt, before.changedAt);
await setPermission(d, { allowed: false });
await d.stop();
d = await bootDaemon(stateRoot);
assert.equal((await readPermission(d)).allowed, false, "off persists too");
} finally {
await d.stop();
await rm(stateRoot, { recursive: true, force: true });
}
});
test("http: an allowed request reaches the slot and the VRAM read", async () => {
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "vili-gate-slot-"));
const d = await bootDaemon(stateRoot);
try {
await setPermission(d, { allowed: true });
const refused = await generate(d, { prompt: "a person waves hello, never seen before" });
assert.equal(refused.status, 503);
const body = await refused.json();
assert.equal(body.source, "vram", "admitted through acquire(), then refused for VRAM");
assert.equal(body.minFreeMiB, 999999);
assert.deepEqual(await readdir(path.join(stateRoot, "jobs")), [], "no job file");
assert.equal((await readdir(stateRoot)).includes("artifacts"), false);
const state = await readPermission(d);
assert.equal(state.lastRefusal.source, "vram");
assert.equal(state.jobRunning, false);
} finally {
await d.stop();
await rm(stateRoot, { recursive: true, force: true });
}
});
test("http: the switch is the household's", async () => {
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "vili-gate-lan-"));
const d = await bootDaemon(stateRoot, { host: "0.0.0.0" });
try {
const lan = Object.values(os.networkInterfaces()).flat().find((i) => i.family === "IPv4" && !i.internal)?.address;
assert.ok(lan, "the host has a non-loopback IPv4 address");
const before = await readPermission(d);
const refused = await setPermission(d, { allowed: true }, lan);
assert.equal(refused.status, 403);
assert.deepEqual(await refused.json(), { ok: false, code: "household-only" });
assert.deepEqual(await readPermission(d), before, "GET still reads the previous value");
assert.equal((await setPermission(d, { allowed: true })).status, 200, "loopback still sets it");
} finally {
await d.stop();
await rm(stateRoot, { recursive: true, force: true });
}
});
test("http: a web page cannot flip the household's switch or start a job", async () => {
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "vili-gate-csrf-"));
const d = await bootDaemon(stateRoot);
const post = (route, headers, body) => fetch(url(d, route), { method: "POST", headers, body });
const prompt = JSON.stringify({ prompt: "a person waves hello, never seen before" });
try {
const before = await readPermission(d);
for (const route of ["/gpu-permission", "/motion/generate"]) {
const payload = route === "/gpu-permission" ? JSON.stringify({ allowed: true }) : prompt;
const simple = await post(route, { "content-type": "text/plain" }, payload);
assert.equal(simple.status, 403, `${route}: a simple text/plain POST`);
assert.deepEqual(await simple.json(), { ok: false, code: "cross-site" });
assert.equal((await post(route, {}, payload)).status, 403, `${route}: no content-type`);
const hostile = await post(route, { "content-type": "application/json", origin: "http://evil.example-origin" }, payload);
assert.equal(hostile.status, 403, `${route}: a hostile Origin`);
const body = JSON.stringify(await hostile.json());
assert.equal(body.includes("evil.example-origin"), false, "the refusal never echoes the Origin");
assert.equal(
(await post(route, { "content-type": "application/json", origin: `http://127.0.0.1:${d.port}.evil.example` }, payload)).status,
403,
`${route}: an Origin that only starts like ours`,
);
}
assert.deepEqual(await readPermission(d), before, "no refused request changed anything");
assert.deepEqual(await readdir(path.join(stateRoot, "jobs")), [], "no job file");
const preflight = await fetch(url(d, "/gpu-permission"), {
method: "OPTIONS",
headers: { origin: "http://evil.example-origin", "access-control-request-method": "POST", "access-control-request-headers": "content-type" },
});
assert.equal(preflight.ok, false, "the daemon approves no CORS preflight");
assert.equal(preflight.headers.get("access-control-allow-methods"), null);
const own = await post("/gpu-permission", { "content-type": "application/json; charset=utf-8", origin: `http://127.0.0.1:${d.port}` }, JSON.stringify({ allowed: true }));
assert.equal(own.status, 200, "the daemon's own origin still works");
assert.equal((await setPermission(d, { allowed: false })).status, 200, "a caller with no Origin (the tray) still works");
} finally {
await d.stop();
await rm(stateRoot, { recursive: true, force: true });
}
});
test("http: a malformed override refuses to start and names the variable, not its value", async () => {
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "vili-gate-env-"));
const child = spawn(process.execPath, [daemonPath, "--host", "127.0.0.1", "--port", String(nextPort++), "--state-root", stateRoot], {
env: { ...process.env, VILI_GPU_MIN_FREE_MIB: "10240MiB-secret" },
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
});
let output = "";
child.stdout.on("data", (chunk) => { output += chunk; });
child.stderr.on("data", (chunk) => { output += chunk; });
try {
const code = await new Promise((resolve) => child.on("exit", resolve));
assert.notEqual(code, 0);
assert.match(output, /VILI_GPU_MIN_FREE_MIB must be a finite non-negative number/);
assert.equal(output.includes("10240MiB-secret"), false);
assert.equal(output.includes("Vili listening"), false);
} finally {
child.kill();
await rm(stateRoot, { recursive: true, force: true });
}
});