fix(mcp): authenticate inventory facts before prompt completion

This commit is contained in:
morluto committed 2026-10-15 03:43:29 +08:00
1 parent 591c87c4b6
commit 170bfc7b3f
2 files changed
+109 -40

No files matched your search

+27 -17
View File
@@ -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 => {
+82 -23
View File
@@ -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<Readonly<Record<string, unknown>>>,
analysisProfile?: AnalysisProfileCommitment,