feat(evm): add retained interface summary delivery (#2308)

Add opt-in EVM MCP summary delivery and retained/portable function and text views. Keep existing complete defaults, provider arguments and original full Evidence identities and records usable. Require retention before selected analysis; preserve inferred candidates, nulls, diagnostics, provenance and explicit unknowns.

Verify old schemas, historical Evidence migration, 64 focused feature checks, 30 affected MCP checks after latest-main rebase, 59 real EVM CLI/MCP cases and six shared-domain artifact workflows. Repeated SDK delivery is smaller; all real model trials and mixed token/time outcomes, limitation failures and resource qualification gaps remain documented without a uniform performance claim.

Refs #2016

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
Ysopwnable 2026-10-13 03:09:32 +08:00 committed by GitHub
parent bb26622670
commit f76163c246
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1049 additions and 34 deletions

View File

@ -75,7 +75,11 @@ explicit path or endpoint and do not need it.
- Explicit local EVM bytecode carrier: `inspect_evm_interface` with caller-selected
`raw` or `hex` encoding. Preserve carrier/decoded digests and treat selectors,
argument strings and mutability as inferred candidates; this performs no chain
lookup or target execution. See the
lookup or target execution. For the complete inferred interface without hex or
duplicate stdout, discover support and select `detail: "summary"`; complete
Evidence stays retained under its original identity. Use the returned
`parent_evidence_id` for exact function or `bytecode_hex`/stdout/stderr pages.
Candidate coverage does not establish interface completeness. See the
[EVM bytecode guide](https://github.com/morluto/rea/blob/main/docs/evm-bytecode.md).
- Embedded Go compiler, module or build-setting metadata: `inspect_go_binary`
with the explicit local file. This built-in target-free inspection requires no

View File

@ -35,7 +35,7 @@ license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md)
and whitespace. Decoded-byte SHA-256 identifies actual selected EVM bytes.
Neither digest is the Ethereum Keccak code hash.
- Full bytecode, upstream function offsets and original producer representation
are returned inline. Offsets are byte positions in decoded bytecode.
are returned inline by default. Offsets are byte positions in decoded bytecode.
- Upstream also returns terminal metadata during selector recovery. Its reported
values remain in `raw_result`; integer precision and non-text-key coverage are
unknown in the JS binding. Full bytecode retains the original encoded metadata.
@ -47,6 +47,49 @@ license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md)
- Analysis is offline: no target runtime execution, implicit chain/RPC/explorer
lookup, wallet action, transaction broadcast or network request.
## Selected interface delivery and retained views
Existing CLI calls and MCP calls with omitted or `complete` detail keep the
complete inline result. On a retaining MCP server, select `detail: "summary"`
when the question needs every inferred function and its identity/uncertainty,
without bytecode text or duplicated raw stdout:
```json
{
"name": "inspect_evm_interface",
"arguments": {
"path": "/artifacts/runtime.hex",
"encoding": "hex",
"detail": "summary"
}
}
```
The result is distinct derived Evidence linked to its complete parent. Its
`normalized_result.summary` preserves every producer-returned selector, decoded
byte offset, dispatch, argument/mutability string including null, carrier/decoded
identities, original limitations and explicit unknowns. Payload `evidence_kind`
remains `inferred`. It preserves stderr/truncation and records stdout text length.
Complete-within-view coverage covers returned candidates; discovery completeness
remains unknown even for an empty or exhausted collection. Analysis still runs;
delivery selection does not save interpreter work or change source parameters.
Use `normalized_result.parent_evidence_id` with `inspect_analysis_view` source
kind `retained-evidence`. EVM view kind `evm` pages `functions` by original row
order, or `bytecode_hex`, `stdout`, `stderr` by UTF-16 code units. Hex pages preserve
the original lowercase text without a prefix; two characters represent one decoded
byte. A text-page offset differs from a function's decoded-byte offset. Complete
parent Evidence remains accessible through the normal ledger/export workflow;
reading views does not rerun analysis or change its identity. Existing record
retention and close semantics are unchanged.
Portable historical complete EVM Evidence can use source kind `inline` without
a provider. A fixed server without Evidence retention rejects selected summary
delivery before starting analysis and continues to support complete delivery.
Discover the actual advertised schema before sending `detail` to an older server;
use its complete operation when an equivalent workflow is required. The one-shot
CLI keeps its existing complete result.
## Resource and lifecycle limits
Inputs are limited to 4 MiB, complete replies to 16 MiB and combined diagnostic

View File

@ -15,6 +15,10 @@ Follow-up: [EVM descriptor and real-template controls](evm-descriptor.md) extend
the offline corpus with digest-pinned Uniswap runtime templates and an independent
published ABI selector oracle, preserving unknown deployment/immutable metadata.
Follow-up: [EVM interface summary delivery](evm-interface-summary.md) records
the opt-in retained view, old-client/Evidence migration checks and repeated SDK
and real-model pairs, including mixed token/time results and strict-answer limits.
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

View File

