test(evm): verify real worker cancellation and cleanup recovery (#2404)
Verify unchanged real EVMole worker replies through compiled CLI and stdio MCP, including late SDK cancellation, permission-induced retained cleanup failure/retry, preserved malformed-input diagnostics and a healthy real SQLite WASM peer. Add launcher receipts to prove blocked retries do not start another EVM worker. Preserve production contracts/defaults and explicit frozen-runtime/platform/measurement boundaries. Final head cf558a0661eb29bda932242abc3687b1860428e7 passes all 36 CI checks (run38313631216); manual final review and related real Factory rerun pass. Raw artifacts and receipts remain outside Git. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
8fc2043eb8
commit
ae8051302b
|
|
@ -23,6 +23,10 @@ Follow-up: [task-scoped preset lifecycle verification](preset-lifecycle.md) chec
|
|||
real Ghidra profile/snapshot boundaries, startup cancellation and retained cleanup
|
||||
retry while preserving defaults and distinguishing verification from performance.
|
||||
|
||||
Follow-up: [EVM descriptor lifecycle acceptance](evm-lifecycle.md) verifies actual
|
||||
worker replies, late SDK cancellation, permission-induced cleanup failures,
|
||||
healthy WASM peers and old CLI parity with explicit runtime and platform scope.
|
||||
|
||||
The 12 detailed checkpoint receipts were archived byte-for-byte before removal
|
||||
from the final PR tree. The archive `review-receipts-00d51e0fc-20261012` is in the
|
||||
private experiment lab outside the checkout; its `archive-index.json` records
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
# EVM descriptor lifecycle acceptance
|
||||
|
||||
Continues [#2016](https://github.com/morluto/rea/issues/2016) after
|
||||
[#2349](https://github.com/morluto/rea/pull/2349) merged. The
|
||||
[public verifier](verify-evm-lifecycle.mjs) exercises actual compiled CLI and
|
||||
stdio MCP workflows with unchanged EVMole 0.9.3/WASM. This increment adds
|
||||
verification only; production contracts, factories, algorithms and defaults
|
||||
remain unchanged. It does not introduce a plugin SDK or database cache.
|
||||
|
||||
## Runtime and public artifacts
|
||||
|
||||
The verified runtime is explicitly frozen from
|
||||
`67f817e3653efad35f7044e68fbd6f163b86864b`, built with Node 24.18.0 and the
|
||||
matching locked dependencies. Its `dist/` and `bridge/` file-table SHA-256 is
|
||||
`cf03167870ac9def929cfaab536d6bd791089031660d1b35bff20672ac07cbbf`.
|
||||
The table is checked again after each workflow; source and target bytes must
|
||||
remain unchanged before a success receipt is published.
|
||||
|
||||
Inputs are the digest-pinned [corpus](corpus.json): published historical WETH
|
||||
and official Uniswap v3-core 1.0.1 Factory/Pool compiler runtime templates.
|
||||
They are not independently authenticated live deployments. Their deployment,
|
||||
hardfork, immutable-reference and inferred-interface limitations remain as
|
||||
recorded in [the descriptor checkpoint](evm-descriptor.md). The healthy peer
|
||||
is the shipped SQLite WASM 3.53.4-build1 module, inspected by real WABT 1.0.42.
|
||||
No contract, native target, chain/RPC query or transaction is executed.
|
||||
|
||||
## Actual cancellation and retained cleanup
|
||||
|
||||
The verifier supplies a private test launcher through the existing explicit
|
||||
`REA_EVM_PRLIMIT_COMMAND` setting. It passes the original arguments to real
|
||||
`/usr/bin/prlimit`, runs the unchanged Node/EVMole worker, then briefly gates
|
||||
launcher completion after that worker exits and writes its complete reply.
|
||||
This controls a real lifecycle boundary without replacing the analysis engine.
|
||||
It adds process overhead and is not a performance comparison.
|
||||
|
||||
Before cancellation or permission changes, production inspection primitives
|
||||
verify the live launcher's run token, parent/group relationship and native
|
||||
start identity. The complete worker reply must match the original selected
|
||||
bytecode and producer facts. Filesystem changes apply only to the verified,
|
||||
non-root user's temporary runtime directory.
|
||||
|
||||
Three scenarios are verified:
|
||||
|
||||
- **Late cancellation:** the real worker has completed, while the public SDK
|
||||
request remains pending. SDK cancellation reports `REQUEST_TIMEOUT` locally;
|
||||
token sweeps and directory removal independently confirm provider cleanup.
|
||||
A subsequent complete request returns identical analysis facts.
|
||||
- **Successful analysis followed by cleanup failure:** removing write permission
|
||||
produces actual `EACCES`. MCP reports `cleanup_incomplete` and retains the
|
||||
original resource path. A retry before permission repair fails on that same
|
||||
resource. Real SQLite WASM analysis still succeeds. Restoring permission lets
|
||||
the provider retire the old resource and perform a fresh complete EVM query.
|
||||
- **Format failure followed by cleanup failure:** a deliberately malformed
|
||||
odd-nibble carrier reaches the unchanged worker. The cleanup error preserves
|
||||
its original `invalid_input` category and exact producer diagnostic rather
|
||||
than overwriting the failure. The same repair/retry and healthy-peer checks
|
||||
succeed.
|
||||
|
||||
The existing CLI `inspect-evm-interface PATH hex --json` produces the same
|
||||
complete normalized facts, parameters and subject as MCP. Fresh Evidence IDs
|
||||
are not equated. Final verifier lineage is checked after client/transport close;
|
||||
an empty descendant list is a point-in-time observation, not proof that the
|
||||
workflow never launched children. Cleanup callbacks always run and preserve
|
||||
verification and cleanup failures together.
|
||||
|
||||
This proves cancellation after actual analysis, not interruption inside
|
||||
EVMole's synchronous WASM computation. Server-shutdown cleanup retry still has
|
||||
the separately labeled injected-owner SDK tests from the descriptor checkpoint.
|
||||
These Linux x64 observations do not establish Windows/macOS provider behavior.
|
||||
Earlier real CLI/MCP malformed/unavailable/capacity checks remain linked in the
|
||||
descriptor checkpoint; they are not silently rerun or attributed to this script.
|
||||
|
||||
## Measurements and compatibility scope
|
||||
|
||||
Initial three-carrier verifier source SHA-256 is
|
||||
`2fec069b53324ef82070d323b26dd613b6da0caa7f18100adbeb39b045cf98dc`.
|
||||
All three carriers pass all three scenarios and CLI parity with this same source.
|
||||
|
||||
| Carrier | Seconds | Sampled peak family bytes | Last-observed CPU seconds |
|
||||
| ------- | ------: | ------------------------: | ------------------------: |
|
||||
| WETH | 25.595 | 1,116,884,992 | 23.926 |
|
||||
| Factory | 23.979 | 1,132,994,560 | 23.419 |
|
||||
| Pool | 24.589 | 1,141,415,936 | 24.043 |
|
||||
|
||||
Runs are serial under the existing one-CPU quota, 4 GiB hard memory, unchanged
|
||||
3 GiB early-stop, no swap and 192-task ceiling. Resource receipts, complete
|
||||
producer replies, derived malformed input, launcher and runtime copies stay in
|
||||
the private lab outside Git. Final cgroup counters disappear at scope teardown
|
||||
and remain unknown; sampled zero events do not prove final no-OOM/no-limit
|
||||
results. This is compatibility and recovery evidence, not a model-token,
|
||||
whole-task latency or CPU saving claim.
|
||||
|
||||
Latest main `81cca2e0e3ebd98c21f41d7ad63d6a00e4cbc3f9` could not build during
|
||||
setup: its shared partial-error handler passes three arguments to an interface
|
||||
requiring four. Independently completed main CI at
|
||||
`fdd8f826023c568937d237c2f97d855de283a9b8` reports the same TypeScript failure
|
||||
([run 38309578208](https://github.com/morluto/rea/actions/runs/38309578208)).
|
||||
The unrelated regression is recorded in the maintainer-requested private bug
|
||||
handoff; this change does not repair it. The maintainer independently corrected
|
||||
the producing caller in main `8fc2043eb8bcedbf34dce064f977e455438635d6`.
|
||||
The frozen-runtime observations above remain pinned to their original revision;
|
||||
synchronizing the branch does not relabel them as latest-main verification.
|
||||
|
||||
After rebasing, a fresh build and all 10 focused EVM MCP boundary tests pass.
|
||||
The same unchanged source verifier also passes all three real Factory scenarios,
|
||||
CLI parity, healthy SQLite WASM checks and final cleanup on the newly built
|
||||
runtime `b3eadf147221a2fa997ac26667a183ca5bccb1f4` (parent is corrected main
|
||||
`8fc2043eb8bcedbf34dce064f977e455438635d6`). Its `dist/`/`bridge/` file-table
|
||||
SHA-256 is `7a4cec0fa4f7e929356b50a6b13472f80bd6f526221e427125370cca3ef98d00`.
|
||||
This related rerun takes 24.285 seconds, samples 1,146,167,296 peak family bytes
|
||||
and records 23.662 last-observed CPU seconds. The older failed build and earlier
|
||||
cohort receipts are preserved; wider domain/model matrices are not repeated.
|
||||
|
||||
Static composition continues to bind one named canonical contract and one
|
||||
typed factory. Existing SDK checks reject duplicate registration before use
|
||||
and reject malformed inputs before invoking the provider. Producer contract
|
||||
violations are detected when the producer returns a result; they cannot be
|
||||
validated in advance without that result. A dynamic third-party module ABI
|
||||
preflight is not implemented or claimed. The corresponding broader acceptance
|
||||
item remains open rather than creating the deferred generic plugin registry.
|
||||
|
||||
## Final launcher review
|
||||
|
||||
A missing gate marker while gating is disabled cannot establish absence of a
|
||||
new launch. The fixture now appends a launch record before starting the actual
|
||||
worker; both permission-failure retries must leave that log byte-identical.
|
||||
This independently verifies that retained cleanup failure stops a new EVM
|
||||
launch, while preserving the original resource and healthy WASM peer.
|
||||
|
||||
Final verifier source SHA-256 is
|
||||
`fc9a50e2e54e4c8eede4b8a19283e11ddaa1faadea52eebc390b55b5a1902668`.
|
||||
All three related real Factory scenarios, CLI parity and final cleanup pass
|
||||
again on the corrected-main runtime: 23.579 seconds, 1,163,685,888 peak sampled
|
||||
family bytes and 23.110 last-observed CPU seconds. The earlier three-carrier
|
||||
receipts remain pinned to their earlier source; wider artifacts/models are
|
||||
not repeated for this review fix.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Build and freeze the explicit source revision as documented in the measurement
|
||||
README. Supply an object of explicit string-valued provider settings, including
|
||||
the caller-installed WABT paths. As a non-root Linux x64 user, run under the
|
||||
existing resource wrapper:
|
||||
|
||||
```bash
|
||||
python3 experiments/analysis-efficiency/resource-run.py -- \
|
||||
node experiments/analysis-efficiency/verify-evm-lifecycle.mjs \
|
||||
--runtime-root /absolute/frozen-runtime \
|
||||
--revision 67f817e3653efad35f7044e68fbd6f163b86864b \
|
||||
--path /absolute/UniswapV3Factory.runtime.hex \
|
||||
--peer-path /absolute/sqlite3.wasm \
|
||||
--providers /absolute/explicit-providers.json \
|
||||
--output /absolute/new-private-receipt-directory
|
||||
```
|
||||
|
||||
The verifier checks the carrier and SQLite module against the committed corpus,
|
||||
creates an owner-only receipt directory without overwriting existing receipts,
|
||||
and acquires no dependencies. Preserve failed attempts separately. A shorter
|
||||
reply or a successful cleanup verifier does not establish an agent-cost gain.
|
||||
|
|
@ -0,0 +1,504 @@
|
|||
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) }),
|
||||
);
|
||||
Loading…
Reference in New Issue