From 5156ba44c58f1361e8b6304fcd1cb092eaa85de9 Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Sat, 10 Oct 2026 07:57:42 -0700 Subject: [PATCH] feat(native): return a retained summary view of binary layouts on request (#1570) inspect_binary_layout accepts the same optional detail input as analyze_javascript_application. The default complete delivery is unchanged; detail "summary" retains the complete layout Evidence in the session and returns its inspect_analysis_view summary, whose parent_evidence_id selects sections, symbols, mitigations or linkage later. Both tools share one detail schema and output projection. Co-authored-by: Claude Opus 5.5 --- .../skills/reverse-engineer-anything/SKILL.md | 7 +-- docs/binary-diagnostics.md | 6 +++ docs/mcp-contracts.md | 14 +++--- src/contracts/analysisDetail.ts | 17 +++++++ .../javascript/electronToolContracts.ts | 21 +++------ .../native/binaryDiagnosticsToolContracts.ts | 18 +++++-- src/server/createServer.ts | 1 + src/server/registerBinaryDiagnosticsTools.ts | 19 ++++++-- .../mcp/analysisSummaryDetailMcp.test.ts | 47 ++++++++++++++++++- 9 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 src/contracts/analysisDetail.ts diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index 3adf54f0f..c78345ba1 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -150,9 +150,10 @@ list stays complete; availability depends on the operation, target, and host. ## Work summary-first Start with the default result and use its inline Evidence and graph context. -For a JavaScript application whose size is unknown or large, call -`analyze_javascript_application` with `"detail": "summary"`, then inspect the -retained `parent_evidence_id` with `inspect_analysis_view`. +For a JavaScript application or ELF layout whose size is unknown or large, +call `analyze_javascript_application` or `inspect_binary_layout` with +`"detail": "summary"`, then inspect the retained `parent_evidence_id` with +`inspect_analysis_view`. Do not repeat an identical tool call. Make a focused follow-up only when the returned result leaves a specific question unanswered. diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index d1addae13..08c8ad890 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -91,6 +91,12 @@ one-shot invocations pass portable inline Evidence JSON; an MCP session uses the retained `evidence_id`. Page `limit` is required and bounded by the measured MCP stdio budget documented on the tool contract. +Over MCP, `inspect_binary_layout` with `"detail": "summary"` retains the +complete layout Evidence in the session and returns only its summary view. +Its `normalized_result.parent_evidence_id` names the retained layout for later +section, symbol, mitigation or linkage views. The default `complete` detail and +CLI output are unchanged. + Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics and 30-second owned command deadline. The reply budget applies to the decoder record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index bfd6a717a..6594f0733 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -318,12 +318,14 @@ Ordinary responses keep their existing complete result contract. ## Retained application Evidence inputs -`analyze_javascript_application` accepts `detail`: `complete` (default) returns -the complete analysis Evidence; `summary` records that complete Evidence in the -session and returns the derived `inspect_analysis_view` summary Evidence -instead. Its `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 graph through the response. +`analyze_javascript_application` and `inspect_binary_layout` accept `detail`: +`complete` (default) returns the complete analysis Evidence; `summary` records +that complete Evidence in the session and returns the derived +`inspect_analysis_view` summary Evidence instead. Its +`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`. `inspect_analysis_view` projects a caller-selected view of already completed `inspect_binary_layout`, `analyze_javascript_application`, or `analyze_function` diff --git a/src/contracts/analysisDetail.ts b/src/contracts/analysisDetail.ts new file mode 100644 index 000000000..dde2f148e --- /dev/null +++ b/src/contracts/analysisDetail.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +import { analysisViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; + +/** Caller-selected delivery of completed analysis Evidence that supports retained views. */ +export const analysisDetailSchema = z + .enum(["complete", "summary"]) + .default("complete") + .describe( + "complete returns the complete analysis Evidence inline. summary retains that complete Evidence in this session and returns its inspect_analysis_view summary; inspect pages, items or facets through normalized_result.parent_evidence_id.", + ); + +/** Complete analysis Evidence, or the summary view of its retained record. */ +export const analysisDetailOutputSchemaOf = ( + analysis: Schema, +) => evidenceResultOf(z.union([analysis, analysisViewResultSchema])); diff --git a/src/contracts/javascript/electronToolContracts.ts b/src/contracts/javascript/electronToolContracts.ts index 3141d42e5..55c3aa9e4 100644 --- a/src/contracts/javascript/electronToolContracts.ts +++ b/src/contracts/javascript/electronToolContracts.ts @@ -11,9 +11,10 @@ import { electronActiveObservationInputSchema, electronActiveObservationResultSchema, } from "../../domain/javascript/electronActiveObservation.js"; -import { z } from "zod"; - -import { analysisViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { + analysisDetailOutputSchemaOf, + analysisDetailSchema, +} from "../analysisDetail.js"; import { analyzeJavaScriptApplicationInputSchema, javascriptApplicationAnalysisResultSchema, @@ -29,18 +30,10 @@ const inspectionOutputSchema = evidenceResultOf(electronPageInspectionSchema); /** Caller-selected delivery of a completed JavaScript application analysis. */ export const analyzeJavaScriptApplicationRequestSchema = analyzeJavaScriptApplicationInputSchema.extend({ - detail: z - .enum(["complete", "summary"]) - .default("complete") - .describe( - "complete returns the complete application analysis Evidence inline. summary retains that complete Evidence in this session and returns its inspect_analysis_view summary; page modules or select items from normalized_result.parent_evidence_id.", - ), + detail: analysisDetailSchema, }); -const applicationOutputSchema = evidenceResultOf( - z.union([ - javascriptApplicationAnalysisResultSchema, - analysisViewResultSchema, - ]), +const applicationOutputSchema = analysisDetailOutputSchemaOf( + javascriptApplicationAnalysisResultSchema, ); const reconciliationOutputSchema = evidenceResultOf( javascriptRuntimeReconciliationResultSchema, diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index 720578bd0..546692331 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -6,10 +6,18 @@ import { inspectBinaryLayoutInputSchema, binaryLayoutSchema, } from "../../domain/native/binaryLayout.js"; +import { + analysisDetailOutputSchemaOf, + analysisDetailSchema, +} from "../analysisDetail.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; +/** Caller-selected delivery of completed binary layout Evidence. */ +export const inspectBinaryLayoutRequestSchema = + inspectBinaryLayoutInputSchema.extend({ detail: analysisDetailSchema }); + /** Offline binary diagnostics are independent of macOS process/UI providers. */ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [ { @@ -17,14 +25,18 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [ ...toolContractMetadata("inspect_binary_layout"), kind: "native-provider", description: - "Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Initial profile: ELF64 x86-64 little-endian EXEC/DYN/REL on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success.", - inputSchema: inspectBinaryLayoutInputSchema, - outputSchema: evidenceResultOf(binaryLayoutSchema), + "Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Initial profile: ELF64 x86-64 little-endian EXEC/DYN/REL on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view.", + inputSchema: inspectBinaryLayoutRequestSchema, + outputSchema: analysisDetailOutputSchemaOf(binaryLayoutSchema), examples: [ { title: "Inspect ELF linked and file layout", input: { path: "/artifacts/application.elf" }, }, + { + title: "Retain the complete layout and return its summary view", + input: { path: "/artifacts/application.elf", detail: "summary" }, + }, ], }, { diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 569a8d0b5..1896355c6 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -301,6 +301,7 @@ const registerConfiguredAnalysisTools = ( options.binaryLayout ?? createBinaryLayoutService(environment), toolLogger, recordEvidence, + evidenceById, ); registerAnalysisViewTool(server, toolLogger, evidenceById, recordEvidence); registerEvmTools( diff --git a/src/server/registerBinaryDiagnosticsTools.ts b/src/server/registerBinaryDiagnosticsTools.ts index b7b6ad880..95d268428 100644 --- a/src/server/registerBinaryDiagnosticsTools.ts +++ b/src/server/registerBinaryDiagnosticsTools.ts @@ -1,6 +1,8 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; +import { summarizeRetainedAnalysis } from "../application/analysisView/AnalysisViewService.js"; import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; @@ -13,15 +15,24 @@ export const registerBinaryDiagnosticsTools = ( service: BinaryLayoutService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + evidenceById?: EvidenceLookup, ): void => { const contract = toolContract("inspect_binary_layout"); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.inspect(input, { signal: context.mcpReq.signal }), - ); + async ({ detail, ...request }, context) => { + const result = await logToolExecution(logger, contract.name, async () => { + const inspected = await service.inspect(request, { + signal: context.mcpReq.signal, + }); + return detail === "summary" && inspected.ok + ? summarizeRetainedAnalysis(inspected.value, { + recordEvidence, + evidenceById, + }) + : inspected; + }); if (!result.ok) return server.delivery.toCallToolResult(result, contract); const recorded = recordEvidence?.(result.value); return server.delivery.toEvidenceToolResult( diff --git a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts index 30218c084..548619ec4 100644 --- a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts +++ b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts @@ -3,15 +3,22 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, onTestFinished } from "vitest"; +import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { ok } from "../../../src/domain/result.js"; import { createServer } from "../../../src/server/createServer.js"; +import { analysisViewLayoutFixture } from "../../fixtures/analysisView.js"; +import { BINARY_LAYOUT_TEST_PROVIDER } from "../../fixtures/binaryDiagnostics/layout.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const contract = toolContract("analyze_javascript_application"); -const connect = async (retainsEvidence: boolean) => { +const connect = async ( + retainsEvidence: boolean, + binaryLayout?: BinaryLayoutService, +) => { const session = createTestBinarySession(() => { throw new Error("JavaScript analysis must not start a deep provider"); }); @@ -26,6 +33,7 @@ const connect = async (retainsEvidence: boolean) => { }, }, }, + binaryLayout === undefined ? {} : { binaryLayout }, ); const client = new Client({ name: "analysis-summary-detail", version: "1" }); onTestFinished(async () => { @@ -123,3 +131,40 @@ it("refuses summary detail when the server cannot retain the complete Evidence", message: expect.stringContaining("detail complete"), }); }); + +it("returns the summary of retained binary layout Evidence", async () => { + const layout = analysisViewLayoutFixture(); + const client = await connect( + true, + new BinaryLayoutService({ + identity: BINARY_LAYOUT_TEST_PROVIDER, + inspect: () => Promise.resolve(ok(layout)), + }), + ); + const layoutContract = toolContract("inspect_binary_layout"); + const summary = await client.callTool({ + name: layoutContract.name, + arguments: { path: layout.artifact.path, detail: "summary" }, + }); + expect(summary.isError).not.toBe(true); + const evidence = layoutContract.outputSchema.parse(summary.structuredContent); + expect(evidence).toMatchObject({ + operation: "inspect_analysis_view", + normalized_result: { + kind: "summary", + parent_operation: "inspect_binary_layout", + }, + }); + const [parentId] = evidence.evidence_links; + const sections = await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { kind: "retained-evidence", evidence_id: parentId }, + view: { kind: "page", collection: "sections", offset: 0, limit: 8 }, + }, + }); + expect(sections.isError).not.toBe(true); + expect(sections.structuredContent).toMatchObject({ + normalized_result: { kind: "page", parent_evidence_id: parentId }, + }); +});