From 0b7145361ec02d27260eb1f5bc0be955c9374a96 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:01:47 +0800 Subject: [PATCH] feat(wasm): add opt-in first-call summary delivery (#2193) Add explicit first-call WASM summary delivery while preserving omitted/complete MCP requests, CLI output, original Evidence identity and provider inputs. Reuse exact retained export views and schema-based capability discovery. Nine paired public SDK comparisons and ten actual Codex CLI answers preserve selected facts and source references. Record token reductions alongside mixed latency and unverified final resource-counter conditions. Current-main integration passes seven relevant WASM MCP workflows, a fresh real-WABT SQLite workflow and required static gates. Refs #2016. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../references/native-and-artifacts.md | 4 + docs/mcp-contracts.md | 8 +- experiments/analysis-efficiency/README.md | 9 +- experiments/analysis-efficiency/corpus.json | 34 ++++ experiments/analysis-efficiency/measure.mjs | 183 ++++++++++-------- .../questions/wasm-interface.txt | 1 + .../analysis-efficiency/wasm-first-call.md | 149 ++++++++++++++ src/contracts/wasm/wasmToolContracts.ts | 21 +- src/domain/analysisView/analysisView.ts | 7 + src/server/createServer.ts | 1 + src/server/registerWasmTools.ts | 26 ++- tests/boundary/mcp/wasmArtifactMcp.test.ts | 144 +++++++++++++- 12 files changed, 499 insertions(+), 88 deletions(-) create mode 100644 experiments/analysis-efficiency/questions/wasm-interface.txt create mode 100644 experiments/analysis-efficiency/wasm-first-call.md diff --git a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md index 1b02a7ed2b..dfb7017608 100644 --- a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md +++ b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md @@ -39,6 +39,10 @@ read a single object. ## Offline WASM Use `inspect_wasm_artifact` with caller-supplied WABT for selected local bytes. +For counts, validation and WAT identity, select `detail: "summary"` on the first +MCP call. The complete original Evidence stays retained; use +`normalized_result.parent_evidence_id` for exact exports or text pages. Omitted +or complete detail keeps the existing complete result. For a completed result, use `inspect_analysis_view` with its reported retained reference for a summary or `view: {kind: "wasm", facet: "exports", offset: 0, limit: 32}`. Facets also include sections, imports, candidates, glue, WAT, headers, details diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 8e5755cafc..1fb3b5e5dc 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -412,7 +412,8 @@ Ordinary responses keep their existing complete result contract. ## Retained application Evidence inputs -`analyze_javascript_application`, `inspect_binary_layout`, and `inspect_artifact` accept `detail`: +`analyze_javascript_application`, `inspect_binary_layout`, `inspect_artifact`, and +`inspect_wasm_artifact` accept `detail`: `complete` (default) returns the complete analysis Evidence; `summary` records that complete Evidence in the session and returns the derived `inspect_analysis_view` summary Evidence instead. Its @@ -476,6 +477,11 @@ The original producer still returns complete Evidence by default. When its complete MCP reply exceeds the frame, use the exact reported retained reference with these views, or export the complete source. Views do not run WABT again and do not turn decoded WAT into original source. +For counts, validation and WAT identity, select `detail: "summary"` on the first +`inspect_wasm_artifact` call. It retains the original complete source without +attempting oversized delivery, and returns the same summary with an exact +`parent_evidence_id` for later export or facet reads. Delivery detail does not +reach WABT or change the original source Evidence ID. `trace_application_feature`, `trace_javascript_semantics`, `compare_application_versions`, `compare_source_to_bundle`, and diff --git a/experiments/analysis-efficiency/README.md b/experiments/analysis-efficiency/README.md index 8c17080098..9a09e9d365 100644 --- a/experiments/analysis-efficiency/README.md +++ b/experiments/analysis-efficiency/README.md @@ -137,8 +137,11 @@ inventory. It must be supported by the selected server's advertised schema. The default `complete` runs the existing calls unchanged. Compare the same analyst facts and complete source Evidence before crediting a smaller response. Projection can reduce delivery bytes without reducing upstream analysis time. -For SQLite WASM, summary delivery reads the completed retained result through -the summary and exports views after preserving the original transport error. +For WASM, the runner validates a summary request against the actual advertised +2020-12 input schema, including SDK-generated references. Supporting servers +return the first-call summary; older servers retain their complete path and use +summary/export views after an observed transport constraint. Original errors +remain in the receipts, and the complete source Evidence ID is a compared fact. `--javascript-question selected` analyzes only the exact Obsidian sources `package.json` and `main.js`, keeps inventory identity, and reads each observed `main.js` module by its exact node ID. Package metadata is not a module selector. @@ -538,3 +541,5 @@ observations, not verified final counters. Their bytes, facts and token reports remain original observations; resource-qualified performance conclusions need this additional closeout evidence. No analyzer/build/schema/package behavior is changed, so unrelated verification is not repeated for this research-driver fix. + +The next bounded WASM delivery cohort is documented in [first-call summary measurements](wasm-first-call.md). diff --git a/experiments/analysis-efficiency/corpus.json b/experiments/analysis-efficiency/corpus.json index c851805687..911fa88f5f 100644 --- a/experiments/analysis-efficiency/corpus.json +++ b/experiments/analysis-efficiency/corpus.json @@ -115,6 +115,40 @@ "transport_bytes": 168796487, "derivation": "selected regular ZIP member ShareX.HelpersLib.dll; no target execution" } + }, + { + "id": "vscode.tree-sitter.wasm", + "file": "tree-sitter.wasm", + "bytes": 206218, + "sha256": "3a31af706ffdf4a7116b064cdd4988df6791823298615009fc5b6bccbd42909b", + "provenance": { + "project": "VS Code Tree-sitter published WASM package", + "version": "0.3.1", + "project_url": "https://github.com/microsoft/vscode-tree-sitter-wasm", + "upstreamFile": "tree-sitter-wasm-0.3.1.tgz", + "url": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz", + "transport_sha256": "985684b2cfbe5e8f90499a9f36e37519887c7d4e2697d0457d0f1e923ce5184c", + "transport_bytes": 2053205, + "transport_integrity": "sha512-RJFoomET6FajjG511fmQxeBQfU6M24a0aFZPqpid+ttIxanWf1VGytBG0UmsGjt07qmIPJS8U31D+aecuCucsQ==", + "derivation": "selected regular tar member package/wasm/tree-sitter.wasm; no target execution" + } + }, + { + "id": "vscode.tree-sitter-css.wasm", + "file": "tree-sitter-css.wasm", + "bytes": 128668, + "sha256": "8a23977fe271357cce6f254ef88c9bebf3854602d8046605aef6a45c02135c59", + "provenance": { + "project": "VS Code Tree-sitter published WASM package", + "version": "0.3.1", + "project_url": "https://github.com/microsoft/vscode-tree-sitter-wasm", + "upstreamFile": "tree-sitter-wasm-0.3.1.tgz", + "url": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz", + "transport_sha256": "985684b2cfbe5e8f90499a9f36e37519887c7d4e2697d0457d0f1e923ce5184c", + "transport_bytes": 2053205, + "transport_integrity": "sha512-RJFoomET6FajjG511fmQxeBQfU6M24a0aFZPqpid+ttIxanWf1VGytBG0UmsGjt07qmIPJS8U31D+aecuCucsQ==", + "derivation": "selected regular tar member package/wasm/tree-sitter-css.wasm; no target execution" + } } ] } diff --git a/experiments/analysis-efficiency/measure.mjs b/experiments/analysis-efficiency/measure.mjs index a10eb23d8b..348bf02c2a 100644 --- a/experiments/analysis-efficiency/measure.mjs +++ b/experiments/analysis-efficiency/measure.mjs @@ -13,6 +13,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { Ajv2020 } from "ajv/dist/2020.js"; const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); const { values } = parseArgs({ @@ -243,6 +244,106 @@ const inspectManagedTarget = async () => { }; }; +/** The same counts/export question for each pinned shipped WASM module. */ +const inspectWasmTarget = async () => { + let info; + let recovery = null; + const arguments_ = { + path: values.path, + glue_paths: [], + candidate_paths: [], + }; + const definition = catalogDefinitions.get("inspect_wasm_artifact"); + if (!definition) throw new Error("Missing advertised WASM definition"); + // Validate the real advertised dialect, including SDK-generated local refs. + const supportsSummary = new Ajv2020({ + strict: false, + validateFormats: false, + }).compile(definition.inputSchema)({ ...arguments_, detail: "summary" }); + try { + info = await call("inspect_wasm_artifact", { + ...arguments_, + ...(values.delivery === "summary" && supportsSummary + ? { detail: "summary" } + : {}), + }); + } catch (error) { + const reference = + error instanceof ToolFailure && + error.value?.error?.code === "resource_constraint" + ? error.value.error.details?.reported_limits?.evidence_reference + : undefined; + if (reference?.kind !== "retained-evidence") throw error; + if (values.delivery === "summary") { + info = await call("inspect_analysis_view", { + source: reference, + view: { kind: "summary" }, + }); + recovery = + "existing completed producer reference read through typed WASM views"; + } else { + const path = resolve(values.output, "retained-evidence.json"); + await call("export_evidence_bundle", { path }); + const bundle = JSON.parse(await readFile(path, "utf8")); + const record = bundle.records.find( + (row) => row.evidence_id === reference.evidence_id, + ); + const { parseEvidence } = await import( + pathToFileURL( + resolve(values["runtime-root"], "dist/domain/evidence.js"), + ).href + ); + info = parseEvidence(record); + recovery = + "complete producer Evidence exported after an observed transport constraint"; + } + } + const n = normalized(info); + if (n.kind === "summary") { + const summary = n.summary; + const exportsEvidence = await call("inspect_analysis_view", { + source: { kind: "retained-evidence", evidence_id: n.parent_evidence_id }, + view: { kind: "wasm", facet: "exports", offset: 0, limit: 4 }, + }); + return { + artifact_sha256: targetSha(info), + validation: summary.validation, + section_count: summary.available_counts.sections, + import_count: summary.available_counts.imports, + export_count: summary.available_counts.exports, + exports_first4: normalized(exportsEvidence).item, + wat_sha256: summary.wat.sha256, + wat_code_units: summary.wat.code_units, + tool_profile: summary.tool_profile, + runtime_execution: summary.runtime_execution, + network_fetch: summary.network_fetch, + evidence_ids: [ + n.parent_evidence_id, + info.evidence_id, + exportsEvidence.evidence_id, + ], + source_evidence_id: n.parent_evidence_id, + recovery, + }; + } + return { + artifact_sha256: targetSha(info), + validation: n.validation, + section_count: n.sections.length, + import_count: n.imports.length, + export_count: n.exports.length, + exports_first4: n.exports.slice(0, 4), + wat_sha256: n.wat.sha256, + wat_code_units: n.wat.text.length, + tool_profile: n.tool_profile, + runtime_execution: n.runtime_execution, + network_fetch: n.network_fetch, + evidence_ids: [info.evidence_id], + source_evidence_id: info.evidence_id, + recovery, + }; +}; + const workflows = { "obsidian.asar": async () => { if (values["javascript-question"] === "selected") { @@ -424,85 +525,9 @@ const workflows = { evidence_ids: [info.evidence_id], }; }, - "sqlite.wasm": async () => { - let info; - let recovery = null; - try { - info = await call("inspect_wasm_artifact", { - path: values.path, - glue_paths: [], - candidate_paths: [], - }); - } catch (error) { - const reference = - error instanceof ToolFailure && - error.value?.error?.code === "resource_constraint" - ? error.value.error.details?.reported_limits?.evidence_reference - : undefined; - if (reference?.kind !== "retained-evidence") throw error; - if (values.delivery === "summary") { - const summaryEvidence = await call("inspect_analysis_view", { - source: reference, - view: { kind: "summary" }, - }); - const summary = normalized(summaryEvidence).summary; - const exportsEvidence = await call("inspect_analysis_view", { - source: reference, - view: { kind: "wasm", facet: "exports", offset: 0, limit: 4 }, - }); - return { - artifact_sha256: targetSha(summaryEvidence), - validation: summary.validation, - section_count: summary.available_counts.sections, - import_count: summary.available_counts.imports, - export_count: summary.available_counts.exports, - exports_first4: normalized(exportsEvidence).item, - wat_sha256: summary.wat.sha256, - wat_code_units: summary.wat.code_units, - tool_profile: summary.tool_profile, - runtime_execution: summary.runtime_execution, - network_fetch: summary.network_fetch, - evidence_ids: [ - reference.evidence_id, - summaryEvidence.evidence_id, - exportsEvidence.evidence_id, - ], - recovery: - "existing completed producer reference read through typed WASM views", - }; - } - const path = resolve(values.output, "retained-evidence.json"); - await call("export_evidence_bundle", { path }); - const bundle = JSON.parse(await readFile(path, "utf8")); - const record = bundle.records.find( - (row) => row.evidence_id === reference.evidence_id, - ); - const { parseEvidence } = await import( - pathToFileURL( - resolve(values["runtime-root"], "dist/domain/evidence.js"), - ).href - ); - info = parseEvidence(record); - recovery = - "complete producer Evidence exported after an observed transport constraint"; - } - const n = normalized(info); - return { - artifact_sha256: targetSha(info), - validation: n.validation, - section_count: n.sections.length, - import_count: n.imports.length, - export_count: n.exports.length, - exports_first4: n.exports.slice(0, 4), - wat_sha256: n.wat.sha256, - wat_code_units: n.wat.text.length, - tool_profile: n.tool_profile, - runtime_execution: n.runtime_execution, - network_fetch: n.network_fetch, - evidence_ids: [info.evidence_id], - recovery, - }; - }, + "sqlite.wasm": inspectWasmTarget, + "vscode.tree-sitter.wasm": inspectWasmTarget, + "vscode.tree-sitter-css.wasm": inspectWasmTarget, "weth.hex": async () => { const info = await call("inspect_evm_interface", { path: values.path, diff --git a/experiments/analysis-efficiency/questions/wasm-interface.txt b/experiments/analysis-efficiency/questions/wasm-interface.txt new file mode 100644 index 0000000000..fd09040cd0 --- /dev/null +++ b/experiments/analysis-efficiency/questions/wasm-interface.txt @@ -0,0 +1 @@ +Statically inspect the real shipped WASM module at {{artifact_path}} using only REA tools. Report exact selected artifact SHA-256, valid/invalid status, section count, import count, export count, first four complete export WAT forms in producer order, decoded WAT SHA-256 and code-unit length, WABT release/source commit, and exact runtime_execution/network_fetch states. If complete delivery exceeds the MCP frame, use the exact retained reference for summary and wasm exports views. Preserve original strings, uncertainties and real Evidence IDs. Do not execute WASM, fetch URLs or use shell/file tools. Final JSON fields: artifact_sha256, validation, section_count, import_count, export_count, exports_first4, wat_sha256, wat_code_units, wabt_release, wabt_source_commit, runtime_execution, network_fetch, evidence_ids, limitations. diff --git a/experiments/analysis-efficiency/wasm-first-call.md b/experiments/analysis-efficiency/wasm-first-call.md new file mode 100644 index 0000000000..fb554507a2 --- /dev/null +++ b/experiments/analysis-efficiency/wasm-first-call.md @@ -0,0 +1,149 @@ +# WASM first-call summary delivery + +Refs [#2016](https://github.com/morluto/rea/issues/2016). This bounded follow-up +adds optional MCP `inspect_wasm_artifact detail: summary`. Omitted/complete MCP +requests and CLI output remain complete. Full original Evidence is retained; +delivery selection is stripped before provider inputs, and exact export views +read the original parent without another WABT invocation. + +## Pinned repeated public SDK cohort + +- Control: `18e5d6275965c5f1d3ef9fe373d1792822098003`. +- Candidate: `45831c622a13368494d2a8f3a9718effa91c937c`. +- Runtime file-table SHA-256: control `ef65527c2d41b621f71059b7553050660d801ad905a61a71b81aba266f70e1ec`; + candidate `353f5513ea09b0a627fa99814cebe0a1231c6c8f7e0e12a28205521133478b1a`. +- Actual WABT 1.0.42, identical explicitly selected provider configuration and + effective search-path fingerprint, pinned Node 24.18.0 and locked SDK. +- Three paired fresh-process trials per digest-pinned shipped module, serial + on one CPU: control/candidate, candidate/control, control/candidate. +- Same validation/counts/first-four complete export forms/WAT identity/provider + identity/execution and fetch states; complete original source Evidence ID is + also compared. All nine pairs preserve these selected facts and source IDs. + +| Shipped module | Result JSON bytes, control → summary | Calls / errors | Median seconds, control → summary (ranges) | +| ----------------------------- | -----------------------------------: | -------------- | ----------------------------------------------- | +| `sqlite.wasm` | 20,118 → 18,533 | 3/1 → 2/0 | 15.962 (15.719–21.138) → 18.171 (10.242–18.649) | +| `vscode.tree-sitter-css.wasm` | 20,366 → 18,781 | 3/1 → 2/0 | 4.920 (4.885–4.936) → 5.191 (4.971–5.232) | +| `vscode.tree-sitter.wasm` | 6,433,872 → 18,891 | 1/0 → 2/0 | 5.225 (5.172–5.519) → 4.763 (4.729–4.830) | + +Byte counts include all investigation call results, including failed delivery +and extra views. They exclude catalog responses, raw wire framing and file +exports; they are not actual model tokens. The complete catalog is 2,359,082 +versus 2,362,451 JSON bytes (+3,369 bytes, about 0.14%). An extra export-view +call is included when summary replaces an otherwise fitting complete reply. + +| Shipped module | Median owned CPU seconds, control → summary | Median sampled family MiB, control → summary (maxima) | +| ----------------------------- | ------------------------------------------- | ----------------------------------------------------- | +| `sqlite.wasm` | 15.376 → 15.954 | 660.84 (671.27) → 633.07 (671.51) | +| `vscode.tree-sitter-css.wasm` | 5.760 → 6.027 | 692.91 (694.93) → 678.98 (693.88) | +| `vscode.tree-sitter.wasm` | 6.134 → 5.663 | 674.04 (695.66) → 550.66 (553.47) | + +All 18 scopes have no sampled abort and their last observations contain no +memory-high/hard/OOM/OOM-kill or task-limit event. Final counters are unverified +under the subsequent closeout audit below. Sampling can miss brief peaks. Application caches are cold per process; +host page caches are not flushed. SDK elapsed time includes startup, discovery, +schema work and persistence, not just provider analysis. Three observations +give descriptive medians/ranges, not a statistical or cross-domain speed claim. + +SQLite and the CSS grammar already recovered an oversized complete result +through retained views in the control. The new path removes the initial error +and one call; result-byte savings are correspondingly modest. The Tree-sitter +runtime fits the MCP frame but delivers megabytes of unneeded WAT; its summary +plus exact exports greatly reduces delivery. Full provider analysis and complete +Evidence retention still occur, so this does not establish an engine shortcut. + +An earlier 18-run cohort did not activate the treatment: the driver looked +for an inline enum and missed SDK-generated `$ref` definitions. Those original +runs and their source are archived separately and excluded from benefit claims. +The corrected driver validates the actual advertised 2020-12 request schema. + +## Real model verification + +Ten actual Codex CLI 0.161.0 trials request `gpt-5.6-terra` high: three paired +SQLite repetitions and one pair for each published VS Code component. The +resolved server-side model identity is not inferred from the requested alias. +Same question, fixed developer instructions, four allowlisted REA tools and +explicit WABT settings; private profiles, shell/web/other-agent restrictions +and target-byte preservation remain verified. + +All ten answers match the 12 requested producer facts, including exact export +forms and explicit execution/fetch states. Reported Evidence IDs occur in real +responses; all copied profiles are removed. Manual review retains static-analysis +limitations and makes no original-source/runtime claim. Every candidate chooses +`detail: summary` on its first inspection and then reads exports without WABT +rerun. Original control errors and client-context clipping remain in transcripts. + +| Module / observations per side | Total input, control → summary | Cached input | Uncached input | Output | +| --------------------------------- | -----------------------------: | ----------------: | --------------: | ------------: | +| `sqlite.wasm` / 3 | 96,832 → 79,264 | 70,656 → 51,456 | 26,215 → 28,004 | 1,470 → 1,512 | +| `vscode.tree-sitter-css.wasm` / 1 | 247,889 → 79,389 | 207,872 → 61,440 | 40,017 → 17,949 | 1,931 → 1,327 | +| `vscode.tree-sitter.wasm` / 1 | 556,424 → 282,165 | 478,208 → 239,360 | 78,216 → 42,805 | 2,560 → 2,110 | + +SQLite rows show medians of three observations; VS Code rows are single pairs. +Usage is the CLI-reported total for the turn, with cached/uncached input listed +separately. Lower total input does not imply a proportional monetary saving, +uncached-input reduction or faster answer. + +| Module | Calls / observed errors, control → summary | Seconds, control → summary | +| ----------------------------- | ------------------------------------------ | ----------------------------------------------- | +| `sqlite.wasm` | 3/1 → 2/0 | 61.521 (54.301–80.579) → 55.656 (42.960–62.145) | +| `vscode.tree-sitter-css.wasm` | 3/1 → 2/0 | 87.126 → 106.580 | +| `vscode.tree-sitter.wasm` | 4/0 → 2/0 | 56.952 → 104.999 | + +SQLite total input falls about 18% and median answer time about 10%, while +its median uncached input rises about 7%. The two VS Code pairs reduce total +input by about 49% and 68%, but observed answer time rises about 84% and 22%. +These data support optional smaller delivery; they do not establish uniform +model speedup. The runtime control also makes one protocol resource-discovery +call, included in the four-call count. Cache state, model reasoning and service +latency are not separated by this small sample. + +The model and SDK cohorts ran with the original resource wrapper. Their final +receipts retain last-sampled cgroup counters, not verified final counters. +The subsequent closeout correction regrades all ten existing answers: selected +facts, real IDs, unchanged target bytes and profile cleanup still pass, while +the final resource conditions are **unverified**. No model/build/provider run +was repeated for that review-only correction. Last-sampled peaks are 656–944 +MiB for model scopes with no observed limit events; one CPU, 4 GiB hard ceiling, +3 GiB early-stop and no-swap controls were active. This is not a proof of absent +late resource events or a fully resource-qualified performance conclusion. +The original transcripts/usage and regrades remain archived separately. + +## Reproduction and verification + +Use [the common measurement/model instructions](README.md) with the public +digest-pinned inputs in [corpus.json](corpus.json), `--schemas selected` and +`--delivery summary`. For each artifact run the above three paired orders +against separately frozen runtimes, then `compare.mjs`. Use the same +[WASM interface question](questions/wasm-interface.txt), four exact enabled +tools (`inspect_wasm_artifact`, `inspect_analysis_view`, `get_evidence_bundle`, +`export_evidence_bundle`), explicit WABT settings and resource scope for models. + +The VS Code artifacts are two published components from +[@vscode/tree-sitter-wasm 0.3.1](https://github.com/microsoft/vscode-tree-sitter-wasm). +The selected regular archive members have verified digests and package SHA-512 +integrity. This is component analysis, not proof of the whole VS Code runtime. + +The prototype passes 54 focused assertions and full static/docs gates. After +rebasing, 29 focused assertions and 42 actual installed-package WABT CLI/MCP +cases pass; the final rebased snapshot passes build/fast static and seven WASM +MCP workflows again. Omitted/explicit-complete behavior, retained identity, +fixed-server absence of retention, oversized continuation and invalid WAT +digest remain checked. No version publication or target execution occurs. +The final static/build scope has 51 soft memory-high events and no hard/OOM +events; this is not credited as a performance trial. + +Detailed JSON, transcripts, target bytes, profiles and acquisition receipts +stay outside Git. Source inputs, scripts and this concise measured scope are +reviewable in the repository. + +## Main integration checkpoint + +Prerequisite #2122 is merged at `58fb372071e56da9c5df0e37593ee3f0da6b64bc`. +The follow-up is rebased directly onto latest fetched main. Intervening changes +outside the prerequisite concern CLI/logging/CI planning rather than WASM +production/delivery. Seven focused WASM MCP workflows and one fresh real-WABT +SQLite public-SDK request pass against the new build: selected facts and original +source ID match the pinned candidate, summary uses two calls with no tool error. +Only the relevant integration checks and required pre-push gate are repeated; +earlier measurements retain their original runtimes, not the new main label. diff --git a/src/contracts/wasm/wasmToolContracts.ts b/src/contracts/wasm/wasmToolContracts.ts index 901dd7ded2..34f26f11de 100644 --- a/src/contracts/wasm/wasmToolContracts.ts +++ b/src/contracts/wasm/wasmToolContracts.ts @@ -4,16 +4,29 @@ import { } from "../../domain/wasm/wasmArtifact.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; -import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; +import { + analysisDetailSchema, + analysisDetailOutputSchemaOf, +} from "../analysisDetail.js"; +import { wasmArtifactSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; + +/** Delivery selection does not change the provider input or original Evidence. */ +export const wasmArtifactInspectionInputSchema = + inspectWasmArtifactInputSchema.extend({ + detail: analysisDetailSchema.unwrap().optional(), + }); /** Optional caller-supplied WABT capability with target-free CLI/MCP parity. */ export const INSPECT_WASM_ARTIFACT_CONTRACT = { name: "inspect_wasm_artifact", ...toolContractMetadata("inspect_wasm_artifact"), kind: "artifact-provider", description: - "Validate an explicit local WASM artifact with caller-supplied WABT 1.0.42, retain exact byte SHA-256, sections, upstream import/export WAT forms and decoded WAT inline as Evidence. Optionally associate selected JavaScript glue literals with explicit local candidates while preserving ambiguous URL/basename matches. Configure absolute REA_WABT_BIN_DIRECTORY. No installation, implicit fetch, WASM execution or original-source claim.", - inputSchema: inspectWasmArtifactInputSchema, - outputSchema: evidenceResultOf(wasmArtifactSchema), + 'Validate an explicit local WASM artifact with caller-supplied WABT 1.0.42. Omitted or complete detail returns exact byte SHA-256, sections, upstream import/export WAT forms and decoded WAT inline as Evidence. For counts, validation and WAT identity without WAT text, pass detail: "summary" on this first call; it retains the complete original Evidence and returns the same typed summary as inspect_analysis_view, avoiding oversized complete delivery. Read exact imports/exports or WAT pages with inspect_analysis_view using normalized_result.parent_evidence_id. Delivery selection does not change analysis, provider inputs or source identity. Optionally associate selected JavaScript glue literals with explicit local candidates while preserving ambiguous URL/basename matches. Configure absolute REA_WABT_BIN_DIRECTORY. No installation, implicit fetch, WASM execution or original-source claim.', + inputSchema: wasmArtifactInspectionInputSchema, + outputSchema: analysisDetailOutputSchemaOf( + wasmArtifactSchema, + wasmArtifactSummaryViewResultSchema, + ), examples: [ { title: "Inspect a selected local WASM module", diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index ce0b55946e..5395578770 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -302,6 +302,13 @@ export const artifactInventorySummaryViewResultSchema = z.strictObject({ summary: inventorySummarySchema, }); +/** Exact summary carrier for WASM inspection's optional delivery contract. */ +export const wasmArtifactSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_wasm_artifact"), + summary: wasmSummarySchema, +}); + /** Projected facts for one selected view of retained analysis Evidence. */ export const analysisViewResultSchema = z.discriminatedUnion("kind", [ z.strictObject({ diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 3220fe8e74..742511ffcc 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -453,6 +453,7 @@ const registerConfiguredAnalysisTools = ( toolLogger, recordEvidence, withAdmittedAnalysis, + evidenceById, ); registerEvmTools( server, diff --git a/src/server/registerWasmTools.ts b/src/server/registerWasmTools.ts index 9c0bd7699e..494972b2ff 100644 --- a/src/server/registerWasmTools.ts +++ b/src/server/registerWasmTools.ts @@ -2,6 +2,10 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { WasmArtifactService } from "../application/wasm/WasmArtifactService.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; +import { summarizeRetainedAnalysis } from "../application/analysisView/AnalysisViewService.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import { err } from "../domain/result.js"; import { wasmArtifactModule } from "../composition/wasm.js"; import type { Logger } from "pino"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; @@ -15,11 +19,31 @@ export const registerWasmTools = ( logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], withAdmittedAnalysis?: WithAdmittedAnalysis, + evidenceById?: EvidenceLookup, ): void => { const contract = wasmArtifactModule.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 inspected = await service.inspect(input, options); + return detail === "summary" && inspected.ok + ? summarizeRetainedAnalysis(inspected.value, { + recordEvidence, + evidenceById, + }) + : inspected; + }, logger, recordEvidence, withAdmittedAnalysis, diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index ce42e9b9c3..49c7ad7629 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { WasmArtifactService } from "../../../src/application/wasm/WasmArtifactService.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; +import { wasmArtifactSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; import { createHash } from "node:crypto"; import { z } from "zod"; import { createServer } from "../../../src/server/createServer.js"; @@ -33,6 +34,63 @@ const wasmToolSchemas = async (client: Client) => { return { name: advertised.name, ajv, inputSchema, outputSchema }; }; +const assertFirstSummaryDelivery = async ( + harness: Awaited>, + value: ReturnType, + inspect: WasmArtifactService["provider"]["inspect"], +) => { + const { + ajv: wasmAjv, + inputSchema, + outputSchema, + } = await wasmToolSchemas(harness.client); + const summaryInput = { path: value.artifact.path, detail: "summary" }; + expect(wasmAjv.validate(inputSchema, summaryInput)).toBe(true); + expect( + wasmAjv.validate(inputSchema, { ...summaryInput, detail: "brief" }), + ).toBe(false); + const directSummary = await harness.client.callTool({ + name: "inspect_wasm_artifact", + arguments: summaryInput, + }); + expect(directSummary.isError).not.toBe(true); + expect( + wasmAjv.validate(outputSchema, directSummary.structuredContent), + JSON.stringify(wasmAjv.errors), + ).toBe(true); + const summaryEvidence = parseEvidence(directSummary.structuredContent); + const summary = wasmArtifactSummaryViewResultSchema.parse( + summaryEvidence.normalized_result, + ); + expect(summaryEvidence.confidence).toBe("derived"); + expect(summaryEvidence.evidence_links).toEqual([summary.parent_evidence_id]); + expect(summary.summary.wat).toMatchObject({ + sha256: value.wat.sha256, + code_units: value.wat.text.length, + }); + expect(summary.summary.wat).not.toHaveProperty("text"); + const retained = harness.session.evidenceById(summary.parent_evidence_id); + if (retained === undefined) throw new Error("Summary lost original Evidence"); + expect(retained.normalized_result).toEqual(value); + expect(retained.parameters).toEqual({ + path: value.artifact.path, + glue_paths: [], + candidate_paths: [], + }); + expect(inspect).toHaveBeenCalledTimes(1); + expect(inspect).toHaveBeenLastCalledWith( + retained.parameters, + expect.objectContaining({ signal: expect.any(AbortSignal) }), + ); + expect( + wasmAjv.validate(outputSchema, { + ...summaryEvidence, + normalized_result: { ...summary, parent_operation: "inventory_artifact" }, + }), + ).toBe(false); + return { summary, retained }; +}; + it("directs rejected native WASM opening to the advertised static tool while preserving missing-WABT diagnostics", async () => { const directory = await createTestTempDirectory("rea-wasm-recovery-"); const path = join( @@ -84,6 +142,16 @@ it("directs rejected native WASM opening to the advertised static tool while pre }, }, }); + const summary = await client.callTool({ + name, + arguments: { ...input, detail: "summary" }, + }); + expect(parseMcpToolError(summary)).toMatchObject({ + error: { + code: "capability_unavailable", + details: { reason: expect.stringContaining("REA_WABT_BIN_DIRECTORY") }, + }, + }); await client.ping(); expect(createClient).not.toHaveBeenCalled(); expect(await readFile(path)).toEqual( @@ -157,14 +225,22 @@ it("reads an oversized completed WASM result through its reported retained refer representation: "decoded-wat" as const, }, }; + const inspect = vi.fn(() => + Promise.resolve(ok(wasmArtifactExecution(value))), + ); const service = new WasmArtifactService({ close: () => Promise.resolve(), - inspect: () => Promise.resolve(ok(wasmArtifactExecution(value))), + inspect, }); const harness = await createApplicationMcpHarness({ serverOptions: { wasmArtifact: service }, }); onTestFinished(harness.close); + const { summary, retained } = await assertFirstSummaryDelivery( + harness, + value, + inspect, + ); const reply = await harness.client.callTool({ name: "inspect_wasm_artifact", arguments: { path: value.artifact.path }, @@ -182,6 +258,17 @@ it("reads an oversized completed WASM result through its reported retained refer }) .parse(parseMcpToolError(reply).error).details .reported_limits.evidence_reference; + expect(reference.evidence_id).toBe(summary.parent_evidence_id); + const explicitComplete = await harness.client.callTool({ + name: "inspect_wasm_artifact", + arguments: { path: value.artifact.path, detail: "complete" }, + }); + expect(parseMcpToolError(explicitComplete)).toMatchObject({ + error: { + code: "resource_constraint", + details: { reported_limits: { evidence_reference: reference } }, + }, + }); const catalog = await harness.client.listTools(); const advertised = catalog.tools.find( (tool) => tool.name === "inspect_analysis_view", @@ -244,6 +331,17 @@ it("reads an oversized completed WASM result through its reported retained refer } const original = harness.session.evidenceById(reference.evidence_id); expect(original?.normalized_result).toEqual(value); + expect(original).toEqual(retained); + expect(inspect).toHaveBeenCalledTimes(3); + await harness.client.ping(); +}); + +it("rejects inconsistent decoded-WAT digests in portable Evidence without a provider run", async () => { + const value = wasmArtifactFixture(); + const harness = await createApplicationMcpHarness({ + serverOptions: { environment: {} }, + }); + onTestFinished(harness.close); const inconsistent = createEvidence( { path: value.artifact.path, @@ -325,6 +423,50 @@ it("rejects absent or inconsistent WASM source identity before deriving portable await harness.client.ping(); }); +it("requires retention for WASM summary delivery while keeping fixed-server complete delivery", async () => { + const value = wasmArtifactFixture(); + const service = new WasmArtifactService({ + close: () => Promise.resolve(), + inspect: () => Promise.resolve(ok(wasmArtifactExecution(value))), + }); + const session = createTestBinarySession(() => { + throw new Error("No deep provider"); + }); + const server = createServer( + { kind: "fixed", analysis: session }, + { wasmArtifact: service }, + ); + const client = new Client({ name: "wasm-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_wasm_artifact", + arguments: { path: value.artifact.path, detail: "summary" }, + }); + expect(parseMcpToolError(summary)).toMatchObject({ + error: { + code: "capability_unavailable", + message: expect.stringContaining("does not retain Evidence"), + }, + }); + const complete = await client.callTool({ + name: "inspect_wasm_artifact", + arguments: { path: value.artifact.path }, + }); + expect(complete.isError).not.toBe(true); + expect(parseEvidence(complete.structuredContent).normalized_result).toEqual( + value, + ); + await client.ping(); + } finally { + await client.close(); + await server.close(); + await session.close(); + } +}); + it("retries cleanup of the injected WASM module after server shutdown fails", async () => { const close = vi .fn()