feat(native): inspect oversized retained function dossiers via bounded views

Add caller-selected native dossier facets with preserved Evidence identity, provenance, unknowns and transport recovery.

Refs #1247 and #1065.

Co-authored-by: jhuang-tw <77732008+jhuang-tw@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
This commit is contained in:
jhuang-tw 2026-10-10 12:18:50 +08:00 committed by GitHub
parent 4d95f2b2e7
commit a9ee2360fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 769 additions and 22 deletions

View File

@ -237,7 +237,12 @@ Ordinary responses keep their existing complete result contract.
## Retained application Evidence inputs
`inspect_analysis_view` projects a caller-selected view of already completed
`inspect_binary_layout` or `analyze_javascript_application` Evidence. Source is
`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
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
an exact same-session retained reference or portable inline Evidence. Views are
a summary, a layout mitigations or linkage facet, one section/symbol/module, or
a stable page with a caller-selected positive `limit`. Module pages include

View File

@ -34,6 +34,14 @@ cross-references. Addresses and recovered pseudocode are analysis observations,
not original source. Provider unavailability and unsupported metadata remain
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
code units; use the returned `next_offset` for continuation. Provider limitations
and unavailable facts remain explicit; the complete parent Evidence is retained.
To see which functions or Objective-C methods a Mach-O actually calls in one
run, use `observe_native_calls` with explicit breakpoints and a bounded
`duration_ms`/`max_events`. It launches a new process under LLDB, so the target

View File

@ -2,6 +2,8 @@ import { expect, it } from "vitest";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
import {
analysisViewJavaScriptAnalysisWithSource,
analysisViewJavaScriptEvidence,
@ -9,6 +11,96 @@ import {
} from "../../../tests/fixtures/analysisView.js";
import { inspectAnalysisView } from "./AnalysisViewService.js";
it.each(["/fixtures/unknown.exe", ""])(
"preserves recorded empty native identity fields: path=%j",
(path) => {
const original = functionDossierSchema.parse(ghidraFunctionDossier());
const parent = createEvidence(
{ path, format: "pe", sha256: "c".repeat(64) },
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "analyze_function",
parameters: {},
result: {
...original,
procedure: {
...original.procedure,
address: "",
body: { available: false, reason: "Not recorded" },
},
},
},
);
const result = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view: {
kind: "native",
facet: "procedure",
offset: 0,
limit: 64,
},
});
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
procedure_address: "",
artifact: { path },
item: { address: "" },
});
expect(result.value.locations).toEqual(
path.length > 0 ? [{ kind: "artifact-path", path }] : [],
);
},
);
it("projects a small native view from an authenticated >10 MiB retained dossier", () => {
const original = functionDossierSchema.parse(ghidraFunctionDossier());
const parent = createEvidence(
{ path: "/fixtures/large.exe", format: "pe", sha256: "c".repeat(64) },
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "analyze_function",
parameters: { address: "0x401000" },
result: { ...original, pseudocode: "X".repeat(11 * 1024 * 1024) },
limitations: ["Synthetic fixture"],
},
);
const input = {
source: {
kind: "retained-evidence" as const,
evidence_id: parent.evidence_id,
},
view: {
kind: "native" as const,
facet: "pseudocode" as const,
offset: 0,
limit: 128,
},
};
const view = inspectAnalysisView(input, (id) =>
id === parent.evidence_id ? parent : undefined,
);
if (!view.ok) throw view.error;
expect(view.value.evidence_links).toEqual([parent.evidence_id]);
expect(view.value.normalized_result).toMatchObject({
kind: "native",
parent_operation: "analyze_function",
parent_evidence_id: parent.evidence_id,
procedure_address: "0x401000",
artifact: { path: "/fixtures/large.exe", sha256: "c".repeat(64) },
coverage: { total: 11 * 1024 * 1024, examined: 128, next_offset: 128 },
});
expect(view.value.locations).toEqual([
{ kind: "artifact-path", path: "/fixtures/large.exe" },
{ kind: "address", address: "0x401000" },
]);
expect(JSON.stringify(view.value).length).toBeLessThan(12000);
expect(JSON.stringify(parent).length).toBeGreaterThan(10 * 1024 * 1024);
expect(inspectAnalysisView(input, () => undefined)).toMatchObject({
ok: false,
error: { _tag: "EvidenceIntegrityError" },
});
});
it("projects inline layout Evidence and retains a derived view record", () => {
const parent = analysisViewLayoutEvidence();
const result = inspectAnalysisView({

View File

@ -53,6 +53,13 @@ export const inspectAnalysisViewValidated = (
operation: parent.operation,
normalizedResult: parent.normalized_result,
limitations: parent.limitations,
artifact:
parent.subject === null
? null
: {
path: parent.subject.local_path,
sha256: parent.subject.digest.sha256,
},
},
input.view,
);
@ -126,7 +133,16 @@ const createAnalysisViewEvidence = (
confidence: "derived",
authority: parent.authority,
limitations: result.limitations,
locations: [{ kind: "artifact-path", path: result.artifact.path }],
locations: [
...(result.artifact.path.length > 0
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
: []),
...(result.kind === "native" &&
result.procedure_address !== null &&
result.procedure_address.length > 0
? [{ kind: "address" as const, address: result.procedure_address }]
: []),
],
evidenceLinks: [parent.evidence_id],
});
};

View File

@ -18,7 +18,7 @@ export const registerAnalysisViewCommands = (
): void => {
cli.command(CLI_COMMANDS.inspectAnalysisView, {
description:
"Project a selected view of completed layout or JavaScript application Evidence JSON",
"Project a selected view of completed layout, JavaScript application or native function Evidence JSON",
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
}),

View File

@ -8,14 +8,14 @@ import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
const RETAINED_EXAMPLE_ID = `ev_${"a".repeat(64)}`;
/** Selected views of already completed layout and JavaScript analysis Evidence. */
/** Selected views of already completed layout, JavaScript and native function Evidence. */
export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
{
name: "inspect_analysis_view",
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
"Inspect a selected view of completed binary-layout or JavaScript application 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 with a positive limit. Module pages contain JavaScript assets, bundled modules and source modules; use node_id when paths are ambiguous or unavailable. Returns selected facts inline with 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 retained Evidence when 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, 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.",
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
@ -43,6 +43,21 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
},
},
},
{
title: "Page native high-pcode operations from retained Evidence",
input: {
source: {
kind: "retained-evidence",
evidence_id: RETAINED_EXAMPLE_ID,
},
view: {
kind: "native",
facet: "value_flow_operations",
offset: 0,
limit: 32,
},
},
},
{
title: "Page module identities from retained application Evidence",
input: {

View File

@ -1,6 +1,8 @@
import { expect, it } from "vitest";
import { resolveJavaScriptSourceMapReference } from "../javascript/javascriptSourceMapPaths.js";
import { functionDossierSchema } from "../hopperValues.js";
import { ghidraFunctionDossier } from "../ghidraValues.fixture.js";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import {
analysisViewBindJavaScriptGraphs,
@ -48,21 +50,190 @@ const javascriptParent = (
limitations: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.limitations,
});
it("accepts caller-selected page sizes and rejects malformed bounds", () => {
const input = {
source: { kind: "retained-evidence", evidence_id: `ev_${"a".repeat(64)}` },
view: { kind: "page", collection: "sections", offset: 0, limit: 500 },
};
expect(inspectAnalysisViewInputSchema.safeParse(input).success).toBe(true);
for (const limit of [0, -1, 0.5, Infinity, Number.MAX_SAFE_INTEGER + 1])
expect(
inspectAnalysisViewInputSchema.safeParse({
...input,
view: { ...input.view, limit },
}).success,
).toBe(false);
const nativeParent = (
result: unknown = ghidraFunctionDossier(),
): AnalysisViewParent => ({
evidenceId: "ev_" + "b".repeat(64),
operation: "analyze_function",
normalizedResult: result,
limitations: ["Retained provider limitation"],
artifact: { path: "/fixtures/example.exe", sha256: "a".repeat(64) },
});
it("projects native procedure and bounded assembly and high-pcode with exact parent identity", () => {
const parent = nativeParent();
const procedure = projectAnalysisView(parent, {
kind: "native",
facet: "procedure",
offset: 0,
limit: 64,
});
if (!procedure.ok) throw procedure.error;
expect(procedure.value).toMatchObject({
kind: "native",
parent_operation: "analyze_function",
artifact: parent.artifact,
item: { address: "0x401000" },
});
expect(procedure.value.view_digest).not.toBe(parent.evidenceId.slice(3));
const assembly = projectAnalysisView(parent, {
kind: "native",
facet: "assembly",
offset: 0,
limit: 1,
});
if (!assembly.ok) throw assembly.error;
expect(assembly.value).toMatchObject({
coverage: { examined: 1, total: 2, next_offset: 1 },
item: ["0x401000: CALL 0x401020"],
});
const flow = projectAnalysisView(parent, {
kind: "native",
facet: "value_flow_operations",
offset: 0,
limit: 1,
});
if (!flow.ok) throw flow.error;
expect(flow.value).toMatchObject({ item: [{ opcode: "COPY" }] });
});
it("rejects invalid native parents, unsupported source operations and singleton offsets", () => {
const view = {
kind: "native",
facet: "assembly",
offset: 0,
limit: 4,
} as const;
expect(projectAnalysisView(nativeParent({}), view)).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(
projectAnalysisView({ ...nativeParent(), artifact: null }, view),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(projectAnalysisView(layoutParent(), view)).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(projectAnalysisView(javascriptParent(), view)).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(
projectAnalysisView(nativeParent(), {
kind: "native",
facet: "procedure",
offset: 1,
limit: 2,
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
});
it("preserves native API limitations and unavailable residual unknowns", () => {
const original = functionDossierSchema.parse(ghidraFunctionDossier());
const view = {
kind: "native",
facet: "procedure",
offset: 0,
limit: 64,
} as const;
const unavailable = projectAnalysisView(
nativeParent({
...original,
native_api: {
available: false,
reason: "No decompiler",
residual_unknowns: ["Calling convention unresolved"],
},
}),
view,
);
if (!unavailable.ok) throw unavailable.error;
expect(unavailable.value.unknowns).toContain("Calling convention unresolved");
const available = projectAnalysisView(nativeParent(), view);
if (!available.ok) throw available.error;
if (!original.native_api?.available) throw new Error("expected API fixture");
expect(available.value.limitations).toEqual(
expect.arrayContaining(original.native_api.limitations),
);
});
it("reports the malformed native dossier field", () => {
const result = projectAnalysisView(
nativeParent({
...functionDossierSchema.parse(ghidraFunctionDossier()),
pseudocode: 12,
}),
{
kind: "native",
facet: "pseudocode",
offset: 0,
limit: 64,
},
);
expect(result).toMatchObject({
ok: false,
error: {
issues: expect.arrayContaining([
expect.objectContaining({
path: ["source", "normalized_result", "pseudocode"],
}),
]),
},
});
});
it("native pseudocode pages do not split surrogate pairs", () => {
const original = functionDossierSchema.parse(ghidraFunctionDossier());
const parent = nativeParent({ ...original, pseudocode: "ab😀cd" });
expect(
projectAnalysisView(parent, {
kind: "native",
facet: "pseudocode",
offset: 3,
limit: 2,
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
const safe = projectAnalysisView(parent, {
kind: "native",
facet: "pseudocode",
offset: 2,
limit: 3,
});
if (!safe.ok) throw safe.error;
expect(safe.value).toMatchObject({
item: { text: "😀c", unit: "utf16-code-units" },
coverage: { examined: 3, next_offset: 5 },
});
});
it.each([
{ kind: "page", collection: "sections", offset: 0, limit: 500 },
{ kind: "native", facet: "assembly", offset: 0, limit: 500 },
])(
"accepts caller-selected page sizes and rejects malformed bounds: $kind",
(view) => {
const input = {
source: {
kind: "retained-evidence",
evidence_id: `ev_${"a".repeat(64)}`,
},
view,
};
expect(inspectAnalysisViewInputSchema.safeParse(input).success).toBe(true);
for (const limit of [0, -1, 0.5, Infinity, Number.MAX_SAFE_INTEGER + 1])
expect(
inspectAnalysisViewInputSchema.safeParse({
...input,
view: { ...input.view, limit },
}).success,
).toBe(false);
},
);
it("projects layout summary, facet, item, and stable pages", () => {
const parent = layoutParent();
const layout = analysisViewLayoutFixture();

View File

@ -12,6 +12,7 @@ import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js";
import { err, ok, type Result } from "../result.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import { binaryLayoutSchema } from "../native/binaryLayout.js";
import { functionDossierSchema } from "../hopperValues.js";
import {
parseOwnedJavaScriptApplicationAnalysisSteps,
type JavaScriptApplicationAnalysisResult,
@ -19,6 +20,7 @@ import {
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import { projectBinaryLayoutView } from "./binaryLayoutView.js";
import { projectJavaScriptApplicationView } from "./javascriptApplicationView.js";
import { projectNativeFunctionView } from "./nativeFunctionView.js";
/** Public name of the selected-view workflow. */
export const INSPECT_ANALYSIS_VIEW_OPERATION = "inspect_analysis_view" as const;
@ -73,12 +75,35 @@ const pageViewSchema = z.strictObject({
limit: z.number().int().positive(),
});
/** Bounded native function facets of already authenticated analyze_function Evidence. */
const nativeViewSchema = z.strictObject({
kind: z.literal("native"),
facet: z.enum([
"procedure",
"pseudocode",
"assembly",
"callers",
"callees",
"incoming_references",
"outgoing_references",
"value_flow_summary",
"value_flow_operations",
"value_flow_def_use",
"value_flow_effects",
"value_flow_parameters",
"value_flow_parameter_uses",
]),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
});
/** Caller-selected projection of already completed analysis Evidence. */
export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
summaryViewSchema,
facetViewSchema,
itemViewSchema,
pageViewSchema,
nativeViewSchema,
]);
/** Inline Evidence or an exact same-session retained reference. */
@ -108,7 +133,9 @@ const coverageSchema = z.strictObject({
});
const artifactSchema = z.strictObject({
path: z.string().min(1),
path: z
.string()
.describe("Exact recorded artifact path, including an empty string."),
sha256: digestSchema,
});
@ -150,6 +177,7 @@ const viewResultBase = {
parent_operation: z.enum([
"inspect_binary_layout",
"analyze_javascript_application",
"analyze_function",
]),
parent_digest: digestSchema,
view_digest: digestSchema,
@ -185,6 +213,13 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
items: z.array(jsonValueSchema),
...viewResultBase,
}),
z.strictObject({
kind: z.literal("native"),
view: nativeViewSchema,
procedure_address: z.string().nullable(),
item: jsonValueSchema,
...viewResultBase,
}),
]);
export type AnalysisViewRequest = z.output<typeof analysisViewRequestSchema>;
@ -200,6 +235,7 @@ export interface AnalysisViewParent {
readonly operation: string;
readonly normalizedResult: unknown;
readonly limitations: readonly string[];
readonly artifact?: { readonly path: string; readonly sha256: string } | null;
}
/** Construct a typed input failure for one selected-view constraint. */
@ -216,7 +252,11 @@ export type UnsignedAnalysisView =
>
| Omit<Extract<AnalysisViewResult, { readonly kind: "facet" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "item" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "page" }>, "view_digest">;
| Omit<Extract<AnalysisViewResult, { readonly kind: "page" }>, "view_digest">
| Omit<
Extract<AnalysisViewResult, { readonly kind: "native" }>,
"view_digest"
>;
/** Attach the content digest that must change when projected bytes change. */
export const sealAnalysisView = (
@ -261,7 +301,7 @@ const unsupportedParent = (
new AnalysisUnsupportedTargetError(
INSPECT_ANALYSIS_VIEW_OPERATION,
parentArtifactPath(parent),
`Parent Evidence operation ${JSON.stringify(parent.operation)} is not inspect_binary_layout or analyze_javascript_application.`,
`Parent Evidence operation ${JSON.stringify(parent.operation)} is not inspect_binary_layout, analyze_javascript_application or analyze_function.`,
);
const parentArtifactPath = (parent: AnalysisViewParent): string => {
@ -327,5 +367,22 @@ export const projectAnalysisView = (
);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (parent.operation === "analyze_function") {
const native = functionDossierSchema.safeParse(parent.normalizedResult);
if (!native.success)
return err(
analysisInputErrorFromIssues(
INSPECT_ANALYSIS_VIEW_OPERATION,
native.error.issues.map((issue) => ({
...issue,
path: ["source", "normalized_result", ...issue.path],
})),
{ source: { normalized_result: parent.normalizedResult } },
{ cause: native.error },
),
);
const projected = projectNativeFunctionView(parent, native.data, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
return err(unsupportedParent(parent));
};

View File

@ -152,6 +152,10 @@ export const projectBinaryLayoutView = (
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
const shared = parentFields(parent, layout);
if (view.kind === "native")
return err(
layoutIncompatible("Native views require analyze_function Evidence."),
);
if (view.kind === "summary")
return ok({
kind: "summary",

View File

@ -243,6 +243,10 @@ export const projectJavaScriptApplicationView = (
): Result<UnsignedAnalysisView, AnalysisError> => {
const shared = parentFields(parent, analysis);
const modules = moduleEntries(analysis.graph.nodes);
if (view.kind === "native")
return err(
jsIncompatible("Native views require analyze_function Evidence."),
);
if (view.kind === "summary")
return ok({
kind: "summary",

View File

@ -0,0 +1,202 @@
import type { AnalysisError } from "../analysisErrorBase.js";
import { uniqueSorted } from "../canonicalOrdering.js";
import type { FunctionDossier } from "../hopperValues.js";
import { jsonValueSchema } from "../jsonValue.js";
import { err, ok, type Result } from "../result.js";
import {
analysisViewInputError,
type AnalysisViewCoverage,
type AnalysisViewParent,
type AnalysisViewRequest,
type UnsignedAnalysisView,
} from "./analysisView.js";
import {
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
type Artifact = { readonly path: string; readonly sha256: string };
const viewError = (message: string): AnalysisError =>
analysisViewInputError([
{ path: ["view"], reason: "invalid_value", message },
]);
const parentFields = (
parent: AnalysisViewParent,
dossier: FunctionDossier,
artifact: Artifact,
) => {
const flow = dossier.native_value_flow;
const api = dossier.native_api;
const unknowns: string[] = [];
if (flow === null) unknowns.push("native_value_flow was not recorded");
else if (!flow.available)
unknowns.push("native_value_flow unavailable: " + flow.reason);
else if (flow.truncated)
unknowns.push(
"native_value_flow is truncated; omitted observations remain unknown",
);
if (api === null) unknowns.push("native_api was not recorded");
else if (!api.available)
unknowns.push(
"native_api unavailable: " + api.reason,
...api.residual_unknowns,
);
return {
parent_evidence_id: parent.evidenceId,
parent_operation: "analyze_function" as const,
parent_digest: parent.evidenceId.slice(3),
procedure_address: dossier.procedure.address,
artifact,
limitations: uniqueSorted([
...parent.limitations,
...dossier.limitations,
...(flow?.limitations ?? []),
...(api?.available ? api.limitations : []),
]),
unknowns: uniqueSorted(unknowns),
};
};
const pseudocodePage = (
body: string,
offset: number,
limit: number,
): Result<
{
readonly item: { readonly text: string; readonly unit: "utf16-code-units" };
readonly coverage: AnalysisViewCoverage;
},
AnalysisError
> => {
const start = Math.min(offset, body.length);
const high = (n: number) => n >= 0xd800 && n <= 0xdbff;
const low = (n: number) => n >= 0xdc00 && n <= 0xdfff;
if (
start > 0 &&
start < body.length &&
high(body.charCodeAt(start - 1)) &&
low(body.charCodeAt(start))
)
return err(viewError("Pseudocode offset splits a UTF-16 surrogate pair."));
let end = Math.min(start + limit, body.length);
if (
end > start &&
end < body.length &&
high(body.charCodeAt(end - 1)) &&
low(body.charCodeAt(end))
)
end--;
if (end === start && start < body.length)
return err(
viewError(
"Increase pseudocode limit to include the next complete Unicode character.",
),
);
return ok({
item: { text: body.slice(start, end), unit: "utf16-code-units" },
coverage: pageViewCoverage(start, end - start, body.length),
});
};
const nativeRows = (
dossier: FunctionDossier,
facet: Exclude<
Extract<AnalysisViewRequest, { readonly kind: "native" }>["facet"],
"procedure" | "pseudocode" | "value_flow_summary"
>,
) => {
const sourceFlow = dossier.native_value_flow;
const flow = sourceFlow?.available ? sourceFlow : undefined;
return {
assembly: dossier.assembly,
callers: dossier.callers,
callees: dossier.callees,
incoming_references: dossier.incoming_references,
outgoing_references: dossier.outgoing_references,
value_flow_operations: flow?.operations,
value_flow_def_use: flow?.def_use,
value_flow_effects: flow?.effects,
value_flow_parameters: flow?.parameters,
value_flow_parameter_uses: flow?.parameter_uses,
}[facet];
};
/** Project bounded native function facets from complete retained Evidence without reanalysis. */
export const projectNativeFunctionView = (
parent: AnalysisViewParent,
dossier: FunctionDossier,
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
if (view.kind !== "native")
return err(viewError("analyze_function Evidence requires a native view."));
const artifact = parent.artifact;
if (!artifact)
return err(
analysisViewInputError([
{
path: ["source", "subject"],
reason: "invalid_value",
message:
"Native function Evidence requires an artifact-bound subject.",
},
]),
);
const common = parentFields(parent, dossier, artifact);
const singleton = (item: unknown): UnsignedAnalysisView => ({
kind: "native",
view,
item: jsonValueSchema.parse(item),
coverage: completeWithinViewCoverage(1, 1),
...common,
});
const flow = dossier.native_value_flow;
if (view.facet === "procedure" || 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));
const status =
flow === null
? { available: false, reason: "not recorded" }
: !flow.available
? { available: false, reason: flow.reason }
: {
available: true,
provenance: flow.provenance,
truncated: flow.truncated,
omitted_operations_lower_bound:
flow.omitted_operations_lower_bound,
known_omitted_inputs: flow.known_omitted_inputs,
known_omitted_edges: flow.known_omitted_edges,
operations: flow.operations.length,
def_use: flow.def_use.length,
effects: flow.effects.length,
parameters: flow.parameters.length,
parameter_uses: flow.parameter_uses.length,
};
return ok(singleton(status));
}
if (view.facet === "pseudocode") {
const page = pseudocodePage(dossier.pseudocode, view.offset, view.limit);
return page.ok
? ok({ kind: "native", view, ...page.value, ...common })
: page;
}
const rows = nativeRows(dossier, view.facet);
if (rows === undefined)
return ok(
singleton({
available: false,
reason: flow?.available === false ? flow.reason : "not recorded",
}),
);
const items = rows.slice(view.offset, view.offset + view.limit);
return ok({
kind: "native",
view,
item: jsonValueSchema.parse(items),
coverage: pageViewCoverage(view.offset, items.length, rows.length),
...common,
});
};

View File

@ -7,6 +7,8 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analysisViewLayoutEvidence } from "../../fixtures/analysisView.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
cliTest(
"projects one section from a JSON file of inline layout Evidence",
async ({ cli }) => {
@ -43,6 +45,48 @@ cliTest(
},
);
cliTest(
"projects native function pseudocode without requiring Ghidra",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-native-view-cli-");
const parent = createEvidence(
{ path: "/fixtures/native.exe", format: "pe", sha256: "a".repeat(64) },
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "analyze_function",
parameters: { address: "0x401000" },
result: ghidraFunctionDossier(),
},
);
const input = join(root, "native-view.json");
await writeFile(
input,
JSON.stringify({
source: { kind: "inline", evidence: parent },
view: { kind: "native", facet: "pseudocode", offset: 0, limit: 12 },
}),
);
const result = await cli.run({
arguments: ["inspect-analysis-view", input, "--json"],
environment: { HOME: root, XDG_CONFIG_HOME: root, XDG_CACHE_HOME: root },
});
expect(result.exitCode).toBe(0);
expect(result.json).toMatchObject({
operation: "inspect_analysis_view",
normalized_result: {
kind: "native",
parent_evidence_id: parent.evidence_id,
procedure_address: "0x401000",
item: { text: "int fixture_", unit: "utf16-code-units" },
},
locations: [
{ kind: "artifact-path", path: "/fixtures/native.exe" },
{ kind: "address", address: "0x401000" },
],
});
},
);
cliTest("rejects malformed JSON before projection", async ({ cli }) => {
const root = await createTestTempDirectory("rea-analysis-view-cli-bad-");
const result = await cli.run({

View File

@ -6,7 +6,9 @@ import { z } from "zod";
import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { ok } from "../../../src/domain/result.js";
import {
createJavaScriptApplicationGraph,
@ -109,6 +111,68 @@ it("advertises exact schemas and projects one layout section from retained Evide
);
});
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());
const parent = createEvidence(
{
path: "/fixtures/oversized-native.exe",
format: "pe",
sha256: "b".repeat(64),
},
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "analyze_function",
parameters: { address: "0x401000" },
result: { ...original, pseudocode: "X".repeat(11 * 1024 * 1024) },
},
);
expect(session.recordEvidence(parent).ok).toBe(true);
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_analysis_view",
);
if (advertised?.outputSchema === undefined)
throw new Error("missing view schemas");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
const arguments_ = {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: { kind: "native", facet: "pseudocode", offset: 0, limit: 500 },
};
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
expect(
ajv.validate(inputSchema, arguments_),
JSON.stringify(ajv.errors),
).toBe(true);
const response = await client.callTool({
name: "inspect_analysis_view",
arguments: arguments_,
});
expect(response.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
response.structuredContent,
);
expect(parsed.normalized_result).toMatchObject({
kind: "native",
parent_evidence_id: parent.evidence_id,
procedure_address: "0x401000",
coverage: { examined: 500, next_offset: 500, total: 11 * 1024 * 1024 },
});
expect(parsed.evidence_links).toEqual([parent.evidence_id]);
expect(parsed.locations).toEqual([
{ kind: "artifact-path", path: "/fixtures/oversized-native.exe" },
{ kind: "address", address: "0x401000" },
]);
expect(JSON.stringify(response.structuredContent).length).toBeLessThan(12000);
await client.ping();
});
it("retains an oversized selected observation and keeps subsequent views usable", async () => {
const { client, session } = await connect();
const analysis = analysisViewJavaScriptAnalysisWithSource();
@ -185,6 +249,71 @@ it("retains an oversized selected observation and keeps subsequent views usable"
expect(summary.isError).not.toBe(true);
});
it("retains an oversized native assembly row and permits a subsequent smaller view", async () => {
const { client, session } = await connect();
const row = "\0".repeat(1_000_000);
const parent = createEvidence(
{ path: "/fixtures/large-row.exe", format: "pe", sha256: "c".repeat(64) },
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "analyze_function",
parameters: {},
result: {
...functionDossierSchema.parse(ghidraFunctionDossier()),
assembly: [row],
},
},
);
expect(session.recordEvidence(parent).ok).toBe(true);
const source = { kind: "retained-evidence", evidence_id: parent.evidence_id };
const response = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source,
view: { kind: "native", facet: "assembly", offset: 0, limit: 1 },
},
});
expect(response.isError).toBe(true);
const error = z
.object({
error: z.object({
details: z.object({
reported_limits: z.object({
evidence_reference: z.object({
kind: z.literal("retained-evidence"),
evidence_id: z.string(),
}),
}),
}),
}),
})
.parse(parseMcpToolError(response));
const retained = session.evidenceById(
error.error.details.reported_limits.evidence_reference.evidence_id,
);
expect(retained?.normalized_result).toMatchObject({
item: [row],
parent_evidence_id: parent.evidence_id,
});
const next = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source,
view: { kind: "native", facet: "pseudocode", offset: 0, limit: 12 },
},
});
expect(next.isError).not.toBe(true);
expect(
toolContract("inspect_analysis_view").outputSchema.parse(
next.structuredContent,
).normalized_result,
).toMatchObject({
item: { text: "int fixture_" },
parent_evidence_id: parent.evidence_id,
});
await client.ping();
});
it("returns a JavaScript summary without graph payloads from a retained reference", async () => {
const { client, session } = await connect();
const parent = analysisViewJavaScriptEvidence();