mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
Bind runtime and descriptor cleanup to exact retry leases and retain command supervisors until stop is confirmed. Preserve primary errors, partial Evidence and finalization cancellation reasons. Validate with 88 focused tests, official WABT 1.0.42 CLI/MCP verifier (51 cases), build, static and documentation checks. Command stop failures use controlled source seams; real permission failures and target execution are unverified. Fixes #4787 Fixes #4798
461 lines
15 KiB
JavaScript
461 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { isAbsolute, join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
|
|
import { parseEvidence } from "../dist/domain/evidence.js";
|
|
import {
|
|
mcpTextValue,
|
|
requireMcpToolError,
|
|
} from "./lib/mcp-verifier-results.mjs";
|
|
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
|
import { WABT_OPERATION_TIMEOUT_MS } from "./lib/wabt-verifier-timeouts.mjs";
|
|
|
|
import { verifyWabtCleanup } from "./lib/verify-wabt-cleanup.mjs";
|
|
|
|
const directory = process.env.REA_WABT_BIN_DIRECTORY;
|
|
if (!isAbsolute(directory ?? "")) {
|
|
if (process.argv.includes("--require-tools"))
|
|
throw new Error(
|
|
"Supply absolute REA_WABT_BIN_DIRECTORY containing WABT 1.0.42 tools.",
|
|
);
|
|
console.log(
|
|
JSON.stringify({
|
|
status: "skipped",
|
|
reason:
|
|
"WABT real-tool lane requires caller-supplied REA_WABT_BIN_DIRECTORY; no tools installed.",
|
|
}),
|
|
);
|
|
process.exit(0);
|
|
}
|
|
const entrypoint =
|
|
process.argv.slice(2).find((arg) => !arg.startsWith("--")) ??
|
|
fileURLToPath(new URL("./rea.mjs", import.meta.url));
|
|
const execute = promisify(execFile);
|
|
const environment = Object.fromEntries(
|
|
Object.entries(process.env).filter(([, value]) => typeof value === "string"),
|
|
);
|
|
const toolPath = (tool) =>
|
|
join(directory, `${tool}${process.platform === "win32" ? ".exe" : ""}`);
|
|
const tool = (name, args, cwd) =>
|
|
execute(toolPath(name), args, {
|
|
cwd,
|
|
env: environment,
|
|
timeout: 30_000,
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
});
|
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
const run = createVerifierRun();
|
|
const root = await PrivateRuntimeRoot.create({ prefix: "rea-wabt-verifier-" });
|
|
// Keep provider scratch outside the leased private input root. On Windows the
|
|
// root's retained DELETE handle blocks child temp creation beneath that root.
|
|
let childTempRoot;
|
|
try {
|
|
childTempRoot = await mkdtemp(join(tmpdir(), "rea-wabt-child-temp-"));
|
|
} catch (cause) {
|
|
try {
|
|
await root.close();
|
|
} catch (cleanupCause) {
|
|
throw new AggregateError(
|
|
[cause, cleanupCause],
|
|
"Could not allocate verifier temp space or release its private root.",
|
|
);
|
|
}
|
|
throw cause;
|
|
}
|
|
const client = new Client({ name: "wabt-artifact-verifier", version: "1" });
|
|
const callTool = (request) =>
|
|
client.callTool(request, { timeout: WABT_OPERATION_TIMEOUT_MS });
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [entrypoint, "mcp"],
|
|
env: {
|
|
...environment,
|
|
TMPDIR: childTempRoot,
|
|
TMP: childTempRoot,
|
|
TEMP: childTempRoot,
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
let cases = 0;
|
|
const failures = [];
|
|
try {
|
|
for (const name of ["wat2wasm", "wasm-validate", "wasm-objdump", "wasm2wat"])
|
|
assert.equal(
|
|
(await tool(name, ["--version"], root.path)).stdout.trim(),
|
|
"1.0.42",
|
|
);
|
|
await client.connect(transport);
|
|
const advertised = (await client.listTools()).tools.find(
|
|
({ name }) => name === "inspect_wasm_artifact",
|
|
);
|
|
assert.ok(advertised?.outputSchema, "WASM tool must advertise output schema");
|
|
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
|
assert.ok(ajv.validateSchema(advertised.inputSchema));
|
|
assert.ok(ajv.validateSchema(advertised.outputSchema));
|
|
const inspectCli = async (input) => {
|
|
try {
|
|
return JSON.parse(
|
|
(
|
|
await execute(
|
|
process.execPath,
|
|
[
|
|
entrypoint,
|
|
"inspect-wasm-artifact",
|
|
input.path,
|
|
...input.glue_paths.flatMap((path) => ["--glue_paths", path]),
|
|
...input.candidate_paths.flatMap((path) => [
|
|
"--candidate_paths",
|
|
path,
|
|
]),
|
|
"--format",
|
|
"json",
|
|
],
|
|
{
|
|
env: {
|
|
...environment,
|
|
TMPDIR: childTempRoot,
|
|
TMP: childTempRoot,
|
|
TEMP: childTempRoot,
|
|
},
|
|
timeout: WABT_OPERATION_TIMEOUT_MS,
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
},
|
|
)
|
|
).stdout,
|
|
);
|
|
} catch (cause) {
|
|
if (typeof cause.code === "number" && typeof cause.stdout === "string")
|
|
return JSON.parse(cause.stdout);
|
|
throw cause;
|
|
}
|
|
};
|
|
const sources = [
|
|
await readFile(
|
|
new URL("../tests/fixtures/wasm/module.wat", import.meta.url),
|
|
"utf8",
|
|
),
|
|
"(module)\n",
|
|
'(module (import "env.with.dot" "field.with.dot" (global i32)) (import "env" "table" (table 1 funcref)) (memory (export "memory") 1) (export "g" (global 0)) (export "t" (table 0)) (func (export "name\\0aline") (result i32) i32.const 42))\n',
|
|
await readFile(
|
|
new URL("../tests/fixtures/wasm/custom-names.wat", import.meta.url),
|
|
"utf8",
|
|
),
|
|
`(module
|
|
(memory 1)
|
|
(data "x")
|
|
(func (export "init")
|
|
i32.const 0
|
|
i32.const 0
|
|
i32.const 1
|
|
memory.init 0
|
|
data.drop 0))\n`,
|
|
].map((source) => ({ source, debugNames: false }));
|
|
sources.push(
|
|
{
|
|
source:
|
|
'(module $named (func (export "answer") (result i32) i32.const 42))\n',
|
|
debugNames: true,
|
|
},
|
|
{
|
|
source: await readFile(
|
|
new URL("../tests/fixtures/wasm/module-names.wat", import.meta.url),
|
|
"utf8",
|
|
),
|
|
debugNames: false,
|
|
},
|
|
);
|
|
for (const [index, { source, debugNames }] of sources.entries()) {
|
|
const fixtureRoot = join(root.path, `fixture-${index}`);
|
|
await mkdir(fixtureRoot);
|
|
const path = join(fixtureRoot, "module.wasm");
|
|
await writeFile(join(fixtureRoot, "source.wat"), source);
|
|
await tool(
|
|
"wat2wasm",
|
|
[
|
|
"--enable-all",
|
|
...(debugNames ? ["--debug-names"] : []),
|
|
"source.wat",
|
|
"-o",
|
|
"module.wasm",
|
|
],
|
|
fixtureRoot,
|
|
);
|
|
await tool("wasm-validate", ["--enable-all", "module.wasm"], fixtureRoot);
|
|
const original = await readFile(path);
|
|
// Independent modes avoid treating a custom name's text as a framing marker.
|
|
const headers = (
|
|
await tool("wasm-objdump", ["-h", "module.wasm"], fixtureRoot)
|
|
).stdout;
|
|
const details = (
|
|
await tool("wasm-objdump", ["-x", "module.wasm"], fixtureRoot)
|
|
).stdout;
|
|
const wat = (
|
|
await tool("wasm2wat", ["--enable-all", "module.wasm"], fixtureRoot)
|
|
).stdout;
|
|
await writeFile(join(fixtureRoot, "decoded.wat"), wat);
|
|
await tool(
|
|
"wat2wasm",
|
|
["--enable-all", "decoded.wat", "-o", "roundtrip.wasm"],
|
|
fixtureRoot,
|
|
);
|
|
await tool(
|
|
"wasm-validate",
|
|
["--enable-all", "roundtrip.wasm"],
|
|
fixtureRoot,
|
|
);
|
|
const candidateRoot = join(fixtureRoot, "other");
|
|
await mkdir(candidateRoot);
|
|
const candidate = join(candidateRoot, "module.wasm");
|
|
await writeFile(join(candidateRoot, "source.wat"), "(module (memory 2))");
|
|
await tool("wat2wasm", ["source.wat", "-o", "module.wasm"], candidateRoot);
|
|
const glueSource =
|
|
'fetch("./module.wasm"); new URL("https://example.invalid/module.wasm?version=1", import.meta.url);';
|
|
const glueSources = [
|
|
glueSource,
|
|
`\uFEFF${glueSource}`,
|
|
`\uFEFF\n${glueSource}`,
|
|
`#!/usr/bin/env node\n${glueSource}`,
|
|
`\uFEFF#!/usr/bin/env node\n${glueSource}`,
|
|
];
|
|
const gluePaths = glueSources.map((_, index) =>
|
|
join(fixtureRoot, `glue-${index}.js`),
|
|
);
|
|
for (const [index, gluePath] of gluePaths.entries())
|
|
await writeFile(gluePath, glueSources[index]);
|
|
const input = { path, glue_paths: gluePaths, candidate_paths: [candidate] };
|
|
assert.ok(ajv.validate(advertised.inputSchema, input));
|
|
const cliValue = await inspectCli(input);
|
|
assert.equal(cliValue.category, undefined, JSON.stringify(cliValue));
|
|
const cli = parseEvidence(cliValue);
|
|
const reply = await callTool({
|
|
name: "inspect_wasm_artifact",
|
|
arguments: input,
|
|
});
|
|
assert.notEqual(reply.isError, true, mcpTextValue(reply));
|
|
assert.ok(
|
|
ajv.validate(advertised.outputSchema, reply.structuredContent),
|
|
JSON.stringify(ajv.errors),
|
|
);
|
|
const mcp = parseEvidence(reply.structuredContent);
|
|
assert.equal(cli.evidence_id, mcp.evidence_id);
|
|
assert.deepEqual(cli, mcp);
|
|
const report = mcp.normalized_result;
|
|
assert.equal(report.artifact.sha256, sha256(original));
|
|
assert.equal(report.artifact.bytes, original.length);
|
|
assert.equal(report.wat.text, wat);
|
|
assert.equal(report.wat.sha256, sha256(wat));
|
|
assert.equal(report.headers, `${headers}\n`);
|
|
assert.equal(report.details, details.slice(1));
|
|
assert.equal(report.glue.length, glueSources.length);
|
|
for (const [index, source] of glueSources.entries()) {
|
|
const observed = report.glue[index];
|
|
assert.equal(observed.artifact.path, gluePaths[index]);
|
|
assert.equal(observed.artifact.sha256, sha256(source));
|
|
assert.equal(observed.artifact.bytes, Buffer.byteLength(source));
|
|
assert.equal(await readFile(gluePaths[index], "utf8"), source);
|
|
assert.equal(observed.parse_status, "complete");
|
|
assert.deepEqual(
|
|
observed.references.map(({ candidate_paths }) => candidate_paths),
|
|
[[path], [path, candidate]],
|
|
);
|
|
assert.deepEqual(
|
|
observed.references.map(({ line, column }) => ({ line, column })),
|
|
["./module.wasm", "https://example.invalid/module.wasm?version=1"].map(
|
|
(value) => {
|
|
const offset = source.indexOf(`"${value}"`);
|
|
return {
|
|
line: source.slice(0, offset).split("\n").length,
|
|
column: offset - source.lastIndexOf("\n", offset) - 1,
|
|
};
|
|
},
|
|
),
|
|
);
|
|
}
|
|
assert.notEqual(report.candidates[0].sha256, report.candidates[1].sha256);
|
|
for (const command of report.tool_profile.commands)
|
|
assert.equal(command.sha256, sha256(await readFile(command.path)));
|
|
if (index === 0) {
|
|
assert.ok(
|
|
report.sections.some(
|
|
({ kind, description }) =>
|
|
kind === "Custom" && description === '"test"',
|
|
),
|
|
);
|
|
assert.ok(report.wat.text.includes('(@custom "test" "payload")'));
|
|
assert.equal(
|
|
sha256(await readFile(join(fixtureRoot, "roundtrip.wasm"))),
|
|
sha256(original),
|
|
);
|
|
}
|
|
if (index === 3)
|
|
assert.deepEqual(
|
|
report.sections.map(({ description }) => description),
|
|
[
|
|
'"test\nline"',
|
|
'"fake"\nSection Details:\n Type start=0x0000000a end=0x0000000b (size=0x00000001) count: 0\n"end"',
|
|
'"unicode-é\\\t"',
|
|
'"visible"',
|
|
'"\uFEFFsection"',
|
|
'"\uFEFFname"',
|
|
'"\uFEFF"',
|
|
'""',
|
|
],
|
|
);
|
|
if (debugNames) {
|
|
assert.ok(report.headers.includes("\nmodule name: <named>\n"));
|
|
assert.deepEqual(report.exports, ['(export "answer" (func 0))']);
|
|
}
|
|
if (index === 4) {
|
|
assert.match(
|
|
headers,
|
|
/^DataCount start=0x[\da-f]+ end=0x[\da-f]+ \(size=0x[\da-f]+\) count: 1$/m,
|
|
);
|
|
assert.deepEqual(
|
|
report.sections.find(({ kind }) => kind === "DataCount"),
|
|
{
|
|
kind: "DataCount",
|
|
start: 0x23,
|
|
end: 0x24,
|
|
bytes: 1,
|
|
description: "count: 1",
|
|
},
|
|
);
|
|
}
|
|
const bundleReply = await callTool({
|
|
name: "get_evidence_bundle",
|
|
arguments: {},
|
|
});
|
|
assert.notEqual(bundleReply.isError, true, mcpTextValue(bundleReply));
|
|
const bundle = JSON.parse(mcpTextValue(bundleReply)).result;
|
|
const retained = bundle.records.find(
|
|
({ evidence_id }) => evidence_id === mcp.evidence_id,
|
|
);
|
|
assert.deepEqual(parseEvidence(retained), mcp);
|
|
assert.deepEqual(await readFile(path), original);
|
|
cases += 2;
|
|
for (const [label, invalidBytes] of [
|
|
["truncated", original.subarray(0, 7)],
|
|
["invalid", Buffer.from("not a wasm module")],
|
|
]) {
|
|
const invalidPath = join(fixtureRoot, `${label}.wasm`);
|
|
await writeFile(invalidPath, invalidBytes);
|
|
const invalidInput = {
|
|
path: invalidPath,
|
|
glue_paths: [],
|
|
candidate_paths: [],
|
|
};
|
|
const cliError = await inspectCli(invalidInput);
|
|
assert.equal(
|
|
cliError.category,
|
|
"invalid_input",
|
|
JSON.stringify(cliError),
|
|
);
|
|
const error = requireMcpToolError(
|
|
await callTool({
|
|
name: "inspect_wasm_artifact",
|
|
arguments: invalidInput,
|
|
}),
|
|
);
|
|
assert.equal(error.category, "invalid_input");
|
|
const partial = error.details.partial_observation;
|
|
assert.equal(partial.kind, "retained-evidence");
|
|
const failedBundleReply = await callTool({
|
|
name: "get_evidence_bundle",
|
|
arguments: {},
|
|
});
|
|
assert.notEqual(
|
|
failedBundleReply.isError,
|
|
true,
|
|
mcpTextValue(failedBundleReply),
|
|
);
|
|
const failedBundle = JSON.parse(mcpTextValue(failedBundleReply)).result;
|
|
const failed = parseEvidence(
|
|
failedBundle.records.find(
|
|
({ evidence_id }) => evidence_id === partial.evidence_id,
|
|
),
|
|
);
|
|
assert.equal(failed.subject.digest.sha256, sha256(invalidBytes));
|
|
assert.equal(
|
|
failed.normalized_result.validation,
|
|
"rejected-by-selected-profile",
|
|
);
|
|
assert.equal(
|
|
cliError.details.partial_observation.evidence_id,
|
|
failed.evidence_id,
|
|
);
|
|
assert.ok(
|
|
error.details?.captured_output?.stderr?.length > 0,
|
|
JSON.stringify(error),
|
|
);
|
|
assert.deepEqual(await readFile(invalidPath), invalidBytes);
|
|
cases += 2;
|
|
}
|
|
}
|
|
cases += await verifyWabtCleanup(entrypoint, environment, childTempRoot);
|
|
} catch (cause) {
|
|
failures.push(cause);
|
|
}
|
|
try {
|
|
await client.close();
|
|
} catch (cause) {
|
|
failures.push(cause);
|
|
}
|
|
try {
|
|
await transport.close();
|
|
} catch (cause) {
|
|
failures.push(cause);
|
|
}
|
|
try {
|
|
assert.ok(
|
|
!(await readdir(childTempRoot)).some((name) =>
|
|
name.startsWith("rea-wabt-"),
|
|
),
|
|
"Provider workspace leaked",
|
|
);
|
|
} catch (cause) {
|
|
failures.push(cause);
|
|
}
|
|
// Each directory is an independent owner. Attempt every cleanup even if a
|
|
// residue assertion or another owner's close fails, retaining every error.
|
|
try {
|
|
await root.close();
|
|
} catch (cause) {
|
|
failures.push(cause);
|
|
}
|
|
try {
|
|
await rm(childTempRoot, { recursive: true, force: true });
|
|
} catch (cause) {
|
|
failures.push(cause);
|
|
}
|
|
if (failures.length > 0)
|
|
throw new AggregateError(failures, "WABT real-tool verification failed.");
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
status: "passed",
|
|
cases,
|
|
tool_profile: "wabt@1.0.42",
|
|
entrypoint,
|
|
verifier: await completeVerifierRun(run),
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|