Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 fd4130d654 fix(native): bind native-trace pages to their collection rows
Review follow-up.

- Advertise each native-trace page with exactly its own row schema, so a
  references page cannot validate with match or unknown rows.
- Export the coverage and reference-cap unknowns from their owner and
  verify a provider-capped trace keeps truncated and the cap warning on
  its summary, filtered, exhausted and unknown pages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 15:12:54 +00:00
Kaoru0822andClaude Opus 5.5 0a9da652a8 fix(native): drop the unused native-trace page schema re-export
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 15:02:25 +00:00
Kaoru0822andClaude Opus 5.5 c5f935031b feat(native): page retained feature traces through analysis views
A large trace_feature result can carry thousands of references with
unavailable owners. inspect_analysis_view refused it as an unsupported
parent and trace_feature rejected detail: summary, so agents had to
transfer the complete result or export the session.

- Give the trace result one domain owner (literalFeatureTrace.ts) used by
  the tool output contract and the new view.
- Add a native-trace view: a summary with match counts by type, reference
  counts by owner outcome and the residual-unknown count, and pages of
  references, matches and residual_unknowns in retained order with exact
  filters and source indexes. Residual unknowns are paged rather than
  repeated in every view.
- Accept detail: summary on trace_feature through the existing enhanced
  summary delivery; session Unknowns are recorded as before.
- Offer a ready-to-call first references page when a retained complete
  trace exceeds the MCP response budget.

Fixes #8373

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 15:00:06 +00:00
12 changed files with 965 additions and 26 deletions

No files matched your search

