Files
TJWaterAgent/tests/sandbox/landlockSandbox.test.ts
T
jiang 80cfc1f2ab
Generic Container CI/CD / test-build-publish (push) Successful in 2m44s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m44s
feat(agent): sandbox conversation analysis
2026-08-25 16:08:33 +08:00

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");
}
});
});