From ae8051302b5834322fa00cfa31c968ae74ab6ed9 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:05:20 +0800 Subject: [PATCH] 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> --- experiments/analysis-efficiency/README.md | 4 + .../analysis-efficiency/evm-lifecycle.md | 159 ++++++ .../verify-evm-lifecycle.mjs | 504 ++++++++++++++++++ 3 files changed, 667 insertions(+) create mode 100644 experiments/analysis-efficiency/evm-lifecycle.md create mode 100644 experiments/analysis-efficiency/verify-evm-lifecycle.mjs diff --git a/experiments/analysis-efficiency/README.md b/experiments/analysis-efficiency/README.md index 894eb0347..fe6a38796 100644 --- a/experiments/analysis-efficiency/README.md +++ b/experiments/analysis-efficiency/README.md @@ -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 diff --git a/experiments/analysis-efficiency/evm-lifecycle.md b/experiments/analysis-efficiency/evm-lifecycle.md new file mode 100644 index 000000000..f02895b45 --- /dev/null +++ b/experiments/analysis-efficiency/evm-lifecycle.md @@ -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. diff --git a/experiments/analysis-efficiency/verify-evm-lifecycle.mjs b/experiments/analysis-efficiency/verify-evm-lifecycle.mjs new file mode 100644 index 000000000..0a16f78d2 --- /dev/null +++ b/experiments/analysis-efficiency/verify-evm-lifecycle.mjs @@ -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) }), +);