mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
test(chaos): allow only the node executable through the oracle sandbox; accept table-driven R1 tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
55d8762258
commit
d40a749409
5 files changed
+16
-12
No files matched your search
@@ -243,6 +243,9 @@ describe("T-oracles", () => {
|
|||||||
slug: { exitCode: 0, stdout: "[\"hello-chaos-world\",\"-r1-paperclip-chaos-2026-\"]\n", stderr: "" } } }).verdict).toBe("fail");
|
slug: { exitCode: 0, stdout: "[\"hello-chaos-world\",\"-r1-paperclip-chaos-2026-\"]\n", stderr: "" } } }).verdict).toBe("fail");
|
||||||
expect(r1TestChecksSlug("import test from \"node:test\";")).toBe(false);
|
expect(r1TestChecksSlug("import test from \"node:test\";")).toBe(false);
|
||||||
expect(r1TestChecksSlug("import test from \"node:test\"; import assert from \"node:assert\"; assert.ok(chaosSlug(\"x\"));")).toBe(false);
|
expect(r1TestChecksSlug("import test from \"node:test\"; import assert from \"node:assert\"; assert.ok(chaosSlug(\"x\"));")).toBe(false);
|
||||||
|
expect(r1TestChecksSlug([ "import test from \"node:test\";", "import assert from \"node:assert/strict\";",
|
||||||
|
"for (const [input, want] of [[\"Hello Chaos World\", \"hello-chaos-world\"], [\"a b\", \"a-b\"], [\"-x-\", \"x\"]])",
|
||||||
|
" test(input, () => assert.equal(chaosSlug(input), want));"].join("\n"))).toBe(true);
|
||||||
const argv = workloadDefinitions.R1.collect.commands.find(command => command.name === "node_test")!.argv;
|
const argv = workloadDefinitions.R1.collect.commands.find(command => command.name === "node_test")!.argv;
|
||||||
expect(typeof argv === "function" ? argv("abcd1234") : argv).toEqual([process.execPath, "--test", "packages/shared/src/chaos/slug-abcd1234.test.ts"]);
|
expect(typeof argv === "function" ? argv("abcd1234") : argv).toEqual([process.execPath, "--test", "packages/shared/src/chaos/slug-abcd1234.test.ts"]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { readFile, readdir, realpath } from "node:fs/promises";
|
import { readFile, readdir } from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { setTimeout as delay } from "node:timers/promises";
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
@@ -761,7 +761,7 @@ export class TaskObserver {
|
|||||||
for (const command of definition.collect.commands) {
|
for (const command of definition.collect.commands) {
|
||||||
const argv = typeof command.argv === "function" ? command.argv(this.plan.nonce) : command.argv;
|
const argv = typeof command.argv === "function" ? command.argv(this.plan.nonce) : command.argv;
|
||||||
commands[command.name] = await runOracleCommand(argv, workspacePath, { protectedRoots: this.protectedRoots,
|
commands[command.name] = await runOracleCommand(argv, workspacePath, { protectedRoots: this.protectedRoots,
|
||||||
readableRoots: argv[0] === process.execPath ? [path.dirname(path.dirname(await realpath(process.execPath)))] : [] });
|
readableFiles: argv[0] === process.execPath ? [process.execPath] : [] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ describe("oracle sandbox", () => {
|
|||||||
expect(profile.indexOf('(allow file-read* (subpath "/w")')).toBeGreaterThan(profile.indexOf("(deny file-read*"));
|
expect(profile.indexOf('(allow file-read* (subpath "/w")')).toBeGreaterThan(profile.indexOf("(deny file-read*"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("re-allows only the named readable roots, such as the node install, under a protected root", () => {
|
it("re-allows only the named files, such as the node executable, never a directory under a protected root", () => {
|
||||||
const profile = oracleSandboxProfile({ workspace: "/w", scratch: "/s", protectedRoots: ["/Users/me"], readableRoots: ["/Users/me/.nvm/versions/node/v26"] });
|
const profile = oracleSandboxProfile({ workspace: "/w", scratch: "/s", protectedRoots: ["/Users/me"], readableFiles: ["/Users/me/bin/node"] });
|
||||||
expect(profile.indexOf('(allow file-read* (subpath "/Users/me/.nvm/versions/node/v26"))')).toBeGreaterThan(profile.indexOf('(deny file-read* (subpath "/Users/me"))'));
|
expect(profile.indexOf('(allow file-read* (literal "/Users/me/bin/node"))')).toBeGreaterThan(profile.indexOf('(deny file-read* (subpath "/Users/me"))'));
|
||||||
expect(oracleSandboxProfile({ workspace: "/w", scratch: "/s", protectedRoots: ["/Users/me"] })).not.toContain(".nvm");
|
expect(profile).not.toContain('(allow file-read* (subpath "/Users/me")');
|
||||||
|
expect(profile).not.toContain('(allow file-read* (subpath "/Users/me/bin")');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refuses to run unsandboxed on other platforms unless allowed", async () => {
|
it("refuses to run unsandboxed on other platforms unless allowed", async () => {
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ function sbplString(value: string) { return `"${value.replaceAll("\\", "\\\\").r
|
|||||||
* example ~/.secrets or ~/.ssh), write anywhere except a scratch directory, or
|
* example ~/.secrets or ~/.ssh), write anywhere except a scratch directory, or
|
||||||
* use the network. The workspace stays readable.
|
* use the network. The workspace stays readable.
|
||||||
*/
|
*/
|
||||||
export function oracleSandboxProfile(input: { workspace: string; scratch: string; protectedRoots: readonly string[]; readableRoots?: readonly string[] }): string {
|
export function oracleSandboxProfile(input: { workspace: string; scratch: string; protectedRoots: readonly string[]; readableFiles?: readonly string[] }): string {
|
||||||
const lines = ["(version 1)", "(allow default)", "(deny network*)", "(deny file-write*)",
|
const lines = ["(version 1)", "(allow default)", "(deny network*)", "(deny file-write*)",
|
||||||
`(allow file-write* (subpath ${sbplString(input.scratch)}) (literal "/dev/null") (literal "/dev/tty"))`];
|
`(allow file-write* (subpath ${sbplString(input.scratch)}) (literal "/dev/null") (literal "/dev/tty"))`];
|
||||||
for (const root of input.protectedRoots) lines.push(`(deny file-read* (subpath ${sbplString(root)}))`);
|
for (const root of input.protectedRoots) lines.push(`(deny file-read* (subpath ${sbplString(root)}))`);
|
||||||
// Later rules win: re-allow the workspace and scratch even if they sit under a protected root.
|
// Later rules win: re-allow the workspace and scratch even if they sit under a protected root.
|
||||||
lines.push(`(allow file-read* (subpath ${sbplString(input.workspace)}) (subpath ${sbplString(input.scratch)}))`);
|
lines.push(`(allow file-read* (subpath ${sbplString(input.workspace)}) (subpath ${sbplString(input.scratch)}))`);
|
||||||
for (const root of input.readableRoots ?? []) lines.push(`(allow file-read* (subpath ${sbplString(root)}))`);
|
for (const file of input.readableFiles ?? []) lines.push(`(allow file-read* (literal ${sbplString(file)}))`);
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export function oracleSandboxProfile(input: { workspace: string; scratch: string
|
|||||||
* temporary HOME alone does not stop absolute-path reads.
|
* temporary HOME alone does not stop absolute-path reads.
|
||||||
*/
|
*/
|
||||||
export async function runOracleCommand(argv: readonly string[], workspace: string, options: {
|
export async function runOracleCommand(argv: readonly string[], workspace: string, options: {
|
||||||
protectedRoots?: readonly string[]; readableRoots?: readonly string[]; timeoutMs?: number; platform?: NodeJS.Platform; allowUnsandboxed?: boolean;
|
protectedRoots?: readonly string[]; readableFiles?: readonly string[]; timeoutMs?: number; platform?: NodeJS.Platform; allowUnsandboxed?: boolean;
|
||||||
} = {}): Promise<OracleCommandResult> {
|
} = {}): Promise<OracleCommandResult> {
|
||||||
const platform = options.platform ?? process.platform;
|
const platform = options.platform ?? process.platform;
|
||||||
const sandboxed = platform === "darwin";
|
const sandboxed = platform === "darwin";
|
||||||
@@ -51,7 +51,7 @@ export async function runOracleCommand(argv: readonly string[], workspace: strin
|
|||||||
const [command, args] = sandboxed
|
const [command, args] = sandboxed
|
||||||
? ["/usr/bin/sandbox-exec", ["-p", oracleSandboxProfile({ workspace: await resolve(workspace), scratch,
|
? ["/usr/bin/sandbox-exec", ["-p", oracleSandboxProfile({ workspace: await resolve(workspace), scratch,
|
||||||
protectedRoots: await Promise.all((options.protectedRoots ?? [os.homedir()]).map(resolve)),
|
protectedRoots: await Promise.all((options.protectedRoots ?? [os.homedir()]).map(resolve)),
|
||||||
readableRoots: await Promise.all((options.readableRoots ?? []).map(resolve)) }), ...argv]]
|
readableFiles: await Promise.all((options.readableFiles ?? []).map(resolve)) }), ...argv]]
|
||||||
: [argv[0]!, argv.slice(1)];
|
: [argv[0]!, argv.slice(1)];
|
||||||
try {
|
try {
|
||||||
const { stdout, stderr } = await execFileAsync(command, args, { cwd: workspace, env, timeout: options.timeoutMs ?? 60_000, maxBuffer: 1024 * 1024 });
|
const { stdout, stderr } = await execFileAsync(command, args, { cwd: workspace, env, timeout: options.timeoutMs ?? 60_000, maxBuffer: 1024 * 1024 });
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ const r1Test = (nonce: string) => `packages/shared/src/chaos/slug-${nonce}.test.
|
|||||||
|
|
||||||
export function r1TestChecksSlug(text: string | null | undefined): boolean {
|
export function r1TestChecksSlug(text: string | null | undefined): boolean {
|
||||||
if (!text) return false;
|
if (!text) return false;
|
||||||
const assertions = text.match(/\bassert(\.\w+)?\s*\(/g)?.length ?? 0;
|
return text.includes("node:test") && text.includes("node:assert") && /\bassert(\.\w+)?\s*\(/.test(text)
|
||||||
return text.includes("node:test") && text.includes("node:assert") && text.includes("chaosSlug(") && assertions >= 3;
|
&& text.includes("chaosSlug") && text.includes("hello-chaos-world");
|
||||||
}
|
}
|
||||||
/** bench.sh must have actually run: at least this long between start and finish. */
|
/** bench.sh must have actually run: at least this long between start and finish. */
|
||||||
export const BENCH_MIN_SECONDS = 200;
|
export const BENCH_MIN_SECONDS = 200;
|
||||||
|
|||||||
Reference in new issue
Block a user