fix(target): probe WASM before inferred database routing (#2007)

* fix(target): probe WASM before inferred database routing

* test(wasm): execute recovery arguments from native refusal

* test(ci): repair fixture alignment and share MCP assertions

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
SII-YDDandmorluto authored and GitHub committed 2026-10-13 23:49:33 +08:00
1 parent 554ea7611e
commit 168bfe5060
8 files changed
+261 -151

No files matched your search

+5
View File
@@ -13,6 +13,11 @@ Check `binary_session` for configured capability availability. A recognized
header is not validation of the complete module; missing WABT is reported
separately as an unavailable capability, not as missing native engine support.
The recognized WASM header is checked before automatic database routing from a
.hop filename. Renaming a module to .hop therefore keeps the static-inspection
diagnostic. An explicit database selection or raw/COM format selection retains
the caller's requested interpretation; ordinary .hop database routing is unchanged.
Set `REA_WABT_BIN_DIRECTORY` to an absolute directory containing executable
`wasm-validate`, `wasm-objdump` and `wasm2wat` (with `.exe` on Windows). Missing
executables, unsupported banners and inaccessible tools produce actionable
+6 -1
View File
@@ -162,6 +162,12 @@ export const parseBinaryTarget = async (
availableArchitectures: ["x86"],
});
}
// Probe WASM content before suffix-only database routing; an explicit
// database selection keeps the caller's chosen interpretation.
const artifactFormat =
targetKind === "database"
? undefined
: await detectArtifactFormat(path, handle, signal);
if (
targetKind === "database" ||
(targetKind === undefined && path.toLowerCase().endsWith(".hop"))
@@ -173,7 +179,6 @@ export const parseBinaryTarget = async (
kind: "database",
format: "analysis-database",
});
const artifactFormat = await detectArtifactFormat(path, handle, signal);
if (artifactFormat !== undefined) {
const identity = {
path,
@@ -1,4 +1,4 @@
import { chmod, realpath, writeFile } from "node:fs/promises";
import { chmod, readFile, realpath, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { systemDoctorHost } from "../../../src/application/Doctor.js";
@@ -78,7 +78,7 @@ describe("executable format refusal diagnostics", () => {
});
describe("target route diagnostics", () => {
it.each(["module.wasm", "module.bin", "misleading.js"])(
it.each(["module.wasm", "module.bin", "misleading.js", "misleading.hop"])(
"identifies a WASM v1 header in %s without suggesting native provider installation",
async (name) => {
const directory = await createTestTempDirectory("rea-wasm-target-");
@@ -346,3 +346,68 @@ describe("raw-binary load rejection diagnostics", () => {
});
});
});
describe("caller-selected and inferred target interpretation", () => {
it.each(["archive.hop", "archive.HOP"])(
"preserves inferred database routing for ZIP content named %s",
async (name) => {
const directory = await createTestTempDirectory("rea-zip-database-");
const bytes = Buffer.alloc(22);
bytes.writeUInt32LE(0x06054b50, 0);
const zipPath = join(directory, "archive.zip");
const databasePath = join(directory, name);
await writeFile(zipPath, bytes);
await writeFile(databasePath, bytes);
expect(await parseBinaryTarget(zipPath)).toMatchObject({
ok: true,
value: { kind: "archive", format: "zip" },
});
expect(await parseBinaryTarget(databasePath)).toMatchObject({
ok: true,
value: { kind: "database", format: "analysis-database" },
});
expect(await readFile(databasePath)).toEqual(bytes);
},
);
it("preserves explicit DOS COM interpretation for bytes with a WASM header", async () => {
const directory = await createTestTempDirectory("rea-wasm-explicit-com-");
const path = join(directory, "module.hop");
await writeFile(path, Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]));
expect(
await parseBinaryTarget(path, { formatHint: "dos-com" }),
).toMatchObject({
ok: true,
value: { kind: "executable", format: "dos-com", architecture: "x86" },
});
});
it.each(["module.wasm", "misleading.hop"])(
"preserves explicitly selected database routing for %s",
async (name) => {
const directory = await createTestTempDirectory(
"rea-wasm-explicit-database-",
);
const path = join(directory, name);
await writeFile(path, Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]));
expect(
await parseBinaryTarget(path, { targetKind: "database" }),
).toMatchObject({
ok: true,
value: { kind: "database", format: "analysis-database" },
});
},
);
it.each(["ordinary.hop", "ordinary.HOP"])(
"retains automatic database routing for non-WASM %s",
async (name) => {
const directory = await createTestTempDirectory("rea-ordinary-database-");
const path = join(directory, name);
await writeFile(path, "existing database content");
expect(await parseBinaryTarget(path)).toMatchObject({
ok: true,
value: { kind: "database", format: "analysis-database" },
});
},
);
});
+4 -9
View File
@@ -1,3 +1,4 @@
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
import { API_SURFACE_EXAMPLE } from "../../../src/contracts/analysisViewApiSurfaceExample.js";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
@@ -212,16 +213,10 @@ it("advertises exact schemas and projects one layout section from retained Evide
inspect: () => Promise.resolve(ok(layout)),
});
const { client, session } = await connect(service);
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_analysis_view",
const { ajv, inputSchema, outputSchema } = await advertisedMcpToolSchemas(
client,
"inspect_analysis_view",
);
if (advertised?.outputSchema === undefined)
throw new Error("inspect_analysis_view must publish both schemas");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
expect(
ajv.validate(inputSchema, {
source: { kind: "inline", evidence: analysisViewLayoutEvidence() },
+15 -23
View File
@@ -1,9 +1,11 @@
import {
advertisedMcpToolSchemas,
assertRecordedMcpEvidence,
} from "../../fixtures/advertisedMcpTool.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it, onTestFinished } from "vitest";
import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
@@ -43,16 +45,11 @@ it("advertises exact valid schemas and records inline layout without a binary ta
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_binary_layout",
const schemas = await advertisedMcpToolSchemas(
client,
"inspect_binary_layout",
);
if (advertised?.outputSchema === undefined)
throw new Error("layout must publish both schemas");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
const { ajv, inputSchema } = schemas;
expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true);
expect(
ajv.validate(inputSchema, {
@@ -64,19 +61,14 @@ it("advertises exact valid schemas and records inline layout without a binary ta
name: "inspect_binary_layout",
arguments: { path: value.artifact.path },
});
expect(response.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const parsed = toolContract("inspect_binary_layout").outputSchema.parse(
response.structuredContent,
const evidence = assertRecordedMcpEvidence(
response,
"inspect_binary_layout",
schemas,
session,
value,
"observed",
);
const evidence = parseEvidence(parsed);
expect(evidence.confidence).toBe("observed");
expect(evidence.subject?.digest.sha256).toBe(value.artifact.sha256);
expect(parsed.normalized_result).toEqual(value);
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
const viewed = await client.callTool({
name: "inspect_analysis_view",
arguments: {
+15 -21
View File
@@ -1,3 +1,7 @@
import {
advertisedMcpToolSchemas,
assertRecordedMcpEvidence,
} from "../../fixtures/advertisedMcpTool.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createHash } from "node:crypto";
import { Ajv2020 } from "ajv/dist/2020.js";
@@ -66,16 +70,11 @@ it("advertises exact valid schemas and records inline EVM interface evidence wit
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_evm_interface",
const schemas = await advertisedMcpToolSchemas(
client,
"inspect_evm_interface",
);
if (advertised?.outputSchema === undefined)
throw new Error("EVM interface inspection must publish both schemas");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
const { ajv, inputSchema } = schemas;
expect(
ajv.validate(inputSchema, { path: value.artifact.path, encoding: "hex" }),
).toBe(true);
@@ -99,19 +98,14 @@ it("advertises exact valid schemas and records inline EVM interface evidence wit
name: "inspect_evm_interface",
arguments: { path: value.artifact.path, encoding: "hex" },
});
expect(response.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const parsed = toolContract("inspect_evm_interface").outputSchema.parse(
response.structuredContent,
assertRecordedMcpEvidence(
response,
"inspect_evm_interface",
schemas,
session,
value,
"inferred",
);
const evidence = parseEvidence(parsed);
expect(evidence.confidence).toBe("inferred");
expect(evidence.subject?.digest.sha256).toBe(value.artifact.sha256);
expect(parsed.normalized_result).toEqual(value);
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
expect(
(
await client.callTool({
+97 -95
View File
@@ -1,5 +1,9 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import {
advertisedMcpToolSchemas,
assertRecordedMcpEvidence,
} from "../../fixtures/advertisedMcpTool.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it, onTestFinished, vi } from "vitest";
import { readFile, realpath, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -20,20 +24,6 @@ import {
wasmArtifactExecution,
} from "../../fixtures/wasm/artifact.js";
const wasmToolSchemas = async (client: Client) => {
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_wasm_artifact",
);
if (advertised?.outputSchema === undefined)
throw new Error("WASM recovery must name an advertised static tool");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
return { name: advertised.name, ajv, inputSchema, outputSchema };
};
const assertFirstSummaryDelivery = async (
harness: Awaited<ReturnType<typeof createApplicationMcpHarness>>,
value: ReturnType<typeof wasmArtifactFixture>,
@@ -43,7 +33,7 @@ const assertFirstSummaryDelivery = async (
ajv: wasmAjv,
inputSchema,
outputSchema,
} = await wasmToolSchemas(harness.client);
} = await advertisedMcpToolSchemas(harness.client, "inspect_wasm_artifact");
const summaryInput = { path: value.artifact.path, detail: "summary" };
expect(wasmAjv.validate(inputSchema, summaryInput)).toBe(true);
expect(
@@ -91,73 +81,86 @@ const assertFirstSummaryDelivery = async (
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(
await realpath(directory),
process.platform === "win32" ? "module space.bin" : 'module "quoted".bin',
);
await writeFile(path, Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]));
const createClient = vi.fn(() => {
throw new Error("native provider must not start for WASM");
});
const session = createTestBinarySession(createClient);
const harness = await createApplicationMcpHarness({
session,
serverOptions: { environment: {} },
});
onTestFinished(harness.close);
const { client } = harness;
const { name, ajv, inputSchema } = await wasmToolSchemas(client);
const input = { path };
expect(
toolContract("inspect_wasm_artifact").inputSchema.safeParse(input).success,
).toBe(true);
const opening = await client.callTool({
name: "open_binary",
arguments: input,
});
expect(opening.isError).toBe(true);
const failure = parseMcpToolError(opening);
expect(failure).toMatchObject({
error: {
code: "target_unavailable",
category: "unsupported_target",
details: { constraint: "wasm_requires_static_inspection" },
},
});
expect(failure.error.remediation.action).toContain(JSON.stringify({ path }));
expect(ajv.validate(inputSchema, input)).toBe(true);
const inspection = await client.callTool({
name,
arguments: input,
});
expect(inspection.isError).toBe(true);
expect(parseMcpToolError(inspection)).toMatchObject({
error: {
code: "capability_unavailable",
details: {
operation: "inspect_wasm_artifact",
reason: expect.stringContaining("REA_WABT_BIN_DIRECTORY"),
it.each(["bin", "hop"])(
"directs rejected native WASM opening (%s suffix) to the advertised static tool while preserving missing-WABT diagnostics",
async (extension) => {
const directory = await createTestTempDirectory("rea-wasm-recovery-");
const path = join(
await realpath(directory),
process.platform === "win32"
? `module space.${extension}`
: `module "quoted".${extension}`,
);
await writeFile(path, Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]));
const createClient = vi.fn(() => {
throw new Error("native provider must not start for WASM");
});
const session = createTestBinarySession(createClient);
const harness = await createApplicationMcpHarness({
session,
serverOptions: { environment: {} },
});
onTestFinished(harness.close);
const { client } = harness;
const { name, ajv, inputSchema } = await advertisedMcpToolSchemas(
client,
"inspect_wasm_artifact",
);
const input = { path };
const opening = await client.callTool({
name: "open_binary",
arguments: input,
});
expect(opening.isError).toBe(true);
const failure = parseMcpToolError(opening);
expect(failure).toMatchObject({
error: {
code: "target_unavailable",
category: "unsupported_target",
details: { constraint: "wasm_requires_static_inspection" },
},
},
});
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(
Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]),
);
});
});
const [, recoveryName, serializedInput] =
failure.error.remediation.action.match(
/call (\w+) with (\{.*\}) or run/,
) ?? [];
expect(recoveryName).toBe(name);
const recoveryInput = z
.object({ path: z.literal(path) })
.strict()
.parse(JSON.parse(serializedInput ?? "null"));
expect(ajv.validate(inputSchema, recoveryInput)).toBe(true);
const inspection = await client.callTool({
name: recoveryName ?? "",
arguments: recoveryInput,
});
expect(inspection.isError).toBe(true);
expect(parseMcpToolError(inspection)).toMatchObject({
error: {
code: "capability_unavailable",
details: {
operation: "inspect_wasm_artifact",
reason: expect.stringContaining("REA_WABT_BIN_DIRECTORY"),
},
},
});
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(
Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]),
);
},
);
it("advertises exact valid schemas and records inline WASM interface evidence without a binary target", async () => {
const value = wasmArtifactFixture();
@@ -175,7 +178,11 @@ it("advertises exact valid schemas and records inline WASM interface evidence wi
});
onTestFinished(harness.close);
const { client } = harness;
const { ajv, inputSchema, outputSchema } = await wasmToolSchemas(client);
const schemas = await advertisedMcpToolSchemas(
client,
"inspect_wasm_artifact",
);
const { ajv, inputSchema } = schemas;
expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true);
expect(
ajv.validate(inputSchema, {
@@ -188,19 +195,14 @@ it("advertises exact valid schemas and records inline WASM interface evidence wi
name: "inspect_wasm_artifact",
arguments: { path: value.artifact.path },
});
expect(response.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const parsed = toolContract("inspect_wasm_artifact").outputSchema.parse(
response.structuredContent,
assertRecordedMcpEvidence(
response,
"inspect_wasm_artifact",
schemas,
session,
value,
"observed",
);
const evidence = parseEvidence(parsed);
expect(evidence.confidence).toBe("observed");
expect(evidence.subject?.digest.sha256).toBe(value.artifact.sha256);
expect(parsed.normalized_result).toEqual(value);
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
expect(
(
await client.callTool({
+52
View File
@@ -0,0 +1,52 @@
import type { Client } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import type { BinarySession } from "../../src/application/binary/BinarySession.js";
/** Verify the actual advertised schemas, retaining a private Ajv registry. */
export const advertisedMcpToolSchemas = async (
client: Client,
name: string,
) => {
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === name,
);
if (advertised?.outputSchema === undefined)
throw new Error(`${name} must publish both schemas`);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
return { name: advertised.name, ajv, inputSchema, outputSchema };
};
/** Keep public output, provenance and session retention checks together. */
export const assertRecordedMcpEvidence = (
response: Awaited<ReturnType<Client["callTool"]>>,
name:
| "inspect_wasm_artifact"
| "inspect_binary_layout"
| "inspect_evm_interface",
schemas: Awaited<ReturnType<typeof advertisedMcpToolSchemas>>,
session: BinarySession,
expected: { readonly artifact: { readonly sha256: string } },
confidence: ReturnType<typeof parseEvidence>["confidence"],
) => {
expect(response.isError).not.toBe(true);
expect(
schemas.ajv.validate(schemas.outputSchema, response.structuredContent),
JSON.stringify(schemas.ajv.errors),
).toBe(true);
const parsed = toolContract(name).outputSchema.parse(
response.structuredContent,
);
const evidence = parseEvidence(parsed);
expect(evidence.confidence).toBe(confidence);
expect(evidence.subject?.digest.sha256).toBe(expected.artifact.sha256);
expect(parsed.normalized_result).toEqual(expected);
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
return evidence;
};