feat(target): explain static WASM inspection after native rejection

Explain recognized WASM v1 native-admission rejection with an exact call to the existing static inspection tool and its caller-supplied WABT prerequisites. Preserve selected paths, explicit raw-binary choices, typed missing-tool outcomes and connection usability.

Refs #1852

Co-authored-by: SII-YDD <38822679+SII-YDD@users.noreply.github.com>
Co-authored-by: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 07:00:30 +08:00
1 parent 89f5e489ad
commit 1f17ac8228
9 files changed
+238 -47

No files matched your search

+9
View File
@@ -4,6 +4,15 @@
local WASM bytes using caller-supplied WABT 1.0.42. No active binary target is
required. REA does not install tools or fetch artifacts and does not execute WASM.
`open_binary` is not the WASM inspection route. A recognized WASM v1 header
returns `target_unavailable` with category `unsupported_target` and constraint
`wasm_requires_static_inspection`, including the selected path in a valid
`inspect_wasm_artifact` call. Use that target-free tool or
`rea inspect-wasm-artifact <path> --format json` with the toolchain below.
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.
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
+9
View File
@@ -247,6 +247,15 @@ const detectArtifactFormat = async (
const magic = Buffer.alloc(8);
const observed = await handle.read(magic, 0, magic.length, 0);
throwIfTargetResolutionCancelled(signal);
if (
observed.bytesRead === 8 &&
magic.equals(Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]))
)
throw new BinaryTargetError(
path,
"WebAssembly v1 artifacts are not supported by the native opening route",
{ constraint: "wasm_requires_static_inspection" },
);
if (hasZipSignature(magic.subarray(0, observed.bytesRead))) {
// The HarmonyOS App Pack suffix `.app` is a root-input concept; a nested
// ZIP member ending in `.app` keeps the generic zip classification.
+1 -1
View File
@@ -421,7 +421,7 @@ export const systemDoctorHost = (
name: "target",
ok: false,
classification:
targetError?.constraint === "directory_requires_file"
targetError?.constraint !== undefined
? "unsupported_target"
: "config_drift",
detail: path,
+15
View File
@@ -76,6 +76,11 @@ export const analysisErrorRemediationAction = (
error.constraint === "directory_requires_file"
)
return "For a JavaScript/Electron application directory, call analyze_javascript_application with input_path or run `rea analyze <directory>`. For binary analysis, select its executable file.";
if (
error instanceof BinaryTargetError &&
error.constraint === "wasm_requires_static_inspection"
)
return `For static inspection, call inspect_wasm_artifact with ${JSON.stringify({ path: error.path })} or run \`rea inspect-wasm-artifact <path> --format json\`. This separate lane requires caller-supplied WABT 1.0.42 via absolute REA_WABT_BIN_DIRECTORY; check its availability in binary_session. REA does not execute WASM or install tools, and the header alone does not establish that the complete module is valid.`;
if (
error instanceof AnalysisAccessDeniedError ||
(error instanceof BinaryTargetError && error.systemCode !== undefined)
@@ -95,6 +100,11 @@ export const analysisErrorRemediationAction = (
export const analysisErrorCategory = (
error: AnalysisError,
): AnalysisErrorProjection["category"] => {
if (
error instanceof BinaryTargetError &&
error.constraint === "wasm_requires_static_inspection"
)
return "unsupported_target";
if (
error instanceof ProviderSelectionError &&
error.reason === "provider_unavailable"
@@ -213,6 +223,11 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
)
.join("; ")}.`;
if (error instanceof NoBinaryOpenError) return error.message;
if (
error instanceof BinaryTargetError &&
error.constraint === "wasm_requires_static_inspection"
)
return `${error.message}. Static WebAssembly inspection uses a separate optional WABT lane, not a native analysis engine.`;
if (error instanceof BinaryTargetError)
return error.constraint === "directory_requires_file"
? "open_binary accepts files and macOS app bundles. This target is a directory; JavaScript/Electron application directories can be analyzed directly with analyze_javascript_application or `rea analyze <directory>`."
+3 -1
View File
@@ -38,7 +38,9 @@ export class NoBinaryOpenError extends AnalysisError {
/** Additional constraints identified while admitting a filesystem target. */
export interface BinaryTargetErrorOptions extends AnalysisErrorOptions {
readonly constraint?: "directory_requires_file";
readonly constraint?:
| "directory_requires_file"
| "wasm_requires_static_inspection";
}
/** A supplied target path could not be safely opened as a supported app or binary. */
+25 -13
View File
@@ -131,6 +131,8 @@ for (const little of [true, false]) {
const fp64 = await inspect(bytes, little);
assert.ok(fp64.ok);
const changed = target({ ...metadata(little), abiFlags: fp64.value });
assert.equal(fp64.value?.cpr1Size, 2);
assert.equal(fp64.value?.fpAbi, 6);
assert.match(ghidraMipsUnsupportedReason(changed) ?? "", /FP64/u);
assert.notDeepEqual(ghidraMipsProfileParameters(changed), before);
});
@@ -271,6 +273,13 @@ const unsupported: [string, (bytes: Buffer) => void, RegExp][] = [
},
/MIPS32r2/u,
],
[
"soft-float with declared CPR1",
(b) => {
b[abiOffset + 7] = 3;
},
/soft-float without CPR1/u,
],
[
"unspecified FP ABI",
(b) => {
@@ -301,19 +310,22 @@ for (const [name, change, reason] of unsupported) {
);
});
}
it("resolves soft-float ABI metadata and commits its integer-register storage policy", async () => {
const bytes = fixture();
bytes[abiOffset + 5] = 0;
bytes[abiOffset + 7] = 3;
const resolved = await resolveFixture(bytes);
assert.equal(resolved.mips.abiFlags?.cpr1Size, 0);
assert.equal(resolved.mips.abiFlags?.fpAbi, 3);
assert.equal(ghidraMipsUnsupportedReason(resolved), null);
assert.equal(
ghidraMipsProfileParameters(resolved).mips_soft_float_storage,
"o32-soft-float-prototype-v1",
);
});
it.each([true, false])(
"resolves soft-float ABI metadata and commits its integer-register storage policy (little endian: %s)",
async (little) => {
const bytes = fixture(little);
bytes[abiOffset + 5] = 0;
bytes[abiOffset + 7] = 3;
const resolved = await resolveFixture(bytes);
assert.equal(resolved.mips.abiFlags?.cpr1Size, 0);
assert.equal(resolved.mips.abiFlags?.fpAbi, 3);
assert.equal(ghidraMipsUnsupportedReason(resolved), null);
assert.equal(
ghidraMipsProfileParameters(resolved).mips_soft_float_storage,
"o32-soft-float-prototype-v1",
);
},
);
it("preserves the caller's cancellation error during table inspection", async () => {
const cancelled = new Error("caller cancellation sentinel");
let checks = 0;
@@ -7,6 +7,81 @@ import { projectAnalysisError } from "../../../src/domain/analysisErrorProjectio
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("target route diagnostics", () => {
it.each(["module.wasm", "module.bin", "misleading.js"])(
"identifies a WASM v1 header in %s without suggesting native provider installation",
async (name) => {
const directory = await createTestTempDirectory("rea-wasm-target-");
const path = join(directory, name);
const bytes = Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]);
await writeFile(path, bytes);
const result = await parseBinaryTarget(path);
if (result.ok) throw new Error("WASM was admitted to native opening");
const projected = projectAnalysisError(result.error);
expect(projected).toMatchObject({
code: "target_unavailable",
category: "unsupported_target",
retryable: false,
details: {
path: await realpath(path),
constraint: "wasm_requires_static_inspection",
},
});
expect(projected.message).toContain("WebAssembly");
expect(projected.remediation.action).toContain("inspect_wasm_artifact");
expect(projected.remediation.action).toContain("REA_WABT_BIN_DIRECTORY");
expect(projected.remediation.action).toContain("WABT 1.0.42");
expect(projected.remediation.action).toContain("does not execute");
expect(await systemDoctorHost().inspectTarget?.(path)).toMatchObject({
ok: false,
classification: "unsupported_target",
details: { constraint: "wasm_requires_static_inspection" },
remediation: projected.remediation.action,
});
},
);
it.each([
[0, 0x61, 0x73, 0x6d],
[0, 0x61, 0x73, 0x6d, 2, 0, 0, 0],
[1, 2, 3, 4, 5, 6, 7, 8],
])(
"does not infer supported WASM encoding from a suffix or incomplete header",
async (...bytes) => {
const directory = await createTestTempDirectory("rea-wasm-unknown-");
const path = join(directory, "module.wasm");
await writeFile(path, Buffer.from(bytes));
const result = await parseBinaryTarget(path);
if (result.ok) throw new Error("unknown header was admitted");
const projected = projectAnalysisError(result.error);
expect(projected.details).not.toHaveProperty("constraint");
expect(projected.remediation.action).not.toContain(
"inspect_wasm_artifact",
);
},
);
it("preserves an explicit raw-binary selection even when the bytes have a WASM header", async () => {
const directory = await createTestTempDirectory("rea-wasm-explicit-raw-");
const path = join(directory, "module.wasm");
await writeFile(path, Buffer.from([0, 0x61, 0x73, 0x6d, 1, 0, 0, 0]));
expect(
await parseBinaryTarget(path, {
formatHint: "raw-binary",
architecture: "x86",
entryPoint: "0x001000",
}),
).toMatchObject({
ok: true,
value: {
path: await realpath(path),
format: "raw-binary",
architecture: "x86",
entryPoint: "0x1000",
entryPointSource: "0x001000",
},
});
});
it("identifies the directory opening constraint and gives the direct analysis route", async () => {
const directory = await createTestTempDirectory("rea-js-target-");
await writeFile(join(directory, "index.js"), "export const answer = 42;\n");
+85 -26
View File
@@ -1,17 +1,93 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { Client } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it, onTestFinished, vi } from "vitest";
import { readFile, realpath, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { WasmArtifactService } from "../../../src/application/wasm/WasmArtifactService.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 { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import {
wasmArtifactFixture,
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 };
};
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"),
},
},
});
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();
const close = vi.fn(() => Promise.resolve());
@@ -22,30 +98,13 @@ it("advertises exact valid schemas and records inline WASM interface evidence wi
const session = createTestBinarySession(() => {
throw new Error("deep provider must not start");
});
const server = createServer(
{ kind: "session", session },
{ wasmArtifact: service },
);
const client = new Client({ name: "wasm-artifact-contract", version: "1" });
onTestFinished(async () => {
await client.close();
await server.close();
await session.close();
const harness = await createApplicationMcpHarness({
session,
serverOptions: { wasmArtifact: service },
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_wasm_artifact",
);
if (advertised?.outputSchema === undefined)
throw new Error("WASM 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);
onTestFinished(harness.close);
const { client } = harness;
const { ajv, inputSchema, outputSchema } = await wasmToolSchemas(client);
expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true);
expect(
ajv.validate(inputSchema, {
@@ -79,6 +138,6 @@ it("advertises exact valid schemas and records inline WASM interface evidence wi
})
).isError,
).toBe(true);
await server.close();
await harness.close();
expect(close).toHaveBeenCalled();
});
+16 -6
View File
@@ -14,12 +14,22 @@ export interface ApplicationMcpHarness {
}
/** Connect application workflows through the production MCP registration. */
export async function createApplicationMcpHarness(): Promise<ApplicationMcpHarness> {
const session = createTestBinarySession(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(resultOk(null)),
}));
const server = createServer({ kind: "session", session });
export async function createApplicationMcpHarness(
options: {
readonly session?: BinarySession;
readonly serverOptions?: Parameters<typeof createServer>[1];
} = {},
): Promise<ApplicationMcpHarness> {
const session =
options.session ??
createTestBinarySession(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(resultOk(null)),
}));
const server = createServer(
{ kind: "session", session },
options.serverOptions,
);
const client = new Client({
name: "application-workflow-test",
version: "1",