From 514df0918c0cc3ac4e68f7af53f0a605eea3475c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:33:34 +0800 Subject: [PATCH] fix(analysis-view): preserve inline Evidence error paths Project authenticated inline Evidence failures at source.evidence while retaining their reasons and messages. Supply the matching request root to input issue projection so nested values remain identifiable. Validation: both service entrypoints and SDK MCP integrity regressions, advertised-schema consumer suite, check:fast and architecture guards. No real-provider verification. --- .../analysisView/AnalysisViewService.test.ts | 30 +++++++++++++++++++ .../analysisView/AnalysisViewService.ts | 7 +++-- tests/boundary/mcp/analysisViewMcp.test.ts | 30 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index 0ad7917de..aa7477662 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -11,6 +11,7 @@ import { } from "../../../tests/fixtures/analysisView.js"; import { inspectAnalysisView, + inspectAnalysisViewValidated, summarizeRetainedAnalysis, } from "./AnalysisViewService.js"; import type { Evidence } from "../../domain/evidence.js"; @@ -58,6 +59,35 @@ it.each(["/fixtures/unknown.exe", ""])( }, ); +it("reports inline Evidence identity failures at the complete source path", () => { + const parent = analysisViewLayoutEvidence(); + const input = { + source: { + kind: "inline", + evidence: { ...parent, evidence_id: `ev_${"0".repeat(64)}` }, + }, + view: { kind: "summary" }, + } as const; + + for (const result of [ + inspectAnalysisView(input), + inspectAnalysisViewValidated(input), + ]) + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + issues: [ + { + path: ["source", "evidence", "evidence_id"], + reason: "invalid_value", + message: "Evidence semantic identifier does not match its record", + }, + ], + }, + }); +}); + it("projects a small native view from an authenticated >10 MiB retained dossier", () => { const original = functionDossierSchema.parse(ghidraFunctionDossier()); const parent = createEvidence( diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index ea079802e..380feb59e 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -132,8 +132,11 @@ const resolveAnalysisViewSource = ( return err( analysisInputErrorFromIssues( INSPECT_ANALYSIS_VIEW_OPERATION, - cause.issues, - source.evidence, + cause.issues.map((issue) => ({ + ...issue, + path: ["source", "evidence", ...issue.path], + })), + { source }, { cause }, ), ); diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts index 3c15312aa..0de1f0b02 100644 --- a/tests/boundary/mcp/analysisViewMcp.test.ts +++ b/tests/boundary/mcp/analysisViewMcp.test.ts @@ -19,6 +19,7 @@ import { analysisViewJavaScriptEvidence, analysisViewJavaScriptAnalysisWithSource, analysisViewBindJavaScriptGraphs, + analysisViewLayoutEvidence, analysisViewLayoutFixture, } from "../../fixtures/analysisView.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; @@ -111,6 +112,35 @@ it("advertises exact schemas and projects one layout section from retained Evide ); }); +it("reports a tampered inline Evidence ID at its full MCP source path", async () => { + const { client } = await connect(); + const parent = analysisViewLayoutEvidence(); + const response = await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { + kind: "inline", + evidence: { ...parent, evidence_id: `ev_${"0".repeat(64)}` }, + }, + view: { kind: "summary" }, + }, + }); + + expect(parseMcpToolError(response).error).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + issues: [ + { + path: ["source", "evidence", "evidence_id"], + reason: "invalid_value", + message: "Evidence semantic identifier does not match its record", + }, + ], + }, + }); +}); + it("delivers a bounded native dossier view over MCP without a Ghidra provider or lost connection", async () => { const { client, session } = await connect(); const original = functionDossierSchema.parse(ghidraFunctionDossier());