Files
reverse-engineering-anything/tests/boundary/providers/desktopTaskReaderRunner.test.ts
T
morlutoandCursor Agent 77100250de fix(desktop)!: preserve build evidence and validate task inputs (#9898)
* fix(desktop)!: preserve build evidence and validate task inputs

BREAKING CHANGE: Witnessing requires the current generated verifier. Re-export bundles with older evaluators using the original capture Evidence and task inputs.

* test(desktop): match reader source inspection diagnostics

Unreadable reader sources now report the source inspection failure, including the operating-system reason, instead of the old file wording.

* fix(environment): advertise canonical bundle names (#9939)

fix(environment): advertise portable bundle name constraints

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-11 04:10:44 -05:00

428 lines
14 KiB
TypeScript

import { execFile } from "node:child_process";
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { readerViewsOfBytes } from "../../../src/application/desktop/DesktopTaskReaders.js";
import type { DesktopTaskReader } from "../../../src/domain/desktop/appTask.js";
import {
readerAnswerView,
readerCommand,
PYTHON_READER_CLEANUP,
} from "../../../src/domain/desktop/appTaskReaders.js";
import { runReaderHere } from "../../../src/desktop/TaskReaderProcess.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execute = promisify(execFile);
const NOTE = "Documents/today.txt";
const SECRET = "control-token-secret";
/** A fake app's headless mode: it reports what it was given, then changes the file it read. */
const REPORTING_APP = String.raw`
import json, os, sys
path = sys.argv[1]
data = open(path, "rb").read().decode()
open(path, "ab").write(b" tampered")
print("App 1.0 starting")
print("REA " + json.dumps({
"home": os.environ.get("HOME"), "display": os.environ.get("DISPLAY"), "bus": os.environ.get("DBUS_SESSION_BUS_ADDRESS"),
"config": os.environ.get("XDG_CONFIG_HOME"), "control": sorted(k for k in os.environ if k.startswith("AGENTENV_")),
"path": path, "cwd": os.getcwd(), "content": data, "stdin": sys.stdin.read()}))
print("App quit")
`;
/** A computer's home with the note in it and the fake app beside it. */
const computer = async () => {
const root = await createTestTempDirectory("reader-runner-");
const home = join(root, "home");
await mkdir(join(home, "Documents"), { recursive: true });
await writeFile(join(home, NOTE), "milk\neggs\n");
const app = join(root, "app.py");
await writeFile(app, REPORTING_APP);
return { home, app };
};
/** Run the runner as computer-exec runs it in the session: the session's environment, its control token included. */
const run = async (home: string, reader: DesktopTaskReader, path?: string) => {
const [python = "", ...rest] = readerCommand(reader, path);
const { stdout } = await execute(python, rest, {
cwd: home,
env: {
PATH: process.env.PATH ?? "",
HOME: home,
DISPLAY: ":99",
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/1000/bus",
AGENTENV_CONTROL_TOKEN: SECRET,
},
maxBuffer: 4 * 1024 * 1024,
});
return stdout;
};
const reporting = (app: string, fields: Partial<DesktopTaskReader> = {}) =>
({
id: "report",
file: NOTE,
argv: ["python3", app, "{file}"],
output: "json",
...fields,
}) as DesktopTaskReader;
it("runs the reader on a private copy of the file in a fresh home, without the display, the session bus or the control token", async () => {
const { home, app } = await computer();
const stdout = await run(home, reporting(app, { stdin: "open {file}" }));
expect(stdout).not.toContain(SECRET);
const view = readerAnswerView(stdout);
const report = view?.view as Record<string, unknown>;
expect(view?.format).toBe("json");
expect(report).toMatchObject({
display: null,
bus: null,
control: [],
content: "milk\neggs\n",
});
const copy = String(report.path);
expect(copy.endsWith("/today.txt")).toBe(true);
expect(copy.startsWith(home)).toBe(false);
expect(report.stdin).toBe(`open ${copy}`);
// The copy and the fresh home are siblings in the runner's own folder.
expect(String(report.home).startsWith(dirname(dirname(copy)))).toBe(true);
expect(String(report.config).startsWith(String(report.home))).toBe(true);
expect(report.cwd).toBe(dirname(copy));
// The reader changed only its copy, which is gone with its folder.
expect(await readFile(join(home, NOTE), "utf8")).toBe("milk\neggs\n");
await expect(access(dirname(dirname(copy)))).rejects.toThrow();
});
it("retains a remotely owned copy until the matching lease removes it", async () => {
const { home, app } = await computer();
const root = await createTestTempDirectory("reader-lease-");
const lease = { path: join(root, "owned"), token: "owner-token" };
const [python = "", ...args] = readerCommand(
reporting(app),
undefined,
undefined,
lease,
);
const { stdout } = await execute(python, args, {
env: { ...process.env, HOME: home },
});
expect(readerAnswerView(stdout)?.format).toBe("json");
await expect(
access(join(lease.path, "file", "today.txt")),
).resolves.toBeUndefined();
await expect(
execute(python, [
"-I",
"-c",
PYTHON_READER_CLEANUP,
JSON.stringify({ ...lease, token: "other" }),
]),
).rejects.toThrow();
await expect(access(lease.path)).resolves.toBeUndefined();
const refused = `import shutil\ndef fail(path): raise PermissionError("denied")\nshutil.rmtree = fail\nexec(${JSON.stringify(PYTHON_READER_CLEANUP)})`;
await expect(
execute(python, ["-I", "-c", refused, JSON.stringify(lease)]),
).rejects.toThrow();
expect(await readFile(`${lease.path}.owner`, "utf8")).toBe(lease.token);
await execute(python, [
"-I",
"-c",
PYTHON_READER_CLEANUP,
JSON.stringify(lease),
]);
await expect(access(lease.path)).rejects.toThrow();
await expect(access(`${lease.path}.owner`)).rejects.toThrow();
});
it("passes maximum-length Unicode stdin through bounded arguments to the real reader", async () => {
const { home, app } = await computer();
const input = "é".repeat(8190) + "😀" + "é".repeat(57344);
const reader = reporting(app, { stdin: input });
const command = readerCommand(reader, undefined);
expect(Buffer.byteLength(command.slice(4).join(""))).toBeGreaterThan(131072);
expect(
Math.max(...command.slice(4).map((part) => Buffer.byteLength(part))),
).toBeLessThan(131072);
const report = readerAnswerView(await run(home, reader))?.view as Record<
string,
unknown
>;
expect(report.stdin).toBe(input);
});
it("keeps the display for a reader that asks for it, still without the control token", async () => {
const { home, app } = await computer();
const report = readerAnswerView(
await run(home, reporting(app, { display: true })),
)?.view as Record<string, unknown>;
expect(report).toMatchObject({ display: ":99", control: [] });
});
it("reads the file a path names instead of the home's, under the home file's name", async () => {
const { home, app } = await computer();
const blob = join(home, "..", "blob-0123");
await writeFile(blob, "seed bytes");
const report = readerAnswerView(await run(home, reporting(app), blob))
?.view as Record<string, unknown>;
expect(report.content).toBe("seed bytes");
expect(String(report.path).endsWith("/today.txt")).toBe(true);
});
it("makes the marked lines a text view and drops the rest", async () => {
const { home } = await computer();
const view = readerAnswerView(
await run(home, {
id: "lines",
file: NOTE,
argv: [
"python3",
"-c",
"import sys\nprint('ts> banner')\nfor line in open(sys.argv[1]): print('REA item ' + line.strip())\nprint('REA')\nprint('REAL news')",
"{file}",
],
output: "lines",
}),
);
expect(view).toEqual({
format: "text",
view: {
lines: ["item milk", "item eggs"],
line_count: 2,
truncated: false,
},
});
});
it("answers absent for a file the home does not hold, without running the reader", async () => {
const { home } = await computer();
const stdout = await run(home, {
id: "gone",
file: "Documents/gone.txt",
argv: ["sh", "-c", `touch '${home}/ran'; echo 'REA x' {file}`],
output: "lines",
});
expect(readerAnswerView(stdout)).toBeUndefined();
await expect(access(join(home, "ran"))).rejects.toThrow();
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"keeps an inaccessible source unknown instead of answering absent",
async () => {
const { home, app } = await computer();
const documents = join(home, "Documents");
await chmod(documents, 0);
try {
const view = readerAnswerView(await run(home, reporting(app)));
expect(view).toMatchObject({ format: null, view: null });
expect(view?.error).toContain("cannot inspect");
} finally {
await chmod(documents, 0o700);
}
},
);
it.each(["json", "lines"] as const)(
"refuses malformed UTF-8 in marked %s output while preserving literal replacement characters",
async (output) => {
const { home } = await computer();
const reader = (bytes: string): DesktopTaskReader => ({
id: "encoding",
file: NOTE,
argv: [
"python3",
"-I",
"-c",
`import sys; sys.stdout.buffer.write(${bytes})`,
"{file}",
],
output,
});
const malformed = readerAnswerView(
await run(home, reader(String.raw`b'REA {"value":"\xff"}\n'`)),
);
expect(malformed).toMatchObject({ format: null, view: null });
expect(malformed?.error).toContain("UTF-8");
const valid = readerAnswerView(
await run(
home,
reader(
String.raw`b'\xff ignored banner\nREA {"value":"\xef\xbf\xbd"}\r\n'`,
),
),
);
expect(valid).toMatchObject(
output === "json"
? { format: "json", view: { value: "\ufffd" } }
: {
format: "text",
view: {
lines: ['{"value":"\ufffd"}'],
line_count: 1,
truncated: false,
},
},
);
},
);
it.each([
[
"exits non-zero",
["sh", "-c", "echo 'REA x'; echo 'cannot open {file}' >&2; exit 3"],
"json",
["the reader exited 3", "cannot open"],
],
[
"prints no marked line",
["sh", "-c", "echo hello {file}"],
"lines",
['printed no line starting with "REA "'],
],
[
"prints a view that is not JSON",
["sh", "-c", "echo 'REA {not json' {file}"],
"json",
["are not one JSON value"],
],
[
"prints a number JSON cannot hold exactly",
["sh", "-c", "echo 'REA {\"id\": 9007199254740993}'; : {file}"],
"json",
["cannot be read exactly"],
],
...["9007199254740993.0", "1e-400", "-0.0"].map((number) => [
`prints an inexact decimal ${number}`,
["sh", "-c", `echo 'REA {"id": ${number}}'; : {file}`],
"json",
["cannot be read exactly"],
]),
[
"prints more than 1 MiB",
["python3", "-c", "print('REA ' + 'x' * (1 << 20))", "{file}"],
"lines",
["printed more than 1 MiB"],
],
[
"cannot start",
["no-such-app-binary", "{file}"],
"lines",
["could not start no-such-app-binary"],
],
])(
"reports a reader that %s as an error view",
async (_, argv, output, facts) => {
const { home } = await computer();
const view = readerAnswerView(
await run(home, {
id: "broken",
file: NOTE,
argv: argv as string[],
output: output as "json" | "lines",
}),
);
expect(view?.format).toBeNull();
for (const fact of facts) expect(view?.error).toContain(fact);
},
);
it("stops a reader past its timeout, with the processes it started", async () => {
const { home } = await computer();
const started = Date.now();
const view = readerAnswerView(
await run(home, {
id: "slow",
file: NOTE,
argv: [
"sh",
"-c",
`(sleep 3; touch "${home}/late") & echo 'REA x'; sleep 30; : {file}`,
],
output: "lines",
timeout_s: 1,
}),
);
expect(view?.error).toContain("ran past its 1 s");
expect(Date.now() - started).toBeLessThan(10_000);
await new Promise((resolve) => setTimeout(resolve, 3500));
await expect(access(join(home, "late"))).rejects.toThrow();
});
it("runs a reader whose file shares a name with the runner's own folders and files", async () => {
const { home } = await computer();
for (const name of ["home", "stdout", "stderr"]) {
await writeFile(join(home, "Documents", name), `${name} bytes`);
const view = readerAnswerView(
await run(home, {
id: "named",
file: `Documents/${name}`,
argv: ["sh", "-c", 'echo REA $(cat "$1")', "sh", "{file}"],
output: "lines",
}),
);
expect(view?.view, name).toEqual({
lines: [`${name} bytes`],
line_count: 1,
truncated: false,
});
}
});
it("stops a reader once it has printed more than 1 MiB, rather than at its timeout", async () => {
const { home } = await computer();
const started = Date.now();
const view = readerAnswerView(
await run(home, {
id: "flood",
file: NOTE,
argv: ["sh", "-c", "yes 'REA x'; : {file}"],
output: "lines",
timeout_s: 30,
}),
);
expect(view?.error).toContain("printed more than 1 MiB");
expect(Date.now() - started).toBeLessThan(10_000);
});
it("stops the reader, the processes it started and its folder when the export is cancelled", async () => {
const { home } = await computer();
const out = join(home, "..", "out");
await mkdir(out);
const controller = new AbortController();
const read = readerViewsOfBytes(
[
{
id: "stuck",
file: NOTE,
// Python, not the system's sleep, whose environment macOS withholds from the run token sweep.
argv: [
"python3",
"-c",
`import os, time; open("${out}/cwd", "w").write(os.getcwd()); open("${out}/pid", "w").write(str(os.getpid())); time.sleep(60)`,
"{file}",
],
output: "lines",
},
],
() => Promise.resolve({ kind: "bytes", bytes: Buffer.from("milk\n") }),
(argv, timeoutS) => runReaderHere(argv, timeoutS, controller.signal),
);
const pid = async (): Promise<number> => {
for (let tries = 0; tries < 200; tries += 1) {
const text = await readFile(join(out, "pid"), "utf8").catch(() => "");
if (text.trim() !== "") return Number(text.trim());
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error("the reader never started");
};
const reader = await pid();
controller.abort(new Error("cancelled"));
await expect(read).rejects.toThrow();
expect(() => process.kill(reader, 0)).toThrow();
const folder = (await readFile(join(out, "cwd"), "utf8")).trim();
await expect(access(folder)).rejects.toThrow();
});