Files
reverse-engineering-anything/tests/boundary/providers/desktopTaskPython.test.ts
T
morluto cb4e5cfbff feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts

* feat(analysis): expose ASAR and Ghidra runtime admission controls

* feat(artifacts): expose reader admission and preserve limited recognition

* feat(javascript): expose recoverable semantic precision budgets

* feat(web): add recoverable source-map budgets and compact retained views

* feat(web): make historical capture decoding budgets recoverable

* fix(desktop): enforce selected view policies across capture and export

* feat(runtime): make Frida and LLDB retention budgets selectable

* feat(native): expose diagnostic budgets and preserve exhaustion causes

* feat(managed): expose recoverable snapshot and representation budgets

* feat(metadata): make Go and Rust inspection budgets recoverable

* feat(wasm): expose representation and producer admissions

* feat(evm): expose per-call worker resource budgets

* feat(android): make resource and string-pool omissions recoverable

* feat(producers): expose command and recorded-capture budgets

* docs(analysis): record recovery contracts and integrated validation

* fix(docs): link calibration oracle from published verification pages

* fix(docs): restore desktop task anchors and receipt links

* test(analysis): align fixtures with selectable budget contracts

* fix(analysis): remove source-location projector dependency cycle

* test(analysis): refresh budget recovery and verifier regressions

* test(analysis): preserve format and publication recovery expectations

* test(windows): align conformance with owned runtime admission

* fix(desktop): preserve selected alias kinds in exported state

* test(analysis): align provider fixtures with recoverable budgets

* test(desktop): validate emitted alias metadata structurally

* fix(ghidra): classify bounded Windows endpoint refusals

* test(cli): assert selectable artifact admission recovery

* fix(cli): preserve capture provenance in environment builds
2026-11-10 05:11:20 -05:00