+17
View File
@@ -218,6 +218,23 @@ Malformed provider replies return `unreadable_output` for
`resolve_containing_procedure` before successful-result projection;
cancellation and incomplete cleanup remain errors.
A large trace can be read on demand. `detail: "summary"` retains the complete
trace in the session and returns its `query`, `truncated`, match counts by type,
reference counts by owner outcome (`found`, `not-found`, `unavailable`) and the
residual-unknown count. `inspect_analysis_view` with the returned
`parent_evidence_id` and `{"kind":"native-trace","collection":...}` pages
`references`, `matches` or `residual_unknowns` in retained order. Each row keeps
its `source_index`, and reference rows also carry their derived `owner`.
References filter by exact `where.target_addresses` (no address normalization)
and `where.owners`; matches filter by `where.types`. Omitted filters select all;
empty arrays select none. Page coverage counts the retained provider-reported
rows only: a missing row never establishes an absent reference, and `truncated`
stays visible on every page. Residual unknowns are paged, not repeated in each
view, and their session Unknowns are recorded exactly as for complete
delivery. An oversized complete trace offers a ready-to-call first `references`
page. The CLI keeps complete JSON; save it and use `inspect-analysis-view` with
an `evidence-file` source.
REA prepares complete MCP results within the pinned stdio client's 10 MiB
receive-buffer budget, including both text and structured representations and
room for the JSON-RPC envelope. Tool errors omit `structuredContent` from the
+4 -2
View File
@@ -80,8 +80,10 @@ export const ENHANCED_TOOL_CONTRACTS = [
...FUNCTION_WORKFLOW_TOOL_CONTRACTS,
enhanced(
"trace_feature",
"Trace the complete literal query, preserving spaces, through every matching string and procedure and their xrefs. Preserve an explicit unavailable owner for a non-retryable containment capability refusal. Returns observations and residual unknowns without inferring reference kinds; truncated only reports an observed reference cap.",
enhancedInputSchemas.trace_feature,
"Trace the complete literal query, preserving spaces, through every matching string and procedure and their xrefs. Preserve an explicit unavailable owner for a non-retryable containment capability refusal. Returns observations and residual unknowns without inferring reference kinds; truncated only reports an observed reference cap. Select detail summary for match and reference counts by owner outcome; native-trace pages of references, matches and residual unknowns retain complete source access.",
enhancedInputSchemas.trace_feature.extend({
detail: analysisDetailSchema,
}),
),
enhanced(
"trace_call_path",
+5 -17
View File
@@ -1,7 +1,6 @@
import {
nativeAddressListSchema,
nativeObservationSourceSchemas,
traceContainingProcedureSchema,
} from "../domain/native/nativeObservation.js";
import { overviewInventoryCoverageSchema } from "../domain/native/inventoryCoverage.js";
import {
@@ -16,8 +15,9 @@ import {
machoSummaryViewResultSchema,
managedArtifactSummaryViewResultSchema,
managedMemberSummaryViewResultSchema,
nativeTraceSummaryViewResultSchema,
} from "../domain/analysisView/analysisView.js";
import { literalFeatureMatchSchema } from "../domain/native/literalFeatureMatch.js";
import { literalFeatureTraceResultSchema } from "../domain/native/literalFeatureTrace.js";
import { byteEntropySchema } from "../domain/native/byteEntropy.js";
import { nativeFunctionMatchingSchema } from "../domain/native/functionMatching.js";
import { nativeByteReadSchema } from "../domain/native/nativeByteRead.js";
@@ -252,21 +252,9 @@ export const officialOutputSchemas = {
xrefs: evidenceResultOf(nativeAddressListSchema),
} as const satisfies Readonly<Record<string, z.ZodObject>>;
const literalTraceOutput = evidenceResultOf(
z.object({
query: z.string(),
search_mode: z.literal("literal"),
matches: z.array(literalFeatureMatchSchema),
references: z.array(
z.object({
target_address: z.string(),
source_address: z.string(),
containing_procedure: traceContainingProcedureSchema,
}),
),
truncated: z.boolean(),
residual_unknowns: z.array(z.string()),
}),
const literalTraceOutput = analysisDetailOutputSchemaOf(
literalFeatureTraceResultSchema,
nativeTraceSummaryViewResultSchema,
);
const callPathTraceOutput = evidenceResultOf(
+5
View File
@@ -17,6 +17,7 @@ import {
import { projectJavaScriptRoutesView } from "./javascriptRoutesView.js";
import { projectWebBundleView } from "./webBundleView.js";
import { projectSourceToBundleView } from "./sourceToBundleView.js";
import { projectNativeTraceView } from "./nativeTraceView.js";
import { projectGpuCodeObjectsView } from "./gpuCodeObjectsView.js";
import { projectModelArtifactView } from "./modelArtifactView.js";
import { projectCommandObservationView } from "./commandObservationView.js";
@@ -147,6 +148,7 @@ import {
nativeFunctionSummaryViewResultSchema,
sourceToBundleSummaryViewResultSchema,
sourceToBundlePageViewResultSchema,
nativeTraceSummaryViewResultSchema,
analysisViewResultSchema,
type AnalysisViewResult,
type UnsignedAnalysisView,
@@ -211,6 +213,7 @@ export {
nativeFunctionSummaryViewResultSchema,
sourceToBundleSummaryViewResultSchema,
sourceToBundlePageViewResultSchema,
nativeTraceSummaryViewResultSchema,
analysisViewResultSchema,
type AnalysisViewResult,
type UnsignedAnalysisView,
@@ -327,6 +330,7 @@ const retainedViewProjectors = new Map<string, RetainedViewProjector>([
retain("inspect_gpu_code_objects", projectGpuCodeObjectsView),
retain("match_native_functions", projectNativeFunctionMatchingView),
retain("compare_source_to_bundle", projectSourceToBundleView),
retain("trace_feature", projectNativeTraceView),
retain("inspect_artifact", projectArtifactInventoryView),
retain("inventory_artifact", projectArtifactInventoryView),
retain("analyze_web_bundle", projectWebBundleView),
@@ -426,6 +430,7 @@ export const projectAnalysisView = (
parent.operation === "match_native_functions"
? NATIVE_FUNCTION_MATCHING_PREDICATE
: parent.operation === "analyze_function" ||
parent.operation === "trace_feature" ||
isNativeObservationOperation(parent.operation) ||
isNativeInventoryOperation(parent.operation)
? ANALYSIS_EVIDENCE_PREDICATE
@@ -10,6 +10,7 @@ import { nativeExportsViewRequestSchema } from "./nativeExportsView.js";
import { javaScriptRoutesViewRequestSchema } from "./javascriptRoutesView.js";
import { webBundleViewRequestSchema } from "./webBundleView.js";
import { sourceToBundleViewRequestSchema } from "./sourceToBundleView.js";
import { nativeTraceViewRequestSchema } from "./nativeTraceView.js";
import { gpuCodeObjectsViewRequestSchema } from "./gpuCodeObjectsView.js";
import { modelArtifactViewRequestSchema } from "../model/modelArtifactQuery.js";
import { rustBinaryViewRequestSchema } from "../rust/rustBinaryQuery.js";
@@ -66,6 +67,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
webBundleViewRequestSchema,
nativeFunctionMatchingViewRequestSchema,
sourceToBundleViewRequestSchema,
nativeTraceViewRequestSchema,
summaryViewSchema,
goBinaryViewRequestSchema,
rustBinaryViewRequestSchema,
@@ -63,6 +63,11 @@ import {
sourceToBundleViewItemsSchema,
sourceToBundleViewContextSchema,
} from "./sourceToBundleView.js";
import {
nativeTraceSummarySchema,
nativeTraceViewCollections,
nativeTraceViewContextSchema,
} from "./nativeTraceView.js";
import {
gpuCodeObjectsViewRequestSchema,
gpuCodeObjectsSummarySchema,
@@ -946,6 +951,42 @@ export const sourceToBundlePageViewResultSchema = z.strictObject({
items: sourceToBundleViewItemsSchema,
});
/** Compact retained native feature-trace summary; pages carry the same trace identity. */
export const nativeTraceSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
...nativeViewSourceContext,
artifact: artifactSchema.nullable(),
parent_operation: z.literal("trace_feature"),
summary: nativeTraceSummarySchema,
});
const nativeTracePage = <
Collection extends keyof typeof nativeTraceViewCollections,
>(
collection: Collection,
) =>
z.strictObject({
...viewResultBase,
...nativeViewSourceContext,
...nativeTraceViewContextSchema.shape,
kind: z.literal("native-trace"),
artifact: artifactSchema.nullable(),
parent_operation: z.literal("trace_feature"),
view: nativeTraceViewCollections[collection].request,
items: nativeTraceViewCollections[collection].rows,
});
// As with Mach-O pages, the kind tag keeps dispatch while each collection is
// advertised with exactly its own rows.
const nativeTraceViewResultSchema = z
.object({ kind: z.literal("native-trace") })
.passthrough()
.pipe(
z.union([
nativeTracePage("references"),
nativeTracePage("matches"),
nativeTracePage("residual_unknowns"),
]),
);
/** Bind the existing nested facet selection to its producer-owned rows. */
const machoPage = <Facet extends keyof typeof machoViewRows>(facet: Facet) =>
z.strictObject({
@@ -984,6 +1025,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
javaScriptRoutesViewResultSchema,
webBundlePageViewResultSchema,
sourceToBundlePageViewResultSchema,
nativeTraceViewResultSchema,
z.strictObject({
...viewResultBase,
...nativeFunctionMatchingContextSchema.shape,
@@ -1073,6 +1115,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
nativeFunctionMatchingSummaryViewResultSchema,
nativeFunctionSummaryViewResultSchema,
sourceToBundleSummaryViewResultSchema,
nativeTraceSummaryViewResultSchema,
webBundleSummaryViewResultSchema,
]),
z.strictObject({
+281
View File
@@ -0,0 +1,281 @@
import { z } from "zod";
import { jsonValueSchema } from "../jsonValue.js";
import { literalFeatureMatchSchema } from "../native/literalFeatureMatch.js";
import {
literalFeatureReferenceSchema,
literalFeatureTraceResultSchema,
literalReferenceOwner,
} from "../native/literalFeatureTrace.js";
import { err, ok } from "../result.js";
import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import {
createAnalysisViewResultReader,
type AnalysisViewParent,
} from "./analysisViewParent.js";
import {
ownedView,
type AdmittedView,
type SummaryAnalysisView,
} from "./analysisViewShape.js";
import { selectViewRows } from "./selectViewRows.js";
const OPERATION = "trace_feature" as const;
/** Every view states that retained rows are not complete discovery. */
export const NATIVE_TRACE_COVERAGE_UNKNOWN =
"Counts and pages describe the provider-reported matches and references retained by this trace, not complete discovery. A missing row does not establish an absent reference.";
/** Every view of a capped trace carries the provider's reference cap. */
export const NATIVE_TRACE_CAP_UNKNOWN =
"truncated is true: the provider reported a reference cap, so references beyond it were not observed.";
const ownerSchema = z.enum(["found", "not-found", "unavailable"]);
const page = {
kind: z.literal("native-trace"),
offset: z.int().nonnegative().default(0),
limit: z.int().positive().default(64),
};
const referencesRequestSchema = z.strictObject({
...page,
collection: z.literal("references"),
where: z
.strictObject({
target_addresses: z
.array(z.string())
.optional()
.describe(
"Exact match addresses as recorded; no address normalization. Omitted selects all; [] selects none.",
),
owners: z
.array(ownerSchema)
.optional()
.describe(
"Containing-procedure outcome: found, not-found (outside segments or no procedure) or unavailable (the provider could not resolve ownership).",
),
})
.default({}),
});
const matchesRequestSchema = z.strictObject({
...page,
collection: z.literal("matches"),
where: z
.strictObject({
types: z.array(z.enum(["string", "procedure"])).optional(),
})
.default({}),
});
const unknownsRequestSchema = z.strictObject({
...page,
collection: z.literal("residual_unknowns"),
});
/** Page one retained collection of a completed literal feature trace. */
export const nativeTraceViewRequestSchema = z.discriminatedUnion("collection", [
referencesRequestSchema,
matchesRequestSchema,
unknownsRequestSchema,
]);
type NativeTraceViewRequest = z.output<typeof nativeTraceViewRequestSchema>;
const sourceIndex = z.int().nonnegative();
/**
* Each collection's request bound to its own rows, so a reply cannot pair one
* collection with another's rows. source_index addresses the complete retained
* array, so duplicates stay distinct.
*/
export const nativeTraceViewCollections = {
references: {
request: referencesRequestSchema,
rows: z.array(
z.strictObject({
source_index: sourceIndex,
owner: ownerSchema,
reference: literalFeatureReferenceSchema,
}),
),
},
matches: {
request: matchesRequestSchema,
rows: z.array(
z.strictObject({
source_index: sourceIndex,
match: literalFeatureMatchSchema,
}),
),
},
residual_unknowns: {
request: unknownsRequestSchema,
rows: z.array(
z.strictObject({ source_index: sourceIndex, unknown: z.string() }),
),
},
} as const;
const counts = z.int().nonnegative();
/** Trace identity, provider outcome and counts repeated with every page. */
export const nativeTraceViewContextSchema = z.strictObject({
query: z.string(),
search_mode: z.literal("literal"),
truncated: z
.boolean()
.describe("The provider reported a reference cap for at least one match."),
});
export const nativeTraceSummarySchema = nativeTraceViewContextSchema.extend({
matches: z.strictObject({
total: counts,
string: counts,
procedure: counts,
}),
references: z.strictObject({
total: counts,
found: counts,
"not-found": counts,
unavailable: counts,
}),
residual_unknowns: counts,
});
const readTrace = createAnalysisViewResultReader(
literalFeatureTraceResultSchema,
);
const selected = <Value>(values: readonly Value[] | undefined) =>
values === undefined ? undefined : new Set(values);
/** Select retained trace rows without another provider read or reinterpretation. */
export const projectNativeTraceView = (
parent: AnalysisViewParent,
admitted: AdmittedView,
) => {
const view = ownedView<SummaryAnalysisView | NativeTraceViewRequest>(
admitted,
"summary",
"native-trace",
);
if (view === undefined)
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message:
"Native feature traces support summary and native-trace pages of references, matches or residual_unknowns.",
},
]),
);
const parsed = readTrace(parent);
if (!parsed.ok) return parsed;
const trace = parsed.value;
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: OPERATION,
parent_digest: parent.evidenceId.slice(3),
artifact: parent.artifact ?? null,
source_provider: parent.provider ?? null,
source_analysis_profile_digest: parent.analysisProfile?.digest ?? null,
source_parameters: jsonValueSchema.parse(parent.parameters ?? null),
limitations: boundedViewMessages(
parent.limitations,
parent.evidenceId,
"limitations",
),
// Residual unknowns can number one per reference; page them instead of
// repeating every text in each view.
unknowns: [
NATIVE_TRACE_COVERAGE_UNKNOWN,
...(trace.truncated ? [NATIVE_TRACE_CAP_UNKNOWN] : []),
...(trace.residual_unknowns.length > 0
? [
`${String(trace.residual_unknowns.length)} residual unknowns remain in the parent trace; page them with collection residual_unknowns.`,
]
: []),
],
};
const context = {
query: trace.query,
search_mode: trace.search_mode,
truncated: trace.truncated,
};
if (view.kind === "summary") {
const owners = { found: 0, "not-found": 0, unavailable: 0 };
for (const reference of trace.references)
owners[literalReferenceOwner(reference)] += 1;
const strings = trace.matches.filter(({ type }) => type === "string");
return ok({
...shared,
kind: "summary" as const,
view,
summary: {
...context,
matches: {
total: trace.matches.length,
string: strings.length,
procedure: trace.matches.length - strings.length,
},
references: { total: trace.references.length, ...owners },
residual_unknowns: trace.residual_unknowns.length,
},
coverage: completeWithinViewCoverage(1, 1),
});
}
const pageOf = <View extends NativeTraceViewRequest, Row>(
selection: View,
rows: { readonly rows: Row[]; readonly total: number },
) => ({
...shared,
...context,
kind: "native-trace" as const,
view: selection,
items: rows.rows,
coverage: pageViewCoverage(selection.offset, rows.rows.length, rows.total),
});
if (view.collection === "residual_unknowns")
return ok(
pageOf(
view,
selectViewRows(
trace.residual_unknowns,
view,
() => true,
(unknown, source_index) => ({ source_index, unknown }),
),
),
);
if (view.collection === "matches") {
const types = selected(view.where.types);
return ok(
pageOf(
view,
selectViewRows(
trace.matches,
view,
(match) => types === undefined || types.has(match.type),
(match, source_index) => ({ source_index, match }),
),
),
);
}
const targets = selected(view.where.target_addresses);
const owners = selected(view.where.owners);
return ok(
pageOf(
view,
selectViewRows(
trace.references,
view,
(reference) =>
(targets === undefined || targets.has(reference.target_address)) &&
(owners === undefined ||
owners.has(literalReferenceOwner(reference))),
(reference, source_index) => ({
source_index,
owner: literalReferenceOwner(reference),
reference,
}),
),
),
);
};
+35
View File
@@ -0,0 +1,35 @@
import { z } from "zod";
import { literalFeatureMatchSchema } from "./literalFeatureMatch.js";
import { traceContainingProcedureSchema } from "./nativeObservation.js";
/** One provider-reported reference to a literal match and its owner outcome. */
export const literalFeatureReferenceSchema = z.object({
target_address: z.string(),
source_address: z.string(),
containing_procedure: traceContainingProcedureSchema,
});
export type LiteralFeatureReference = z.infer<
typeof literalFeatureReferenceSchema
>;
/** Complete trace_feature result, owned once for delivery and retained views. */
export const literalFeatureTraceResultSchema = z.object({
query: z.string(),
search_mode: z.literal("literal"),
matches: z.array(literalFeatureMatchSchema),
references: z.array(literalFeatureReferenceSchema),
truncated: z.boolean(),
residual_unknowns: z.array(z.string()),
});
export type LiteralFeatureTraceResult = z.infer<
typeof literalFeatureTraceResultSchema
>;
/** Owner outcome of one reference, without reinterpreting its provider facts. */
export const literalReferenceOwner = (
reference: LiteralFeatureReference,
): "found" | "not-found" | "unavailable" => {
const owner = reference.containing_procedure;
if ("status" in owner) return "unavailable";
return owner.found ? "found" : "not-found";
};
+2 -1
View File
@@ -204,10 +204,11 @@ export const registerEnhancedTools = (
server.registerTool(
traceFeature.name,
toolRegistrationOptions(traceFeature),
(input, context) =>
({ detail, ...input }, context) =>
executeEnhancedTool(registration, traceFeature, {
validatedCall: { name: "trace_feature", input },
context,
detail,
}),
);
server.registerTool(
+13 -6
View File
@@ -16,18 +16,25 @@ export const createToolResultDelivery = (
retainedEvidenceContinuation,
);
/** Offer a named, schema-validated source page only for retained native analysis. */
/** Offer a named, schema-validated first page for retained analysis whose owner supports paging. */
const retainedEvidenceContinuation = (
evidence: Evidence,
): Readonly<Record<string, JsonValue>> => {
const view =
evidence.operation === "analyze_function"
? { kind: "native", facet: "pseudocode", offset: 0 }
: isNativeInventoryOperation(evidence.operation)
? { kind: "native-inventory", offset: 0, limit: 64 }
: isNativeObservationOperation(evidence.operation)
? { kind: "native-observation", offset: 0, limit: 64 }
: undefined;
: evidence.operation === "trace_feature"
? {
kind: "native-trace",
collection: "references",
offset: 0,
limit: 64,
}
: isNativeInventoryOperation(evidence.operation)
? { kind: "native-inventory", offset: 0, limit: 64 }
: isNativeObservationOperation(evidence.operation)
? { kind: "native-observation", offset: 0, limit: 64 }
: undefined;
if (view === undefined) return {};
const input = inspectAnalysisViewInputSchema.safeParse({
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
@@ -1421,3 +1421,85 @@ cliTest.for(
expect(proof.listeners).toBeGreaterThan(0);
},
);
cliTest(
"pages a saved native feature trace through the shared view owner",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-native-trace-cli-");
const unavailable = {
query_address: "0x21",
status: "unavailable" as const,
provider_id: "ghidra",
reason: "not supported",
};
const parent = createEvidence(
{ path: "/fixtures/native.exe", format: "pe", sha256: "a".repeat(64) },
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "trace_feature",
parameters: { query: "license", case_sensitive: false },
result: {
query: "license",
search_mode: "literal",
matches: [{ type: "string", address: "0x10", value: "license" }],
references: [
{
target_address: "0x10",
source_address: "0x20",
containing_procedure: {
query_address: "0x20",
found: false,
procedure: null,
reason: "not_in_procedure",
},
},
{
target_address: "0x10",
source_address: "0x21",
containing_procedure: unavailable,
},
],
truncated: false,
residual_unknowns: ["Which procedure contains 0x21?"],
},
},
);
const path = join(root, "trace.json");
await writeFile(path, JSON.stringify(parent));
const inspect = async (view: Record<string, unknown>) => {
const response = await cli.run({
arguments: [
"inspect-analysis-view",
JSON.stringify({ source: { kind: "evidence-file", path }, view }),
"--json",
],
});
expect(response.exitCode, response.stderr).toBe(0);
return parseEvidence(response.json).normalized_result;
};
expect(await inspect({ kind: "summary" })).toMatchObject({
parent_evidence_id: parent.evidence_id,
parent_operation: "trace_feature",
summary: {
references: { total: 2, "not-found": 1, unavailable: 1, found: 0 },
residual_unknowns: 1,
},
});
expect(
await inspect({
kind: "native-trace",
collection: "references",
where: { owners: ["unavailable"] },
}),
).toMatchObject({
items: [
{
source_index: 1,
owner: "unavailable",
reference: { containing_procedure: unavailable },
},
],
coverage: { total: 1, exhausted: true },
});
},
);
@@ -0,0 +1,476 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterAll, expect, it, onTestFinished } from "vitest";
import { z } from "zod";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { jsonObjectSchema } from "../../../src/domain/jsonValue.js";
import {
literalFeatureTraceResultSchema,
literalReferenceOwner,
type LiteralFeatureTraceResult,
} from "../../../src/domain/native/literalFeatureTrace.js";
import { err, ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import {
NATIVE_TRACE_CAP_UNKNOWN,
NATIVE_TRACE_COVERAGE_UNKNOWN,
} from "../../../src/domain/analysisView/nativeTraceView.js";
import { ghidraFunctionIdentity } from "../../../src/domain/ghidraValues.fixture.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const schemaValidation = createMcpSchemaValidation();
afterAll(() => schemaValidation.clear());
const PROVIDER = { id: "fixture", name: "Fixture", version: "1" } as const;
const STRINGS = ["0x10", "0x11", "0x12"];
/** Each match is referenced from several sources with mixed owner outcomes. */
const SOURCES: Readonly<Record<string, readonly string[]>> = {
"0x10": ["0x20", "0x21", "0x22"],
"0x11": ["0x23"],
"0x12": ["0x24", "0x25"],
"0x30": ["0x26"],
};
const UNAVAILABLE = new Set(["0x21", "0x24"]);
const NOT_IN_PROCEDURE = new Set(["0x22"]);
const connect = async (
options: {
/** The provider reports a reference cap for the first string match. */
readonly capped?: boolean;
readonly profile?: "full" | "compact";
readonly budget?: number;
} = {},
) => {
const session = createTestBinarySession(() => ({
execute: async (operation, input) => {
const address = String(input.address);
switch (operation) {
case "search_strings":
return ok(
createAnalysisExecution(
STRINGS.map((value) => ({ address: value, value: "license" })),
PROVIDER,
),
);
case "search_procedures":
return ok(
createAnalysisExecution(
[{ address: "0x30", value: "check_license" }],
PROVIDER,
),
);
case "xrefs":
return ok(
createAnalysisExecution([...(SOURCES[address] ?? [])], PROVIDER, {
...(options.capped === true && address === "0x10"
? { referencesTruncated: true }
: {}),
}),
);
case "resolve_containing_procedure":
if (UNAVAILABLE.has(address))
return err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"not supported",
),
);
return ok(
createAnalysisExecution(
NOT_IN_PROCEDURE.has(address)
? {
query_address: address,
found: false,
procedure: null,
reason: "not_in_procedure",
}
: {
query_address: address,
found: true,
procedure: ghidraFunctionIdentity(),
},
PROVIDER,
),
);
default:
return ok(createAnalysisExecution(null, PROVIDER));
}
},
close: async () => ok(null),
}));
const { client } = await createApplicationMcpHarness({
session,
clientIdentity: { name: "native-trace-view", version: "1" },
schemaValidator: schemaValidation,
serverOptions: {
environment: {
...process.env,
REA_MCP_OUTPUT_SCHEMA_PROFILE: options.profile ?? "full",
},
...(options.budget === undefined
? {}
: { delivery: createToolResultDelivery(options.budget) }),
},
});
const directory = await createTestTempDirectory("rea-native-trace-view-");
const target = join(directory, "fixture.com");
await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21]));
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
});
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const reply = (name: string, args: Record<string, unknown>) =>
client.callTool({ name, arguments: args });
const call = async (name: string, args: Record<string, unknown>) => {
const result = await reply(name, args);
expect(result.isError, JSON.stringify(result)).not.toBe(true);
return { result, evidence: parseEvidence(result.structuredContent) };
};
return { client, session, reply, call };
};
const traced = (value: unknown): LiteralFeatureTraceResult =>
literalFeatureTraceResultSchema.parse(value);
it.each(["full", "compact"] as const)(
"summarizes and pages a retained native feature trace (%s)",
async (profile) => {
const { client, session, call, reply } = await connect({ profile });
// Summary first: the summary call alone retains the complete trace.
const summary = await call("trace_feature", {
query: "license",
detail: "summary",
});
const parentId = z
.object({ parent_evidence_id: z.string() })
.parse(summary.evidence.normalized_result).parent_evidence_id;
const parent = parseEvidence(session.evidenceById(parentId));
expect(parent.operation).toBe("trace_feature");
const full = traced(parent.normalized_result);
const owners = (owner: string) =>
full.references.filter(
(reference) => literalReferenceOwner(reference) === owner,
);
expect(owners("unavailable")).toHaveLength(UNAVAILABLE.size);
expect(full.residual_unknowns.length).toBeGreaterThan(0);
expect(summary.evidence.normalized_result).toMatchObject({
kind: "summary",
parent_operation: "trace_feature",
source_provider: parent.provider,
source_analysis_profile_digest: parent.analysis_profile?.digest ?? null,
summary: {
query: "license",
truncated: false,
matches: { total: 4, string: 3, procedure: 1 },
references: {
total: full.references.length,
found: owners("found").length,
"not-found": owners("not-found").length,
unavailable: owners("unavailable").length,
},
residual_unknowns: full.residual_unknowns.length,
},
});
// Summary delivery still records one residual Unknown per unresolved owner.
const unknowns = await reply("list_unknowns", {});
expect(
z
.object({ result: z.object({ total: z.int() }) })
.parse(unknowns.structuredContent).result.total,
).toBe(full.residual_unknowns.length);
// Unknown texts are paged rather than repeated in every view.
const unknownTexts = z
.object({ unknowns: z.array(z.string()) })
.parse(summary.evidence.normalized_result).unknowns;
for (const text of full.residual_unknowns)
expect(unknownTexts).not.toContain(text);
const { tools } = await client.listTools();
const tool = tools.find(({ name }) => name === "trace_feature")!;
const ajv = schemaValidation.assertions;
expect(ajv.validateSchema(tool.inputSchema)).toBe(true);
const outputSchema: Record<string, unknown> = tool.outputSchema!;
expect(ajv.validate(outputSchema, summary.result.structuredContent)).toBe(
true,
);
const complete = await call("trace_feature", { query: "license" });
expect(complete.evidence.evidence_id).toBe(parentId);
expect(ajv.validate(outputSchema, complete.result.structuredContent)).toBe(
true,
);
await assertRetainedTracePages({ call, reply }, parentId, full);
},
);
/** Page the retained trace through inspect_analysis_view without another provider read. */
const assertRetainedTracePages = async (
{ call, reply }: Pick<Awaited<ReturnType<typeof connect>>, "call" | "reply">,
parentId: string,
full: LiteralFeatureTraceResult,
) => {
const source = { kind: "retained-evidence", evidence_id: parentId };
const view = async (selection: Record<string, unknown>) => {
const page = analysisViewResultSchema.parse(
(await call("inspect_analysis_view", { source, view: selection }))
.evidence.normalized_result,
);
if (page.kind !== "native-trace") throw new Error("Expected trace page");
return page;
};
const collect = async (selection: Record<string, unknown>) => {
const rows: Record<string, unknown>[] = [];
let offset: number | null = 0;
while (offset !== null) {
const page = await view({
kind: "native-trace",
...selection,
offset,
limit: 2,
});
expect(page).toMatchObject({ query: "license", truncated: false });
rows.push(...(page.items as Record<string, unknown>[]));
offset = page.coverage.next_offset;
}
return rows;
};
// Pages reproduce every retained row with its source index and derived owner.
expect(await collect({ collection: "references" })).toEqual(
full.references.map((reference, source_index) => ({
source_index,
owner: literalReferenceOwner(reference),
reference,
})),
);
expect(await collect({ collection: "matches" })).toEqual(
full.matches.map((match, source_index) => ({ source_index, match })),
);
expect(await collect({ collection: "residual_unknowns" })).toEqual(
full.residual_unknowns.map((unknown, source_index) => ({
source_index,
unknown,
})),
);
expect(
await collect({
collection: "references",
where: { owners: ["unavailable"] },
}),
).toEqual(
full.references.flatMap((reference, source_index) =>
literalReferenceOwner(reference) === "unavailable"
? [{ source_index, owner: "unavailable", reference }]
: [],
),
);
expect(
(
await collect({
collection: "references",
where: { target_addresses: ["0x10"] },
})
).map(({ source_index }) => source_index),
).toEqual(
full.references.flatMap(({ target_address }, index) =>
target_address === "0x10" ? [index] : [],
),
);
expect(
await collect({ collection: "matches", where: { types: ["procedure"] } }),
).toEqual(
full.matches.flatMap((match, source_index) =>
match.type === "procedure" ? [{ source_index, match }] : [],
),
);
for (const selection of [
{ collection: "references", where: { owners: [] } },
{ collection: "references", where: { target_addresses: ["0X10"] } },
{ collection: "matches", where: { types: [] } },
{ collection: "references", offset: full.references.length },
])
expect(await view({ kind: "native-trace", ...selection })).toMatchObject({
items: [],
coverage: { status: "empty", exhausted: true },
});
for (const selection of [
{ kind: "native-observation", offset: 0, limit: 1 },
{
kind: "native-trace",
collection: "references",
where: { owners: ["missing"] },
},
{ kind: "native-trace", collection: "callers" },
])
expect(
parseMcpToolError(
await reply("inspect_analysis_view", { source, view: selection }),
),
).toMatchObject({ error: { code: "invalid_request" } });
};
it("recovers an oversized complete trace through a retained references page", async () => {
const budget = 8 * 1024;
const { session, call, reply } = await connect({ budget });
const failed = parseMcpToolError(
await reply("trace_feature", { query: "license" }),
).error;
expect(failed.code).toBe("resource_constraint");
const limits = z
.object({ details: z.object({ reported_limits: jsonObjectSchema }) })
.parse(failed).details.reported_limits;
const reference = z
.object({ kind: z.literal("retained-evidence"), evidence_id: z.string() })
.parse(limits.evidence_reference);
const full = traced(
parseEvidence(session.evidenceById(reference.evidence_id))
.normalized_result,
);
const continuation = z
.object({
name: z.literal("inspect_analysis_view"),
arguments: toolContract("inspect_analysis_view").inputSchema,
})
.parse(limits.continuation);
expect(continuation.arguments).toEqual({
source: reference,
view: {
kind: "native-trace",
collection: "references",
where: {},
offset: 0,
limit: 64,
},
});
// The default page may still be too large for this deliberately tiny budget;
// a smaller page of the same retained collection fits.
const page = await call(continuation.name, {
...continuation.arguments,
view: { ...continuation.arguments.view, limit: 1 },
});
expect(page.evidence.normalized_result).toMatchObject({
kind: "native-trace",
items: [{ source_index: 0, reference: full.references[0] }],
coverage: { total: full.references.length, next_offset: 1 },
});
});
it("refuses summary delivery before analysis when the server retains no Evidence", async () => {
const calls: string[] = [];
const server = createServer({
kind: "fixed",
analysis: {
execute: async (operation) => {
calls.push(operation);
return ok(createAnalysisExecution([], PROVIDER));
},
},
});
const client = new Client({ name: "native-trace-fixed", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
onTestFinished(async () => {
await client.close();
await server.close();
});
await server.connect(serverTransport);
await client.connect(clientTransport);
expect(
parseMcpToolError(
await client.callTool({
name: "trace_feature",
arguments: { query: "license", detail: "summary" },
}),
),
).toMatchObject({ error: { code: "capability_unavailable" } });
expect(calls).toEqual([]);
});
it("keeps a provider reference cap on every view of the trace", async () => {
const { client, call } = await connect({ capped: true });
const summary = await call("trace_feature", {
query: "license",
detail: "summary",
});
const parentId = z
.object({ parent_evidence_id: z.string() })
.parse(summary.evidence.normalized_result).parent_evidence_id;
const source = { kind: "retained-evidence", evidence_id: parentId };
const views = [
summary.evidence.normalized_result,
...(await Promise.all(
[
{ kind: "summary" },
{ kind: "native-trace", collection: "references", limit: 1 },
{
kind: "native-trace",
collection: "references",
where: { owners: ["unavailable"] },
},
{ kind: "native-trace", collection: "matches", offset: 99 },
{ kind: "native-trace", collection: "residual_unknowns" },
].map(
async (view) =>
(await call("inspect_analysis_view", { source, view })).evidence
.normalized_result,
),
)),
];
for (const view of views) {
const parsed = z
.object({
unknowns: z.array(z.string()),
truncated: z.boolean().optional(),
summary: z.object({ truncated: z.boolean() }).optional(),
})
.parse(view);
expect(parsed.truncated ?? parsed.summary?.truncated).toBe(true);
expect(parsed.unknowns).toEqual(
expect.arrayContaining([
NATIVE_TRACE_COVERAGE_UNKNOWN,
NATIVE_TRACE_CAP_UNKNOWN,
]),
);
}
// The advertised page contract binds each collection to its own rows.
const page = await call("inspect_analysis_view", {
source,
view: { kind: "native-trace", collection: "references", limit: 1 },
});
const matches = await call("inspect_analysis_view", {
source,
view: { kind: "native-trace", collection: "matches", limit: 1 },
});
const mixed = {
...jsonObjectSchema.parse(page.evidence.normalized_result),
items: (matches.evidence.normalized_result as { items: unknown }).items,
};
expect(analysisViewResultSchema.safeParse(mixed).success).toBe(false);
const inspectTool = (await client.listTools()).tools.find(
({ name }) => name === "inspect_analysis_view",
)!;
const advertised: Record<string, unknown> = inspectTool.outputSchema!;
const ajv = schemaValidation.assertions;
expect(ajv.validate(advertised, page.result.structuredContent)).toBe(true);
expect(
ajv.validate(advertised, {
...jsonObjectSchema.parse(page.result.structuredContent),
normalized_result: mixed,
}),
).toBe(false);
expect(
analysisViewResultSchema.safeParse(page.evidence.normalized_result).success,
).toBe(true);
});