From 170bfc7b3fc9ce03368cf2df5e7b54107e65b1b8 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 15 Oct 2026 03:37:15 +0800 Subject: [PATCH] fix(mcp): authenticate inventory facts before prompt completion --- src/server/promptCompletion.ts | 44 ++++---- tests/boundary/mcp/promptCompletion.test.ts | 105 +++++++++++++++----- 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/src/server/promptCompletion.ts b/src/server/promptCompletion.ts index 5c4c7a3396..995eecb958 100644 --- a/src/server/promptCompletion.ts +++ b/src/server/promptCompletion.ts @@ -7,8 +7,9 @@ import type { AnalysisOperationPort } from "../application/AnalysisProvider.js"; import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; import type { PromptCompletionKind } from "../contracts/promptContracts.js"; import { currentMcpRequestSignal } from "./EvidenceMcpServer.js"; -import { artifactInventoryResultSchema } from "../domain/artifactGraph.js"; +import { parseArtifactInventoryEvidence } from "../domain/artifactInventoryEvidence.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; +import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; import { parseProcessCapture } from "../domain/process/processCaptureParsing.js"; const documentListSchema = z.array(z.string().min(1)); @@ -180,29 +181,38 @@ const evidenceValues = ( if (kind === "evidence") return [evidence.evidence_id]; if (kind === "capture") return isProcessCaptureEvidence(evidence) ? [evidence.evidence_id] : []; - let inventoryResults: readonly unknown[]; if (evidence.operation === "inventory_artifact") - inventoryResults = [evidence.normalized_result]; - else if (evidence.operation === "inspect_artifact") { + return artifactInventoryValues(kind, evidence); + if (evidence.operation === "inspect_artifact") { const inspection = artifactInspectionResultSchema.safeParse( evidence.normalized_result, ); if (!inspection.success) return []; - inventoryResults = inspection.data.substeps.map( - ({ evidence: inventoryEvidence }) => inventoryEvidence.normalized_result, + return inspection.data.substeps.flatMap(({ evidence: inventoryEvidence }) => + artifactInventoryValues(kind, inventoryEvidence), ); - } else return []; - const values: string[] = []; - for (const inventoryResult of inventoryResults) { - const inventory = artifactInventoryResultSchema.safeParse(inventoryResult); - if (!inventory.success) continue; - if (kind === "manifest") values.push(inventory.data.manifest.manifest_id); - else - values.push( - ...inventory.data.occurrences.map(({ occurrence_id: id }) => id), - ); } - return values; + return []; +}; + +const artifactInventoryValues = ( + kind: "manifest" | "occurrence", + evidence: unknown, +): readonly string[] => { + try { + const { inventory } = parseArtifactInventoryEvidence(evidence); + return kind === "manifest" + ? [inventory.manifest.manifest_id] + : inventory.occurrences.map(({ occurrence_id: id }) => id); + } catch (cause: unknown) { + if ( + cause instanceof TypeError || + cause instanceof z.ZodError || + cause instanceof EvidenceIntegrityError + ) + return []; + throw cause; + } }; const isProcessCaptureEvidence = (evidence: LedgerEvidence): boolean => { diff --git a/tests/boundary/mcp/promptCompletion.test.ts b/tests/boundary/mcp/promptCompletion.test.ts index f795b116aa..789f07c9db 100644 --- a/tests/boundary/mcp/promptCompletion.test.ts +++ b/tests/boundary/mcp/promptCompletion.test.ts @@ -14,7 +14,7 @@ import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js"; import { PROCESS_CAPTURE_REFERENCE } from "../../../src/contracts/investigationExamples.js"; import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js"; -import { createEvidence } from "../../../src/domain/evidence.js"; +import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { artifactInspectionResultSchema, createArtifactInspection, @@ -212,28 +212,7 @@ describe("guided prompt completion from investigation records", () => { describe("guided prompt completion from retained records", () => { it("offers identifiers from every retained inspection inventory substep", async () => { const session = createTestBinarySession(() => client([])); - const leftInspection = createArtifactInspection( - ARTIFACT_COMPARISON_EXAMPLE.left, - ); - const rightInspection = createArtifactInspection( - ARTIFACT_COMPARISON_EXAMPLE.right, - ); - const inspection = artifactInspectionResultSchema.parse({ - ...leftInspection, - substeps: [...leftInspection.substeps, ...rightInspection.substeps], - evidence_links: [ - ...leftInspection.evidence_links, - ...rightInspection.evidence_links, - ], - }); - const inspectionEvidence = createEvidence(undefined, fixtureProvider, { - operation: "inspect_artifact", - parameters: {}, - result: jsonValueSchema.parse(inspection), - confidence: "observed", - authority: "shipped-artifact", - evidenceLinks: inspection.evidence_links, - }); + const inspectionEvidence = createCompositeInspectionEvidence(); expect(session.recordEvidence(inspectionEvidence).ok).toBe(true); const completion = createPromptCompletionSource(session, session); const expectedManifests = [ @@ -267,6 +246,38 @@ describe("guided prompt completion from retained records", () => { await session.close(); }); + it("skips an inner inventory whose payload no longer matches its Evidence ID", async () => { + const session = createTestBinarySession(() => client([])); + const inspectionEvidence = createCompositeInspectionEvidence(true); + const corruptedInner = artifactInspectionResultSchema.parse( + inspectionEvidence.normalized_result, + ).substeps[1]?.evidence; + if (corruptedInner === undefined) + throw new Error("composite fixture must contain a second inventory"); + expect(corruptedInner.evidence_id).toBe( + ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id, + ); + expect( + artifactInventoryResultSchema.safeParse(corruptedInner.normalized_result) + .success, + ).toBe(true); + expect(() => parseEvidence(corruptedInner)).toThrow(); + expect(session.recordEvidence(inspectionEvidence).ok).toBe(true); + const completion = createPromptCompletionSource(session, session); + const firstInventory = artifactInventoryResultSchema.parse( + ARTIFACT_COMPARISON_EXAMPLE.left.normalized_result, + ); + + expect(await completion.complete("manifest", "agm_")).toEqual([ + firstInventory.manifest.manifest_id, + ]); + expect(await completion.complete("occurrence", "occ_")).toEqual( + firstInventory.occurrences.map(({ occurrence_id }) => occurrence_id), + ); + + await session.close(); + }); + it("deduplicates, case-folds, and returns deterministic live evidence IDs", async () => { const session = createTestBinarySession(() => client([])); for (let index = 149; index >= 0; index -= 1) { @@ -340,6 +351,54 @@ const fixtureEnvironment = { isolation: "process" as const, }; +const createCompositeInspectionEvidence = (corruptSecondInventory = false) => { + const leftInspection = createArtifactInspection( + ARTIFACT_COMPARISON_EXAMPLE.left, + ); + const rightInspection = createArtifactInspection( + ARTIFACT_COMPARISON_EXAMPLE.right, + ); + const substeps = [...leftInspection.substeps, ...rightInspection.substeps]; + if (corruptSecondInventory) { + const secondSubstep = substeps[1]; + if (secondSubstep === undefined) + throw new Error("composite fixture must contain a second inventory"); + const inventory = artifactInventoryResultSchema.parse( + secondSubstep.evidence.normalized_result, + ); + substeps[1] = { + ...secondSubstep, + evidence: { + ...secondSubstep.evidence, + normalized_result: { + ...inventory, + coverage: { ...inventory.coverage, status: "unknown" }, + }, + }, + }; + } + const inspection = artifactInspectionResultSchema.parse({ + ...leftInspection, + substeps, + evidence_links: [ + ...leftInspection.evidence_links, + ...rightInspection.evidence_links, + ], + coverage: { + ...leftInspection.coverage, + substeps_completed: substeps.length, + }, + }); + return createEvidence(undefined, fixtureProvider, { + operation: "inspect_artifact", + parameters: {}, + result: jsonValueSchema.parse(inspection), + confidence: "observed", + authority: "shipped-artifact", + evidenceLinks: inspection.evidence_links, + }); +}; + const client = ( requests: Array>>, analysisProfile?: AnalysisProfileCommitment,