mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 06:46:10 +00:00
380 lines
13 KiB
TypeScript
380 lines
13 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { access, readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import { expect, it } from "vitest";
|
|
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { itWithCaptureCapability as captureTest } from "../../boundary/process/processCaptureCapability.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const execute = promisify(execFile);
|
|
const cli = fileURLToPath(new URL("../../../scripts/rea.mjs", import.meta.url));
|
|
const nul = "/tmp/selected\0filename";
|
|
const electron = {
|
|
executable_path: "/tmp/Electron",
|
|
application_path: "/tmp/main.js",
|
|
};
|
|
const pathRequests: readonly [string, Record<string, unknown>][] = [
|
|
["open_binary", { path: nul }],
|
|
["open_binary", { path: process.execPath, snapshot_path: nul }],
|
|
["close_binary", { snapshot_path: nul }],
|
|
["binary_session", { expected_server_path: nul }],
|
|
["export_evidence_bundle", { path: nul }],
|
|
["import_evidence_bundle", { path: nul }],
|
|
["inspect_managed_artifact", { path: nul }],
|
|
["inspect_android_package", { path: nul }],
|
|
["search_android_classes", { path: nul, query: "" }],
|
|
["inspect_android_class", { path: nul, class_name: "probe.Class" }],
|
|
[
|
|
"inspect_android_method",
|
|
{ path: nul, class_name: "probe.Class", method_name: "run" },
|
|
],
|
|
["trace_android_references", { path: nul, class_name: "probe.Class" }],
|
|
["inspect_firmware_regions", { path: nul }],
|
|
["extract_firmware", { path: nul, output_directory: "/tmp/out" }],
|
|
["extract_firmware", { path: "/tmp/firmware.bin", output_directory: nul }],
|
|
["inspect_plist", { path: nul }],
|
|
...[
|
|
{ kind: "function", name: "main\0suffix" },
|
|
{ kind: "function", name: "main", module: "lib.dylib\0suffix" },
|
|
{ kind: "objc-method", selector: "run\0suffix" },
|
|
{ kind: "objc-method", class_name: "Probe\0suffix", selector: "run" },
|
|
].map((breakpoint): [string, Record<string, unknown>] => [
|
|
"observe_native_calls",
|
|
{ breakpoints: [breakpoint] },
|
|
]),
|
|
[
|
|
"observe_native_calls",
|
|
{
|
|
breakpoints: [{ kind: "function", name: "main" }],
|
|
working_directory: nul,
|
|
},
|
|
],
|
|
[
|
|
"observe_native_calls",
|
|
{ breakpoints: [{ kind: "function", name: "main" }], arguments: ["a\0b"] },
|
|
],
|
|
["analyze_javascript_application", { input_path: nul }],
|
|
["recover_javascript_sources", { path: nul, output_directory: "/tmp/out" }],
|
|
[
|
|
"recover_javascript_sources",
|
|
{ path: "/tmp/main.js", output_directory: nul },
|
|
],
|
|
[
|
|
"compare_bundles",
|
|
{ left_bundle_path: nul, right_bundle_path: "/tmp/right.json" },
|
|
],
|
|
[
|
|
"compare_bundles",
|
|
{ left_bundle_path: "/tmp/left.json", right_bundle_path: nul },
|
|
],
|
|
[
|
|
"capture_browser_scenario",
|
|
{
|
|
browser: { mode: "launch", executable_path: nul },
|
|
start_url: { url: "http://127.0.0.1:1" },
|
|
actions: [
|
|
{ step_id: "settle", action: "wait_for_timeout", duration_ms: 0 },
|
|
],
|
|
},
|
|
],
|
|
...["executable_path", "application_path", "application_root"].map(
|
|
(key): [string, Record<string, unknown>] => [
|
|
"capture_electron_scenario",
|
|
{ ...electron, [key]: nul },
|
|
],
|
|
),
|
|
["capture_electron_scenario", { ...electron, args: ["a\0b"] }],
|
|
["demangle_swift", { symbols: ["abc\0def"] }],
|
|
];
|
|
|
|
it("rejects OS-invalid local inputs through the real advertised MCP contracts", async () => {
|
|
const { call } = await connectLocalToolsMcp();
|
|
for (const [name, arguments_] of pathRequests) {
|
|
const response = await call(name, arguments_);
|
|
expect(response.isError, name).toBe(true);
|
|
expect(JSON.stringify(response.content), name).toContain("NUL");
|
|
}
|
|
const relative = await call("extract_firmware", {
|
|
path: "/tmp/firmware.bin",
|
|
output_directory: "relative-output",
|
|
});
|
|
expect(relative.isError).toBe(true);
|
|
expect(JSON.stringify(relative.content)).toContain(
|
|
"absolute local filesystem path",
|
|
);
|
|
});
|
|
|
|
const ignoredOptions = [
|
|
{ terminal: { columns: 80, row: 12 } },
|
|
{ events: [{ type: "input", at_ms: 0, data: "x", secret: true }] },
|
|
{ events: [{ type: "resize", at_ms: 0, columns: 80, rows: 24, row: 12 }] },
|
|
{ events: [{ type: "signal", at_ms: 0, signal: "SIGTERM", pid: 1 }] },
|
|
{ normalization: { path: true } },
|
|
{ limits: { file_count: 1 } },
|
|
{
|
|
normalization: {
|
|
patterns: [{ pattern: "x", replacement: "y", regex: true }],
|
|
},
|
|
},
|
|
];
|
|
|
|
captureTest(
|
|
"rejects malformed process choices through CLI and MCP before launching the command",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-local-contracts-");
|
|
const marker = join(root, "launched");
|
|
const scenario = {
|
|
executable: process.execPath,
|
|
arguments: [
|
|
"-e",
|
|
"require('node:fs').writeFileSync(process.argv[1],'launched')",
|
|
marker,
|
|
],
|
|
};
|
|
const { call } = await connectLocalToolsMcp();
|
|
const invalid = [
|
|
...ignoredOptions,
|
|
...["", "KEY=VALUE", "KEY\0VALUE", "REA_PROCESS_RUN_ID"].map((key) => ({
|
|
environment: { [key]: "value" },
|
|
})),
|
|
{ executable: `node\0` },
|
|
{ arguments: ["\0"] },
|
|
{ working_directory: "/tmp/dir\0" },
|
|
{ environment: { KEY: "\0" } },
|
|
{ filesystem_observation_paths: ["\0"] },
|
|
{ timeout_ms: 10, events: [{ type: "input", at_ms: 11, data: "late" }] },
|
|
{
|
|
events: [
|
|
{ type: "resize", at_ms: 2, columns: 80, rows: 24 },
|
|
{ type: "input", at_ms: 1, data: "out of order" },
|
|
],
|
|
},
|
|
...["command_shims", "replay", "reactive", "checkpoints"].map(
|
|
(field) => ({
|
|
[field]: [],
|
|
}),
|
|
),
|
|
];
|
|
for (const options of invalid) {
|
|
const request = { ...scenario, ...options };
|
|
expect(
|
|
(await call("capture_process_scenario", request)).isError,
|
|
JSON.stringify(options),
|
|
).toBe(true);
|
|
const path = join(root, "scenario.json");
|
|
await writeFile(path, JSON.stringify(request));
|
|
await expect(
|
|
execute(process.execPath, [cli, "capture-process", path, "--json"]),
|
|
).rejects.toMatchObject({
|
|
code: 1,
|
|
stdout: expect.stringContaining("invalid_request"),
|
|
});
|
|
}
|
|
await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" });
|
|
const missing = join(root, "missing-executable");
|
|
const failed = await call("capture_process_scenario", {
|
|
executable: missing,
|
|
});
|
|
expect(failed.structuredContent).toMatchObject({
|
|
error: {
|
|
code: "process_capture_failed",
|
|
message: expect.stringContaining(missing),
|
|
details: { execution_failure: expect.stringContaining(missing) },
|
|
},
|
|
});
|
|
await writeFile(
|
|
join(root, "scenario.json"),
|
|
JSON.stringify({ executable: missing }),
|
|
);
|
|
await expect(
|
|
execute(process.execPath, [
|
|
cli,
|
|
"capture-process",
|
|
join(root, "scenario.json"),
|
|
"--json",
|
|
]),
|
|
).rejects.toMatchObject({
|
|
code: 1,
|
|
stdout: expect.stringContaining(missing),
|
|
});
|
|
},
|
|
60_000,
|
|
);
|
|
|
|
captureTest.each(["unknown", "truncated"] as const)(
|
|
"captures real process choices and composes a %s self-comparison",
|
|
async (status) => {
|
|
const root = await createTestTempDirectory("rea-local-capture-");
|
|
const { call } = await connectLocalToolsMcp();
|
|
const captured = await call("capture_process_scenario", {
|
|
executable: process.execPath,
|
|
arguments: [
|
|
"-e",
|
|
`require('node:fs').writeFileSync('state','selected');console.log(process.env['app.setting'],process.env.MY_PASSWORD,process.env['REA_PROCESS_RUN_ID'+String.fromCharCode(10)]);${status === "truncated" ? "console.log('x'.repeat(5000))" : ""}`,
|
|
],
|
|
working_directory: root,
|
|
environment: {
|
|
"app.setting": "value",
|
|
"1": "numbered",
|
|
MY_PASSWORD: "ordinary-evidence",
|
|
"REA_PROCESS_RUN_ID\n": "caller-value",
|
|
},
|
|
filesystem_observation_paths: [root],
|
|
limits: status === "truncated" ? { output_bytes: 128 } : {},
|
|
});
|
|
expect(captured.isError, JSON.stringify(captured)).not.toBe(true);
|
|
const capture = toolContract("capture_process_scenario").outputSchema.parse(
|
|
captured.structuredContent,
|
|
);
|
|
const source = capture.evidence;
|
|
if (status === "unknown")
|
|
expect(
|
|
capture.result.frames.map((frame) => frame.data).join(""),
|
|
).toContain("value ordinary-evidence caller-value");
|
|
else expect(capture.result.truncated).toBe(true);
|
|
const environment = await call("capture_process_scenario", {
|
|
executable: "/usr/bin/printenv",
|
|
arguments: ["1"],
|
|
environment: { "1": "numbered" },
|
|
});
|
|
const environmentEnvelope = toolContract(
|
|
"capture_process_scenario",
|
|
).outputSchema.parse(environment.structuredContent);
|
|
const received = environmentEnvelope.result.frames
|
|
.map((frame) => frame.data)
|
|
.join("");
|
|
expect(received).toBe("numbered\r\n");
|
|
expect(await readFile(join(root, "state"), "utf8")).toBe("selected");
|
|
expect(capture.result.filesystem_effects).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ status: "created" })]),
|
|
);
|
|
expect(capture.result.cleanup).toMatchObject({
|
|
owned_process_group: "verified",
|
|
temporary_root: "removed",
|
|
});
|
|
const compared = await call("compare_process_captures", {
|
|
left: source,
|
|
right: source,
|
|
});
|
|
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
|
|
const comparison = toolContract(
|
|
"compare_process_captures",
|
|
).outputSchema.parse(compared.structuredContent);
|
|
expect(comparison.result.status).toBe(status);
|
|
const verification = await call("verify_reconstruction", {
|
|
specification: {
|
|
name: "Self comparison retains uncertainty",
|
|
claims: [
|
|
{
|
|
claim_id: "control",
|
|
title: "Control capture",
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
kind: "behavioral",
|
|
dimension: "overall",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(verification.isError, JSON.stringify(verification)).not.toBe(true);
|
|
expect(verification.structuredContent).toMatchObject({
|
|
result: {
|
|
status: "unknown",
|
|
claims: {
|
|
items: [
|
|
{
|
|
left_evidence_ids: [source.evidence_id],
|
|
right_evidence_ids: [source.evidence_id],
|
|
evidence_links: expect.arrayContaining([
|
|
source.evidence_id,
|
|
comparison.evidence_id,
|
|
]),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
const behavior = await call("find_changed_behavior", {
|
|
comparisons: [comparison.evidence],
|
|
});
|
|
expect(behavior.isError, JSON.stringify(behavior)).not.toBe(true);
|
|
expect(behavior.structuredContent).toMatchObject({
|
|
result: { behavior_status: status },
|
|
});
|
|
const unknowns = await call("list_unknowns", {
|
|
domain: "process-comparison",
|
|
});
|
|
expect(unknowns.structuredContent).toMatchObject({
|
|
result: {
|
|
items: [
|
|
{
|
|
status: "open",
|
|
supporting_evidence_ids: [source.evidence_id],
|
|
contradicting_evidence_ids: [],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
const bundle = await call("get_evidence_bundle", {});
|
|
expect(
|
|
(await call("compare_process_captures", { left: source, right: source }))
|
|
.isError,
|
|
).not.toBe(true);
|
|
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
|
|
bundle.structuredContent,
|
|
);
|
|
const path = join(root, "capture.json");
|
|
await writeFile(path, JSON.stringify(source));
|
|
const cliComparison = await execute(process.execPath, [
|
|
cli,
|
|
"compare-process-captures",
|
|
path,
|
|
path,
|
|
"--json",
|
|
]);
|
|
expect(parseEvidence(JSON.parse(cliComparison.stdout))).toEqual(
|
|
comparison.evidence,
|
|
);
|
|
},
|
|
);
|
|
|
|
captureTest(
|
|
"accepts NUL as actual terminal input while preserving capture defaults",
|
|
async () => {
|
|
const { call } = await connectLocalToolsMcp();
|
|
const response = await call("capture_process_scenario", {
|
|
executable: process.execPath,
|
|
arguments: [
|
|
"-e",
|
|
"process.stdin.setRawMode(true);process.stdin.resume();process.stdin.once('data',bytes=>{console.log('stdin-byte:'+bytes[0]);process.exit(0)});console.log('ready')",
|
|
],
|
|
events: [{ type: "input", at_ms: 1000, data: "\0" }],
|
|
});
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
const capture = toolContract("capture_process_scenario").outputSchema.parse(
|
|
response.structuredContent,
|
|
);
|
|
expect(capture.result.frames.map((frame) => frame.data).join("")).toContain(
|
|
"stdin-byte:0",
|
|
);
|
|
expect(capture.result.manifest.scenario).toMatchObject({
|
|
environment: {},
|
|
filesystem_observation_paths: [],
|
|
terminal: { columns: 80, rows: 24, scrollback: 1000 },
|
|
});
|
|
expect(capture.result.interaction_events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
type: "input",
|
|
data: "\0",
|
|
outcome: "dispatched",
|
|
}),
|
|
]),
|
|
);
|
|
},
|
|
);
|