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:
9 files changed
+238
-47
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>`."
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user