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 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-10 07:57:42 -07:00 committed by GitHub
parent c19e15a2b3
commit 5156ba44c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 119 additions and 31 deletions

View File

@ -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.

View File

@ -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

View File

@ -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`

View File

@ -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 = <Schema extends z.ZodType>(
analysis: Schema,
) => evidenceResultOf(z.union([analysis, analysisViewResultSchema]));

View File

@ -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,

View File

@ -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" },
},
],
},
{

View File

@ -301,6 +301,7 @@ const registerConfiguredAnalysisTools = (
options.binaryLayout ?? createBinaryLayoutService(environment),
toolLogger,
recordEvidence,
evidenceById,
);
registerAnalysisViewTool(server, toolLogger, evidenceById, recordEvidence);
registerEvmTools(

View File

@ -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(

View File

@ -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 },
});
});