95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import { afterEach, describe, expect, it } from "bun:test";
|
|
import { mkdir, rm } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
|
|
import { config } from "../../src/config.js";
|
|
import {
|
|
resolveConversationWorkspace,
|
|
setSandboxOwnership,
|
|
} from "../../src/runtime/conversationWorkspace.js";
|
|
import {
|
|
executeSandboxCommand,
|
|
probeLandlockSandbox,
|
|
} from "../../src/sandbox/landlockSandbox.js";
|
|
|
|
const createdPaths: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
createdPaths.splice(0).map((path) => rm(path, { force: true, recursive: true })),
|
|
);
|
|
});
|
|
|
|
const createWorkspace = async (name: string = crypto.randomUUID()) => {
|
|
const importRoot = resolve(config.RESULT_REF_IMPORT_DIR);
|
|
await mkdir(importRoot, { recursive: true });
|
|
const workspace = resolve(importRoot, `conversation-sandbox-${name}`);
|
|
await mkdir(workspace, { mode: 0o700 });
|
|
await setSandboxOwnership(workspace);
|
|
createdPaths.push(workspace);
|
|
return workspace;
|
|
};
|
|
|
|
describe("Landlock sandbox", () => {
|
|
it("requires Landlock ABI 4 and seccomp", async () => {
|
|
const probe = await probeLandlockSandbox();
|
|
expect(probe.landlockAbi).toBeGreaterThanOrEqual(4);
|
|
expect(probe.seccomp).toBe(true);
|
|
});
|
|
|
|
it("allows local Python analysis while denying filesystem escape, secrets, and network", async () => {
|
|
const workspace = await createWorkspace("primary");
|
|
const sibling = await createWorkspace("sibling");
|
|
await resolveConversationWorkspace(workspace, config.RESULT_REF_IMPORT_DIR);
|
|
|
|
const allowed = await executeSandboxCommand(
|
|
workspace,
|
|
'python3 -c "import json; open(\'result.json\', \'w\').write(json.__name__)" && cat result.json',
|
|
10,
|
|
);
|
|
expect(allowed).toMatchObject({ exitCode: 0, stdout: "json" });
|
|
|
|
const escaped = await executeSandboxCommand(
|
|
workspace,
|
|
`cat ${resolve("package.json")}`,
|
|
10,
|
|
);
|
|
expect(escaped.exitCode).not.toBe(0);
|
|
expect(escaped.stderr).toContain("Permission denied");
|
|
|
|
const siblingRead = await executeSandboxCommand(
|
|
workspace,
|
|
`ls ${sibling}`,
|
|
10,
|
|
);
|
|
expect(siblingRead.exitCode).not.toBe(0);
|
|
|
|
const originalApiKey = process.env.DEEPSEEK_API_KEY;
|
|
process.env.DEEPSEEK_API_KEY = "must-not-leak";
|
|
try {
|
|
const environment = await executeSandboxCommand(
|
|
workspace,
|
|
'test -z "$DEEPSEEK_API_KEY"',
|
|
10,
|
|
);
|
|
expect(environment.exitCode).toBe(0);
|
|
} finally {
|
|
if (originalApiKey === undefined) {
|
|
delete process.env.DEEPSEEK_API_KEY;
|
|
} else {
|
|
process.env.DEEPSEEK_API_KEY = originalApiKey;
|
|
}
|
|
}
|
|
|
|
for (const socketType of ["SOCK_STREAM", "SOCK_DGRAM"]) {
|
|
const network = await executeSandboxCommand(
|
|
workspace,
|
|
`python3 -c "import socket; socket.socket(socket.AF_INET, socket.${socketType})"`,
|
|
10,
|
|
);
|
|
expect(network.exitCode).not.toBe(0);
|
|
expect(network.stderr).toContain("Operation not permitted");
|
|
}
|
|
});
|
|
});
|