refactor(evm): bind offline analysis through a static descriptor

Bind existing EVM CLI/MCP metadata and the legacy service factory through one static descriptor, and register the same selected service that owns cleanup. Preserve original names, schemas, inputs/full defaults, configuration, Evidence and injection identity.

Verified exact SDK catalog/fact parity across six real EVM pairs, 59 actual EVM CLI/MCP cases, six real cross-domain workflows, six actual Codex CLI fact/reference/lifecycle trials and relevant local checks. CI run 38283026233 succeeds for exact head d999d9874b. No token reduction from wiring or fully qualified resource claim; final counters remain unknown. Continues #2016.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Ysopwnableandfactory-droid[bot] authored and GitHub committed 2026-10-13 01:26:43 +08:00
1 parent d96550f2f6
commit ad08510a11
13 files changed
+590 -48

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ graph TD
end
subgraph "Production Composition"
COMPOSITION["src/composition/<br/>fresh typed provider and session factories<br/>one-shot binding and independent optional loading<br/>static WASM descriptor: named contract, factory, retryable lifetime"]
COMPOSITION["src/composition/<br/>fresh typed provider and session factories<br/>one-shot binding and independent optional loading<br/>static WASM/EVM descriptors: named contracts, factories, retryable lifetimes"]
end
subgraph "Adapters"
+7 -2
View File
@@ -11,6 +11,10 @@ Follow-up: [native PE/DOS entry views and real model verification](native-entry-
records the post-merge native continuation, including a small-function case where
four selected replies cost more bytes than the complete dossier.
Follow-up: [EVM descriptor and real-template controls](evm-descriptor.md) extends
the offline corpus with digest-pinned Uniswap runtime templates and an independent
published ABI selector oracle, preserving unknown deployment/immutable metadata.
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
@@ -40,8 +44,9 @@ acceptance conditions for every optimization.
experiment control; it does not itself establish reduced CPU consumption.
Migrate domain by domain with focused real-artifact checks and relevant shared
contract checks. The domain matrix is a growing roadmap; native PE and DOS, for
example, remain unverified in this study. A passed lane does not imply all
contract checks. The native follow-up verifies selected PE and DOS MZ entry facts;
DOS COM and complete native application behavior remain unverified. The domain
matrix is a growing roadmap. A passed lane does not imply all
advertised platforms/providers have passed. Newly discovered REA bugs remain
in the separate local handoff document for later team repair.
@@ -179,6 +179,62 @@
"transport_integrity": "sha512-RJFoomET6FajjG511fmQxeBQfU6M24a0aFZPqpid+ttIxanWf1VGytBG0UmsGjt07qmIPJS8U31D+aecuCucsQ==",
"derivation": "selected regular tar member package/wasm/tree-sitter-css.wasm; no target execution"
}
},
{
"id": "uniswap-v3.factory.hex",
"file": "UniswapV3Factory.runtime.hex",
"bytes": 49073,
"sha256": "fb152cc9b597ef4e54e9d72e81dd72c202050aae5e98c63df139864274504276",
"decoded_bytes": 24535,
"decoded_sha256": "95c34fe1e6b26c79ae42800041a3e2bdb6a978d19297cd74001ac6b316a48d8d",
"provenance": {
"project": "Uniswap v3-core UniswapV3Factory",
"version": "1.0.1",
"url": "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.1.tgz",
"transport_bytes": 86371,
"transport_sha256": "141b001e2a376fff867cb24e0713b2834095cb1250c13aae263d9a646446d329",
"transport_integrity": "sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==",
"archive_member": "package/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json",
"compiler_artifact_bytes": 104076,
"compiler_artifact_sha256": "599479f60ebb056804aff7b2d05bdd0830ddbb1fdfaa0b6c62c02294ca7188b0",
"derivation": "Parse the exact published Hardhat deployedBytecode field and preserve its original hex carrier with one trailing LF; preserve template placeholders",
"authority": "Published compiler runtime template, not a live deployed instance; original placeholders/immutables preserved; no execution or chain lookup",
"published_abi_function_count": 7,
"deployed_link_references": {},
"immutable_references": {
"recorded": false,
"value": null
},
"immutable_scope": "Published Hardhat artifact does not record an immutable-reference map; absence is unknown, not proof of no immutables. Runtime template bytes remain exact."
}
},
{
"id": "uniswap-v3.pool.hex",
"file": "UniswapV3Pool.runtime.hex",
"bytes": 44287,
"sha256": "84d90218eb125074383287877cdc564eca3765241e3bf6f6afdd64a922fbed2b",
"decoded_bytes": 22142,
"decoded_sha256": "3ef71385c2a22b945bead7904f24b25db877de9db51d1401f2f9daea07ea5c0c",
"provenance": {
"project": "Uniswap v3-core UniswapV3Pool",
"version": "1.0.1",
"url": "https://registry.npmjs.org/@uniswap/v3-core/-/v3-core-1.0.1.tgz",
"transport_bytes": 86371,
"transport_sha256": "141b001e2a376fff867cb24e0713b2834095cb1250c13aae263d9a646446d329",
"transport_integrity": "sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ==",
"archive_member": "package/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json",
"compiler_artifact_bytes": 111557,
"compiler_artifact_sha256": "204fb2fa7ad1a639b1df5601f035f9924b064b721b97adf60c7d5d4712643e34",
"derivation": "Parse the exact published Hardhat deployedBytecode field and preserve its original hex carrier with one trailing LF; preserve template placeholders",
"authority": "Published compiler runtime template, not a live deployed instance; original placeholders/immutables preserved; no execution or chain lookup",
"published_abi_function_count": 26,
"deployed_link_references": {},
"immutable_references": {
"recorded": false,
"value": null
},
"immutable_scope": "Published Hardhat artifact does not record an immutable-reference map; absence is unknown, not proof of no immutables. Runtime template bytes remain exact."
}
}
]
}
@@ -0,0 +1,181 @@
# EVM descriptor and real-template checkpoint
Continues [#2016](https://github.com/morluto/rea/issues/2016) after the native
checkpoint merged. The internal descriptor binds the existing exact EVM
contract, CLI name and typed service lifetime. Existing complete inputs, output,
provider selection and cleanup remain the baseline. Wiring alone does not establish
token or latency savings.
The descriptor contains only the named contract, existing CLI name and existing
factory. The legacy `createEvmInterfaceService` export remains usable. The MCP
host selects one complete service instance, preserves injection identity, passes
that same instance to registration and retains it for cleanup/retry. No bound
service wrapper, generic registry, automatic dependency acquisition or new pack
selection is introduced.
Five actual in-memory SDK boundary scenarios verify converted schemas, duplicate
registration rejection before use, malformed inputs, explicit unavailable or
incompatible-output failures with a healthy WASM peer, cleanup retry, and request
cancellation followed by recovery. Their injected ports are recording seams, not
real EVMole/WABT claims. Nine service and 26 provider-boundary cases also pass;
two unsupported-host scenarios are skipped on this Linux host. The existing seven
WASM MCP scenarios pass. Full `npm run check` passes; its approximately 3 GiB
sampled peak and `memory.high` events are excluded from performance claims.
Candidate real-artifact/catalog and model observations are recorded below.
## Public inputs and independent checks
[corpus.json](corpus.json) retains the original WETH historical example and adds
Uniswap v3-core 1.0.1 Factory/Pool compiler runtime templates from the official npm
package. Archive integrity, exact regular member, compiler JSON, original carrier
and decoded-byte digests are pinned. The carrier is the original `deployedBytecode`
field plus one trailing LF. No placeholder is rewritten and no contract is executed.
These are compiler templates, not live deployed instances. Their published Hardhat
artifacts omit immutable-reference maps; that metadata remains explicitly unknown.
The recorded empty link-reference maps do not establish absence of immutables.
WETH remains a published historical RPC example, with unknown live authenticity.
Actual control CLI observations infer 7 Factory and 26 Pool functions. The
[independent verifier](verify-evm-template.mjs) hashes each canonical published ABI
signature with caller-supplied Ethereum Keccak and checks the complete ABI-dispatch
selector multiset against the original REA response. Both sets match. This does not
establish inferred argument/mutability equivalence, deployment authenticity or
runtime semantics; all producer unknowns remain visible.
## Latest-main control
Production control: `1770f38cfc6cbe87491d0de8b8730c305c5cf8ea`, Node 24.18.0,
npm 11.16.0, matching locked dependencies and unchanged EVMole 0.9.3.
The frozen `dist/`/`bridge/` file-table SHA-256 is
`7fd6c2c37b4e166df158ec29d48d31f3712b9b087d9807ee019ee997d06ea144`.
Measurement driver SHA-256 is
`f767c0c0232be1e11918e70e3197a6919680d16688076bef28d4320662f712f7`.
Each public SDK run validates the selected named advertised schema and verifies
original carrier and decoded identities, inference/unknown scope and unchanged
input bytes. All three runs return the same complete 2,420,709-byte catalog.
| Question | Inferred functions | SDK tool-result JSON bytes | First workflow seconds |
| ------------------------ | -----------------: | -------------------------: | ---------------------: |
| WETH historical carrier | 11 | 26,760 | 11.432 |
| Uniswap Factory template | 7 | 110,424 | 3.628 |
| Uniswap Pool template | 26 | 110,998 | 11.211 |
These are initial observations, not paired performance conclusions or model-token
counts. Each workflow includes connection, complete catalog discovery and one
interface query. Full bytecode remains inline. Candidate/control repetitions must
use the same source driver, inputs, provider settings and Node/worker limits.
Preparation against earlier frozen main `b1bf4a70d44191348ca20a91a52fb663da4daed0`
also passed all 59 cases of the existing real CLI/MCP EVM verifier with privately
supplied solc 0.8.30 and strace 6.1. Those cases cover compiler configurations,
raw/hex identity, malformed/unsupported carriers, missing limiter and resource
constraints. Its measured scope took 233.1 seconds, sampled peak 1,192,730,624
bytes and last-observed CPU 181.018 seconds. This is a control, not candidate proof.
All heavy runs are serial under one CPU, 4 GiB hard memory/3 GiB early-stop, no swap
and bounded tasks. Final cgroup counters disappear at scope teardown and remain
unverified; last-observed zero limit events are not substituted for final counters.
Raw replies, tools, compiler artifacts and detailed receipts remain outside Git.
## Reproduction
Build and freeze each pinned runtime using the repository toolchain, then run
`measure.mjs --artifact uniswap-v3.factory.hex` or `uniswap-v3.pool.hex` with the
exact derived carrier, `--schemas selected`, explicit runtime/revision and a fresh
private output directory. Existing `weth.hex` answer fields and invocation remain
unchanged. The same common workflow verifies all three offline carriers.
Run `verify-evm-template.mjs` with `--artifact` selecting the corpus ID,
`--artifact-json` selecting its original published compiler JSON, `--evidence`
selecting the recorded CLI Evidence or SDK structured reply, `--keccak-module`
selecting the absolute `js-sha3/src/sha3.js` module from the private solc toolchain,
and `--output` selecting a new private receipt. It acquires no dependencies and
does not overwrite an existing receipt.
The [template model question](questions/evm-uniswap.txt) requires the same complete
inferred interface facts and explicit unknowns. An ABI-count match is not used as
a model grade.
## Candidate compatibility and real-provider verification
Candidate: `152984da771450048ee9d60db8a410994a125878`, frozen runtime file-table
SHA-256 `725563640c27eb50ddb6f977d2dfbe1bdaddf7e05c02897318bceaa36fa602d7`.
Two repetitions for each EVM input produce six passing paired fact/settings
comparisons with the same driver and explicit limits. Each report preserves both
original Evidence reference maps. The complete actual SDK catalog is byte-identical
on every pair, including order, names, descriptions, schemas and annotations;
catalog SHA-256 is
`8664e42fd5225645cfd910f440f731c67ac198d3d53aa85b5c485374c74cf603`.
Result byte counts remain unchanged. Candidate Factory/Pool selectors also match
the independent published ABI oracle.
| Question | Control seconds, repetitions 1 / 2 | Candidate seconds, repetitions 1 / 2 |
| -------- | ---------------------------------: | -----------------------------------: |
| Factory | 3.628 / 3.650 | 3.708 / 3.605 |
| Pool | 11.211 / 3.591 | 3.749 / 3.581 |
| WETH | 11.432 / 3.694 | 3.572 / 3.677 |
First controls preceded wiring; each second pair ran candidate then control.
Every measurement starts a new MCP process; host cache/page state is not fully
controlled. Slower initial controls do not establish a descriptor speedup.
Second-pair latency and last-observed CPU are similar. Sampled EVM SDK family peaks
range from 510,255,104 to 527,622,144 bytes. No scope abort occurred; final counters
remain unknown, so these measurements are descriptive.
The candidate passes all 59 existing real CLI/MCP verifier cases with solc 0.8.30
and unchanged EVMole 0.9.3. Actual syscall traces report target execution and network
sockets absent for those scenarios, and the verifier reports no remaining
descendants. The scope took 177.456 seconds, sampled peak 1,237,020,672 bytes and
last-observed CPU 144.913 seconds. Different totals from the earlier control are
not attributed to unchanged provider algorithms.
Post-descriptor public MCP regression uses the original pinned shipped corpus:
| Domain / selected question | Actual provider / scope | Calls | Seconds |
| ------------------------------------------- | -------------------------------------------------- | ----: | ------: |
| SQLite WASM counts / exports | WABT 1.0.42; selected summary and retained read | 2 | 5.516 |
| ShareX managed metadata / native boundaries | Static PE/CLI, no assembly execution | 2 | 3.710 |
| Obsidian manifest / inventory | Complete inventory retained, no AST claim | 4 | 5.376 |
| NewPipe package / MainActivity methods | Real headless JADX 0.7.1 | 3 | 21.669 |
| OpenWrt regions | Real firmware signature scan, no device execution | 1 | 3.581 |
| jq native entry | Ghidra 12.1.4, default analysis, ephemeral Program | 4 | 270.986 |
All six workflows pass named SDK output validation and original digest checks;
input bytes remain unchanged. Maximum sampled family peak is 1,284,648,960 bytes.
They establish those available Linux workflows, not paired performance or
Windows/macOS, IDA/Hopper, Android-device, DOS COM, browser or JavaScript AST
behavior. Browser/AST were not repeated for this narrow migration.
## Real model complete-output calibration
Six serial Codex CLI 0.161.0 trials request `gpt-5.6-terra`, high reasoning, with
isolated temporary profiles pointing to the frozen candidate. Resolved server-side
model identity is not inferred. Only the EVM inspection tool is enabled, matching
the earlier WETH calibration; this is not full-catalog tool selection verification.
Every trial makes one successful call and no failed call.
| Trial | Total input | Uncached input | Output | Seconds |
| --------- | ----------: | -------------: | -----: | ------: |
| Factory 1 | 66,158 | 36,974 | 1,329 | 43.483 |
| Pool 1 | 65,600 | 36,416 | 2,307 | 69.546 |
| WETH 1 | 54,700 | 25,516 | 1,144 | 56.372 |
| Factory 2 | 65,964 | 36,780 | 1,453 | 26.412 |
| Pool 2 | 65,753 | 36,569 | 1,939 | 74.706 |
| WETH 2 | 54,502 | 25,318 | 1,208 | 35.137 |
Every trial reports 29,184 cached input tokens. Selected fact/reference/lifecycle
grades all pass: original/decoded identities, every inferred selector, offset,
dispatch, argument/mutability string including null, explicit unknowns, no runtime
execution, observed Evidence references, unchanged bytes and removed profiles.
Raw transcripts and grades remain private. Sampled peaks are
594,853,888–776,335,360 bytes; last-observed CPU is 6.181–7.034 seconds. Strict
overall grades remain unverified because final resource counters are unknown;
they are not relabeled as wholly passing resource-qualified runs.
This calibrates the current complete path after compatible wiring, not a paired
model performance experiment. The next token experiment should answer the same
interface question through an explicitly selected precise view, retain full
bytecode/original Evidence under the existing lifetime, and preserve complete
defaults. Discover support before sending new options to an older server. Include
discovery, follow-up, failures and retries in the complete task cost.
+35 -17
View File
@@ -350,6 +350,38 @@ const inspectWasmTarget = async () => {
};
};
/** Identical complete interface question for pinned offline EVM carriers. */
const inspectEvmTarget = async () => {
const info = await call("inspect_evm_interface", {
path: values.path,
encoding: "hex",
});
const n = normalized(info);
if (
n.evidence_kind !== "inferred" ||
n.discovery_completeness !== "unknown" ||
n.runtime_execution !== "not-performed" ||
["kind", "hardfork", "deployment_authenticity"].some(
(key) => n.bytecode[key] !== "unknown",
) ||
(artifact.decoded_sha256 !== undefined &&
n.bytecode.sha256 !== artifact.decoded_sha256) ||
(artifact.decoded_bytes !== undefined &&
n.bytecode.bytes !== artifact.decoded_bytes)
)
throw new Error("EVM interface identity or uncertainty changed");
return {
artifact_sha256: targetSha(info),
artifact: n.artifact,
bytecode: n.bytecode,
functions: n.functions,
discovery_completeness: n.discovery_completeness,
runtime_execution: n.runtime_execution,
limitations: n.limitations,
evidence_ids: [info.evidence_id],
};
};
const workflows = {
"obsidian.asar": async () => {
if (values["javascript-question"] === "selected") {
@@ -534,23 +566,9 @@ const workflows = {
"sqlite.wasm": inspectWasmTarget,
"vscode.tree-sitter.wasm": inspectWasmTarget,
"vscode.tree-sitter-css.wasm": inspectWasmTarget,
"weth.hex": async () => {
const info = await call("inspect_evm_interface", {
path: values.path,
encoding: "hex",
});
const n = normalized(info);
return {
artifact_sha256: targetSha(info),
artifact: n.artifact,
bytecode: n.bytecode,
functions: n.functions,
discovery_completeness: n.discovery_completeness,
runtime_execution: n.runtime_execution,
limitations: n.limitations,
evidence_ids: [info.evidence_id],
};
},
"weth.hex": inspectEvmTarget,
"uniswap-v3.factory.hex": inspectEvmTarget,
"uniswap-v3.pool.hex": inspectEvmTarget,
...Object.fromEntries(
NATIVE_ARTIFACTS.map((artifact) => [
artifact,
@@ -0,0 +1 @@
Statically inspect the published Uniswap v3-core 1.0.1 compiler runtime template at {{artifact_path}} using only REA inspect_evm_interface with encoding hex. Report original carrier SHA-256/byte count, decoded bytecode SHA-256/byte count, every inferred function selector with bytecode_offset, dispatch, inferred_arguments and inferred_state_mutability exactly including null. Preserve evidence_kind, discovery_completeness, bytecode kind/hardfork/deployment_authenticity, runtime_execution, real Evidence IDs and limitations. The unchanged template came from the official published compiler artifact; constructor-dependent values and immutable-reference metadata are not established. Do not infer a live deployed instance, original source signatures or runtime behavior. No RPC, transactions, target execution, shell/file tools or other agents. Final JSON fields: artifact_sha256, artifact_bytes, bytecode_sha256, bytecode_bytes, functions, evidence_kind, discovery_completeness, bytecode_kind, hardfork, deployment_authenticity, runtime_execution, evidence_ids, limitations.
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { isAbsolute } from "node:path";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
const { values } = parseArgs({
options: Object.fromEntries(
["artifact", "artifact-json", "evidence", "keccak-module", "output"].map(
(key) => [key, { type: "string" }],
),
),
});
assert.ok(values.artifact, "Select the exact corpus artifact ID");
for (const key of ["artifact-json", "evidence", "keccak-module", "output"])
assert.ok(isAbsolute(values[key] ?? ""), `${key} requires an absolute path`);
const corpus = JSON.parse(
await readFile(new URL("./corpus.json", import.meta.url), "utf8"),
);
const selected = corpus.artifacts.find(({ id }) => id === values.artifact);
assert.ok(selected?.provenance.compiler_artifact_sha256);
const source = await readFile(values["artifact-json"]);
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
assert.equal(hash(source), selected.provenance.compiler_artifact_sha256);
assert.equal(source.length, selected.provenance.compiler_artifact_bytes);
const artifact = JSON.parse(source);
const carrier = Buffer.from(artifact.deployedBytecode + "\n");
assert.equal(hash(carrier), selected.sha256);
assert.equal(carrier.length, selected.bytes);
const { keccak_256 } = (await import(pathToFileURL(values["keccak-module"])))
.default;
assert.equal(
typeof keccak_256,
"function",
"Supply js-sha3's Ethereum Keccak implementation",
);
const canonicalType = (input) =>
input.type.startsWith("tuple")
? `(${input.components.map(canonicalType).join(",")})${input.type.slice(5)}`
: input.type;
const selectors = artifact.abi
.filter((entry) => entry.type === "function")
.map((entry) => {
const signature = `${entry.name}(${entry.inputs.map(canonicalType).join(",")})`;
return { signature, selector: `0x${keccak_256(signature).slice(0, 8)}` };
});
const reply = JSON.parse(await readFile(values.evidence, "utf8"));
const evidence = reply.structuredContent ?? reply;
assert.equal(evidence.operation, "inspect_evm_interface");
assert.equal(evidence.subject.digest.sha256, selected.sha256);
assert.equal(evidence.normalized_result.artifact.sha256, selected.sha256);
assert.equal(
evidence.normalized_result.bytecode.sha256,
selected.decoded_sha256,
);
assert.equal(
hash(Buffer.from(evidence.normalized_result.bytecode.hex, "hex")),
selected.decoded_sha256,
);
assert.equal(evidence.normalized_result.bytecode.bytes, selected.decoded_bytes);
assert.equal(evidence.normalized_result.evidence_kind, "inferred");
assert.equal(evidence.normalized_result.discovery_completeness, "unknown");
const observed = evidence.normalized_result.functions
.filter((entry) => entry.dispatch === "abi")
.map((entry) => entry.selector);
assert.deepEqual(
observed.toSorted(),
selectors.map((entry) => entry.selector).toSorted(),
);
await writeFile(
values.output,
JSON.stringify(
{
artifact: selected.id,
compiler_artifact_sha256: hash(source),
keccak_module_sha256: hash(await readFile(values["keccak-module"])),
evidence_id: evidence.evidence_id,
selector_set_matches_published_abi: true,
selectors,
scope:
"Independent published ABI-derived selector oracle; no deployed authenticity, inferred argument/mutability equivalence or runtime verification is established.",
},
null,
2,
) + "\n",
{ flag: "wx", mode: 0o600 },
);
process.stdout.write(
JSON.stringify({
artifact: selected.id,
selector_count: selectors.length,
selector_set_matches_published_abi: true,
}) + "\n",
);
+4 -5
View File
@@ -1,7 +1,6 @@
import { z } from "incur";
import { resolve } from "node:path";
import { createEvmInterfaceService } from "../composition/evm.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { evmInterfaceModule } from "../composition/evm.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { Logger } from "pino";
@@ -14,8 +13,8 @@ export const registerEvmCommands = (
logger: Logger,
environment: Readonly<NodeJS.ProcessEnv>,
): void => {
const service = createEvmInterfaceService(environment);
cli.command(CLI_COMMANDS.inspectEvmInterface, {
const service = evmInterfaceModule.create(environment);
cli.command(evmInterfaceModule.cliCommand, {
description: "Infer interface candidates from explicit local EVM bytecode",
args: z.object({
path: z.string().describe("Local bytecode carrier"),
@@ -28,7 +27,7 @@ export const registerEvmCommands = (
try {
return await logCliCommand(
logger,
CLI_COMMANDS.inspectEvmInterface,
evmInterfaceModule.cliCommand,
async () => {
const result = await service.inspect(
{ path: resolve(args.path), encoding: args.encoding },
+13
View File
@@ -1,8 +1,21 @@
import { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import { EvmoleInterfaceProvider } from "../evm/EvmoleInterfaceProvider.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { INSPECT_EVM_INTERFACE_CONTRACT } from "../contracts/evm/evmToolContracts.js";
/** Compose offline interface analysis without import-time I/O or dependency acquisition. */
export const createEvmInterfaceService = (
environment: Readonly<NodeJS.ProcessEnv> = process.env,
): EvmInterfaceService =>
new EvmInterfaceService(new EvmoleInterfaceProvider(environment));
/**
* Static binding of the existing operation, command and fresh service factory.
* The host owns injected service identity and its retryable cleanup lifetime.
* Construction performs no acquisition, target I/O or import-time execution.
*/
export const evmInterfaceModule = {
contract: INSPECT_EVM_INTERFACE_CONTRACT,
cliCommand: CLI_COMMANDS.inspectEvmInterface,
create: createEvmInterfaceService,
} as const;
+18 -15
View File
@@ -7,20 +7,23 @@ import { toolContractMetadata } from "../toolEffects.js";
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
/** Target-free EVM capabilities are independent of archive/Apple artifact providers. */
export const INSPECT_EVM_INTERFACE_CONTRACT = {
name: "inspect_evm_interface",
...toolContractMetadata("inspect_evm_interface"),
kind: "artifact-provider",
description:
"Inspect an explicit local flat EVM bytecode carrier and infer dispatch selectors, argument strings and mutability without executing the target or accessing a chain/RPC endpoint. Choose raw or hex encoding explicitly. Returns original carrier and decoded-byte SHA-256, full bytecode and interface candidates inline as inferred Evidence; bytecode kind, hardfork, deployed authenticity and discovery completeness remain unknown. Initial real-verified Linux x64 profile uses unchanged bundled EVMole 0.9.3/WASM in a bounded owned worker; EOF-style EF00 containers are unsupported. No signature lookup, wallet action or transaction broadcast.",
inputSchema: inspectEvmInterfaceInputSchema,
outputSchema: evidenceResultOf(evmInterfaceSchema),
examples: [
{
title: "Inspect a local runtime bytecode hex carrier",
input: { path: "/artifacts/runtime.hex", encoding: "hex" },
},
],
} as const satisfies ToolContract;
/** Preserve the default catalog's exact operation and ordering. */
export const EVM_TOOL_CONTRACTS = [
{
name: "inspect_evm_interface",
...toolContractMetadata("inspect_evm_interface"),
kind: "artifact-provider",
description:
"Inspect an explicit local flat EVM bytecode carrier and infer dispatch selectors, argument strings and mutability without executing the target or accessing a chain/RPC endpoint. Choose raw or hex encoding explicitly. Returns original carrier and decoded-byte SHA-256, full bytecode and interface candidates inline as inferred Evidence; bytecode kind, hardfork, deployed authenticity and discovery completeness remain unknown. Initial real-verified Linux x64 profile uses unchanged bundled EVMole 0.9.3/WASM in a bounded owned worker; EOF-style EF00 containers are unsupported. No signature lookup, wallet action or transaction broadcast.",
inputSchema: inspectEvmInterfaceInputSchema,
outputSchema: evidenceResultOf(evmInterfaceSchema),
examples: [
{
title: "Inspect a local runtime bytecode hex carrier",
input: { path: "/artifacts/runtime.hex", encoding: "hex" },
},
],
},
INSPECT_EVM_INTERFACE_CONTRACT,
] as const satisfies readonly ToolContract[];
+5 -3
View File
@@ -5,7 +5,7 @@ import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { createPeResourcesService } from "../composition/binaryDiagnostics.js";
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import { createEvmInterfaceService } from "../composition/evm.js";
import { evmInterfaceModule } from "../composition/evm.js";
import { registerEvmTools } from "./registerEvmTools.js";
import { registerRecordedCrashTools } from "./registerRecordedCrashTools.js";
import { createRecordedCrashService } from "../composition/binaryDiagnostics.js";
@@ -242,7 +242,7 @@ export const createServer = (
const environment = snapshotEnvironment(options.environment ?? process.env);
const delivery = selectToolResultDelivery(environment, options.delivery);
const evmInterface =
options.evmInterface ?? createEvmInterfaceService(environment);
options.evmInterface ?? evmInterfaceModule.create(environment);
const webSourceLocation =
options.webSourceLocation ?? createWebSourceLocationService(environment);
const webModuleTrace =
@@ -372,6 +372,7 @@ export const createServer = (
toolContext,
android,
wasmArtifact,
evmInterface,
jeb,
firmwareAnalysis,
);
@@ -449,6 +450,7 @@ const registerConfiguredAnalysisTools = (
}: ServerToolContext,
android: AndroidAnalysisPort,
wasmArtifact: Pick<WasmArtifactService, "inspect" | "close">,
evmInterface: EvmInterfaceService,
jeb: JebAnalysisPort,
firmwareAnalysis: FirmwareAnalysisService,
): void => {
@@ -505,7 +507,7 @@ const registerConfiguredAnalysisTools = (
);
registerEvmTools(
server,
options.evmInterface ?? createEvmInterfaceService(environment),
evmInterface,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
+2 -2
View File
@@ -2,7 +2,7 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import { evmInterfaceModule } from "../composition/evm.js";
import type { Logger } from "pino";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
@@ -16,7 +16,7 @@ export const registerEvmTools = (
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_evm_interface");
const contract = evmInterfaceModule.contract;
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
+172 -3
View File
@@ -1,12 +1,25 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it, onTestFinished } from "vitest";
import { expect, it, onTestFinished, vi } from "vitest";
import { EvmInterfaceService } from "../../../src/application/evm/EvmInterfaceService.js";
import { WasmArtifactService } from "../../../src/application/wasm/WasmArtifactService.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../../../src/domain/analysisErrorCore.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { ok } from "../../../src/domain/result.js";
import { err, ok } from "../../../src/domain/result.js";
import { silentLogger } from "../../../src/logger.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { registerEvmTools } from "../../../src/server/registerEvmTools.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import {
createDeferred,
createTestBinarySession,
} from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { wasmArtifactExecution } from "../../fixtures/wasm/artifact.js";
import {
evmInterfaceFixture,
evmInterfaceExecution,
@@ -54,6 +67,14 @@ it("advertises exact valid schemas and records inline EVM interface evidence wit
approval: true,
}),
).toBe(false);
expect(() => registerEvmTools(server, service, silentLogger)).toThrow(
/inspect_evm_interface.*already registered/,
);
expect(
(await client.listTools()).tools.filter(
(tool) => tool.name === "inspect_evm_interface",
),
).toHaveLength(1);
const response = await client.callTool({
name: "inspect_evm_interface",
arguments: { path: value.artifact.path, encoding: "hex" },
@@ -80,3 +101,151 @@ it("advertises exact valid schemas and records inline EVM interface evidence wit
).isError,
).toBe(true);
});
it.each(["unavailable", "incompatible-output"])(
"preserves a healthy WASM peer and explicit EVM failure: %s",
async (scenario) => {
const value = evmInterfaceFixture();
const inspect = vi.fn(() =>
Promise.resolve(
scenario === "unavailable"
? err(
new AnalysisCapabilityUnavailableError(
"fixture-evm",
"inspect_evm_interface",
"selected engine unavailable",
),
)
: ok(
evmInterfaceExecution({
...value,
artifact: { ...value.artifact, encoding: "raw" },
}),
),
),
);
const harness = await createApplicationMcpHarness({
serverOptions: {
evmInterface: new EvmInterfaceService({ inspect }),
wasmArtifact: new WasmArtifactService({
close: () => Promise.resolve(),
inspect: () => Promise.resolve(ok(wasmArtifactExecution())),
}),
},
});
onTestFinished(harness.close);
const invalid = await harness.client.callTool({
name: "inspect_evm_interface",
arguments: { path: value.artifact.path, encoding: "hex", approval: true },
});
expect(invalid.isError).toBe(true);
expect(inspect).not.toHaveBeenCalled();
const failure = await harness.client.callTool({
name: "inspect_evm_interface",
arguments: { path: value.artifact.path, encoding: "hex" },
});
expect(parseMcpToolError(failure).error.code).toBe(
scenario === "unavailable"
? "capability_unavailable"
: "unreadable_output",
);
const peer = await harness.client.callTool({
name: "inspect_wasm_artifact",
arguments: { path: "/artifacts/module.wasm" },
});
expect(peer.isError).not.toBe(true);
const parsed = toolContract("inspect_wasm_artifact").outputSchema.parse(
peer.structuredContent,
);
const evidence = parseEvidence(parsed);
expect(harness.session.evidenceById(evidence.evidence_id)).toEqual(
evidence,
);
await harness.client.ping();
},
);
it("retains the injected EVM owner and retries failed cleanup on server shutdown", async () => {
const close = vi
.fn()
.mockRejectedValueOnce(new Error("retained EVM worker"))
.mockResolvedValue(undefined);
const service = new EvmInterfaceService({
close,
inspect: () => Promise.resolve(ok(evmInterfaceExecution())),
});
const session = createTestBinarySession(() => {
throw new Error("deep provider must not start");
});
const server = createServer(
{ kind: "session", session },
{ evmInterface: service },
);
const client = new Client({ name: "evm-cleanup-retry", version: "1" });
const [a, b] = InMemoryTransport.createLinkedPair();
try {
await server.connect(b);
await client.connect(a);
const response = await client.callTool({
name: "inspect_evm_interface",
arguments: { path: "/artifacts/source-owned.hex", encoding: "hex" },
});
expect(response.isError).not.toBe(true);
await expect(server.close()).rejects.toThrow("REA session cleanup failed");
await server.close();
expect(close).toHaveBeenCalledTimes(2);
} finally {
await client.close();
await server.close();
await session.close();
}
});
it("settles the admitted EVM operation after SDK cancellation and accepts a subsequent request", async () => {
const entered = createDeferred<void>();
const stopped = createDeferred<void>();
let invocations = 0;
const harness = await createApplicationMcpHarness({
serverOptions: {
evmInterface: new EvmInterfaceService({
inspect: async (_input, options) => {
if (invocations++ > 0) return ok(evmInterfaceExecution());
const signal = options?.signal;
if (signal === undefined)
throw new Error("MCP request omitted cancellation signal");
const aborted = new Promise<void>((resolve) => {
if (signal.aborted) resolve();
else
signal.addEventListener("abort", () => resolve(), { once: true });
});
entered.resolve();
await aborted;
stopped.resolve();
return err(new AnalysisCancelledError("inspect_evm_interface"));
},
}),
},
});
onTestFinished(harness.close);
const controller = new AbortController();
const input = {
name: "inspect_evm_interface",
arguments: { path: "/artifacts/source-owned.hex", encoding: "hex" },
};
const cancellation = harness.client
.callTool(input, { signal: controller.signal })
.then(
() => "completed",
(error: unknown) => error,
);
await entered.promise;
controller.abort(new Error("SDK EVM cancellation verification"));
expect(await cancellation).toMatchObject({
message: expect.stringContaining("SDK EVM cancellation verification"),
code: "REQUEST_TIMEOUT",
});
await stopped.promise;
await harness.client.ping();
expect((await harness.client.callTool(input)).isError).not.toBe(true);
expect(invocations).toBe(2);
});