mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(wasm): name the compare_wasm_interfaces side of rejected Evidence (#7302)
* fix(wasm): name the compare_wasm_interfaces side of rejected Evidence compare_wasm_interfaces parsed both sides inside one try and reported any rejection, such as tampered content, a different operation or a rebound subject, as an input issue at path [], which names no argument. Each side is now parsed separately and a rejection is reported at [left] or [right], as the function, native and managed comparers already do. * test(wasm): cover both comparison sides through the MCP boundary
This commit is contained in:
1 parent
d0d31fcf21
commit
1861059f8b
3 files changed
+96
-5
No files matched your search
@@ -168,6 +168,40 @@ it("requires the producer's file subject format even for authentic Evidence", as
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
|
||||
});
|
||||
|
||||
it.each(["left", "right"] as const)(
|
||||
"names the %s argument whose Evidence is rejected",
|
||||
async (side) => {
|
||||
const tampered = JSON.parse(JSON.stringify(evidence(facts("f"))));
|
||||
tampered.normalized_result.interface.entries[0].name = "tampered";
|
||||
const report = wasmArtifactFixture();
|
||||
const otherSelection = createEvidence(
|
||||
{
|
||||
path: report.artifact.path,
|
||||
sha256: report.artifact.sha256,
|
||||
format: "file",
|
||||
},
|
||||
{ id: "fixture", name: "typed fixture", version: "1" },
|
||||
{
|
||||
operation: "inspect_wasm_artifact",
|
||||
parameters: { path: "/artifacts/other.wasm" },
|
||||
result: report,
|
||||
confidence: "observed",
|
||||
},
|
||||
);
|
||||
for (const rejected of [tampered, otherSelection])
|
||||
expect(
|
||||
await compareWasmInterfaces({
|
||||
left: side === "left" ? rejected : evidence(facts("f")),
|
||||
right: side === "right" ? rejected : evidence(facts("f")),
|
||||
scope: "both",
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { issues: [{ path: [side], reason: "invalid_value" }] },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("resolves exact retained references and permits cancellation between comparison batches", async () => {
|
||||
const left = evidence(facts("f"));
|
||||
const right = evidence(facts("f", "i64"));
|
||||
|
||||
@@ -40,16 +40,19 @@ import {
|
||||
const OPERATION = "compare_wasm_interfaces";
|
||||
const parseInspection = (
|
||||
raw: unknown,
|
||||
side: "left" | "right",
|
||||
): Result<{ evidence: Evidence; result: WasmArtifact }, AnalysisInputError> => {
|
||||
const authenticated = safeParseEvidence(raw);
|
||||
if (!authenticated.ok) return inspectionInputFailure(authenticated.error);
|
||||
if (!authenticated.ok)
|
||||
return inspectionInputFailure(authenticated.error, side);
|
||||
const evidence = authenticated.value;
|
||||
if (evidence.operation !== "inspect_wasm_artifact")
|
||||
return inspectionInputFailure(
|
||||
new TypeError("Expected complete inspect_wasm_artifact Evidence"),
|
||||
side,
|
||||
);
|
||||
const parsed = wasmArtifactSchema.safeParse(evidence.normalized_result);
|
||||
if (!parsed.success) return inspectionInputFailure(parsed.error);
|
||||
if (!parsed.success) return inspectionInputFailure(parsed.error, side);
|
||||
const result = parsed.data;
|
||||
if (
|
||||
!wasmArtifactMatchesSubject(
|
||||
@@ -67,16 +70,19 @@ const parseInspection = (
|
||||
new TypeError(
|
||||
"WASM Evidence subject/selection does not match normalized artifact identity",
|
||||
),
|
||||
side,
|
||||
);
|
||||
return ok({ evidence, result });
|
||||
};
|
||||
|
||||
/** Attribute a rejected side to the input argument that supplied it. */
|
||||
const inspectionInputFailure = (
|
||||
cause: Error,
|
||||
side: "left" | "right",
|
||||
): Result<never, AnalysisInputError> =>
|
||||
err(
|
||||
new AnalysisInputError(OPERATION, { cause }, [
|
||||
{ path: [], reason: "invalid_value", message: cause.message },
|
||||
{ path: [side], reason: "invalid_value", message: cause.message },
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -107,9 +113,9 @@ export const compareWasmInterfaces = async (
|
||||
try {
|
||||
await setImmediate();
|
||||
assertActive();
|
||||
const parsedLeft = parseInspection(resolved.value.input.left);
|
||||
const parsedLeft = parseInspection(resolved.value.input.left, "left");
|
||||
if (!parsedLeft.ok) return parsedLeft;
|
||||
const parsedRight = parseInspection(resolved.value.input.right);
|
||||
const parsedRight = parseInspection(resolved.value.input.right, "right");
|
||||
if (!parsedRight.ok) return parsedRight;
|
||||
const left = parsedLeft.value;
|
||||
const right = parsedRight.value;
|
||||
|
||||
@@ -33,6 +33,7 @@ import { OwnedFileHandle } from "../../src/filesystem/OwnedFileHandle.js";
|
||||
import { primaryAnalysisError } from "../../src/domain/analysisErrorBase.js";
|
||||
import { buildManagedPeFixture } from "../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { wasmArtifactFixture } from "../fixtures/wasm/artifact.js";
|
||||
import { ProcessCaptureError } from "../../src/process/capture/ProcessCaptureError.js";
|
||||
|
||||
import { reconstructionObligationLedgerSchema } from "../../src/domain/reconstructionObligationLedgerSchemas.js";
|
||||
@@ -919,3 +920,53 @@ describe("native matching failure ownership through the public SDK", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("compare_wasm_interfaces Evidence attribution", () => {
|
||||
const inspection = (selectedPath?: string) => {
|
||||
const report = wasmArtifactFixture();
|
||||
return createEvidence(
|
||||
{
|
||||
path: report.artifact.path,
|
||||
sha256: report.artifact.sha256,
|
||||
format: "file",
|
||||
},
|
||||
{ id: "typed-fixture", name: "Typed fixture", version: "1" },
|
||||
{
|
||||
operation: "inspect_wasm_artifact",
|
||||
parameters: { path: selectedPath ?? report.artifact.path },
|
||||
result: report,
|
||||
confidence: "observed",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
it.each(["left", "right"] as const)(
|
||||
"names the %s argument whose authentic Evidence selects another artifact",
|
||||
async (side) => {
|
||||
await withSdk(async ({ client, session }) => {
|
||||
const rejected = inspection("/artifacts/other.wasm");
|
||||
const response = await client.callTool({
|
||||
name: "compare_wasm_interfaces",
|
||||
arguments: {
|
||||
left: side === "left" ? rejected : inspection(),
|
||||
right: side === "right" ? rejected : inspection(),
|
||||
scope: "both",
|
||||
},
|
||||
});
|
||||
|
||||
expect(parseMcpToolError(response).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: [side],
|
||||
reason: "invalid_value",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(session.exportEvidenceBundle().records).toEqual([]);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user