This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { assertEquals, assertRejects } from "jsr:@std/assert@1";
|
||||
import {
|
||||
agentCommand,
|
||||
isRunnableProfileProcess,
|
||||
killProfileProcesses,
|
||||
readProfileFile,
|
||||
} from "./isolation.ts";
|
||||
import type { RuntimeProfile } from "./runtime.ts";
|
||||
|
||||
const profile = (uid: number, home: string): RuntimeProfile => ({
|
||||
index: uid - 20_000,
|
||||
type: "claude",
|
||||
model: "test",
|
||||
uid,
|
||||
gid: uid,
|
||||
home,
|
||||
tokenFile: null,
|
||||
});
|
||||
|
||||
Deno.test("zombie process entries are already quiescent", () => {
|
||||
const status = "State:\tZ (zombie)\nUid:\t20010\t20010\t20010\t20010\n";
|
||||
assertEquals(isRunnableProfileProcess(status, 20_010), false);
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "refuses to read a profile result symlink as root",
|
||||
permissions: { read: true, write: true, sys: ["uid"] },
|
||||
async fn() {
|
||||
if (Deno.build.os === "windows") return;
|
||||
const root = await Deno.makeTempDir();
|
||||
try {
|
||||
const secret = `${root}/reviewer-token`;
|
||||
const result = `${root}/result.md`;
|
||||
await Deno.writeTextFile(secret, "reviewer-secret", { mode: 0o600 });
|
||||
await Deno.symlink(secret, result);
|
||||
|
||||
await assertRejects(
|
||||
() => readProfileFile(profile(Deno.uid()!, root), result),
|
||||
Error,
|
||||
"regular private file",
|
||||
);
|
||||
} finally {
|
||||
await Deno.remove(root, { recursive: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "a profile process can read only its own private files",
|
||||
permissions: { read: true, write: true, run: true, sys: ["uid"] },
|
||||
async fn() {
|
||||
if (Deno.uid() !== 0) return;
|
||||
const root = await Deno.makeTempDir();
|
||||
await Deno.chmod(root, 0o711);
|
||||
try {
|
||||
const homeA = `${root}/a`;
|
||||
const homeB = `${root}/b`;
|
||||
await Deno.mkdir(homeA, { mode: 0o700 });
|
||||
await Deno.mkdir(homeB, { mode: 0o700 });
|
||||
await Deno.chown(homeA, 20_000, 20_000);
|
||||
await Deno.chown(homeB, 20_001, 20_001);
|
||||
await Deno.writeTextFile(`${homeA}/token`, "own", { mode: 0o600 });
|
||||
await Deno.chown(`${homeA}/token`, 20_000, 20_000);
|
||||
await Deno.writeTextFile(`${homeB}/token`, "sibling", { mode: 0o600 });
|
||||
await Deno.chown(`${homeB}/token`, 20_001, 20_001);
|
||||
await Deno.writeTextFile(`${root}/reviewer`, "root", { mode: 0o600 });
|
||||
|
||||
const selected = profile(20_000, homeA);
|
||||
const own = await agentCommand(selected, "/bin/cat", [`${homeA}/token`], {
|
||||
cwd: "/",
|
||||
}).output();
|
||||
const sibling = await agentCommand(selected, "/bin/cat", [
|
||||
`${homeB}/token`,
|
||||
], { cwd: "/" }).output();
|
||||
const reviewer = await agentCommand(selected, "/bin/cat", [
|
||||
`${root}/reviewer`,
|
||||
], { cwd: "/" }).output();
|
||||
|
||||
assertEquals(new TextDecoder().decode(own.stdout), "own");
|
||||
assertEquals(own.success, true);
|
||||
assertEquals(sibling.success, false);
|
||||
assertEquals(reviewer.success, false);
|
||||
} finally {
|
||||
await Deno.remove(root, { recursive: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "cleanup leaves no runnable daemonized process",
|
||||
permissions: "inherit",
|
||||
async fn() {
|
||||
if (Deno.uid() !== 0) return;
|
||||
const selected = profile(20_010, "/tmp");
|
||||
const launched = await agentCommand(selected, "/bin/sh", [
|
||||
"-c",
|
||||
"sleep 60 >/dev/null 2>&1 & echo $!",
|
||||
], { cwd: "/" }).output();
|
||||
const pid = Number(new TextDecoder().decode(launched.stdout).trim());
|
||||
assertEquals(Number.isInteger(pid), true);
|
||||
|
||||
await killProfileProcesses(selected.uid);
|
||||
|
||||
const status = await Deno.readTextFile(`/proc/${pid}/status`).catch(
|
||||
(error) => {
|
||||
if (error instanceof Deno.errors.NotFound) return null;
|
||||
throw error;
|
||||
},
|
||||
);
|
||||
assertEquals(
|
||||
status === null || !isRunnableProfileProcess(status, selected.uid),
|
||||
true,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Deno.test({
|
||||
name: "the agent process cannot read its coordinator environment",
|
||||
permissions: { run: true, sys: ["uid"] },
|
||||
async fn() {
|
||||
if (Deno.uid() !== 0) return;
|
||||
const selected = profile(20_020, "/tmp");
|
||||
const result = await agentCommand(selected, "/usr/bin/cat", [
|
||||
`/proc/${Deno.pid}/environ`,
|
||||
], {
|
||||
cwd: "/",
|
||||
env: { PARENT_SECRET: "must-not-cross-boundary" },
|
||||
clearEnv: true,
|
||||
stderr: "piped",
|
||||
}).output();
|
||||
|
||||
assertEquals(result.success, false);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user