fix(native): page every retained function dossier collection (#1507)

inspect_analysis_view is the recovery path when an analyze_function
dossier exceeds the MCP response budget, but its native facets covered
only procedure, pseudocode, assembly, callers/callees, references, and
value flow. Basic blocks, comments, unresolved calls, referenced strings
and names, and the native API record could not be read from retained
Evidence at all; basic blocks grow with the function just like assembly.

These are now facets: collections page by rows, and native_api is a
singleton. A dossier without an unresolved_calls record reports "not
recorded" rather than the value-flow failure reason.

Refs #1247 and #1065.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-10 01:31:15 -07:00
1 parent f63f12d8ff
commit 911323c5dd
6 files changed
+95 -9

No files matched your search

@@ -37,8 +37,9 @@ unknown rather than false.
For a completed `analyze_function` dossier, use `inspect_analysis_view` with
`view: {kind: "native", facet: "value_flow_operations", offset: 0, limit: 32}`
and the exact retained Evidence ID to read selected facts without reanalysis.
Native facets also include procedure, pseudocode, assembly, callers/callees,
references, and value-flow summaries. Pseudocode offsets and limits count UTF-16
Native facets also include procedure, pseudocode, assembly, basic blocks,
comments, callers/callees, references, unresolved calls, referenced strings and
names, the native API record, and value-flow summaries. Pseudocode offsets and limits count UTF-16
code units; use the returned `next_offset` for continuation. Provider limitations
and unavailable facts remain explicit; the complete parent Evidence is retained.
+3 -2
View File
@@ -262,8 +262,9 @@ Ordinary responses keep their existing complete result contract.
`inspect_analysis_view` projects a caller-selected view of already completed
`inspect_binary_layout`, `analyze_javascript_application`, or `analyze_function`
Evidence. Native views select procedure, pseudocode, assembly, callers/callees,
references, and high-pcode facets without starting a provider. Native offset
Evidence. Native views select procedure, pseudocode, assembly, basic blocks,
comments, callers/callees, references, unresolved calls, referenced strings and
names, the native API record, and high-pcode facets without starting a provider. Native offset
and limit default to 0 and 64; pseudocode uses UTF-16 code units and never
splits surrogate pairs, while other pages count rows. Unavailable facts and
provider limitations remain explicit. Source is
+1 -1
View File
@@ -15,7 +15,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
"Inspect selected views of completed binary-layout, JavaScript application or native analyze_function Evidence without repeating analysis. Use an exact same-session retained evidence_id or portable inline Evidence. Select a summary, layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Native views select procedure, pseudocode, assembly, callers/callees, references and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.",
"Inspect selected views of completed binary-layout, JavaScript application or native analyze_function Evidence without repeating analysis. Use an exact same-session retained evidence_id or portable inline Evidence. Select a summary, layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Native views select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.",
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
@@ -161,6 +161,64 @@ it("preserves native API limitations and unavailable residual unknowns", () => {
);
});
it("pages every retained dossier collection and the native API record", () => {
const original = functionDossierSchema.parse(ghidraFunctionDossier());
const page = (
facet:
| "basic_blocks"
| "comments"
| "unresolved_calls"
| "referenced_strings"
| "referenced_names",
result: unknown = original,
) => {
const view = projectAnalysisView(nativeParent(result), {
kind: "native",
facet,
offset: 0,
limit: 64,
});
if (!view.ok) throw view.error;
return view.value;
};
expect(page("basic_blocks")).toMatchObject({
item: original.basic_blocks,
coverage: { total: original.basic_blocks.length, exhausted: true },
});
expect(page("referenced_strings")).toMatchObject({
item: original.referenced_strings,
});
expect(
page("comments", {
...original,
comments: [{ address: "0x401000", kind: "comment", text: "entry" }],
}),
).toMatchObject({ item: [{ text: "entry" }] });
expect(page("referenced_names")).toMatchObject({ coverage: { total: 0 } });
const { unresolved_calls: _omitted, ...withoutUnresolved } = original;
expect(
page("unresolved_calls", {
...withoutUnresolved,
native_value_flow: {
available: false,
reason: "No high p-code",
limitations: [],
},
}),
).toMatchObject({ item: { available: false, reason: "not recorded" } });
const api = projectAnalysisView(nativeParent(), {
kind: "native",
facet: "native_api",
offset: 0,
limit: 64,
});
if (!api.ok) throw api.error;
expect(api.value).toMatchObject({
item: original.native_api,
coverage: { status: "complete-within-view" },
});
});
it("reports the malformed native dossier field", () => {
const result = projectAnalysisView(
nativeParent({
+6
View File
@@ -82,10 +82,16 @@ const nativeViewSchema = z.strictObject({
"procedure",
"pseudocode",
"assembly",
"basic_blocks",
"comments",
"callers",
"callees",
"incoming_references",
"outgoing_references",
"unresolved_calls",
"referenced_strings",
"referenced_names",
"native_api",
"value_flow_summary",
"value_flow_operations",
"value_flow_def_use",
+24 -4
View File
@@ -104,17 +104,22 @@ const nativeRows = (
dossier: FunctionDossier,
facet: Exclude<
Extract<AnalysisViewRequest, { readonly kind: "native" }>["facet"],
"procedure" | "pseudocode" | "value_flow_summary"
"procedure" | "pseudocode" | "native_api" | "value_flow_summary"
>,
) => {
): readonly unknown[] | undefined => {
const sourceFlow = dossier.native_value_flow;
const flow = sourceFlow?.available ? sourceFlow : undefined;
return {
assembly: dossier.assembly,
basic_blocks: dossier.basic_blocks,
comments: dossier.comments,
callers: dossier.callers,
callees: dossier.callees,
incoming_references: dossier.incoming_references,
outgoing_references: dossier.outgoing_references,
unresolved_calls: dossier.unresolved_calls,
referenced_strings: dossier.referenced_strings,
referenced_names: dossier.referenced_names,
value_flow_operations: flow?.operations,
value_flow_def_use: flow?.def_use,
value_flow_effects: flow?.effects,
@@ -152,10 +157,20 @@ export const projectNativeFunctionView = (
...common,
});
const flow = dossier.native_value_flow;
if (view.facet === "procedure" || view.facet === "value_flow_summary") {
if (
view.facet === "procedure" ||
view.facet === "native_api" ||
view.facet === "value_flow_summary"
) {
if (view.offset !== 0)
return err(viewError("Singleton native facets require offset 0."));
if (view.facet === "procedure") return ok(singleton(dossier.procedure));
if (view.facet === "native_api")
return ok(
singleton(
dossier.native_api ?? { available: false, reason: "not recorded" },
),
);
const status =
flow === null
? { available: false, reason: "not recorded" }
@@ -188,7 +203,12 @@ export const projectNativeFunctionView = (
return ok(
singleton({
available: false,
reason: flow?.available === false ? flow.reason : "not recorded",
// Only value-flow facets inherit the value-flow failure reason; an
// older dossier simply has no unresolved_calls record.
reason:
view.facet.startsWith("value_flow_") && flow?.available === false
? flow.reason
: "not recorded",
}),
);
const items = rows.slice(view.offset, view.offset + view.limit);