113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import type { SpawnSyncReturns } from "node:child_process";
|
|
import { expect, test } from "vite-plus/test";
|
|
import { chmod, chown, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
import process from "node:process";
|
|
|
|
import { isNotFound } from "./errors.ts";
|
|
import { isRunnableProfileProcess, killProfileProcesses, readProfileFile } from "./isolation.ts";
|
|
import type { RuntimeProfile } from "./runtime.ts";
|
|
import { currentUid, IS_ROOT, IS_WINDOWS, makeTempDir } from "#testing";
|
|
|
|
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?: Record<string, string>,
|
|
): SpawnSyncReturns<string> {
|
|
return spawnSync(command, args, {
|
|
cwd: "/",
|
|
uid: selected.uid,
|
|
gid: selected.gid,
|
|
encoding: "utf8",
|
|
...(env !== undefined && { 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`], {
|
|
PARENT_SECRET: "must-not-cross-boundary",
|
|
});
|
|
|
|
expect(result.status).not.toBe(0);
|
|
});
|