feat(agent): sandbox conversation analysis
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user