import { spawnSync, type SpawnSyncReturns } from "node:child_process"; import { chmod, chown, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; import process from "node:process"; import { expect, test } from "vite-plus/test"; import { isNotFound } from "#common"; import type { RuntimeProfile } from "#runtime"; import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "#testing"; import { isRunnableProfileProcess, killProfileProcesses, readProfileFile } from "./isolation.ts"; const profile = (uid: number, home: string): RuntimeProfile => ({ index: uid - 20_000, type: "claude", model: "test", uid, gid: uid, home, tokenFile: null, }); // Runs a command as the profile's user from the root directory. Without `env` // it inherits this process's environment. function runAs( selected: RuntimeProfile, command: string, args: string[], env?: ReadonlyMap, ): SpawnSyncReturns { return spawnSync(command, args, { cwd: "/", uid: selected.uid, gid: selected.gid, encoding: "utf8", ...(env !== undefined && { env: Object.fromEntries(env) }), }); } test("zombie process entries are already quiescent", () => { const status = "State:\tZ (zombie)\nUid:\t20010\t20010\t20010\t20010\n"; expect(isRunnableProfileProcess(status, 20_010)).toBe(false); }); test.skipIf(IS_WINDOWS)("refuses to read a profile result symlink as root", async () => { const root = await makeTempDir(); try { const secret = `${root}/reviewer-token`; const result = `${root}/result.md`; await writeFile(secret, "reviewer-secret", { mode: 0o600 }); await symlink(secret, result); await expect(() => readProfileFile(profile(currentUid(), root), result)).rejects.toThrow( "regular private file", ); } finally { await rm(root, { recursive: true }); } }); test.skipIf(!IS_ROOT)("a profile process can read only its own private files", async () => { const root = await makeTempDir(); await chmod(root, 0o711); try { const homeA = `${root}/a`; const homeB = `${root}/b`; await mkdir(homeA, { mode: 0o700 }); await mkdir(homeB, { mode: 0o700 }); await chown(homeA, 20_000, 20_000); await chown(homeB, 20_001, 20_001); await writeFile(`${homeA}/token`, "own", { mode: 0o600 }); await chown(`${homeA}/token`, 20_000, 20_000); await writeFile(`${homeB}/token`, "sibling", { mode: 0o600 }); await chown(`${homeB}/token`, 20_001, 20_001); await writeFile(`${root}/reviewer`, "root", { mode: 0o600 }); const selected = profile(20_000, homeA); const own = runAs(selected, "/bin/cat", [`${homeA}/token`]); const sibling = runAs(selected, "/bin/cat", [`${homeB}/token`]); const reviewer = runAs(selected, "/bin/cat", [`${root}/reviewer`]); expect(own.stdout).toBe("own"); expect(own.status).toBe(0); expect(sibling.status).not.toBe(0); expect(reviewer.status).not.toBe(0); } finally { await rm(root, { recursive: true }); } }); test.skipIf(!IS_ROOT)("cleanup leaves no runnable daemonized process", async () => { const selected = profile(20_010, "/tmp"); const launched = runAs(selected, "/bin/sh", ["-c", "sleep 60 >/dev/null 2>&1 & echo $!"]); const pid = Number(launched.stdout.trim()); expect(Number.isInteger(pid)).toBe(true); await killProfileProcesses(selected.uid); const status = await readFile(`/proc/${pid}/status`, "utf8").catch((error: unknown) => { if (isNotFound(error)) { return null; } throw error; }); expect(status === null || !isRunnableProfileProcess(status, selected.uid)).toBe(true); }); test.skipIf(!IS_ROOT)("the agent process cannot read its coordinator environment", () => { const selected = profile(20_020, "/tmp"); const result = runAs( selected, "/usr/bin/cat", [`/proc/${process.pid}/environ`], new Map([["PARENT_SECRET", "must-not-cross-boundary"]]), ); expect(result.status).not.toBe(0); });