mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user