505 lines
17 KiB
JavaScript
505 lines
17 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { execFile } from "node:child_process";
|
|
import { createReadStream } from "node:fs";
|
|
import {
|
|
access,
|
|
chmod,
|
|
lstat,
|
|
mkdir,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { isAbsolute, join } from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { pathToFileURL } from "node:url";
|
|
import { parseArgs, promisify } from "node:util";
|
|
import { z } from "zod";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
|
|
const { values } = parseArgs({
|
|
options: {
|
|
"runtime-root": { type: "string" },
|
|
revision: { type: "string" },
|
|
path: { type: "string" },
|
|
"peer-path": { type: "string" },
|
|
providers: { type: "string" },
|
|
output: { type: "string" },
|
|
},
|
|
});
|
|
for (const key of ["runtime-root", "path", "peer-path", "providers", "output"])
|
|
assert.ok(isAbsolute(values[key] ?? ""), "Supply --" + key + " ABS");
|
|
assert.match(values.revision ?? "", /^[a-f0-9]{40}$/u);
|
|
assert.equal(process.platform, "linux");
|
|
assert.equal(process.arch, "x64");
|
|
assert.notEqual(
|
|
process.getuid(),
|
|
0,
|
|
"Permission faults require a non-root user",
|
|
);
|
|
const runtime = values["runtime-root"],
|
|
lab = values.output;
|
|
const providers = z
|
|
.record(z.string(), z.string())
|
|
.parse(JSON.parse(await readFile(values.providers, "utf8")));
|
|
const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
const fileSha = async (path) => {
|
|
const hash = createHash("sha256");
|
|
for await (const part of createReadStream(path)) hash.update(part);
|
|
return hash.digest("hex");
|
|
};
|
|
const sourceBefore = await fileSha(new URL(import.meta.url));
|
|
const targetBefore = await fileSha(values.path);
|
|
const peerBefore = await fileSha(values["peer-path"]);
|
|
const corpus = JSON.parse(
|
|
await readFile(new URL("./corpus.json", import.meta.url)),
|
|
);
|
|
assert.ok(corpus.artifacts.some((x) => x.sha256 === targetBefore));
|
|
assert.equal(
|
|
corpus.artifacts.find((x) => x.id === "sqlite.wasm").sha256,
|
|
peerBefore,
|
|
);
|
|
const load = (path) => import(pathToFileURL(join(runtime, "dist", path)));
|
|
const { toolContract } = await load("contracts/toolContracts.js");
|
|
const { analysisErrorProjectionSchema } = await load(
|
|
"contracts/errorSchemas.js",
|
|
);
|
|
const { parseEvidence } = await load("domain/evidence.js");
|
|
const { evmoleInterfaceReplySchema } = await load(
|
|
"evm/EvmoleInterfaceAdapter.js",
|
|
);
|
|
const {
|
|
observeOwnedProcessLineage,
|
|
observeProcessStartIdentity,
|
|
systemProcessOwnershipHost: host,
|
|
} = await load("process/ProcessOwnershipObservation.js");
|
|
const { verifyNoTokenOwnedProcesses } = await load(
|
|
"process/ProcessOwnership.js",
|
|
);
|
|
const { createVerifierRun, completeVerifierRun } = await import(
|
|
pathToFileURL(join(runtime, "scripts/lib/verifier-run.mjs"))
|
|
);
|
|
const run = createVerifierRun();
|
|
await mkdir(lab, { mode: 0o700 });
|
|
const runtimeFiles = [];
|
|
const fingerprint = async (directory) => {
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
const path = join(directory, entry.name);
|
|
if (entry.isDirectory()) await fingerprint(path);
|
|
else if (entry.isFile())
|
|
runtimeFiles.push({
|
|
path: path.slice(runtime.length + 1),
|
|
sha256: await fileSha(path),
|
|
});
|
|
else throw new Error("Runtime fingerprint must not follow symlinks");
|
|
}
|
|
};
|
|
await fingerprint(join(runtime, "dist"));
|
|
await fingerprint(join(runtime, "bridge"));
|
|
runtimeFiles.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
await writeFile(join(lab, "runtime-files.json"), JSON.stringify(runtimeFiles), {
|
|
flag: "wx",
|
|
mode: 0o600,
|
|
});
|
|
const work = join(lab, "temporary");
|
|
await mkdir(work, { mode: 0o700 });
|
|
const wrapper = join(lab, "gated-prlimit.mjs");
|
|
// Only gate command completion AFTER unchanged EVMole has produced its reply.
|
|
// This does not measure interruption inside its synchronous WASM computation.
|
|
await writeFile(
|
|
wrapper,
|
|
`#!${process.execPath}
|
|
import { spawn } from "node:child_process";
|
|
import { access, appendFile, readFile, rename, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
const gate = process.env.REA_VERIFY_EVM_GATE;
|
|
await appendFile(join(gate, "launches.log"), process.cwd() + "\\n", { mode: 0o600 });
|
|
const child = spawn("/usr/bin/prlimit", process.argv.slice(2), { stdio: "inherit" });
|
|
const code = await new Promise((resolve, reject) => {
|
|
child.once("error", reject);
|
|
child.once("exit", (code, signal) => signal ? reject(new Error(signal)) : resolve(code));
|
|
});
|
|
if (code !== 0) process.exit(code ?? 1);
|
|
if (await readFile(join(gate, "mode"), "utf8") === "hold") {
|
|
await writeFile(join(gate, "ready.pending.json"), JSON.stringify({
|
|
pid: process.pid, parent_pid: process.ppid, run_id: process.env.REA_PROCESS_RUN_ID,
|
|
root: process.cwd(), worker_exit: code,
|
|
}), { flag: "wx", mode: 0o600 });
|
|
await rename(join(gate, "ready.pending.json"), join(gate, "ready.json"));
|
|
const end = Date.now() + 15000;
|
|
for (;;) {
|
|
try { await access(join(gate, "release")); break; }
|
|
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
if (Date.now() >= end) throw new Error("Verification gate exceeded 15 seconds");
|
|
await delay(25);
|
|
}
|
|
}
|
|
`,
|
|
{ flag: "wx", mode: 0o700 },
|
|
);
|
|
const markerSchema = z.strictObject({
|
|
pid: z.number().int().positive(),
|
|
parent_pid: z.number().int().positive(),
|
|
run_id: z.string().regex(/^rea-evm-interface-[a-f0-9-]+$/u),
|
|
root: z.string(),
|
|
worker_exit: z.literal(0),
|
|
});
|
|
const roots = async () =>
|
|
(await readdir(work)).filter((x) => x.startsWith("rea-evm-interface-"));
|
|
const gateMode = async (mode) => {
|
|
await rm(join(lab, "ready.json"), { force: true });
|
|
await rm(join(lab, "ready.pending.json"), { force: true });
|
|
await rm(join(lab, "release"), { force: true });
|
|
await writeFile(join(lab, "mode"), mode, { mode: 0o600 });
|
|
};
|
|
await gateMode("off");
|
|
const client = new Client({ name: "rea-real-evm-lifecycle", version: "1" });
|
|
const environment = {
|
|
...process.env,
|
|
...providers,
|
|
TMPDIR: work,
|
|
REA_EVM_PRLIMIT_COMMAND: wrapper,
|
|
REA_VERIFY_EVM_GATE: lab,
|
|
};
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [join(runtime, "scripts/rea.mjs"), "mcp"],
|
|
env: environment,
|
|
stderr: "pipe",
|
|
});
|
|
const rows = [];
|
|
let fault;
|
|
const textValue = (reply) =>
|
|
JSON.parse(reply.content.find((x) => x.type === "text").text);
|
|
const errorValue = (reply) => {
|
|
assert.equal(reply.isError, true);
|
|
return analysisErrorProjectionSchema.parse(textValue(reply).error);
|
|
};
|
|
const evidenceValue = (reply, name) => {
|
|
assert.notEqual(reply.isError, true, JSON.stringify(reply));
|
|
return parseEvidence(
|
|
toolContract(name).outputSchema.parse(reply.structuredContent),
|
|
);
|
|
};
|
|
const request = (path = values.path, signal) =>
|
|
client.callTool(
|
|
{
|
|
name: "inspect_evm_interface",
|
|
arguments: { path, encoding: "hex" },
|
|
},
|
|
{ timeout: 45000, ...(signal === undefined ? {} : { signal }) },
|
|
);
|
|
const waitFor = async (probe, message, milliseconds = 10000) => {
|
|
const end = Date.now() + milliseconds;
|
|
while (Date.now() < end) {
|
|
const value = await probe();
|
|
if (value) return value;
|
|
await delay(50);
|
|
}
|
|
throw new Error(message);
|
|
};
|
|
const observeGate = async (expectedSuccess) => {
|
|
const marker = await waitFor(async () => {
|
|
try {
|
|
return markerSchema.parse(
|
|
JSON.parse(await readFile(join(lab, "ready.json"))),
|
|
);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") return;
|
|
throw error;
|
|
}
|
|
}, "No completed real worker at the pending command gate");
|
|
assert.equal(marker.root.startsWith(work + "/rea-evm-interface-"), true);
|
|
const rootStat = await lstat(marker.root);
|
|
assert.equal(rootStat.isDirectory(), true);
|
|
assert.equal(rootStat.uid, process.getuid());
|
|
const processRow = (await host.listProcesses()).find(
|
|
(x) => x.pid === marker.pid,
|
|
);
|
|
assert.ok(processRow, "Gated owned launcher already exited");
|
|
const ownership = {
|
|
runId: marker.run_id,
|
|
leaderPid: marker.pid,
|
|
processGroupId: processRow.processGroupId,
|
|
expectedParentPid: marker.parent_pid,
|
|
};
|
|
const lineage = await observeOwnedProcessLineage(ownership);
|
|
assert.equal(lineage.status, "verified", JSON.stringify(lineage));
|
|
const identity = await observeProcessStartIdentity(
|
|
marker.pid,
|
|
undefined,
|
|
marker.run_id,
|
|
);
|
|
assert.equal(identity?.state, "readable");
|
|
const rawBytes = await readFile(join(marker.root, "reply.json"));
|
|
const raw = JSON.parse(rawBytes);
|
|
assert.equal(raw.ok, expectedSuccess);
|
|
if (raw.ok) {
|
|
assert.equal(raw.version, "0.9.3");
|
|
evmoleInterfaceReplySchema.parse(raw.raw);
|
|
} else assert.equal(raw.reason, "format");
|
|
return {
|
|
marker,
|
|
ownership,
|
|
lineage,
|
|
identity,
|
|
root_mode: rootStat.mode & 0o777,
|
|
worker_reply_sha256: sha(rawBytes),
|
|
worker_reply: raw,
|
|
};
|
|
};
|
|
const confirmCleanup = async (launch, baseline, absentRoot) =>
|
|
waitFor(async () => {
|
|
const result = await verifyNoTokenOwnedProcesses(
|
|
launch.marker.run_id,
|
|
undefined,
|
|
baseline,
|
|
{
|
|
leaderPid: launch.ownership.leaderPid,
|
|
processGroupId: launch.ownership.processGroupId,
|
|
},
|
|
);
|
|
if (!result.cleaned) return;
|
|
if (absentRoot && (await roots()).length !== 0) return;
|
|
return result;
|
|
}, "Owned EVM cleanup not verified");
|
|
const peer = async () =>
|
|
evidenceValue(
|
|
await client.callTool(
|
|
{
|
|
name: "inspect_wasm_artifact",
|
|
arguments: {
|
|
path: values["peer-path"],
|
|
glue_paths: [],
|
|
candidate_paths: [],
|
|
detail: "summary",
|
|
},
|
|
},
|
|
{ timeout: 45000 },
|
|
),
|
|
"inspect_wasm_artifact",
|
|
);
|
|
const restore = async () => {
|
|
if (fault === undefined) return;
|
|
await chmod(fault.path, fault.mode);
|
|
fault = undefined;
|
|
};
|
|
const verifyWithCleanup = async (verify, cleanups) => {
|
|
const failures = [];
|
|
let value;
|
|
try {
|
|
value = await verify();
|
|
} catch (error) {
|
|
failures.push(error);
|
|
}
|
|
for (const cleanup of cleanups) {
|
|
try {
|
|
await cleanup();
|
|
} catch (error) {
|
|
failures.push(error);
|
|
}
|
|
}
|
|
if (failures.length > 0)
|
|
throw new AggregateError(failures, "Verification or cleanup failed");
|
|
return value;
|
|
};
|
|
const verifyLateCancellation = async (original) => {
|
|
const baseline = await host.captureBaseline();
|
|
await gateMode("hold");
|
|
const controller = new AbortController();
|
|
let settled = false;
|
|
const pending = request(values.path, controller.signal).then(
|
|
(reply) => {
|
|
settled = true;
|
|
return { reply };
|
|
},
|
|
(error) => {
|
|
settled = true;
|
|
return {
|
|
error: { name: error.name, code: error.code, message: error.message },
|
|
};
|
|
},
|
|
);
|
|
let launch;
|
|
try {
|
|
launch = await observeGate(true);
|
|
assert.deepEqual(launch.worker_reply.raw, original.raw_result);
|
|
assert.deepEqual(launch.worker_reply.bytecode, {
|
|
sha256: original.normalized_result.bytecode.sha256,
|
|
bytes: original.normalized_result.bytecode.bytes,
|
|
hex: original.normalized_result.bytecode.hex,
|
|
});
|
|
assert.equal(settled, false, "Request completed before SDK cancellation");
|
|
} finally {
|
|
controller.abort(
|
|
new Error("Late real EVM worker cancellation verification"),
|
|
);
|
|
await pending;
|
|
}
|
|
const canceled = await pending;
|
|
assert.equal(canceled.error?.code, "REQUEST_TIMEOUT");
|
|
const cleanup = await confirmCleanup(launch, baseline, true);
|
|
await assert.rejects(access(launch.marker.root), { code: "ENOENT" });
|
|
await gateMode("off");
|
|
const recovered = evidenceValue(await request(), "inspect_evm_interface");
|
|
assert.deepEqual(recovered.normalized_result, original.normalized_result);
|
|
return {
|
|
scenario: "late-cancellation",
|
|
launch,
|
|
canceled,
|
|
cleanup,
|
|
recovery_evidence_id: recovered.evidence_id,
|
|
};
|
|
};
|
|
await verifyWithCleanup(async () => {
|
|
await client.connect(transport);
|
|
const catalog = await client.listTools();
|
|
assert.equal(
|
|
catalog.tools.filter((x) => x.name === "inspect_evm_interface").length,
|
|
1,
|
|
);
|
|
const original = evidenceValue(await request(), "inspect_evm_interface");
|
|
assert.equal(original.subject.digest.sha256, targetBefore);
|
|
assert.equal(original.provider.version, "evmole@0.9.3");
|
|
const cli = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
join(runtime, "scripts/rea.mjs"),
|
|
"inspect-evm-interface",
|
|
values.path,
|
|
"hex",
|
|
"--json",
|
|
],
|
|
{ env: environment, timeout: 45000, maxBuffer: 32 * 1024 * 1024 },
|
|
);
|
|
const cliEvidence = parseEvidence(JSON.parse(cli.stdout));
|
|
assert.deepEqual(cliEvidence.normalized_result, original.normalized_result);
|
|
assert.deepEqual(cliEvidence.parameters, original.parameters);
|
|
assert.deepEqual(cliEvidence.subject, original.subject);
|
|
await writeFile(join(lab, "cli.json"), cli.stdout, {
|
|
flag: "wx",
|
|
mode: 0o600,
|
|
});
|
|
await writeFile(join(lab, "original.json"), JSON.stringify(original), {
|
|
flag: "wx",
|
|
mode: 0o600,
|
|
});
|
|
rows.push(await verifyLateCancellation(original));
|
|
|
|
const malformed = join(lab, "odd-nibble.hex");
|
|
await writeFile(malformed, "0x0\n", { flag: "wx", mode: 0o600 });
|
|
for (const scenario of ["success", "format-failure"]) {
|
|
const baseline = await host.captureBaseline();
|
|
await gateMode("hold");
|
|
const pending = request(scenario === "success" ? values.path : malformed);
|
|
let launch;
|
|
await verifyWithCleanup(async () => {
|
|
launch = await observeGate(scenario === "success");
|
|
fault = { path: launch.marker.root, mode: launch.root_mode };
|
|
await chmod(fault.path, 0o500);
|
|
await writeFile(join(lab, "release"), "release", {
|
|
flag: "wx",
|
|
mode: 0o600,
|
|
});
|
|
const failed = errorValue(await pending);
|
|
assert.equal(failed.code, "cleanup_incomplete");
|
|
assert.deepEqual(failed.details.resources, [launch.marker.root]);
|
|
assert.match(failed.details.diagnostics.reason, /EACCES/u);
|
|
const previous = failed.details.diagnostics.previous_error;
|
|
if (scenario === "success") assert.equal(previous, null);
|
|
else assert.equal(previous.category, "invalid_input");
|
|
const cleanup = await confirmCleanup(launch, baseline, false);
|
|
assert.deepEqual(await roots(), [
|
|
launch.marker.root.slice(work.length + 1),
|
|
]);
|
|
await gateMode("off");
|
|
const launchLogBefore = await readFile(join(lab, "launches.log"));
|
|
const retryBeforeRepair = errorValue(await request());
|
|
assert.equal(retryBeforeRepair.code, "cleanup_incomplete");
|
|
assert.deepEqual(retryBeforeRepair.details.resources, [
|
|
launch.marker.root,
|
|
]);
|
|
assert.deepEqual(
|
|
await readFile(join(lab, "launches.log")),
|
|
launchLogBefore,
|
|
);
|
|
const peerEvidence = await peer();
|
|
assert.equal(peerEvidence.subject.digest.sha256, peerBefore);
|
|
await restore();
|
|
const recovered = evidenceValue(await request(), "inspect_evm_interface");
|
|
assert.deepEqual(recovered.normalized_result, original.normalized_result);
|
|
await assert.rejects(access(launch.marker.root), { code: "ENOENT" });
|
|
assert.deepEqual(await roots(), []);
|
|
rows.push({
|
|
scenario: "cleanup-retry-after-" + scenario,
|
|
launch,
|
|
failed,
|
|
cleanup,
|
|
retry_before_permission_repair: retryBeforeRepair,
|
|
launch_log_sha256_before_retry: sha(launchLogBefore),
|
|
peer_evidence_id: peerEvidence.evidence_id,
|
|
recovery_evidence_id: recovered.evidence_id,
|
|
});
|
|
}, [
|
|
restore,
|
|
() => writeFile(join(lab, "release"), "release", { mode: 0o600 }),
|
|
() => pending,
|
|
]);
|
|
}
|
|
}, [
|
|
restore,
|
|
() => writeFile(join(lab, "release"), "release", { mode: 0o600 }),
|
|
() => client.close(),
|
|
() => transport.close(),
|
|
]);
|
|
assert.deepEqual(await roots(), []);
|
|
const finalRun = await completeVerifierRun(run);
|
|
assert.equal(
|
|
finalRun.process_lineage.status,
|
|
"verified",
|
|
JSON.stringify(finalRun),
|
|
);
|
|
assert.deepEqual(finalRun.process_lineage.descendants, []);
|
|
assert.equal(await fileSha(values.path), targetBefore);
|
|
assert.equal(await fileSha(values["peer-path"]), peerBefore);
|
|
assert.equal(await fileSha(new URL(import.meta.url)), sourceBefore);
|
|
const runtimeTable = JSON.stringify(runtimeFiles);
|
|
runtimeFiles.length = 0;
|
|
await fingerprint(join(runtime, "dist"));
|
|
await fingerprint(join(runtime, "bridge"));
|
|
runtimeFiles.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
assert.equal(
|
|
JSON.stringify(runtimeFiles),
|
|
runtimeTable,
|
|
"Runtime changed during verification",
|
|
);
|
|
await writeFile(
|
|
join(lab, "summary.json"),
|
|
JSON.stringify({
|
|
revision: values.revision,
|
|
verifier_run: finalRun,
|
|
verifier_sha256: sourceBefore,
|
|
runtime_file_table_sha256: sha(JSON.stringify(runtimeFiles)),
|
|
cli_default_parity: true,
|
|
fixture_sha256: await fileSha(wrapper),
|
|
artifact: { path: values.path, sha256: targetBefore },
|
|
peer: { path: values["peer-path"], sha256: peerBefore },
|
|
scope:
|
|
"Linux x64; actual EVMole reply before fixture-held command completion; no mid-WASM interruption claim",
|
|
rows,
|
|
source_bytes_unchanged: true,
|
|
target_bytes_unchanged: true,
|
|
peer_bytes_unchanged: true,
|
|
owned_runtime_roots_removed: true,
|
|
runtime_files_unchanged: true,
|
|
}),
|
|
{ flag: "wx", mode: 0o600 },
|
|
);
|
|
console.log(
|
|
JSON.stringify({ pass: true, scenarios: rows.map((x) => x.scenario) }),
|
|
);
|