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:
parent
bb26622670
commit
f76163c246
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const sameSettings = [
|
|||
"artifact",
|
||||
"artifact_sha256",
|
||||
"artifact_sha256_after",
|
||||
"retained_verification",
|
||||
"native_question",
|
||||
"javascript_question",
|
||||
"provider_settings_sha256",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -527,6 +527,7 @@ const registerConfiguredAnalysisTools = (
|
|||
toolLogger,
|
||||
recordEvidence,
|
||||
withAdmittedAnalysis,
|
||||
evidenceById,
|
||||
);
|
||||
registerSqliteTools(
|
||||
server,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue