fix(wasm): refuse unretained summary delivery before provider work

inspect_wasm_artifact with detail summary ran the WASM provider before
checking whether the server retains Evidence. On a fixed server without
session retention a successful provider run was followed by
capability_unavailable, and a failing provider run replaced that
diagnostic with the provider error, hiding the guidance to select
detail complete.

Check retention right after input validation, as the EVM, pcap and
network-capture handlers do, so the refusal precedes any provider work
and keeps its typed diagnostic. Complete delivery is unchanged. The
fixed-server boundary test now injects a vi.fn provider and asserts it
stays idle for refused summaries while complete delivery still runs it.

Fixes #5100

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Y0UW1SH
2026-10-19 13:26:34 -04:00
parent 0e62094b69
commit 418a66d7d7
3 changed files with 49 additions and 17 deletions
+2 -1
View File
@@ -205,7 +205,8 @@ that complete Evidence in the session and returns the derived
`normalized_result.parent_evidence_id` and `evidence_links` name the retained
analysis, so later views and application workflows read it without repeating
analysis or moving the complete record through the response. A server without
session retention refuses summary detail with `capability_unavailable`.
session retention refuses summary detail with `capability_unavailable` before
any analysis starts.
For `inspect_artifact`, omission also keeps the original request parameters and
complete output shape. Delivery selection never reaches the inventory engine;
the complete inspection and its atomic inventory Evidence remain retained.
+10 -5
View File
@@ -7,7 +7,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 {
requireAnalysisViewRetention,
summarizeRetainedAnalysis,
} from "../application/analysisView/AnalysisViewService.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err } from "../domain/result.js";
import { wasmArtifactModule } from "../composition/wasm.js";
@@ -72,12 +75,14 @@ export const registerWasmTools = (
),
);
const { detail, ...input } = parsed.data;
const retention = { recordEvidence, evidenceById };
if (detail === "summary") {
const ready = requireAnalysisViewRetention(contract.name, retention);
if (!ready.ok) return ready;
}
const inspected = await service.inspect(input, options);
return detail === "summary" && inspected.ok
? summarizeRetainedAnalysis(inspected.value, {
recordEvidence,
evidenceById,
})
? summarizeRetainedAnalysis(inspected.value, retention)
: inspected;
},
logger,
+37 -11
View File
@@ -14,7 +14,8 @@ import { wasmArtifactSummaryViewResultSchema } from "../../../src/domain/analysi
import { createHash } from "node:crypto";
import { z } from "zod";
import { createServer } from "../../../src/server/createServer.js";
import { ok } from "../../../src/domain/result.js";
import { err, ok } from "../../../src/domain/result.js";
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -608,11 +609,14 @@ 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 () => {
it("refuses WASM summary delivery before provider work when retention is unavailable and keeps fixed-server complete delivery", async () => {
const value = wasmArtifactFixture();
const inspect = vi.fn<WasmArtifactService["provider"]["inspect"]>(() =>
Promise.resolve(ok(wasmArtifactExecution(value))),
);
const service = new WasmArtifactService({
close: () => Promise.resolve(),
inspect: () => Promise.resolve(ok(wasmArtifactExecution(value))),
inspect,
});
const session = createTestBinarySession(() => {
throw new Error("No deep provider");
@@ -626,9 +630,10 @@ it("requires retention for WASM summary delivery while keeping fixed-server comp
try {
await server.connect(b);
await client.connect(a);
const summaryInput = { path: value.artifact.path, detail: "summary" };
const summary = await client.callTool({
name: "inspect_wasm_artifact",
arguments: { path: value.artifact.path, detail: "summary" },
arguments: summaryInput,
});
expect(parseMcpToolError(summary)).toMatchObject({
error: {
@@ -636,14 +641,35 @@ it("requires retention for WASM summary delivery while keeping fixed-server comp
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,
expect(inspect).not.toHaveBeenCalled();
for (const input of [
{ path: value.artifact.path },
{ path: value.artifact.path, detail: "complete" },
]) {
const complete = await client.callTool({
name: "inspect_wasm_artifact",
arguments: input,
});
expect(complete.isError).not.toBe(true);
expect(
parseEvidence(complete.structuredContent).normalized_result,
).toEqual(value);
}
expect(inspect).toHaveBeenCalledTimes(2);
inspect.mockImplementation(() =>
Promise.resolve(err(new AnalysisInputError("inspect_wasm_artifact"))),
);
const refused = await client.callTool({
name: "inspect_wasm_artifact",
arguments: summaryInput,
});
expect(parseMcpToolError(refused)).toMatchObject({
error: {
code: "capability_unavailable",
message: expect.stringContaining("Select detail complete"),
},
});
expect(inspect).toHaveBeenCalledTimes(2);
await client.ping();
} finally {
await client.close();