639 lines
21 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import type {
DesktopTaskCheck,
DesktopTaskReader,
} from "../../../src/domain/desktop/appTask.js";
import {
evaluateTaskChecks,
type TaskFileView,
} from "../../../src/domain/desktop/appTaskChecks.js";
import {
CHECK_CASES,
CHECK_VIEWS,
} from "../../../src/domain/desktop/appTaskChecks.fixture.js";
import { SETTLE_TIMING } from "../../../src/domain/desktop/appSettle.js";
import { renderTaskVerifier } from "../../../src/domain/desktop/appTaskPython.js";
import {
readerAnswerView,
readerCommand,
} from "../../../src/domain/desktop/appTaskReaders.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
/** The protocol client calls the verifier makes, replaced so it reads the given views. */
const STUB_CLIENT = `
def find_child(card, name):
return next((child for child in card.get("children_environments") or [] if child.get("name") == name), None)
def find_extension(card, uri):
return next((ext for ext in (card.get("capabilities") or {}).get("extensions") or [] if ext.get("uri") == uri), None)
`;
/**
* Runs the verifier against an app whose files the stdin's `saves` set, each `[at, views]` from the virtual second it
* lands on: a clock only the verifier's sleeps and reads move, so its timing is exact. Read n takes `durations[n]`
* seconds, or `READ_S`, and returns the files as they are when it completes; a timeout past its limit ends it there.
*/
const DRIVER = String.raw`
import asyncio, base64, importlib.util, json, sys, types
folder, verifier = sys.argv[1], sys.argv[2]
sys.path.insert(0, folder)
from agentenv_protocol import client
scenario = json.load(sys.stdin)
saves, durations, called, reads, clock = scenario["saves"], scenario["durations"], [], [], [0.0]
answers, execs = scenario.get("answers") or {}, []
cleanup_failures = [scenario.get("cleanup_failures", 0)]
cancel_cleanup = [scenario.get("cancel_cleanup", False)]
verify_task = None
reader_started, release_reader = asyncio.Event(), asyncio.Event()
reader_calls = [0]
#: Virtual seconds each data/get takes, a binary fraction so the clock stays exact.
READ_S = 0.125
def views_at(now):
return [views for at, views in saves if at <= now][-1]
async def get_card(url, **_):
called.append(url)
extensions = [{"uri": "urn:agentenv:computer-exec/v1"}] if scenario.get("exec", True) else []
return {"name": "computer", "children_environments": [{"name": "notes", "url": "/svc/notes/agentenv"}],
"capabilities": {"extensions": extensions}}
async def invoke_extension(url, card, uri, params=None, timeout=30, verify=True, *, method=None, headers=None):
execs.append({"url": url, "uri": uri, "method": method, "timeout": timeout, **params})
if params["request_id"].startswith("rea-reader-cleanup-"):
if cancel_cleanup[0]:
cancel_cleanup[0] = False
verify_task.cancel()
await asyncio.sleep(0)
if cleanup_failures[0] > 0:
cleanup_failures[0] -= 1
return {"exit_code": 1, "stdout": "", "stderr": "permission denied", "timed_out": False}
if scenario.get("parallel_retry"):
await asyncio.sleep(0)
return {"exit_code": 0, "stdout": "", "stderr": "", "timed_out": False}
if scenario.get("concurrent"):
reader_calls[0] += 1
if reader_calls[0] == 1:
reader_started.set()
await release_reader.wait()
else:
release_reader.set()
answer = answers[json.loads("".join(params["argv"][4:]))["id"]]
stdout = answer if isinstance(answer, str) else "Python noise\n" + json.dumps(answer) + "\n"
return {"exit_code": 0, "stdout": stdout, "stderr": "", "truncated": False, "timed_out": False}
async def get_data(url, **_):
called.append(url)
reads.append(clock[0])
clock[0] += durations[len(reads) - 1] if len(reads) <= len(durations) else READ_S
views = views_at(clock[0])
files = [types.SimpleNamespace(kind="file", metadata={"urn:agentenv:data-objects/v1": {"path": path}},
file=types.SimpleNamespace(bytes=base64.b64encode(json.dumps(view).encode()).decode()))
for path, view in views.items()]
return types.SimpleNamespace(parts=[*files, types.SimpleNamespace(kind="data", data={"state": views})])
async def sleep(seconds):
clock[0] += seconds
class Cut(Exception):
"""The timeout a virtual wait_for raises."""
async def wait_for(awaitable, timeout):
limit = clock[0] + timeout
result = await awaitable
if clock[0] > limit:
clock[0] = limit
raise Cut
return result
client.get_card, client.get_data, client.invoke_extension = get_card, get_data, invoke_extension
def load_verifier():
spec = importlib.util.spec_from_file_location("verifier", verifier)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.time = types.SimpleNamespace(monotonic=lambda: clock[0])
module.asyncio = types.SimpleNamespace(sleep=sleep, wait_for=wait_for, TimeoutError=Cut,
create_task=asyncio.create_task, shield=asyncio.shield,
CancelledError=asyncio.CancelledError)
return module
module = load_verifier()
async def run():
global verify_task, module
first_error = None
if scenario.get("concurrent"):
first = asyncio.create_task(module.verify("http://mcp", "http://computer:8080"))
await asyncio.wait_for(reader_started.wait(), timeout=2)
other = load_verifier()
second = asyncio.create_task(other.verify("http://mcp", "http://computer:8080"))
results, _ = await asyncio.wait_for(asyncio.gather(first, second), timeout=5)
return results, first_error
if scenario.get("retry"):
try:
verify_task = asyncio.create_task(module.verify("http://mcp", "http://computer:8080"))
await verify_task
except (RuntimeError, asyncio.CancelledError) as error:
first_error = str(error) + " ".join(getattr(error, "__notes__", ()))
if scenario.get("reload_verifier"):
module = load_verifier()
if scenario.get("parallel_retry"):
other = load_verifier()
first = asyncio.create_task(module.verify("http://mcp", "http://computer:8080"))
second = asyncio.create_task(other.verify("http://mcp", "http://computer:8080"))
results, _ = await asyncio.gather(first, second)
return results, first_error
results = await module.verify("http://mcp", "http://computer:8080")
return results, first_error
results, first_error = asyncio.run(run())
print(json.dumps({"called": called, "reads": reads, "ended": clock[0], "results": results, "execs": execs, "first_error": first_error}))
`;
type VerifierOutput = {
called: string[];
execs: Record<string, unknown>[];
reads: number[];
ended: number;
first_error: string | null;
results: {
id: string;
result: boolean;
description: string;
settled?: boolean;
read_cut?: boolean;
}[];
};
/** The exported verifier's results on an app whose files change at the given virtual seconds. */
const runVerifier = async (
checks: readonly DesktopTaskCheck[],
saves: readonly (readonly [number, Record<string, unknown>])[],
durations: readonly number[] = [],
computer: {
readonly readers?: readonly DesktopTaskReader[];
readonly answers?: Readonly<Record<string, unknown>>;
readonly exec?: boolean;
readonly cleanupFailures?: number;
readonly retry?: boolean;
readonly cancelCleanup?: boolean;
readonly reloadVerifier?: boolean;
readonly concurrent?: boolean;
readonly parallelRetry?: boolean;
} = {},
): Promise<VerifierOutput> => {
const folder = await createTestTempDirectory("desktop-task-python-");
await mkdir(join(folder, "agentenv_protocol"));
await writeFile(join(folder, "agentenv_protocol/__init__.py"), "");
await writeFile(join(folder, "agentenv_protocol/client.py"), STUB_CLIENT);
await writeFile(
join(folder, "agentenv_protocol/types.py"),
'RPC_PATH = "/agentenv"\n',
);
const verifier = join(folder, "verify.py");
await writeFile(
verifier,
renderTaskVerifier(
{
name: "edit-notes",
prompt: "Edit.",
checks: [...checks],
...(computer.readers === undefined
? {}
: { readers: computer.readers }),
},
"notes",
),
);
return JSON.parse(
execFileSync("python3", ["-I", "-B", "-c", DRIVER, folder, verifier], {
input: JSON.stringify({
saves,
durations,
answers: computer.answers,
exec: computer.exec ?? true,
cleanup_failures: computer.cleanupFailures,
retry: computer.retry,
cancel_cleanup: computer.cancelCleanup,
reload_verifier: computer.reloadVerifier,
concurrent: computer.concurrent,
parallel_retry: computer.parallelRetry,
}),
encoding: "utf8",
}),
) as VerifierOutput;
};
const INTERVAL_S = SETTLE_TIMING.intervalMs / 1000;
const WINDOW_S = SETTLE_TIMING.windowMs / 1000;
const BOUND_S = SETTLE_TIMING.boundMs / 1000;
const today = (...lines: string[]) => ({
"Documents/today.txt": {
format: "text",
view: { lines, line_count: lines.length, truncated: false },
truncated: false,
},
});
const linesPresent = (...lines: string[]): DesktopTaskCheck => ({
id: "today",
kind: "lines_present",
file: "Documents/today.txt",
lines,
});
const graded = ({ results }: VerifierOutput) =>
results.map(({ result, settled, read_cut }) => [result, settled, read_cut]);
it("evaluates every kind of check without computer execution in the exported verifier as REA does", async () => {
const checks: DesktopTaskCheck[] = CHECK_CASES.map(([check], index) => ({
...check,
id: `check-${String(index + 1)}`,
}));
const output = await runVerifier(checks, [[0, CHECK_VIEWS]], [], {
exec: false,
});
expect(output.execs).toEqual([]);
const [card, ...data] = output.called;
expect(card).toBe("http://computer:8080");
expect(new Set(data)).toEqual(new Set(["http://computer:8080/svc/notes"]));
expect(data).toHaveLength(output.reads.length);
const typescript = evaluateTaskChecks(
checks,
new Map(Object.entries(CHECK_VIEWS)),
);
expect(output.results.map(({ id, result }) => [id, result])).toEqual(
typescript.map(({ id, passed }) => [id, passed]),
);
expect(output.results.map(({ result }) => result)).toEqual(
CHECK_CASES.map(([, expected]) => expected),
);
expect(output.results.map(({ description }) => description)).toEqual(
typescript.map(({ description }) => description),
);
});
it("grades an app that saves seconds after the agent stops on what it saved", async () => {
// Gnote saved 2.2 s after the verifier's first read in the run this missed.
const saved = 2.2;
const output = await runVerifier(
[linesPresent("milk", "olive oil")],
[
[0, today("milk")],
[saved, today("milk", "olive oil")],
],
);
expect(graded(output)).toEqual([[true, true, false]]);
// The views graded had held a whole window after the save landed.
expect(output.reads.at(-1)).toBeGreaterThanOrEqual(saved + WINDOW_S);
expect(output.reads.at(-1)).toBeLessThanOrEqual(
saved + WINDOW_S + INTERVAL_S,
);
});
it("grades files that never stop changing at the bound, on the last read, and records that they did not settle", async () => {
const saves = Array.from(
{ length: 2 * BOUND_S + 10 },
(_, index) => [index * INTERVAL_S, today(`save ${String(index)}`)] as const,
);
// The last read starts an interval before the bound and returns the save that landed as it ran.
const last = `save ${String(BOUND_S / INTERVAL_S - 1)}`;
const output = await runVerifier([linesPresent(last)], saves);
expect(output.reads.at(-1)).toBe(BOUND_S - INTERVAL_S);
expect(output.ended).toBeLessThanOrEqual(BOUND_S);
expect(graded(output)).toEqual([[true, false, false]]);
});
it("counts a slow read's changed files as quiet only from when it returned them", async () => {
// The second read starts at 0.5 s and returns a save from 2.75 s at 2.875 s; another save follows at 4.125 s.
const output = await runVerifier(
[linesPresent("milk", "olive oil", "flour")],
[
[0, today("milk")],
[2.75, today("milk", "olive oil")],
[4.125, today("milk", "olive oil", "flour")],
],
[0.125, 2.375],
);
expect(graded(output)).toEqual([[true, true, false]]);
});
it("ends reads slower than the bound at the bound, grading the last completed read", async () => {
const slow = 8;
const output = await runVerifier(
[linesPresent("milk")],
[
[0, today("milk")],
[slow + 2, today("eggs")],
],
Array.from({ length: 4 }, () => slow),
);
expect(output.reads).toEqual([0, slow]);
expect(output.ended).toBe(BOUND_S);
expect(graded(output)).toEqual([[true, false, true]]);
});
it("grades an app that saved before the check within the window plus one interval", async () => {
const output = await runVerifier(
[linesPresent("milk", "olive oil")],
[[0, today("milk", "olive oil")]],
);
expect(graded(output)).toEqual([[true, true, false]]);
expect(output.reads.at(-1)).toBeLessThanOrEqual(WINDOW_S + INTERVAL_S);
});
const READERS: readonly DesktopTaskReader[] = [
{
id: "layers",
file: "Documents/poster.xcf",
argv: ["gimp-console-2.10", "-i", "-b", "-"],
stdin: '(gimp-file-load RUN-NONINTERACTIVE "{file}" "{file}")',
output: "lines",
},
{
id: "objects",
file: "Documents/scene.blend",
argv: ["blender", "-b", "{file}", "--python-expr", "print('REA {}')"],
output: "json",
timeout_s: 20,
},
{
id: "broken",
file: "Documents/broken.xcf",
argv: ["gimp-console-2.10", "{file}"],
output: "lines",
},
{
id: "gone",
file: "Documents/gone.xcf",
argv: ["gimp-console-2.10", "{file}"],
output: "lines",
},
];
/** What REA's reader runner answers for each reader, as its last line. */
const ANSWERS: Readonly<Record<string, unknown>> = {
layers: {
view: {
format: "text",
view: {
lines: ["layer Summer Sale", "layer Background"],
line_count: 2,
truncated: false,
},
},
},
objects: {
view: {
format: "json",
view: {
Crate: { location: [2.0004, 0, 0] },
Camera: { location: [7, -6, 5] },
},
},
},
broken: {
error: "the reader exited 1; its stderr ends: cannot open the file",
},
gone: { absent: true },
};
const READER_CHECKS: DesktopTaskCheck[] = [
{
id: "renamed",
kind: "lines_present",
file: "Documents/poster.xcf",
reader: "layers",
lines: ["layer Summer Sale"],
},
{
id: "kept",
kind: "lines_absent",
file: "Documents/poster.xcf",
reader: "layers",
lines: ["layer Headline"],
},
{
id: "moved",
kind: "value_equals",
file: "Documents/scene.blend",
reader: "objects",
pointer: "/Crate/location/0",
value: 2,
tolerance: 0.001,
},
{
id: "camera",
kind: "value_equals",
file: "Documents/scene.blend",
reader: "objects",
pointer: "/Camera/location/0",
value: 8,
},
{
id: "objects",
kind: "member_count",
file: "Documents/scene.blend",
reader: "objects",
pointer: "/Crate/location",
equals: 3,
},
{
id: "broken",
kind: "line_count",
file: "Documents/broken.xcf",
reader: "broken",
contains: "layer",
at_least: 1,
},
{
id: "gone",
kind: "lines_absent",
file: "Documents/gone.xcf",
reader: "gone",
lines: ["layer"],
},
];
it("sends large reader input in the same bounded arguments as the witness", async () => {
const reader = { ...READERS[0]!, stdin: "é".repeat(65536) };
const output = await runVerifier(READER_CHECKS, [[0, CHECK_VIEWS]], [], {
readers: [reader],
answers: ANSWERS,
});
const argv = output.execs[0]?.argv as string[];
const arguments_ = JSON.parse(argv.slice(4).join("")) as Record<
string,
unknown
>;
expect(arguments_).toMatchObject({
stdin: reader.stdin,
lease: { path: expect.stringMatching(/^\/tmp\/rea-reader-/u) },
});
expect(
argv.slice(4).every((argument) => Buffer.byteLength(argument) < 131072),
).toBe(true);
});
it("reads each reader's view through the computer's computer-exec and grades it as REA does", async () => {
const output = await runVerifier(READER_CHECKS, [[0, CHECK_VIEWS]], [], {
readers: READERS,
answers: ANSWERS,
});
expect(
output.execs
.filter(
({ request_id }) =>
!String(request_id).startsWith("rea-reader-cleanup-"),
)
.map(({ url, uri, method, argv, timeout_s }) => ({
url,
uri,
method,
argv: (argv as string[]).slice(0, 4),
timeout_s,
})),
).toEqual(
READERS.map((reader) => ({
url: "http://computer:8080",
uri: "urn:agentenv:computer-exec/v1",
method: "run",
argv: readerCommand(reader, undefined).slice(0, 4),
timeout_s: (reader.timeout_s ?? 60) + 30,
})),
);
// computer-exec answers a repeated request id with its first result, so each reader's is new.
expect(new Set(output.execs.map(({ request_id }) => request_id)).size).toBe(
READERS.length * 2,
);
const readers = new Map<string, TaskFileView>();
for (const [id, answer] of Object.entries(ANSWERS)) {
const view = readerAnswerView(JSON.stringify(answer));
if (view !== undefined) readers.set(id, view);
}
const typescript = evaluateTaskChecks(
READER_CHECKS,
new Map(Object.entries(CHECK_VIEWS)),
readers,
);
expect(
output.results.map(({ id, result, description }) => [
id,
result,
description,
]),
).toEqual(
typescript.map(({ id, passed, description }) => [id, passed, description]),
);
expect(output.results.map(({ result }) => result)).toEqual([
true,
true,
true,
false,
true,
false,
true,
]);
});
it("reports a cleanup failure with the completed view and retries the same folder on the next verification", async () => {
const output = await runVerifier(READER_CHECKS, [[0, CHECK_VIEWS]], [], {
readers: [READERS[0]!],
answers: ANSWERS,
cleanupFailures: 1,
retry: true,
reloadVerifier: true,
});
expect(output.first_error).toContain("Could not remove owned reader folder");
expect(output.first_error).toContain("completed reader views");
const cleanups = output.execs.filter(({ request_id }) =>
String(request_id).startsWith("rea-reader-cleanup-"),
);
expect(cleanups).toHaveLength(3);
const lease = (entry: Record<string, unknown>) =>
JSON.parse((entry.argv as string[])[4]!);
expect(lease(cleanups[1]!)).toEqual(lease(cleanups[0]!));
expect(cleanups[1]?.request_id).not.toBe(cleanups[0]?.request_id);
});
it("keeps cleanup owned when verification is cancelled during final cleanup", async () => {
const output = await runVerifier(READER_CHECKS, [[0, CHECK_VIEWS]], [], {
readers: [READERS[0]!],
answers: ANSWERS,
cancelCleanup: true,
retry: true,
reloadVerifier: true,
});
expect(output.first_error).toContain("Reader cleanup remains owned");
expect(output.first_error).toContain("completed reader views");
const cleanups = output.execs.filter(({ request_id }) =>
String(request_id).startsWith("rea-reader-cleanup-"),
);
expect(cleanups).toHaveLength(2);
});
it("does not adopt another active verifier's reader lease", async () => {
const output = await runVerifier(READER_CHECKS, [[0, CHECK_VIEWS]], [], {
readers: [READERS[0]!],
answers: ANSWERS,
concurrent: true,
});
expect(
output.execs
.slice(0, 2)
.every(
({ request_id }) =>
!String(request_id).startsWith("rea-reader-cleanup-"),
),
).toBe(true);
});
it("joins concurrent retries of one abandoned reader cleanup", async () => {
const output = await runVerifier(READER_CHECKS, [[0, CHECK_VIEWS]], [], {
readers: [READERS[0]!],
answers: ANSWERS,
cleanupFailures: 1,
retry: true,
parallelRetry: true,
});
expect(output.first_error).toContain("Could not remove owned reader folder");
const cleanups = output.execs.filter(({ request_id }) =>
String(request_id).startsWith("rea-reader-cleanup-"),
);
expect(cleanups).toHaveLength(4);
const lease = (entry: Record<string, unknown>) =>
JSON.parse((entry.argv as string[])[4]!);
expect(lease(cleanups[1]!)).toEqual(lease(cleanups[0]!));
});
it.each([
[{ exec: false }, "offers no urn:agentenv:computer-exec/v1"],
[
{ answers: { ...ANSWERS, layers: "Traceback (most recent call last)\n" } },
"REA's reader runner",
],
])(
"raises rather than grading when the readers cannot be run (%j)",
async (computer, message) => {
await expect(
runVerifier(READER_CHECKS, [[0, CHECK_VIEWS]], [], {
readers: READERS,
answers: ANSWERS,
...computer,
}),
).rejects.toThrow(message);
},
);