@ -24,6 +24,7 @@ const sameSettings = [
"artifact",
"artifact_sha256",
"artifact_sha256_after",
"retained_verification",
"native_question",
"javascript_question",
"provider_settings_sha256",

View File

@ -0,0 +1,208 @@
# EVM interface delivery and migration checkpoint
Continues [#2016](https://github.com/morluto/rea/issues/2016) after the static
descriptor merged in [#2261](https://github.com/morluto/rea/pull/2261). Opt-in
`inspect_evm_interface` MCP `detail: "summary"` returns every inferred interface
candidate without bytecode hex or duplicate stdout. Complete Evidence remains
retained under its original identity. Delivery bytes fall; the measured model
cohort does **not** establish a stable token or task-time improvement.
## Frozen scope
Control production is `ad08510a113d7a9b8d92fb1e385757880e469869`; candidate
production is `32c7548b9ae5c8cda348653d177b1747127c74ba`. Their compiled file-table
SHA-256 values are respectively
`f68e85030cb05082520ea9f0d7650298ae316ce7bba59a8942e0cc1318ae9850` and
`978b3a014e3b2a1e1c6620364dcd793fa7cb40f19892d44cfb0312739a8c14a3`.
The SDK driver SHA-256 is
`a95d8d8e9f72c0b2b885c61907ed13054ebf7d1642f6f13b0126163840b1221a`.
Driver commit `03bff83e70e09a917341c1fe3ebffbfdc67785c5` fixes only reading the
public `get_evidence_bundle.result.records` envelope; production is unchanged.
Both runtimes use Node 24.18.0, matching locked dependencies, SDK client/server
2.3.1 and unchanged bundled EVMole 0.9.3.
Inputs are the digest-pinned WETH historical hex carrier and Uniswap v3-core
1.0.1 Factory/Pool compiler runtime templates in [corpus.json](corpus.json).
Original carrier and decoded-byte identities remain distinct. Templates are
not authenticated deployed instances; constructor-dependent values and absent
immutable-reference metadata stay unknown. No chain, target execution or
signature lookup is introduced.
## Public SDK pairs
Two pairs per artifact run serially: complete control then selected candidate,
followed by candidate then control. Each starts a fresh MCP process, discovers
the catalog and validates the actual named advertised schema. Host caches are
not independently controlled. All six fact/settings comparisons pass. Both
answer projections omit unrequested hex; original complete replies remain intact.
| Input | Complete / summary result bytes | Reduction | Control seconds, 1 / 2 | Candidate seconds, 1 / 2 |
| ------- | ------------------------------: | --------: | ---------------------: | -----------------------: |
| Factory | 110,424 / 14,442 | 86.92% | 3.670 / 3.766 | 3.664 / 3.867 |
| Pool | 110,998 / 19,922 | 82.05% | 4.110 / 3.740 | 3.977 / 3.870 |
| WETH | 26,760 / 15,378 | 42.53% | 3.671 / 3.710 | 3.930 / 3.673 |
These measurements support smaller delivery, not faster interpretation. Analysis
still runs and the full parent still incurs producer/retention costs. Catalog
count and order stay at 184 tools. The complete catalog grows from 2,422,229 to
2,429,938 compact JSON bytes (+7,709); all name/description/input definitions
grow from 512,655 to 514,074 bytes. The EVM input definition grows from 1,443 to
2,029 bytes. Only `inspect_analysis_view` and `inspect_evm_interface` definitions
change. These discovery costs are visible and are not converted into token counts.
Nine separate measurements verify new-server complete defaults, old-server
summary fallback and retained reads for all three inputs. The four-call retained
proofs retrieve the complete canonical parent plus pages and cost 160,110,
177,106 and 79,160 bytes for Factory, Pool and WETH respectively. They are
verification workflows, not examples of one-call delivery savings.
## Real Codex pairs and answer qualification
Twelve serial Codex CLI 0.161.0 trials use the existing identical questions for
each pair, the same artifact path, one enabled EVM tool, temporary MCP profiles
and requested `gpt-5.6-terra` high. The requested alias is not proof of the
resolved model identity. Trials include schema discovery, reasoning, one actual
successful EVM call and final answer. No call fails or retries. Profiles are
removed and input bytes remain unchanged. This fixed one-tool profile does not
test selection from the full catalog.
| Input / repetition | Uncached input, control → candidate | Seconds, control → candidate | Candidate delivery |
| ------------------ | ----------------------------------: | ---------------------------: | ------------------ |
| Factory / 1 | 22,901 → 25,668 | 52.485 → 48.768 | summary |
| Factory / 2 | 37,181 → 15,270 | 53.404 → 45.345 | summary |
| Pool / 1 | 36,652 → 26,657 | 57.764 → 66.642 | summary |
| Pool / 2 | 28,305 → 36,259 | 62.068 → 71.756 | complete |
| WETH / 1 | 21,128 → 25,187 | 44.869 → 46.809 | summary |
| WETH / 2 | 17,417 → 17,529 | 30.364 → 39.943 | summary |
Keep all trials, including the candidate that explicitly chose complete delivery.
Five of six candidates select summary, but uncached input improves in only two
pairs and time improves in only two. Aggregate control → candidate observations
are 362,496 → 357,770 total input; 198,912 → 211,200 cached input;
163,584 → 146,570 uncached input; 8,953 → 10,397 output; and
300.954 → 319.263 seconds. Cache state and model/service variation are not
separated; the aggregate does not establish a repeatable saving for an individual
question or a qualified performance recommendation.
Every answer passes exact core-field checks: both identities/byte counts, every
function selector/offset/dispatch/argument/mutability including null, inference,
unknown completeness/kind/hardfork/authenticity, and absent runtime execution.
All reported IDs occur in actual responses. However, the existing grader requires
the final limitation list to equal the producer list verbatim and in order.
Only three trials pass that additional check; nine fail. The questions request
preserving limitations but do not explicitly request a verbatim ordered list.
Four failures contain all original strings with extra correct scope statements
or different ordering. The other five paraphrase or omit source caveats, including
worker or upstream-metadata limitations. Do not silently waive those omissions
or turn these into twelve fully acceptable answers. The strict reports and
original transcripts remain intact; core-field success is a narrower result.
Model scope sampled peaks range from 562.5 to 710.5 MiB, with last-observed CPU
5.945–6.878 seconds. CPU includes the local process family, not remote model
computation. All heavy jobs use one CPU, a 4 GiB hard limit, 3 GiB early-stop,
no swap and 192 tasks. No scope abort occurs in this cohort. Final cgroup
counters are unavailable after teardown: final OOM/task/limit-event assertions
remain **unverified**. Last-observed counters are not substituted for final ones.
Consequently neither strict answer acceptance nor resource-qualified performance
success is claimed for the whole cohort.
## Existing-user compatibility
Omitted and explicit-complete MCP requests, the existing CLI and provider inputs
preserve their full behavior. `detail` is removed before the unchanged provider
input; original full Evidence parameters, raw result and identity are retained.
Summary receives its own identity linked to that parent. All candidate facts,
original report limitations, stderr/truncation and explicit unknowns stay inline.
Complete-within-view candidate coverage is not global discovery completeness.
Function pages retain original order; text pages use UTF-16 code units, with
hex characters distinguished from decoded-byte function offsets.
Actual new default replies for all three artifacts validate against the frozen
old advertised input/output schemas, and structured/text payloads agree. Older
servers reject neither request nor unsupported fields because the experimental
client discovers support before selecting summary and otherwise calls complete.
This is an explicit client fallback, not universal server forward compatibility.
Three authentic old-runtime records and old-created canonical bundles pass new
MCP import, retained summary, CLI inline summary and export. Original records,
IDs and bundle source bytes stay deeply equal. The reproducible verifier below
also checks function/hex pages with the analysis limiter explicitly unavailable,
so retained reads cannot silently start successful producer analysis. This
proves the pinned old/new Linux workflows, not every historical client release.
The initial retained experiment failed because its driver read the bundle's
wrong envelope. Correcting that consumer produced passing retained proofs; the
failed receipt stays archived and is not recorded as a REA defect. Separately,
one static-check attempt hit the unchanged 3 GiB early-stop. The bounded retry
passes with Node heap 512 MiB/native checker memory controls; its roughly 3 GiB
peak and soft-limit events are excluded from efficiency comparisons.
## Reproduction and checks
Use [the shared instructions](README.md), explicit frozen revisions/runtimes and
fresh private output directories. Run `measure.mjs` for `weth.hex`,
`uniswap-v3.factory.hex` and `uniswap-v3.pool.hex`, with `--schemas selected` and
paired `--delivery complete` / `summary`; run `compare.mjs` on equivalent pairs.
Add `--verify-retained` only for the separate four-call proof. Use the unchanged
[WETH](questions/evm.txt) or [template](questions/evm-uniswap.txt) question and the
same one-tool profile for `agent-trial.py`, preserving all usage and grader results.
Export complete Evidence with the old runtime's public ledger workflow, then run:
```sh
node experiments/analysis-efficiency/verify-evm-retention.mjs \
--runtime-root ABS_FROZEN_NEW_RUNTIME --revision NEW_SHA \
--bundle ABS_OLD_BUNDLE_JSON --output NEW_PRIVATE_ABS_DIR
```
The script acquires no dependencies and preserves the old bundle. Revision is
caller-supplied; retain the frozen file-table receipt alongside it. Raw JSON,
transcripts, profiles, bytecode, compiler inputs and generated artifacts stay
outside Git. This checkpoint adds source verification and curated observations.
The production change passes 64 focused EVM/WASM/generic-view/domain/service
checks and full static/docs gates. Recording provider seams establish exact
schemas, old defaults, inference/null preservation, historical identity failures,
peer availability, cancellation and cleanup retry; they are not real-provider
claims. Real-provider and shared-domain checks are recorded separately.
The same frozen candidate also passes six actual public MCP artifact workflows:
SQLite WASM with WABT summary, ShareX managed metadata, Obsidian inventory,
NewPipe with JADX, an OpenWrt region inspection, and jq entry analysis with
Ghidra. These are shared-contract regressions; they do not establish AST analysis
of all Obsidian code, full firmware reconstruction, browser coverage, or new token
savings in those domains. Native Windows PE and DOS coverage remains at the
[earlier native checkpoint](native-entry-views.md), not a new Ghidra/Windows claim.
The source retention verifier passes for all three authentic old EVM bundles,
including exact summaries, function/hex pages, CLI inline views and unchanged
exported originals with the limiter explicitly unavailable.
The frozen candidate passes all 59 existing real EVM CLI/MCP verifier cases with
caller-supplied solc 0.8.30 and strace 6.1, including malformed/unsupported
carriers, unavailable limiter and capacity failures. Its scope takes 96.076
seconds with 1,345,052,672 sampled peak bytes and 95.130 last-observed CPU seconds.
This is regression evidence, not an interpreter speedup comparison; final
resource counters remain unverified. Fresh fast static and generated-document
checks also pass after adding the migration verifier and this report.
## Latest-main integration
The branch rebases without conflict onto main
`0043d9b134c0a353e8bf85c0bde4492275f1f720`. Production integration commit
`3766749316c345ccb594b1b748747b1af9582b34` preserves the same EVM projection,
provider and delivery semantics; intervening main adds unrelated capabilities.
The above performance cohort stays pinned to its original frozen runtimes and
is not relabeled as a measurement of the larger integrated catalog.
At `cda794fb9d4a5ca2bd661853a899d579fb8a47b2`, all 30 affected EVM/WASM/generic-view
MCP tests pass again, as do full `npm run check` and `npm run docs:check`. A fresh
real Factory SDK selected-summary/retained-parent/page workflow and the original
Pool bundle's SDK/CLI migration verifier also pass against the rebuilt integrated
runtime. The latter still has an explicitly unavailable analysis limiter.
The initial integrated static-check scope stops at 3,221,446,656 sampled bytes,
just above the unchanged 3 GiB early-stop. Running the same typecheck through
its single Turbo task with less launcher overhead passes at 3,219,353,600 sampled
bytes; the subsequent full gate uses that valid cached typecheck and passes.
No memory threshold or check requirement is relaxed. Preserve both receipts;
these near-limit validation jobs are excluded from efficiency conclusions.

View File

@ -3,7 +3,7 @@ import { createReadStream } from "node:fs";
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { isDeepStrictEqual, parseArgs } from "node:util";
import {
NATIVE_ARTIFACTS,
measureNativeEntry,
@ -23,6 +23,7 @@ const { values } = parseArgs({
providers: { type: "string" },
output: { type: "string" },
delivery: { type: "string", default: "complete" },
"verify-retained": { type: "boolean", default: false },
"native-question": { type: "string", default: "entry" },
"javascript-question": { type: "string", default: "inventory" },
schemas: { type: "string", default: "default" },
@ -41,6 +42,10 @@ if (
) ||
!["inventory", "selected"].includes(values["javascript-question"]) ||
!["default", "selected"].includes(values.schemas) ||
(values["verify-retained"] &&
!["weth.hex", "uniswap-v3.factory.hex", "uniswap-v3.pool.hex"].includes(
values.artifact,
)) ||
(!NATIVE_ARTIFACTS.includes(values.artifact) &&
values["native-question"] !== "entry") ||
(values.artifact !== "jq" &&
@ -350,15 +355,44 @@ const inspectWasmTarget = async () => {
};
};
/** Identical complete interface question for pinned offline EVM carriers. */
/** Identical interface facts for complete delivery, selected summary or old-server fallback. */
const inspectEvmTarget = async () => {
const definition = catalogDefinitions.get("inspect_evm_interface");
if (!definition) throw new Error("Missing advertised EVM definition");
const input = { path: values.path, encoding: "hex" };
const supportsSummary = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(definition.inputSchema)({ ...input, detail: "summary" });
const info = await call("inspect_evm_interface", {
path: values.path,
encoding: "hex",
...input,
...(values.delivery === "summary" && supportsSummary
? { detail: "summary" }
: {}),
});
const n = normalized(info);
const result = normalized(info);
const projected = result.kind === "summary";
const n = projected ? result.summary : result;
const references = [info.evidence_id];
if (projected) {
if (
result.parent_operation !== "inspect_evm_interface" ||
result.parent_evidence_id === info.evidence_id ||
!info.evidence_links.includes(result.parent_evidence_id) ||
"hex" in n.bytecode ||
"stdout" in n.diagnostics
)
throw new Error(
"EVM summary changed source provenance or precise delivery",
);
references.push(result.parent_evidence_id);
}
if (
n.evidence_kind !== "inferred" ||
n.artifact.path !== values.path ||
n.artifact.sha256 !== inputSha ||
n.artifact.bytes !== artifact.bytes ||
n.artifact.encoding !== "hex" ||
n.discovery_completeness !== "unknown" ||
n.runtime_execution !== "not-performed" ||
["kind", "hardfork", "deployment_authenticity"].some(
@ -370,15 +404,69 @@ const inspectEvmTarget = async () => {
n.bytecode.bytes !== artifact.decoded_bytes)
)
throw new Error("EVM interface identity or uncertainty changed");
if (values["verify-retained"]) {
if (!projected)
throw new Error(
"Retained verification requires supported selected summary",
);
const bundle = (await call("get_evidence_bundle", {})).result;
const parent = bundle.records.find(
(record) => record.evidence_id === result.parent_evidence_id,
);
if (
!parent ||
parent.operation !== "inspect_evm_interface" ||
parent.subject?.digest.sha256 !== inputSha ||
parent.parameters.path !== values.path ||
parent.parameters.encoding !== "hex" ||
"detail" in parent.parameters
)
throw new Error(
"Complete original EVM source was not retained unchanged",
);
const original = normalized(parent);
const { hex: _hex, ...identity } = original.bytecode;
const { stdout, ...diagnostics } = original.diagnostics;
const expected = {
...original,
bytecode: identity,
diagnostics: { ...diagnostics, stdout_code_units: stdout.length },
};
if (!isDeepStrictEqual(expected, n))
throw new Error("EVM summary changed selected original facts");
for (const [facet, item] of [
["functions", original.functions.slice(0, 64)],
[
"bytecode_hex",
{ text: original.bytecode.hex.slice(0, 64), unit: "utf16-code-units" },
],
]) {
const page = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: { kind: "evm", facet, offset: 0, limit: 64 },
});
const v = normalized(page);
if (
!isDeepStrictEqual(v.item, item) ||
v.parent_evidence_id !== parent.evidence_id ||
v.artifact.sha256 !== inputSha
)
throw new Error("EVM retained read changed source facts or identity");
if (references.includes(page.evidence_id))
throw new Error("EVM retained read reused an Evidence identity");
references.push(page.evidence_id);
}
}
const { hex: _hex, ...bytecode } = n.bytecode;
return {
artifact_sha256: targetSha(info),
artifact: n.artifact,
bytecode: n.bytecode,
bytecode,
functions: n.functions,
discovery_completeness: n.discovery_completeness,
runtime_execution: n.runtime_execution,
limitations: n.limitations,
evidence_ids: [info.evidence_id],
evidence_ids: references,
};
};
@ -672,6 +760,7 @@ const report = {
artifact_sha256_after: finalInputSha,
delivery: values.delivery,
sdk_schema_mode: values.schemas,
retained_verification: values["verify-retained"],
native_question: NATIVE_ARTIFACTS.includes(values.artifact)
? values["native-question"]
: null,

View File

@ -98,8 +98,13 @@ def main():
check('wabt_' + key, answer.get('wabt_' + key), reference['tool_profile'][key])
elif domain == 'evm':
n = producer_results['inspect_evm_interface'][-1]['normalized_result']
if n.get('kind') == 'summary':
check('evm_summary.parent_operation', n.get('parent_operation'), 'inspect_evm_interface')
checks['evm_summary.parent_reference_observed'] = n.get('parent_evidence_id') in observed_ids
n = n['summary']
for key, expected in {'artifact_bytes': n['artifact']['bytes'], 'bytecode_sha256': n['bytecode']['sha256'], 'bytecode_bytes': n['bytecode']['bytes'], 'functions': n['functions'], 'evidence_kind': n['evidence_kind'], 'discovery_completeness': n['discovery_completeness'], 'runtime_execution': n['runtime_execution'], 'bytecode_kind': n['bytecode']['kind'], 'hardfork': n['bytecode']['hardfork'], 'deployment_authenticity': n['bytecode']['deployment_authenticity']}.items():
check(key, answer.get(key), expected)
check('evm_original_limitations', answer.get('limitations'), n['limitations'])
elif domain == 'android':
for key in ['package_name', 'version_name', 'version_code', 'matches']:
check(key, answer.get(key), reference[key])

View File

@ -0,0 +1,167 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { isAbsolute, resolve } from "node:path";
import { parseArgs, promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const { values } = parseArgs({
options: {
"runtime-root": { type: "string" },
revision: { type: "string" },
bundle: { type: "string" },
output: { type: "string" },
},
});
for (const key of ["runtime-root", "bundle", "output"])
assert.ok(values[key] && isAbsolute(values[key]), `Supply --${key} ABS`);
assert.ok(values.revision);
await mkdir(values.output, { mode: 0o700 });
const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
const bytes = await readFile(values.bundle);
const original = JSON.parse(bytes.toString("utf8"));
const parents = original.records.filter(
(record) => record.operation === "inspect_evm_interface",
);
assert.ok(parents.length > 0, "Select a bundle containing old EVM Evidence");
const entrypoint = resolve(values["runtime-root"], "scripts/rea.mjs");
const environment = {
...process.env,
REA_LOG_LEVEL: "silent",
NODE_OPTIONS: "--max-old-space-size=512",
// If a view accidentally starts analysis, its required limiter is unavailable.
REA_EVM_PRLIMIT_COMMAND: resolve(values.output, "absent-prlimit"),
};
const client = new Client({ name: "rea-old-evm-retention", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: environment,
stderr: "pipe",
});
const definitions = new Map();
const calls = [];
const call = async (name, args) => {
const definition = definitions.get(name);
assert.ok(definition, `Missing named contract ${name}`);
const reply = await client.callTool(
{ name, arguments: args },
{ toolDefinition: definition, timeout: 60_000 },
);
await writeFile(
resolve(values.output, `call-${calls.length + 1}.json`),
JSON.stringify(reply),
{ flag: "wx", mode: 0o600 },
);
assert.notEqual(reply.isError, true, JSON.stringify(reply));
const text = JSON.parse(reply.content.find((x) => x.type === "text").text);
assert.deepEqual(reply.structuredContent, text);
calls.push({ name, arguments: args, evidence_id: text.evidence_id ?? null });
return text;
};
const checked = [];
try {
await client.connect(transport);
transport.stderr?.resume();
for (const definition of (await client.listTools()).tools)
definitions.set(definition.name, definition);
await call("import_evidence_bundle", { path: values.bundle });
const imported = (await call("get_evidence_bundle", {})).result.records;
for (const parent of parents) {
assert.deepEqual(
imported.find((record) => record.evidence_id === parent.evidence_id),
parent,
);
const source = {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
};
const view = await call("inspect_analysis_view", {
source,
view: { kind: "summary" },
});
const { hex, ...bytecode } = parent.normalized_result.bytecode;
const { stdout, ...diagnostics } = parent.normalized_result.diagnostics;
assert.deepEqual(view.normalized_result.summary, {
...parent.normalized_result,
bytecode,
diagnostics: { ...diagnostics, stdout_code_units: stdout.length },
});
assert.equal(view.normalized_result.parent_evidence_id, parent.evidence_id);
assert.notEqual(view.evidence_id, parent.evidence_id);
assert.deepEqual(view.evidence_links, [parent.evidence_id]);
for (const [facet, expected] of [
["functions", parent.normalized_result.functions.slice(0, 2)],
["bytecode_hex", { text: hex.slice(0, 64), unit: "utf16-code-units" }],
]) {
const page = await call("inspect_analysis_view", {
source,
view: {
kind: "evm",
facet,
offset: 0,
limit: facet === "functions" ? 2 : 64,
},
});
assert.deepEqual(page.normalized_result.item, expected);
assert.equal(
page.normalized_result.parent_evidence_id,
parent.evidence_id,
);
assert.deepEqual(page.evidence_links, [parent.evidence_id]);
}
const inlinePath = resolve(values.output, `inline-${checked.length}.json`);
await writeFile(
inlinePath,
JSON.stringify({
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
}),
{ flag: "wx", mode: 0o600 },
);
const result = await promisify(execFile)(
process.execPath,
[entrypoint, "inspect-analysis-view", inlinePath, "--json"],
{ env: environment, timeout: 60_000, maxBuffer: 16 * 1024 * 1024 },
);
const inline = JSON.parse(result.stdout);
assert.equal(
inline.normalized_result.parent_evidence_id,
parent.evidence_id,
);
assert.deepEqual(
inline.normalized_result.summary,
view.normalized_result.summary,
);
checked.push(parent.evidence_id);
}
const path = resolve(values.output, "exported.json");
await call("export_evidence_bundle", { path });
const exported = JSON.parse(await readFile(path, "utf8"));
for (const record of original.records)
assert.deepEqual(
exported.records.find((x) => x.evidence_id === record.evidence_id),
record,
);
assert.deepEqual(await readFile(values.bundle), bytes);
} finally {
await client.close();
}
const report = {
runtime_revision: values.revision,
original_bundle_sha256: sha(bytes),
checked_original_evidence_ids: checked,
original_records_and_source_bytes_unchanged: true,
exact_summaries_pages_and_cli_inline_pass: true,
calls,
scope:
"Old EVM Evidence through actual public SDK/CLI with an unavailable analysis limiter; original inferred facts, explicit unknowns, parent identities and export records preserved. Caller supplies the frozen runtime revision; this is migration verification, not a performance pair.",
};
await writeFile(
resolve(values.output, "summary.json"),
JSON.stringify(report, null, 2),
{ flag: "wx", mode: 0o600 },
);
process.stdout.write(JSON.stringify(report) + "\n");

View File

@ -102,6 +102,38 @@ export interface AnalysisViewRetention {
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
}
/** Fail before analysis when selected delivery cannot keep its complete source. */
export const requireAnalysisViewRetention = (
operation: string,
retention: AnalysisViewRetention,
): Result<
{
readonly evidenceById: EvidenceLookup;
readonly recordEvidence: EvidenceWriter["recordEvidence"];
},
AnalysisError
> => {
if (
retention.recordEvidence === undefined ||
retention.evidenceById === undefined
)
return err(
new AnalysisCapabilityUnavailableError(
ANALYSIS_VIEW_PROVIDER.id,
operation,
"summary detail requires a server session that retains Evidence for later views",
{
userMessage:
"This server does not retain Evidence. Select detail complete to receive the complete analysis inline.",
},
),
);
return ok({
evidenceById: retention.evidenceById,
recordEvidence: retention.recordEvidence,
});
};
/**
* Retain complete analysis Evidence in the session, then project its summary
* view from the retained record. Callers page or select the rest through
@ -111,29 +143,16 @@ export const summarizeRetainedAnalysis = (
evidence: Evidence,
retention: AnalysisViewRetention,
): Result<Evidence, AnalysisError> => {
if (
retention.recordEvidence === undefined ||
retention.evidenceById === undefined
)
return err(
new AnalysisCapabilityUnavailableError(
ANALYSIS_VIEW_PROVIDER.id,
evidence.operation,
"summary detail requires a server session that retains Evidence for later views",
{
userMessage:
"This server does not retain Evidence. Select detail complete to receive the complete analysis inline.",
},
),
);
const recorded = retention.recordEvidence(evidence);
const ready = requireAnalysisViewRetention(evidence.operation, retention);
if (!ready.ok) return ready;
const recorded = ready.value.recordEvidence(evidence);
if (!recorded.ok) return recorded;
return inspectAnalysisViewValidated(
{
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
view: { kind: "summary" },
},
retention.evidenceById,
ready.value.evidenceById,
);
};

View File

@ -16,7 +16,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact or inspect_web_network_capture Evidence without repeating analysis. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. For layout, JavaScript, native, inventory and WASM sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions; occurrence logical_paths matches exact recorded strings (omitted means all available, [] means none). source_complete distinguishes complete inventories from partial source records; page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. Native views select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.',
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_evm_interface or inspect_web_network_capture Evidence without repeating analysis. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. For layout, JavaScript, native, inventory, WASM and EVM sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions; occurrence logical_paths matches exact recorded strings (omitted means all available, [] means none). source_complete distinguishes complete inventories from partial source records; page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Native views select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.',
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [

View File

@ -4,7 +4,18 @@ import {
} from "../../domain/evm/evmInterface.js";
import type { ToolContract } from "../toolContractTypes.js";
import { toolContractMetadata } from "../toolEffects.js";
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
import {
analysisDetailSchema,
analysisDetailOutputSchemaOf,
} from "../analysisDetail.js";
import { evmInterfaceSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
/** Delivery selection leaves the existing provider input and original Evidence unchanged. */
const evmInterfaceInspectionInputSchema = inspectEvmInterfaceInputSchema.extend(
{
detail: analysisDetailSchema.unwrap().optional(),
},
);
/** Target-free EVM capabilities are independent of archive/Apple artifact providers. */
export const INSPECT_EVM_INTERFACE_CONTRACT = {
@ -12,14 +23,25 @@ export const INSPECT_EVM_INTERFACE_CONTRACT = {
...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),
'Inspect explicit local flat EVM bytecode without target execution or chain/RPC access. Select raw or hex encoding explicitly. Omitted or complete detail returns original carrier and decoded identities, full bytecode and all inferred interface candidates inline. For all inferred selectors/offsets/arguments/mutability and identities without bytecode hex or duplicated stdout, select detail: "summary" on this first call: it retains unchanged complete Evidence and returns a distinct linked inspect_analysis_view summary. Read original function rows or bytecode_hex/stdout/stderr text pages using view kind evm and normalized_result.parent_evidence_id; original complete Evidence remains accessible. Bytecode kind, hardfork, deployed authenticity and discovery completeness stay unknown; summary coverage covers only producer-returned candidates. Unchanged bundled EVMole 0.9.3/WASM in an owned bounded worker is initially real-verified on Linux x64; EOF-style EF00 containers are unsupported. No signature lookup, wallet action or transaction broadcast.',
inputSchema: evmInterfaceInspectionInputSchema,
outputSchema: analysisDetailOutputSchemaOf(
evmInterfaceSchema,
evmInterfaceSummaryViewResultSchema,
),
examples: [
{
title: "Inspect a local runtime bytecode hex carrier",
input: { path: "/artifacts/runtime.hex", encoding: "hex" },
},
{
title: "Read every inferred interface candidate without bytecode text",
input: {
path: "/artifacts/runtime.hex",
encoding: "hex",
detail: "summary",
},
},
],
} as const satisfies ToolContract;

View File

@ -35,6 +35,12 @@ import {
apiSurfaceViewSchema,
} from "./webNetworkApiSurfaceView.js";
import { projectNativeFunctionView } from "./nativeFunctionView.js";
import {
projectEvmInterfaceView,
evmViewRequestSchema,
evmViewItemSchema,
evmSummarySchema,
} from "./evmInterfaceView.js";
import { projectArtifactInventoryView } from "./artifactInventoryView.js";
import {
projectWasmArtifactView,
@ -154,6 +160,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
nativeViewSchema,
inventoryViewSchema,
wasmViewRequestSchema,
evmViewRequestSchema,
apiSurfaceViewSchema,
]);
@ -266,6 +273,7 @@ const viewResultBase = {
"inspect_artifact",
"inventory_artifact",
"inspect_wasm_artifact",
"inspect_evm_interface",
]),
parent_digest: digestSchema,
view_digest: digestSchema,
@ -309,6 +317,13 @@ export const wasmArtifactSummaryViewResultSchema = z.strictObject({
summary: wasmSummarySchema,
});
/** Exact inferred-interface summary for EVM's caller-selected delivery. */
export const evmInterfaceSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_evm_interface"),
summary: evmSummarySchema,
});
/** Projected facts for one selected view of retained analysis Evidence. */
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
z.strictObject({
@ -329,6 +344,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
javascriptSummarySchema,
inventorySummarySchema,
wasmSummarySchema,
evmSummarySchema,
]),
}),
z.strictObject({
@ -362,6 +378,13 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
item: wasmViewItemSchema,
...viewResultBase,
}),
z.strictObject({
kind: z.literal("evm"),
view: evmViewRequestSchema,
item: evmViewItemSchema,
...viewResultBase,
parent_operation: z.literal("inspect_evm_interface"),
}),
z.strictObject({
kind: z.literal("inventory"),
view: inventoryViewSchema,
@ -427,7 +450,8 @@ export type UnsignedAnalysisView =
Extract<AnalysisViewResult, { readonly kind: "inventory" }>,
"view_digest"
>
| Omit<Extract<AnalysisViewResult, { readonly kind: "wasm" }>, "view_digest">;
| Omit<Extract<AnalysisViewResult, { readonly kind: "wasm" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "evm" }>, "view_digest">;
/** Attach the content digest that must change when projected bytes change. */
export const sealAnalysisView = (
@ -571,6 +595,10 @@ export const projectAnalysisView = (
const projected = projectWasmArtifactView(parent, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (parent.operation === "inspect_evm_interface") {
const projected = projectEvmInterfaceView(parent, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (
parent.operation === "inspect_artifact" ||
parent.operation === "inventory_artifact"

View File

@ -164,6 +164,10 @@ export const projectBinaryLayoutView = (
return err(
layoutIncompatible("WASM views require inspect_wasm_artifact Evidence."),
);
if (view.kind === "evm")
return err(
layoutIncompatible("EVM views require inspect_evm_interface Evidence."),
);
if (view.kind === "summary")
return ok({
kind: "summary",

View File

@ -0,0 +1,145 @@
import { z } from "zod";
import type { AnalysisError } from "../analysisErrorBase.js";
import { uniqueSorted } from "../canonicalOrdering.js";
import { evmInterfaceSchema } from "../evm/evmInterface.js";
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import { err, ok, type Result } from "../result.js";
import {
analysisViewInputError,
type AnalysisViewParent,
type AnalysisViewRequest,
type UnsignedAnalysisView,
} from "./analysisView.js";
import {
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { analysisTextPage } from "./analysisViewText.js";
/** Retained inferred candidates or exact text; offsets for text count UTF-16 code units. */
export const evmViewRequestSchema = z.strictObject({
kind: z.literal("evm"),
facet: z.enum(["functions", "bytecode_hex", "stdout", "stderr"]),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
});
export const evmViewItemSchema = z.union([
evmInterfaceSchema.shape.functions,
z.strictObject({ text: z.string(), unit: z.literal("utf16-code-units") }),
]);
/** Complete inferred interface facts without the unrequested hex or duplicated stdout. */
export const evmSummarySchema = z.strictObject({
artifact: evmInterfaceSchema.shape.artifact,
bytecode: evmInterfaceSchema.shape.bytecode.omit({ hex: true }),
evidence_kind: evmInterfaceSchema.shape.evidence_kind,
functions: evmInterfaceSchema.shape.functions,
discovery_completeness: evmInterfaceSchema.shape.discovery_completeness,
runtime_execution: evmInterfaceSchema.shape.runtime_execution,
diagnostics: evmInterfaceSchema.shape.diagnostics
.omit({ stdout: true })
.extend({
stdout_code_units: z.number().int().nonnegative(),
}),
limitations: evmInterfaceSchema.shape.limitations,
});
/** Select already authenticated producer facts; no interpreter or provider is invoked. */
export const projectEvmInterfaceView = (
parent: AnalysisViewParent,
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
if (view.kind !== "summary" && view.kind !== "evm")
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message: "EVM Evidence supports summary and evm views.",
},
]),
);
const parsed = evmInterfaceSchema.safeParse(parent.normalizedResult);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
"inspect_analysis_view",
parsed.error.issues.map((issue) => ({
...issue,
path: ["source", "normalized_result", ...issue.path],
})),
{ source: { normalized_result: parent.normalizedResult } },
{ cause: parsed.error },
),
);
const report = parsed.data;
if (
parent.subject == null ||
parent.subject.local_path !== report.artifact.path ||
parent.subject.digest.sha256 !== report.artifact.sha256
)
return err(
analysisViewInputError([
{
path: ["source", "subject"],
reason: "invalid_value",
message:
"EVM parent subject must identify the report's exact carrier path and SHA-256.",
},
]),
);
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: "inspect_evm_interface" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
limitations: uniqueSorted([...parent.limitations, ...report.limitations]),
unknowns: [
"Candidates remain inferred; discovery completeness, bytecode kind, hardfork and deployment authenticity are unknown. Runtime execution was not performed.",
],
};
if (view.kind === "summary") {
const { hex: _hex, ...bytecode } = report.bytecode;
const { stdout, ...diagnostics } = report.diagnostics;
return ok({
kind: "summary",
view,
...shared,
summary: {
artifact: report.artifact,
bytecode,
evidence_kind: report.evidence_kind,
functions: report.functions,
discovery_completeness: report.discovery_completeness,
runtime_execution: report.runtime_execution,
diagnostics: { ...diagnostics, stdout_code_units: stdout.length },
limitations: report.limitations,
},
coverage: completeWithinViewCoverage(
report.functions.length,
report.functions.length,
),
});
}
if (view.facet === "functions") {
const item = report.functions.slice(view.offset, view.offset + view.limit);
return ok({
kind: "evm",
view,
item,
...shared,
coverage: pageViewCoverage(
view.offset,
item.length,
report.functions.length,
),
});
}
const text =
view.facet === "bytecode_hex"
? report.bytecode.hex
: report.diagnostics[view.facet];
const page = analysisTextPage(text, view.offset, view.limit, view.facet);
return page.ok ? ok({ kind: "evm", view, ...page.value, ...shared }) : page;
};

View File

@ -253,6 +253,10 @@ export const projectJavaScriptApplicationView = (
return err(
jsIncompatible("WASM views require inspect_wasm_artifact Evidence."),
);
if (view.kind === "evm")
return err(
jsIncompatible("EVM views require inspect_evm_interface Evidence."),
);
if (view.kind === "summary")
return ok({
kind: "summary",

View File

@ -527,6 +527,7 @@ const registerConfiguredAnalysisTools = (
toolLogger,
recordEvidence,
withAdmittedAnalysis,
evidenceById,
);
registerSqliteTools(
server,

View File

@ -2,6 +2,13 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import {
requireAnalysisViewRetention,
summarizeRetainedAnalysis,
} from "../application/analysisView/AnalysisViewService.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err } from "../domain/result.js";
import { evmInterfaceModule } from "../composition/evm.js";
import type { Logger } from "pino";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
@ -15,11 +22,33 @@ export const registerEvmTools = (
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
evidenceById?: EvidenceLookup,
): void => {
const contract = evmInterfaceModule.contract;
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
async (rawInput, options) => {
const parsed = contract.inputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
contract.name,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
const { detail, ...input } = parsed.data;
const retention = { recordEvidence, evidenceById };
if (detail === "summary") {
const ready = requireAnalysisViewRetention(contract.name, retention);
if (!ready.ok) return ready;
}
const inspected = await service.inspect(input, options);
return detail === "summary" && inspected.ok
? summarizeRetainedAnalysis(inspected.value, retention)
: inspected;
},
logger,
recordEvidence,
withAdmittedAnalysis,

View File

@ -8,7 +8,8 @@ import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../../../src/domain/analysisErrorCore.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { evmInterfaceSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { err, ok } from "../../../src/domain/result.js";
import { silentLogger } from "../../../src/logger.js";
import { createServer } from "../../../src/server/createServer.js";
@ -249,3 +250,244 @@ it("settles the admitted EVM operation after SDK cancellation and accepts a subs
expect((await harness.client.callTool(input)).isError).not.toBe(true);
expect(invocations).toBe(2);
});
it("returns the exact inferred interface summary while preserving complete defaults and retained source", async () => {
const value = evmInterfaceFixture();
value.functions = [
{
selector: "0x11223344",
bytecode_offset: 0,
dispatch: "abi",
inferred_arguments: null,
inferred_state_mutability: null,
},
{
selector: "0xaabbccdd",
bytecode_offset: 4,
dispatch: "fallback",
inferred_arguments: "uint256",
inferred_state_mutability: "view",
},
];
value.diagnostics = {
stdout: "exact producer JSON\n",
stderr: "source warning\n",
truncated: false,
};
const inspect = vi.fn(() =>
Promise.resolve(ok(evmInterfaceExecution(value))),
);
const harness = await createApplicationMcpHarness({
serverOptions: { evmInterface: new EvmInterfaceService({ inspect }) },
});
onTestFinished(harness.close);
const definition = (await harness.client.listTools()).tools.find(
(tool) => tool.name === "inspect_evm_interface",
);
if (definition?.outputSchema === undefined)
throw new Error("Missing EVM output schema");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = definition.inputSchema;
const outputSchema: Record<string, unknown> = definition.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
const input = { path: value.artifact.path, encoding: "hex" };
let original;
for (const detail of [undefined, "complete"] as const) {
const response = await harness.client.callTool({
name: "inspect_evm_interface",
arguments: { ...input, ...(detail ? { detail } : {}) },
});
expect(response.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const evidence = parseEvidence(response.structuredContent);
expect(evidence.normalized_result).toEqual(value);
expect(evidence.parameters).toEqual(input);
original = evidence;
}
if (original === undefined) throw new Error("No complete control");
const response = await harness.client.callTool({
name: "inspect_evm_interface",
arguments: { ...input, detail: "summary" },
});
expect(response.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const derived = parseEvidence(response.structuredContent);
const view = evmInterfaceSummaryViewResultSchema.parse(
derived.normalized_result,
);
expect(derived.evidence_id).not.toBe(original.evidence_id);
expect(derived.evidence_links).toEqual([original.evidence_id]);
expect(derived.confidence).toBe("derived");
const { hex: _hex, ...bytecode } = value.bytecode;
expect(view).toMatchObject({
parent_operation: "inspect_evm_interface",
parent_evidence_id: original.evidence_id,
summary: {
artifact: value.artifact,
bytecode,
functions: value.functions,
evidence_kind: "inferred",
discovery_completeness: "unknown",
runtime_execution: "not-performed",
limitations: value.limitations,
diagnostics: {
stderr: value.diagnostics.stderr,
stdout_code_units: value.diagnostics.stdout.length,
},
},
coverage: { examined: 2, total: 2, exhausted: true },
});
expect(view.summary.bytecode).not.toHaveProperty("hex");
expect(view.summary.diagnostics).not.toHaveProperty("stdout");
expect(harness.session.evidenceById(original.evidence_id)).toEqual(original);
for (const [facet, offset, limit, item] of [
["functions", 1, 1, [value.functions[1]]],
[
"bytecode_hex",
2,
4,
{ text: value.bytecode.hex.slice(2, 6), unit: "utf16-code-units" },
],
[
"stdout",
0,
128,
{ text: value.diagnostics.stdout, unit: "utf16-code-units" },
],
] as const) {
const selected = await harness.client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: original.evidence_id,
},
view: { kind: "evm", facet, offset, limit },
},
});
expect(selected.isError).not.toBe(true);
const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
selected.structuredContent,
);
expect(parsed.normalized_result).toMatchObject({
parent_evidence_id: original.evidence_id,
item,
});
}
expect(inspect).toHaveBeenCalledTimes(3);
for (const call of inspect.mock.calls)
expect(call).toEqual([input, expect.any(Object)]);
expect(harness.session.evidenceById(original.evidence_id)).toEqual(original);
});
it.each(["valid", "wrong-subject", "no-subject"])(
"preserves historical portable EVM Evidence and rejects incompatible identity: %s",
async (scenario) => {
const value = evmInterfaceFixture();
const source = createEvidence(
scenario === "no-subject"
? undefined
: {
path: value.artifact.path,
sha256:
scenario === "wrong-subject"
? "b".repeat(64)
: value.artifact.sha256,
format: "file",
},
{ id: "historical-evm", name: "Original EVM producer", version: "1" },
{
operation: "inspect_evm_interface",
parameters: { path: value.artifact.path, encoding: "hex" },
result: value,
confidence: "inferred",
rawResult: { original: true },
},
);
const before = JSON.stringify(source);
const inspect = vi.fn(() => {
throw new Error("portable read must not rerun provider");
});
const harness = await createApplicationMcpHarness({
serverOptions: { evmInterface: new EvmInterfaceService({ inspect }) },
});
onTestFinished(harness.close);
const response = await harness.client.callTool({
name: "inspect_analysis_view",
arguments: {
source: { kind: "inline", evidence: source },
view: { kind: "summary" },
},
});
if (scenario === "valid") {
expect(response.isError).not.toBe(true);
const derived = parseEvidence(response.structuredContent);
const view = evmInterfaceSummaryViewResultSchema.parse(
derived.normalized_result,
);
expect(view.parent_evidence_id).toBe(source.evidence_id);
expect(view.summary.functions).toEqual([]);
expect(view.summary.discovery_completeness).toBe("unknown");
expect(view.coverage).toMatchObject({
total: 0,
examined: 0,
exhausted: true,
});
expect(derived.evidence_links).toEqual([source.evidence_id]);
} else
expect(parseMcpToolError(response).error.code).toBe("invalid_request");
expect(inspect).not.toHaveBeenCalled();
expect(JSON.stringify(source)).toBe(before);
},
);
it("rejects summary before provider work when retention is unavailable and preserves complete delivery", async () => {
const value = evmInterfaceFixture();
const inspect = vi.fn(() =>
Promise.resolve(ok(evmInterfaceExecution(value))),
);
const session = createTestBinarySession(() => {
throw new Error("deep provider must not start");
});
const server = createServer(
{ kind: "fixed", analysis: session },
{ evmInterface: new EvmInterfaceService({ inspect }) },
);
const client = new Client({ name: "evm-fixed-delivery", version: "1" });
const [a, b] = InMemoryTransport.createLinkedPair();
try {
await server.connect(b);
await client.connect(a);
const summary = await client.callTool({
name: "inspect_evm_interface",
arguments: {
path: value.artifact.path,
encoding: "hex",
detail: "summary",
},
});
expect(parseMcpToolError(summary).error.code).toBe(
"capability_unavailable",
);
expect(inspect).not.toHaveBeenCalled();
const complete = await client.callTool({
name: "inspect_evm_interface",
arguments: { path: value.artifact.path, encoding: "hex" },
});
expect(complete.isError).not.toBe(true);
expect(parseEvidence(complete.structuredContent).normalized_result).toEqual(
value,
);
} finally {
await client.close();
await server.close();
await session.close();
}
});