diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index c801887af1..2f776060ae 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -69,13 +69,14 @@ the same Evidence authentication failures: an admitted source that fails authentication is `evidence_integrity_mismatch`, while an authenticated source with an unsupported workflow role or invalid selector is `invalid_request`. -Ghidra inventories and function replies use `raw_result: null` when their facts -are already preserved in `normalized_result`, limitations, locations and -relationship coverage. Distinct overview details, measured load-image -observations, file-offset selection roles and database types supplemented with -derived metadata keep their upstream representation. Read function results from -`normalized_result`; older retained Evidence keeps its original representation -and identity. +Ghidra inventories, measured load-image observations and function replies use +`raw_result: null` when their useful facts are already preserved in +`normalized_result`, limitations, locations and relationship coverage. Distinct +overview details, file-offset selection roles and database types supplemented +with derived metadata keep their upstream representation. Native load-image +attestation continues to use the bridge reply locally before Evidence +projection. Read function results from `normalized_result`; older retained +Evidence keeps its original representation and identity. Artifact source-map filename relationships require a unique, identified regular file at the sibling path. A known directory cannot be the mapped file; the map diff --git a/src/domain/native/nativeLoadImage.ts b/src/domain/native/nativeLoadImage.ts index 069ddc2588..183d12866c 100644 --- a/src/domain/native/nativeLoadImage.ts +++ b/src/domain/native/nativeLoadImage.ts @@ -166,6 +166,17 @@ export type NativeLoadImage = z.infer; export type NativeLoadImageObservation = z.infer< typeof nativeLoadImageObservationSchema >; + +/** Remove entry-context rows that carry no register observations. */ +export const normalizeNativeLoadImageObservation = ( + observation: NativeLoadImageObservation, +): NativeLoadImageObservation => ({ + ...observation, + entry_context: observation.entry_context.filter( + ({ registers }) => registers.length > 0, + ), +}); + /** One independently compared native import fact. */ export type NativeLoadImageCheck = z.infer; diff --git a/src/ghidra/GhidraInventoryValues.test.ts b/src/ghidra/GhidraInventoryValues.test.ts index b565b1cd03..94b0aa5f72 100644 --- a/src/ghidra/GhidraInventoryValues.test.ts +++ b/src/ghidra/GhidraInventoryValues.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; +import { nativeLoadImageObservationFixture } from "../../tests/fixtures/nativeLoadImage.js"; import { parseGhidraInventoryInput, parseGhidraInventoryResult, @@ -98,6 +99,40 @@ it.each(["list_procedures", "search_procedures"] as const)( }, ); +describe("native load-image result projection", () => { + it("omits raw bridge output when the observation keeps every reported fact", () => { + const observations = JSON.parse( + JSON.stringify(nativeLoadImageObservationFixture()), + ); + const parsed = parseGhidraInventoryResult( + "inspect_native_load_image", + observations, + ); + expect(parsed.ok).toBe(true); + if (!parsed.ok) throw parsed.error; + expect(JSON.stringify(parsed.value.value)).toBe( + JSON.stringify(observations), + ); + expect(parsed.value.rawResult).toBeUndefined(); + }); + + it("omits entry-context rows without register observations", () => { + const observations = JSON.parse( + JSON.stringify({ + ...nativeLoadImageObservationFixture(), + entry_context: [{ address: "ram:0x0010", registers: [] }], + }), + ); + const parsed = parseGhidraInventoryResult( + "inspect_native_load_image", + observations, + ); + expect(parsed.ok).toBe(true); + if (!parsed.ok) throw parsed.error; + expect(JSON.stringify(parsed.value.value)).toContain('"entry_context":[]'); + }); +}); + it("validates large masked byte queries at the provider boundary without changing caller spelling", () => { const input = { pattern: "aA??".repeat(1024 * 1024), diff --git a/src/ghidra/GhidraInventoryValues.ts b/src/ghidra/GhidraInventoryValues.ts index 206a451065..9736438d64 100644 --- a/src/ghidra/GhidraInventoryValues.ts +++ b/src/ghidra/GhidraInventoryValues.ts @@ -11,6 +11,7 @@ import { byteSearchResultSchema, } from "../domain/native/bytePattern.js"; import { + normalizeNativeLoadImageObservation, nativeLoadImageObservationSchema, type NativeLoadImageObservation, } from "../domain/native/nativeLoadImage.js"; @@ -365,7 +366,7 @@ export function parseGhidraInventoryResult( if (operation === "inspect_native_load_image") { const parsed = resultSchemas.inspect_native_load_image.safeParse(value); return parsed.success - ? ok({ value: parsed.data, rawResult: value }) + ? ok({ value: normalizeNativeLoadImageObservation(parsed.data) }) : err(inventoryOutputFailure(operation, parsed.error)); } const parsed = resultSchemas[operation].safeParse(value); diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 3c70f66f5c..4aeb19a4c0 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -1811,7 +1811,7 @@ describe("Ghidra measured DOS load-image projection", () => { ok: true, value: { result: { status: state, observations: fixture.observation }, - rawResult: fixture.observation, + rawResult: null, analysisProfile: profile.value.profile, }, }); @@ -1894,7 +1894,7 @@ it("returns read-only NativeAOT metadata inline and drills down by derived Metho ], }, }, - rawResult: observations, + rawResult: null, }, }); for (const address of [ @@ -2149,9 +2149,7 @@ it("shares one session snapshot across callers while isolating cancellation and expect(secondResult).toMatchObject({ ok: true, value: { - rawResult: { - source_files: [{ name: "caller-2" }], - }, + rawResult: null, result: { observations: { source_files: [{ name: "caller-2" }],