Compare commits

...
Author SHA1 Message Date
morluto a366098c11 fix(view): validate recorded page rows once 2026-11-07 22:41:19 +08:00
Cursor Agent 3656ad6957 test(view): narrow recorded-array depth pages before reading parameters 2026-11-07 12:22:44 +00:00
Cursor Agent 5e9ff3a89a fix(view): keep deep recorded-array pages inside Evidence depth
Parent parameters are valid on the Evidence root and one level too deep
under a page result. Omit those parameters, and refuse a row that still
cannot be nested, instead of throwing from Evidence parsing.
2026-11-07 12:22:25 +00:00
Cursor Agent 463aa246d4 merge(codex/kuna-xref-evidence): keep recorded-array pages on the current parent
The child still pages a producer-recorded array. Parent and main views
for instruction references, value traces, and pseudocode stay in the
same result union, and oversized view input keeps the evidence-file route.
2026-11-07 12:09:56 +00:00
Cursor Agent 12c72d389c merge(main): keep stored-pointer facts beside sibling unavailability
Reference classifications and coverage gaps stay on the live execution
and are not snapshotted. Source Evidence can still replay when its
parent closure closes, and sibling unavailability from main is retained.
2026-11-07 12:06:14 +00:00
Cursor Agent baec1f459f merge: keep completed xrefs when ledger retention is refused
Producer-required xref Evidence still enters the ledger, including
one-shot snapshot context. A capacity refusal keeps that completed
observation on the error instead of returning only the ledger failure.
Stored pointer classifications stay on the literal trace reference.
2026-11-04 19:46:06 +00:00
morluto 68aefcaef6 test(kuna): identify pointer fixture version 2026-11-04 13:22:05 +08:00
morluto 24d27d0921 fix: disclose alias scope in recorded references 2026-11-04 13:19:34 +08:00
morluto d9614950c0 fix: keep recorded rows accessible with large context 2026-11-04 13:14:48 +08:00
morluto b22d66eed1 fix: preserve source context in recorded views 2026-11-04 13:07:48 +08:00
morluto 17b3295a3c fix(mcp): guide large evidence views to files 2026-11-04 13:04:44 +08:00
morluto 1556147b12 feat: page recorded evidence arrays 2026-11-04 12:57:07 +08:00
morluto da842598bc fix(kuna): preserve reference target aliases 2026-11-04 11:03:26 +08:00
morluto d588d2986f fix(mcp): clarify xref address page scope 2026-11-04 10:53:37 +08:00
morluto 411f02d218 fix(evidence): retain producer-required xref observations 2026-11-04 10:39:22 +08:00
morluto 97e9cded31 fix(kuna): group image-wide reference coverage gaps 2026-11-04 10:31:34 +08:00
morluto 475043a962 feat(kuna): preserve stored pointer references 2026-11-04 10:27:36 +08:00
morluto 187f3d8ccd feat(kuna): verify release 1.990 2026-11-03 23:33:10 +08:00
29 changed files with 1220 additions and 34 deletions

No files matched your search

+2
View File
@@ -318,6 +318,8 @@ Retained `xrefs` Evidence also supports `native-observation` address pages and
default and does not accept `detail`; when its MCP response exceeds the budget,
follow the returned continuation to page the retained result. These addresses
remain untyped; paging does not add reference kinds or establish completeness.
The page tells callers that any additional provider detail recorded in the parent
requires retrieving or exporting the complete Evidence.
These views reference complete source context through `source_context.digest`
and caveat counts. Read it using the same parent with
+28
View File
@@ -82,6 +82,22 @@ export interface AnalysisExecution extends AnalysisObservation {
* output.
*/
readonly referencesTruncated?: boolean;
/** The producer needs this observation retained as source Evidence for composed workflows. */
readonly requiresSourceEvidence?: true;
/** Known reference-discovery gaps; absent means coverage was not reported. */
readonly referenceCoverageGaps?: readonly {
readonly code: string;
readonly scope: "image" | "observation";
readonly description: string;
}[];
/** Typed source facts for this live xref observation; absent means unreported. */
readonly referenceClassifications?: readonly {
readonly sourceAddress: string;
readonly targetAddress: string;
readonly kind: "stored_pointer";
readonly provenance: string;
readonly width: number;
}[];
/**
* Sibling operations known to be unavailable at the provider level when this
* operation answered from a standalone or fallback path.
@@ -97,6 +113,9 @@ interface CreateAnalysisExecutionOptions {
readonly limitations?: readonly string[];
readonly relationshipCoverage?: RelationshipCoverage;
readonly referencesTruncated?: boolean;
readonly requiresSourceEvidence?: true;
readonly referenceClassifications?: AnalysisExecution["referenceClassifications"];
readonly referenceCoverageGaps?: AnalysisExecution["referenceCoverageGaps"];
readonly siblingUnavailability?: AnalysisCapabilityUnavailableError;
readonly locations?: readonly EvidenceLocation[];
readonly sourceEvidence?: readonly Evidence[];
@@ -156,6 +175,15 @@ export function createAnalysisExecution(
...(options.referencesTruncated === undefined
? {}
: { referencesTruncated: options.referencesTruncated }),
...(options.requiresSourceEvidence === undefined
? {}
: { requiresSourceEvidence: options.requiresSourceEvidence }),
...(options.referenceCoverageGaps === undefined
? {}
: { referenceCoverageGaps: options.referenceCoverageGaps }),
...(options.referenceClassifications === undefined
? {}
: { referenceClassifications: options.referenceClassifications }),
...(options.siblingUnavailability === undefined
? {}
: { siblingUnavailability: options.siblingUnavailability }),
+4 -2
View File
@@ -25,8 +25,10 @@ import {
retainAnalysisResultSources,
type AnalysisResultWithSources,
} from "./investigation/recordEvidenceSources.js";
import { resolveInvestigationEvidenceClosure } from "./investigation/InvestigationEvidence.js";
import { createInvestigationEvidenceClosureChecker } from "./investigation/InvestigationEvidence.js";
import {
resolveInvestigationEvidenceClosure,
createInvestigationEvidenceClosureChecker,
} from "./investigation/InvestigationEvidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
+29
View File
@@ -137,6 +137,7 @@ const traceReferences = async (
const tracedTargets = new Set<string>();
// A source address is resolved once however many matches reference it.
const containing = new Map<string, TraceContainingProcedure>();
const imageCoverageGaps = new Set<string>();
for (const match of request.matches) {
if (tracedTargets.has(match.address)) continue;
tracedTargets.add(match.address);
@@ -154,6 +155,17 @@ const traceReferences = async (
`References to ${match.address} were truncated by the provider; additional references may exist.`,
);
}
for (const gap of observed.value.referenceCoverageGaps ?? []) {
if (gap.scope === "observation") {
residual.push(`References to ${match.address}: ${gap.description}`);
} else {
const key = JSON.stringify([observed.value.provider.id, gap.code]);
if (!imageCoverageGaps.has(key)) {
imageCoverageGaps.add(key);
residual.push(gap.description);
}
}
}
const sources = observed.value.result;
if (!Array.isArray(sources))
return err(
@@ -162,6 +174,20 @@ const traceReferences = async (
"provider returned a non-array result",
),
);
const classifications = new Map<
string,
{ kind: "stored_pointer"; provenance: string; width: number }[]
>();
for (const fact of observed.value.referenceClassifications ?? []) {
if (fact.targetAddress !== match.address) continue;
const values = classifications.get(fact.sourceAddress) ?? [];
values.push({
kind: fact.kind,
provenance: fact.provenance,
width: fact.width,
});
classifications.set(fact.sourceAddress, values);
}
for (const source of sources) {
if (typeof source !== "string")
return err(
@@ -211,6 +237,9 @@ const traceReferences = async (
target_address: match.address,
source_address: source,
containing_procedure: containingProcedure,
...(classifications.has(source)
? { source_classifications: classifications.get(source)! }
: {}),
});
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ export const workflowAnalysisProfile = (
: operation === "batch_decompile"
? { workflow_revision: 4 }
: operation === "trace_feature"
? { workflow_revision: 4 }
? { workflow_revision: 8 }
: operation === "get_call_graph"
? { workflow_revision: 3 }
: operation === "trace_native_values"
@@ -230,6 +230,7 @@ const projectResolvedAnalysisView = (
analysisProfile: parent.analysis_profile,
authority: parent.authority,
environment: parent.environment,
rawResult: parent.raw_result,
normalizedResult:
immutableJsonSnapshotMember(parent, "normalized_result") ??
parent.normalized_result,
@@ -864,6 +864,73 @@ describe("document-scoped snapshot admission", () => {
});
});
it("retains producer-required xrefs without replaying a filesystem-writing query", () => {
const descriptor: CapabilityDescriptor = {
operation: "xrefs",
provider: ANALYSIS_SNAPSHOT_PROVIDER,
available: true,
reason: null,
effects: {
mutatesArtifact: false,
launchesProcess: true,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
};
const xrefs = descriptor;
expect(
isSourceEvidenceRetainable("xrefs", xrefs, { address: "0x1000" }),
).toBe(false);
expect(isSnapshotCacheable("xrefs", xrefs, { address: "0x1000" })).toBe(
false,
);
expect(isSourceEvidenceRetainable("xrefs", xrefs, {})).toBe(false);
const writingXrefs = {
...xrefs,
effects: { ...xrefs.effects, mayWriteFilesystem: true },
};
expect(
isSourceEvidenceRetainable("xrefs", writingXrefs, { address: "0x1000" }),
).toBe(false);
expect(
isSourceEvidenceRetainable(
"xrefs",
writingXrefs,
{ address: "0x1000" },
{ requiresSourceEvidence: true },
),
).toBe(true);
expect(
isSourceEvidenceRetainable(
"xrefs",
writingXrefs,
{},
{ requiresSourceEvidence: true },
),
).toBe(false);
expect(
isSourceEvidenceRetainable(
"xrefs",
{
...writingXrefs,
effects: { ...writingXrefs.effects, mutatesArtifact: true },
},
{ address: "0x1000" },
{ requiresSourceEvidence: true },
),
).toBe(false);
expect(
isSnapshotCacheable("xrefs", writingXrefs, {
address: "0x1000",
document: "fixture",
}),
).toBe(false);
});
describe("analysis snapshot cache partitioning", () => {
it.each<{
operation: Exclude<AnalysisOperation, "health">;
@@ -1490,13 +1557,43 @@ describe("executions that report truncated references", () => {
});
});
it("does not replay references when the snapshot would lose their classifications", () => {
const cache = new AnalysisSnapshotCache();
const parameters = { address: "0x1000", document: "fixture" };
cache.record({
target: ANALYSIS_SNAPSHOT_TARGET,
profile: ANALYSIS_SNAPSHOT_PROFILE,
operation: "xrefs",
parameters,
execution: createAnalysisExecution(["0x2000"], ANALYSIS_SNAPSHOT_PROVIDER, {
referenceClassifications: [
{
sourceAddress: "0x2000",
targetAddress: "0x1000",
kind: "stored_pointer",
provenance: "relocation",
width: 8,
},
],
}),
});
expect(
cache.lookup(
ANALYSIS_SNAPSHOT_TARGET,
ANALYSIS_SNAPSHOT_PROFILE,
"xrefs",
parameters,
),
).toBeUndefined();
});
it("revises the trace_feature workflow profile so earlier results are not replayed", () => {
const profile = ANALYSIS_SNAPSHOT_PROFILE;
expect(
workflowAnalysisProfile(profile, "trace_feature").parameters
.workflow_revision,
).toBe(4);
).toBe(8);
expect(workflowAnalysisProfile(profile, "trace_feature")).not.toEqual(
workflowAnalysisProfile(profile),
@@ -117,25 +117,30 @@ const DOCUMENTLESS_SOURCE_EVIDENCE_OPERATIONS: ReadonlySet<AnalysisOperation> =
/**
* Whether a source observation is safe to retain as Evidence.
* A missing document selector blocks snapshot replay. Only caller and callee
* walks stay in the ledger without one.
* A missing document selector blocks snapshot replay. A producer can require
* its observation as Evidence independently of whether it can be replayed.
*/
export const isSourceEvidenceRetainable = (
operation: AnalysisOperation,
descriptor: CapabilityDescriptor | undefined,
parameters: Readonly<Record<string, JsonValue>>,
context?: ExecutionOptions["snapshotContext"],
options: {
readonly snapshotContext?: ExecutionOptions["snapshotContext"];
readonly requiresSourceEvidence?: boolean;
} = {},
): descriptor is CapabilityDescriptor =>
!STATEFUL_OPERATIONS.has(operation) &&
!RESOURCE_SCOPED_OPERATIONS.has(operation) &&
(!DOCUMENT_SCOPED_OPERATIONS.has(operation) ||
typeof parameters.document === "string" ||
DOCUMENTLESS_SOURCE_EVIDENCE_OPERATIONS.has(operation) ||
isOneShotPseudocode(operation, descriptor, context)) &&
options.requiresSourceEvidence === true ||
isOneShotPseudocode(operation, descriptor, options.snapshotContext)) &&
(!CURSOR_DEFAULT_OPERATIONS.has(operation) ||
typeof parameters.address === "string") &&
descriptor?.effects.mutatesArtifact === false &&
(descriptor.cachePolicy === "snapshot" ||
(options.requiresSourceEvidence === true ||
descriptor.cachePolicy === "snapshot" ||
descriptor.effects.mayWriteFilesystem === false) &&
descriptor.effects.changesPermissions === false;
@@ -146,7 +151,9 @@ export const isSnapshotCacheable = (
parameters: Readonly<Record<string, JsonValue>>,
context?: ExecutionOptions["snapshotContext"],
): descriptor is CapabilityDescriptor =>
isSourceEvidenceRetainable(operation, descriptor, parameters, context) &&
isSourceEvidenceRetainable(operation, descriptor, parameters, {
snapshotContext: context,
}) &&
(replayDocumentSelected(operation, parameters) ||
isOneShotPseudocode(operation, descriptor, context)) &&
descriptor.cachePolicy !== "live";
@@ -589,9 +596,15 @@ export class AnalysisSnapshotCache {
): void {
const { target, profile, operation, parameters, execution } = input;
this.select(target, profile);
// The snapshot format does not carry reference truncation; a replay would
// present a capped read as complete, so it is not recorded.
if (execution.referencesTruncated === true) return;
// The snapshot format does not carry reference truncation or stored-pointer
// classifications. Replaying either would present a capped or untyped read
// as the live observation.
if (
execution.referencesTruncated === true ||
execution.referenceClassifications !== undefined ||
execution.referenceCoverageGaps !== undefined
)
return;
let sourceBinding: SnapshotReplayBinding | undefined;
if ((execution.sourceEvidence?.length ?? 0) > 0) {
if (execution.evidenceId === undefined || lookup === undefined) return;
+9 -7
View File
@@ -1539,12 +1539,10 @@ export class BinarySession
}
if (
execution.ok &&
isSourceEvidenceRetainable(
name,
capability,
arguments_,
options?.snapshotContext,
) &&
isSourceEvidenceRetainable(name, capability, arguments_, {
snapshotContext: options?.snapshotContext,
requiresSourceEvidence: execution.value.requiresSourceEvidence === true,
}) &&
profile !== undefined
) {
const evidence = createAnalysisExecutionEvidence(
@@ -1561,7 +1559,11 @@ export class BinarySession
"observed",
);
const recorded = this.recordEvidence(evidence);
if (!recorded.ok) return recorded;
if (!recorded.ok) {
// The provider finished. A full ledger must not discard that observation.
recorded.error.retainPartialObservation(evidence);
return recorded;
}
const retained = { ...execution.value, evidenceId: evidence.evidence_id };
if (cacheable && execution.value.cachePolicy !== "live")
this.recordSnapshot({
@@ -1,5 +1,10 @@
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { electronSemanticsState } from "./ElectronBoundaryAnalysis.js";
import {
electronSemanticsState,
createElectronIpcContext,
summarizeElectronBoundaries,
type ElectronIpcContext,
} from "./ElectronBoundaryAnalysis.js";
import { ELECTRON_IDENTITY_LIMITATION } from "../../domain/javascript/javascriptElectronMemberWrites.js";
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import {
@@ -47,11 +52,6 @@ import {
} from "./JavaScriptArtifactGraphStructure.js";
import { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
import { addElectronBoundaries } from "./ElectronBoundaryGraph.js";
import {
createElectronIpcContext,
summarizeElectronBoundaries,
type ElectronIpcContext,
} from "./ElectronBoundaryAnalysis.js";
/**
* Build a validated application graph with cooperative immutable ownership.
+1 -1
View File
@@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'For inspect_api_contract parents, graphql-contract pages retained SDL types/fields, operation definitions, field references, fragment-spreads or diagnostics with exact filters and selected columns. Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. For procedure_pseudo_code Evidence, summary reports text availability and the original selection; kind native with facet pseudocode pages the original UTF-16 text. Other native facets require analyze_function. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent also supports summary and bLine truncated
'For inspect_api_contract parents, graphql-contract pages retained SDL types/fields, operation definitions, field references, fragment-spreads or diagnostics with exact filters and selected columns. Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. Use recorded-array with an exact raw_result object-member path to page a provider-recorded array when the producer supplies that path; rows keep their raw meaning and the parent retains coverage. For procedure_pseudo_code Evidence, summary reports text availability and the original selection; kind native with facet pseudocode pages the original UTF-16 text. Other native facets require analyze_function. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, paraLine truncated
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
+4
View File
@@ -5,6 +5,7 @@ import {
NATIVE_OBSERVATION_OPERATIONS,
} from "../native/nativeObservation.js";
import { projectNativeObservationView } from "./nativeObservationView.js";
import { projectRecordedArrayView } from "./recordedArrayView.js";
import { projectNativeInstructionReferencesView } from "./nativeInstructionReferencesView.js";
import { projectNativeValueTraceView } from "./nativeValueTraceView.js";
import { projectDevicetreeView } from "./devicetreeView.js";
@@ -497,6 +498,9 @@ export const projectAnalysisView = (
},
]),
);
if (view.kind === "recorded-array") {
return sealProjected(projectRecordedArrayView(parent, view));
}
if (view.kind === "javascript-routes") {
const projected = projectJavaScriptRoutesView(parent, view);
return sealProjected(projected);
@@ -12,6 +12,7 @@ export interface AnalysisViewParent {
readonly operation: string;
readonly predicateType: string;
readonly normalizedResult: unknown;
readonly rawResult?: unknown;
readonly provider?: Evidence["provider"];
readonly analysisProfile?: Evidence["analysis_profile"];
readonly authority?: Evidence["authority"];
@@ -63,11 +63,23 @@ import {
pageViewSchema,
summaryViewSchema,
} from "./analysisViewShape.js";
export const recordedArrayViewSchema = z.strictObject({
kind: z.literal("recorded-array"),
path: z
.array(z.string())
.describe(
"Exact object-member path from the parent Evidence raw_result to the recorded array. The provider defines the values and their coverage.",
),
offset: z.int().nonnegative().default(0),
limit: z.int().positive().default(64),
});
/** Caller-selected projection of already completed analysis Evidence. */
export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
nativeInstructionReferencesRequestSchema,
nativeObservationViewRequestSchema,
nativeContextViewRequestSchema,
recordedArrayViewSchema,
devicetreeViewRequestSchema,
erlangBeamViewRequestSchema,
pythonPycViewRequestSchema,
@@ -308,6 +308,8 @@ import {
pageViewSchema,
summaryViewSchema,
} from "./analysisViewShape.js";
import { recordedArrayViewSchema } from "./analysisViewRequest.js";
const artifactSchema = z.strictObject({
path: z
.string()
@@ -1202,6 +1204,20 @@ const machoPageViewResultSchema = z
/** Projected facts for one selected view of retained analysis Evidence. */
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
z.strictObject({
...viewResultBase,
...nativeViewSourceContext,
source_parameters: jsonObjectSchema
.nullable()
.describe(
"Null means source parameters were omitted to fit the page; retrieve the parent Evidence.",
),
parent_operation: z.string(),
artifact: artifactSchema.nullable(),
kind: z.literal("recorded-array"),
view: recordedArrayViewSchema,
rows: z.array(jsonValueSchema),
}),
playwrightTracePageViewResultSchema,
z.strictObject({
...viewResultBase,
@@ -30,6 +30,10 @@ import {
} from "./nativeObservationSchemas.js";
import { projectNativeGraphRows } from "./nativeGraphView.js";
/** What an address page leaves in the complete parent, when recorded. */
export const XREFS_ADDRESS_PAGE_LIMITATION =
"This page contains source addresses only, not reference kinds or provenance. Retrieve or export the complete parent Evidence for any additional provider details it recorded.";
const readAddresses = createAnalysisViewResultReader(nativeAddressListSchema);
const readAddress = createAnalysisViewResultReader(
nativeObservationSourceSchemas.procedure_address,
@@ -347,6 +351,9 @@ export const projectNativeObservationView = (
const { total, ...payload } = page;
const observation = {
...common,
...(source.value.operation === "xrefs"
? { unknowns: [...common.unknowns, XREFS_ADDRESS_PAGE_LIMITATION] }
: {}),
...payload,
summary: { observations: total },
coverage: pageViewCoverage(selection.offset, page.items.rows.length, total),
@@ -0,0 +1,123 @@
import { expect, it } from "vitest";
import { createEvidence } from "../evidence.js";
import { MAX_JSON_DEPTH, type JsonValue } from "../jsonValue.js";
import { projectAnalysisError } from "../analysisErrorProjection.js";
import type { AnalysisViewParent } from "./analysisViewParent.js";
import {
analysisViewRequestSchema,
projectAnalysisView,
} from "./analysisView.js";
const nested = (levels: number): JsonValue => {
let value: JsonValue = "leaf";
for (let index = 0; index < levels; index += 1) value = { child: value };
return value;
};
const parent = (
parameters: JsonValue,
row: JsonValue = "row",
): AnalysisViewParent => ({
predicateType: "rea.analysis",
evidenceId: `ev_${"b".repeat(64)}`,
operation: "xrefs",
normalizedResult: ["0x10"],
rawResult: { values: [row] },
sealedResult: true,
provider: { id: "fixture", name: "Fixture", version: "1" },
artifact: null,
subject: null,
parameters,
limitations: [],
});
const project = (source: AnalysisViewParent) =>
projectAnalysisView(
source,
analysisViewRequestSchema.parse({
kind: "recorded-array",
path: ["values"],
offset: 0,
limit: 1,
}),
);
it("omits parent parameters that would exceed Evidence depth by one level", () => {
// Parameters are one field below the Evidence root. levels === 999 puts the
// leaf at depth 1000 there, and one level deeper under normalized_result.
const parameters = nested(MAX_JSON_DEPTH - 1);
const admitted = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "xrefs",
parameters,
result: ["0x10"],
rawResult: { values: ["row"] },
},
);
expect(admitted.parameters).toStrictEqual(parameters);
const projected = project(parent(parameters));
if (!projected.ok) throw projected.error;
expect(projected.value).toMatchObject({
kind: "recorded-array",
source_parameters: null,
rows: ["row"],
});
expect(projected.value.limitations.join("\n")).toContain("JSON depth");
expect(() =>
createEvidence(
undefined,
{ id: "view", name: "View", version: "1" },
{
operation: "inspect_analysis_view",
parameters: { parent_evidence_id: admitted.evidence_id },
result: projected.value,
rawResult: null,
},
),
).not.toThrow();
});
it("keeps parameters that still fit one level below the Evidence root", () => {
const parameters = nested(MAX_JSON_DEPTH - 2);
const projected = project(parent(parameters));
if (!projected.ok) throw projected.error;
if (projected.value.kind !== "recorded-array")
throw new Error("expected a recorded-array page");
expect(projected.value.source_parameters).toEqual(parameters);
});
it("returns a typed depth refusal instead of throwing when a row cannot be nested", () => {
const projected = project(parent({}, nested(MAX_JSON_DEPTH - 1)));
expect(projected.ok).toBe(false);
if (projected.ok) return;
expect(projectAnalysisError(projected.error)).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { maximum_json_depth: MAX_JSON_DEPTH } },
});
expect(projectAnalysisError(projected.error).remediation.action).toContain(
"JSON depth",
);
});
it("stops before a later row exceeds the output Evidence depth", () => {
const source = parent({});
const first = nested(MAX_JSON_DEPTH - 3);
const tooDeep = nested(MAX_JSON_DEPTH - 2);
const projected = projectAnalysisView(
{ ...source, rawResult: { values: [first, tooDeep, "later"] } },
analysisViewRequestSchema.parse({
kind: "recorded-array",
path: ["values"],
offset: 0,
limit: 3,
}),
);
if (!projected.ok) throw projected.error;
expect(projected.value).toMatchObject({
kind: "recorded-array",
rows: [first],
coverage: { examined: 1, total: 3, next_offset: 1 },
});
});
@@ -0,0 +1,187 @@
import type { z } from "zod";
import {
jsonValueValidationIssue,
MAX_JSON_DEPTH,
type JsonValue,
} from "../jsonValue.js";
import { jsonValueByteLength } from "../jsonValueByteLength.js";
import { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
import { err, ok } from "../result.js";
import type { AnalysisViewParent } from "./analysisViewParent.js";
import { pageViewCoverage } from "./analysisViewCoverage.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import type { recordedArrayViewSchema } from "./analysisViewRequest.js";
const PAGE_BYTES = 1024 * 1024;
/**
* Parent parameters sit on the Evidence root. This page stores them under
* `normalized_result.source_parameters`, one level deeper. A parent that used
* the whole depth budget is valid and still cannot be copied unchanged.
*/
const exceedsEvidenceResultDepth = (result: JsonValue): boolean =>
jsonValueValidationIssue({ normalized_result: result }) !== undefined;
/** A row is nested under normalized_result.rows in the output Evidence. */
const exceedsRecordedRowDepth = (row: JsonValue): boolean =>
jsonValueValidationIssue({ normalized_result: { rows: [row] } }) !==
undefined;
const recordedArrayDepthRefusal = () =>
err(
new AnalysisResourceConstraintError(
"inspect_analysis_view",
"memory",
`Recorded array view exceeds the maximum JSON nesting depth of ${MAX_JSON_DEPTH}.`,
{ maximum_json_depth: MAX_JSON_DEPTH },
{
retryable: false,
remediationAction:
"A recorded value cannot be nested inside an Evidence page without exceeding the JSON depth limit. Retrieve or export the complete parent Evidence, or read it from the caller-supplied Evidence file or bundle.",
},
),
);
/** Page an exact array within authenticated provider-recorded raw JSON. */
export const projectRecordedArrayView = (
parent: AnalysisViewParent,
view: z.output<typeof recordedArrayViewSchema>,
) => {
let value: unknown = parent.rawResult;
for (const key of view.path) {
if (
typeof value !== "object" ||
value === null ||
Array.isArray(value) ||
!Object.hasOwn(value, key)
)
return err(
analysisViewInputError([
{
path: ["view", "path"],
reason: "invalid_value",
message:
"The selected path does not identify an object member in recorded raw data.",
},
]),
);
value = (value as Record<string, unknown>)[key];
}
if (!Array.isArray(value))
return err(
analysisViewInputError([
{
path: ["view", "path"],
reason: "invalid_value",
message: "The selected recorded raw value is not an array.",
},
]),
);
const rows: JsonValue[] = [];
let result = {
parent_evidence_id: parent.evidenceId,
parent_operation: parent.operation,
parent_digest: parent.evidenceId.slice(3),
source_provider: parent.provider ?? null,
source_analysis_profile_digest: parent.analysisProfile?.digest ?? null,
source_parameters: (parent.parameters ?? {}) as Record<
string,
JsonValue
> | null,
artifact: parent.artifact ?? null,
kind: "recorded-array" as const,
view,
rows,
coverage: pageViewCoverage(view.offset, 0, value.length),
limitations: [
"These are raw provider-recorded values; interpretation and provider coverage require the parent Evidence.",
"Raw rows may include values excluded from the parent operation's normalized result; use source parameters and parent caveats to interpret their scope.",
],
unknowns: [],
};
// Include the sealed digest and reserve the longest numeric coverage fields
// before adding rows. Output transport may impose a smaller separate limit.
const estimateBase = () =>
jsonValueByteLength(
{
...result,
view_digest: "0".repeat(64),
coverage: {
status: "complete-within-view",
examined: Number.MAX_SAFE_INTEGER,
total: Number.MAX_SAFE_INTEGER,
next_offset: Number.MAX_SAFE_INTEGER,
exhausted: false,
},
},
PAGE_BYTES,
);
const omitParameters = (reason: "bytes" | "depth") => {
result = {
...result,
source_parameters: null,
limitations: [
...result.limitations,
reason === "depth"
? "Source parameters were omitted because nesting them under this Evidence page exceeds the maximum JSON depth; retrieve the parent Evidence for their complete values."
: "Source parameters were omitted to fit the page allowance; retrieve the parent Evidence for their complete values.",
],
};
};
if (result.source_parameters !== null && exceedsEvidenceResultDepth(result))
omitParameters("depth");
let bytes = estimateBase();
if (bytes > PAGE_BYTES) {
omitParameters("bytes");
bytes = estimateBase();
}
const tooLarge = () =>
err(
new AnalysisResourceConstraintError(
"inspect_analysis_view",
"memory",
"Recorded array view exceeds the 1 MiB page allowance.",
{ maximum_page_bytes: PAGE_BYTES },
{
retryable: false,
remediationAction:
"The selected path or one recorded value exceeds the 1,048,576-byte analysis-view page allowance. Retrieve or export the complete parent Evidence, or read it from the caller-supplied Evidence file or bundle.",
},
),
);
if (bytes > PAGE_BYTES) return tooLarge();
const end = Math.min(value.length, view.offset + view.limit);
for (let i = view.offset; i < end; i++) {
const row = value[i] as JsonValue;
const comma = rows.length === 0 ? 0 : 1;
let size = jsonValueByteLength(row, PAGE_BYTES - bytes - comma);
if (
bytes + comma + size > PAGE_BYTES &&
rows.length === 0 &&
result.source_parameters !== null
) {
omitParameters("bytes");
bytes = estimateBase();
if (bytes > PAGE_BYTES) return tooLarge();
size = jsonValueByteLength(row, PAGE_BYTES - bytes);
}
if (bytes + comma + size > PAGE_BYTES) {
if (rows.length === 0) return tooLarge();
break;
}
// The fixed fields are checked separately. Validate only this row at its
// final nesting depth; previously accepted rows cannot change, so walking
// the growing page for every row repeats the same work quadratically.
if (exceedsRecordedRowDepth(row)) {
if (rows.length === 0) return recordedArrayDepthRefusal();
break;
}
bytes += comma + size;
rows.push(row);
}
if (exceedsEvidenceResultDepth(result)) return recordedArrayDepthRefusal();
return ok({
...result,
coverage: pageViewCoverage(view.offset, rows.length, value.length),
});
};
+12
View File
@@ -7,6 +7,18 @@ export const literalFeatureReferenceSchema = z.object({
target_address: z.string(),
source_address: z.string(),
containing_procedure: traceContainingProcedureSchema,
source_classifications: z
.array(
z.strictObject({
kind: z.literal("stored_pointer"),
provenance: z.string(),
width: z.int().positive(),
}),
)
.exactOptional()
.describe(
"Provider-reported source classifications; omission means none were supplied. The list need not exhaust all kinds.",
),
});
/** Complete result of a native literal feature trace. */
+6 -1
View File
@@ -273,7 +273,12 @@ export class KunaProvider implements AnalysisProviderCandidate {
bodies: false,
edge_kind: "call",
},
xrefs: { command: "xrefs", direction: "to", exact_address: true },
xrefs: {
command: "xrefs",
direction: "to",
exact_address: true,
stored_pointers: true,
},
strings: {
command: "strings",
encoding: "all",
+77 -1
View File
@@ -224,6 +224,32 @@ const xrefRow = z.object({
to_function: xrefFunction.nullable(),
instruction: z.string(),
});
const xrefPointer = z.object({
kind: z.literal("pointer"),
from_address: address,
from_address_hex: address,
from_name: z.string().nullable(),
to_address: address,
to_address_hex: address,
to_name: z.string().nullable(),
width: z.int().positive(),
source: z.string().min(1),
});
const xrefWalkCoverage = z.object({
decoded: z.boolean(),
section: z.string().nullable(),
code_section: z.boolean().nullable(),
executable: z.boolean().nullable(),
unwalked: z.array(
z.object({
name: z.string().nullable(),
address,
address_hex: address,
section: z.string(),
}),
),
notes: z.array(z.string()),
});
const xrefsDocument = z.object({
binary: z.string(),
direction: z.literal("to"),
@@ -235,6 +261,10 @@ const xrefsDocument = z.object({
aliases: z.array(xrefFunction),
}),
xrefs: z.array(z.unknown()),
coverage: xrefWalkCoverage.optional(),
data_refs_coverage: z.string().optional(),
data_refs_count: count.optional(),
data_refs: z.array(xrefPointer).optional(),
});
export interface KunaIncomingReferences {
@@ -243,6 +273,15 @@ export interface KunaIncomingReferences {
readonly bytes: number;
readonly rows: number;
readonly excludedAliasRows: number;
readonly targetAliases: readonly {
readonly name: string;
readonly address: string;
}[];
readonly walkCoverage: z.infer<typeof xrefWalkCoverage> | null;
readonly pointerCoverage: string | null;
readonly pointers: Array<
Omit<z.infer<typeof xrefPointer>, "from_address_hex" | "to_address_hex">
> | null;
}
/** Code from incoming-reference admission, plus the address field when one failed. */
@@ -275,8 +314,34 @@ export const parseKunaIncomingReferences = (
throw new KunaDocumentError("xref_alias_hex");
aliases.add(alias.address);
}
if (
(document.data_refs === undefined) !==
(document.data_refs_count === undefined)
)
throw new KunaDocumentError("xref_count");
if (
document.data_refs !== undefined &&
document.data_refs.length !== document.data_refs_count
)
throw new KunaDocumentError("xref_count");
for (const item of document.coverage?.unwalked ?? [])
if (item.address !== item.address_hex)
throw new KunaDocumentError("xref_endpoints");
for (const pointer of document.data_refs ?? []) {
if (
pointer.from_address !== pointer.from_address_hex ||
pointer.to_address !== pointer.to_address_hex
)
throw new KunaDocumentError("xref_endpoints");
if (!aliases.has(pointer.to_address))
throw new KunaDocumentError("xref_target");
}
const addresses = new Set<string>();
let excludedAliasRows = 0;
for (const pointer of document.data_refs ?? []) {
if (pointer.to_address === target) addresses.add(pointer.from_address);
else excludedAliasRows += 1;
}
for (const raw of document.xrefs) {
const row = xrefRow.parse(raw);
if (
@@ -296,8 +361,19 @@ export const parseKunaIncomingReferences = (
}
return ok({
addresses: [...addresses].sort(compareKunaAddresses),
rows: document.xrefs.length,
rows: document.xrefs.length + (document.data_refs?.length ?? 0),
excludedAliasRows,
targetAliases: document.target.aliases.map(({ name, address }) => ({
name,
address,
})),
walkCoverage: document.coverage ?? null,
pointerCoverage: document.data_refs_coverage ?? null,
pointers:
document.data_refs?.map(
({ from_address_hex: _from, to_address_hex: _to, ...pointer }) =>
pointer,
) ?? null,
bytes: Buffer.byteLength(text),
sha256: createHash("sha256").update(text).digest("hex"),
});
+73 -1
View File
@@ -178,8 +178,11 @@ interface Answer {
readonly locations: readonly EvidenceLocation[];
readonly limitations: readonly string[];
readonly referenceSource?: JsonValue;
readonly requiresSourceEvidence?: true;
readonly stringSource?: JsonValue;
readonly relationshipCoverage?: RelationshipCoverage;
readonly referenceClassifications?: AnalysisExecution["referenceClassifications"];
readonly referenceCoverageGaps?: AnalysisExecution["referenceCoverageGaps"];
readonly siblingUnavailability?: AnalysisCapabilityUnavailableError;
}
@@ -725,6 +728,15 @@ export class KunaSessionClient implements AnalysisClient {
...(answer.relationshipCoverage === undefined
? {}
: { relationshipCoverage: answer.relationshipCoverage }),
...(answer.requiresSourceEvidence === undefined
? {}
: { requiresSourceEvidence: answer.requiresSourceEvidence }),
...(answer.referenceCoverageGaps === undefined
? {}
: { referenceCoverageGaps: answer.referenceCoverageGaps }),
...(answer.referenceClassifications === undefined
? {}
: { referenceClassifications: answer.referenceClassifications }),
...(answer.siblingUnavailability === undefined
? {}
: { siblingUnavailability: answer.siblingUnavailability }),
@@ -1005,11 +1017,65 @@ export class KunaSessionClient implements AnalysisClient {
options.signal,
);
if (!references.ok) return references;
const coverageGaps = [
...(references.value.walkCoverage !== null &&
references.value.walkCoverage.unwalked.length > 0
? [
{
code: "unwalked-code",
scope: "observation" as const,
description:
"Kuna reports unwalked code candidates whose outgoing references are not included. Their presence does not establish a reference to the selected target; the source observation identifies them.",
},
]
: []),
...(references.value.pointerCoverage === "none"
? [
{
code: "pointer-relocations-unavailable",
scope: "image" as const,
description:
"Kuna reports no pointer-relocation coverage for this image; stored pointers may still exist.",
},
]
: []),
];
return ok({
result: [...references.value.addresses],
source: null,
locations,
limitations,
limitations: [
...limitations,
...(references.value.pointers !== null &&
references.value.pointers.length > 0
? [
'The recorded pointer rows can be paged from this Evidence with inspect_analysis_view: view {"kind":"recorded-array","path":["references","pointers"]}. These raw Kuna rows may include alias targets; compare each row\'s to_address with the selected address before treating it as an exact-target reference.',
]
: []),
...(references.value.pointerCoverage === null
? []
: [
"Kuna pointer references come from relocation metadata. The reported metadata source does not establish successful or complete enumeration.",
]),
...coverageGaps.map(({ description }) => description),
],
...(coverageGaps.length === 0
? {}
: { referenceCoverageGaps: coverageGaps }),
...(references.value.pointers === null
? {}
: {
referenceClassifications: references.value.pointers
.filter((pointer) => pointer.to_address === address)
.map((pointer) => ({
sourceAddress: pointer.from_address,
targetAddress: pointer.to_address,
kind: "stored_pointer" as const,
provenance: pointer.source,
width: pointer.width,
})),
}),
requiresSourceEvidence: true,
referenceSource: {
kind: "kuna-xrefs",
direction: "to",
@@ -1018,6 +1084,12 @@ export class KunaSessionClient implements AnalysisClient {
bytes: references.value.bytes,
examined_rows: references.value.rows,
excluded_alias_rows: references.value.excludedAliasRows,
target_aliases: references.value.targetAliases.map(
({ name, address }) => ({ name, address }),
),
walk_coverage: references.value.walkCoverage,
pointer_coverage: references.value.pointerCoverage,
pointers: references.value.pointers,
},
});
}
+6 -3
View File
@@ -1032,9 +1032,12 @@ class ErrorEvidenceTransport implements Transport {
"The MCP tool arguments exceed the configured input element allowance; schema validation and tool execution did not start.",
{ max_input_elements: this.maximumInputElements },
{
remediationAction: this.evidenceReferenceAlternatives.has(tool)
? 'If the source Evidence is retained, use {"kind":"retained-evidence","evidence_id":"ev_..."} for declared Evidence fields; use a summary\'s parent_evidence_id for its complete source. Otherwise import the originating Evidence bundle when available. Reduce the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance.'
: "Reduce the combined array elements and object members in the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance for this workload.",
remediationAction:
tool === "inspect_analysis_view"
? 'If using inline Evidence, save the Evidence record as a local JSON file and use source {"kind":"evidence-file","path":"..."}; if it is retained, use its retained-evidence ID. Otherwise reduce the combined array elements and object members, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance.'
: this.evidenceReferenceAlternatives.has(tool)
? 'If the source Evidence is retained, use {"kind":"retained-evidence","evidence_id":"ev_..."} for declared Evidence fields; use a summary\'s parent_evidence_id for its complete source. Otherwise import the originating Evidence bundle when available. Reduce the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance.'
: "Reduce the combined array elements and object members in the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance for this workload.",
},
);
const result = this.delivery.toErrorToolResult(error, targetContent);
+142
View File
@@ -2058,3 +2058,145 @@ it("projects an environment manifest identically from retained, portable and fil
expect(JSON.stringify(refused)).not.toContain("/api/folders");
await client.ping();
});
it("pages retained raw array rows without losing the parent and refuses an oversized row", async () => {
const { client, session } = await connect();
const rows = [
{ from_address: "0x10", to_address: "0x20", source: "fixups" },
{ from_address: "0x18", to_address: "0x20", source: "fixups" },
{ from_address: "0x20", name: "x".repeat(1024 * 1024) },
];
const parent = createEvidence(
{ path: "/fixtures/raw-native", format: "mach-o", sha256: "e".repeat(64) },
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "xrefs",
parameters: { address: "0x20" },
result: ["0x10", "0x18", "0x20"],
rawResult: { references: { pointers: rows } },
},
);
expect(session.recordEvidence(parent).ok).toBe(true);
const call = (offset: number, limit: number) =>
client.callTool({
name: "inspect_analysis_view",
arguments: {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: {
kind: "recorded-array",
path: ["references", "pointers"],
offset,
limit,
},
},
});
const first = await call(0, 10);
expect(first.isError).not.toBe(true);
const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
first.structuredContent,
);
expect(parsed.normalized_result).toMatchObject({
kind: "recorded-array",
parent_evidence_id: parent.evidence_id,
source_provider: { id: "fixture", name: "Fixture", version: "1" },
source_analysis_profile_digest: null,
source_parameters: { address: "0x20" },
limitations: expect.arrayContaining([
expect.stringContaining(
"excluded from the parent operation's normalized result",
),
]),
rows: rows.slice(0, 2),
coverage: { examined: 2, total: 3, next_offset: 2 },
});
expect(parsed.evidence_links).toEqual([parent.evidence_id]);
const oversized = parseMcpToolError(await call(2, 1)).error;
expect(oversized.code).toBe("resource_constraint");
expect(oversized.remediation.action).toContain("1,048,576-byte");
expect((await call(0, 1)).isError).not.toBe(true);
});
it("directs oversized inline Evidence to a usable file source", async () => {
const { client } = await connect(undefined, undefined, 32);
const parent = analysisViewNativeEvidence();
const view = { kind: "native", facet: "procedure", offset: 0, limit: 1 };
const inline = await client.callTool({
name: "inspect_analysis_view",
arguments: { source: { kind: "inline", evidence: parent }, view },
});
const error = parseMcpToolError(inline).error;
expect(error.code).toBe("resource_constraint");
expect(error.remediation.action).toContain("evidence-file");
const root = await createTestTempDirectory("rea-view-input-capacity-");
const path = join(root, "evidence.json");
await writeFile(path, JSON.stringify(parent));
const fromFile = await client.callTool({
name: "inspect_analysis_view",
arguments: { source: { kind: "evidence-file", path }, view },
});
expect(fromFile.isError).not.toBe(true);
const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
fromFile.structuredContent,
);
expect(parsed.normalized_result).toMatchObject({
parent_evidence_id: parent.evidence_id,
});
});
it.each([
{ queryBytes: 1024 * 1024, rowBytes: 0 },
{ queryBytes: 600_000, rowBytes: 600_000 },
])(
"pages a recorded row when its parent parameters do not fit",
async ({ queryBytes, rowBytes }) => {
const { client, session } = await connect();
const { ajv, outputSchema } = await advertisedMcpToolSchemas(
client,
"inspect_analysis_view",
schemaValidation.assertions,
);
const parent = createEvidence(
{
path: "/fixtures/large-context",
format: "elf",
sha256: "f".repeat(64),
},
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "xrefs",
parameters: { query: "x".repeat(queryBytes) },
result: ["0x10"],
rawResult: {
values: [{ address: "0x10", data: "y".repeat(rowBytes) }],
},
},
);
expect(session.recordEvidence(parent).ok).toBe(true);
const response = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: { kind: "recorded-array", path: ["values"], offset: 0, limit: 1 },
},
});
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: "recorded-array",
parent_evidence_id: parent.evidence_id,
source_provider: { id: "fixture", name: "Fixture", version: "1" },
source_parameters: null,
limitations: expect.arrayContaining([
expect.stringContaining("omitted to fit the page allowance"),
]),
rows: [{ address: "0x10", data: "y".repeat(rowBytes) }],
});
},
);
@@ -140,6 +140,106 @@ const trace = async (analysis: AnalysisOperationPort) => {
return result.structuredContent as { normalized_result: unknown };
};
it("reports known reference-discovery gaps without claiming truncation or a missing edge", async () => {
const baseline = provider();
const gap =
"An unwalked candidate may have outgoing references; its presence does not establish a reference to the selected target.";
const output = await trace({
execute: (operation, input, options) =>
operation === "xrefs" && input.address === "0x10"
? Promise.resolve(
ok(
createAnalysisExecution([], PROVIDER, {
referenceCoverageGaps: [
{ code: "unwalked", scope: "observation", description: gap },
],
}),
),
)
: baseline.execute(operation, input, options),
});
expect(output.normalized_result).toMatchObject({
truncated: false,
residual_unknowns: [`References to 0x10: ${gap}`],
references: [{ target_address: "0x11", source_address: "0x21" }],
});
});
it("deduplicates an image-wide gap without merging target-specific gaps", async () => {
const baseline = provider();
const output = await trace({
execute: async (operation, input, options) => {
const result = await baseline.execute(operation, input, options);
if (operation !== "xrefs" || !result.ok) return result;
return ok({
...result.value,
referenceCoverageGaps: [
{
code: "missing-relocations",
scope: "image",
description: "Image relocation coverage unavailable.",
},
{
code: "unwalked",
scope: "observation",
description: "This observation has unwalked sources.",
},
],
});
},
});
expect(output.normalized_result).toMatchObject({
truncated: false,
residual_unknowns: [
"Image relocation coverage unavailable.",
"References to 0x10: This observation has unwalked sources.",
"References to 0x11: This observation has unwalked sources.",
],
});
});
it("carries reported pointer classifications without inferring other reference kinds", async () => {
const baseline = provider();
const output = await trace({
execute: (operation, input, options) =>
operation === "xrefs" && input.address === "0x10"
? Promise.resolve(
ok(
createAnalysisExecution(["0x20"], PROVIDER, {
referenceClassifications: [
{
sourceAddress: "0x20",
targetAddress: "0x10",
kind: "stored_pointer",
provenance: "chained-fixups",
width: 8,
},
{
sourceAddress: "0x20",
targetAddress: "0x99",
kind: "stored_pointer",
provenance: "other",
width: 4,
},
],
}),
),
)
: baseline.execute(operation, input, options),
});
expect(output.normalized_result).toMatchObject({
references: [
{
source_address: "0x20",
source_classifications: [
{ kind: "stored_pointer", provenance: "chained-fixups", width: 8 },
],
},
{ source_address: "0x21" },
],
});
});
it("preserves references and explicitly unknown owners without claiming truncation", async () => {
const baseline = provider();
const output = await trace({
@@ -12,6 +12,7 @@ import {
analysisViewResultSchema,
inspectAnalysisViewInputSchema,
} from "../../../src/domain/analysisView/analysisView.js";
import { XREFS_ADDRESS_PAGE_LIMITATION } from "../../../src/domain/analysisView/nativeObservationView.js";
import { NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS } from "../../../src/domain/native/nativeObservation.js";
import { connectGhidraMcp } from "./ghidraMcpHarness.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
@@ -138,6 +139,9 @@ it("delivers native primitive and graph summaries with exact retained parents an
},
coverage: { total: 1, exhausted: true },
});
expect(
analysisViewResultSchema.parse(page.normalized_result).unknowns,
).toContain(XREFS_ADDRESS_PAGE_LIMITATION);
const caveats = await call("inspect_analysis_view", {
source,
view: { kind: "native-context", facet: "limitations" },
+5 -1
View File
@@ -74,6 +74,8 @@ export interface FakeKunaBehavior {
/** Bytes of an extra root member in the graph JSON. */
readonly graphPadding?: number;
readonly xrefs?: "ok" | "hang" | "not-json";
/** Include stored pointers to the selected target and its forwarding alias. */
readonly pointerAliasReferences?: boolean;
/** Additional exact-target incoming call-site addresses. */
readonly extraXrefSources?: readonly number[];
readonly referenceDelayMs?: number;
@@ -162,7 +164,9 @@ if (args[0] === "--version") {
const row = (from, to, kind) => ({ address: from, address_hex: "0x" + from.toString(16), from_address: from, from_address_hex: "0x" + from.toString(16), to_address: to, to_address_hex: "0x" + to.toString(16), kind, from_function: identity("main", 4096), to_function: null, instruction: "fixture reference" + "x".repeat(behavior.referencePadding ?? 0) });
const rows = [4100, ...(behavior.extraXrefSources ?? [])].map((source) => row(source, parseInt(target, 16), "call"));
if (aliases.length) rows.push(row(4164, 12288, "call"));
setTimeout(() => process.stdout.write(JSON.stringify({ binary: args[1], direction: "to", count: rows.length, target: { ...identity(null, parseInt(target, 16)), aliases }, xrefs: rows })), behavior.referenceDelayMs ?? 0);
const pointer = (from, to) => ({ kind: "pointer", from_address: from, from_address_hex: "0x" + from.toString(16), from_name: null, to_address: to, to_address_hex: "0x" + to.toString(16), to_name: null, width: 8, source: "dynamic-relocations" });
const pointers = behavior.pointerAliasReferences ? [pointer(4352, parseInt(target, 16)), pointer(4368, 12288)] : undefined;
setTimeout(() => process.stdout.write(JSON.stringify({ binary: args[1], direction: "to", count: rows.length, target: { ...identity(null, parseInt(target, 16)), aliases }, xrefs: rows, ...(pointers ? { data_refs: pointers, data_refs_count: pointers.length, data_refs_coverage: "dynamic-relocations" } : {}) })), behavior.referenceDelayMs ?? 0);
}
} else if (args[0] === "decompile-project") {
const input = args[1];
@@ -286,6 +286,226 @@ it("keeps exact endpoints, deduplicates sites and counts excluded forwarding ali
});
});
it("preserves pointer and walk coverage separately and rejects inconsistent pointer identity", () => {
const pointer = {
kind: "pointer",
from_address: "@16384",
from_address_hex: "0x4000",
from_name: "slot",
to_address: "@8192",
to_address_hex: "0x2000",
to_name: "callee",
width: 8,
source: "chained-fixups",
};
const source = {
...xrefs(),
coverage: {
decoded: true,
section: "__TEXT,__text",
code_section: true,
executable: true,
unwalked: [
{
name: "middle",
address: "@20480",
address_hex: "0x5000",
section: "__TEXT,__const",
},
],
notes: ["middle was not walked"],
},
data_refs_coverage: "chained-fixups",
data_refs_count: 1,
data_refs: [pointer],
};
expect(
parseKunaIncomingReferences(wire(source), input, "0x2000"),
).toMatchObject({
ok: true,
value: {
addresses: ["0x1004", "0x4000"],
pointerCoverage: "chained-fixups",
pointers: [
{
from_address: "0x4000",
to_address: "0x2000",
source: "chained-fixups",
},
],
walkCoverage: { unwalked: [{ address: "0x5000" }] },
},
});
pointer.to_address_hex = "0x2001";
expect(
parseKunaIncomingReferences(wire(source), input, "0x2000"),
).toMatchObject({
ok: false,
error: { code: "xref_endpoints" },
});
});
it("counts pointer rows excluded because they target a forwarding alias", () => {
const pointer = (from: number, to: number) => ({
kind: "pointer",
from_address: `@${from}`,
from_address_hex: `0x${from.toString(16)}`,
from_name: null,
to_address: `@${to}`,
to_address_hex: `0x${to.toString(16)}`,
to_name: null,
width: 8,
source: "dynamic-relocations",
});
const source = {
...xrefs(),
data_refs_coverage: "dynamic-relocations",
data_refs_count: 2,
data_refs: [pointer(16384, 8192), pointer(16392, 12288)],
};
expect(
parseKunaIncomingReferences(wire(source), input, "0x2000"),
).toMatchObject({
ok: true,
value: {
addresses: ["0x1004", "0x4000"],
rows: 5,
excludedAliasRows: 2,
targetAliases: [{ name: "slot", address: "0x3000" }],
pointers: [
{ from_address: "0x4000", to_address: "0x2000" },
{ from_address: "0x4008", to_address: "0x3000" },
],
},
});
});
it("preserves full-width pointer and unwalked addresses without numeric rounding", () => {
const source = {
binary: input,
direction: "to",
count: 0,
target: {
name: null,
address: "@18446744073709551615",
address_hex: "0xffffffffffffffff",
aliases: [],
},
xrefs: [],
coverage: {
decoded: false,
section: null,
code_section: null,
executable: null,
unwalked: [
{
name: "candidate",
address: "@9007199254740993",
address_hex: "0x20000000000001",
section: ".text",
},
],
notes: [],
},
data_refs_coverage: "dynamic-relocations",
data_refs_count: 1,
data_refs: [
{
kind: "pointer",
from_address: "@9007199254740995",
from_address_hex: "0x20000000000003",
from_name: null,
to_address: "@18446744073709551615",
to_address_hex: "0xffffffffffffffff",
to_name: null,
width: 8,
source: "dynamic-relocations",
},
],
};
expect(
parseKunaIncomingReferences(wire(source), input, "0xffffffffffffffff"),
).toMatchObject({
ok: true,
value: {
addresses: ["0x20000000000003"],
rows: 1,
excludedAliasRows: 0,
pointers: [
{ from_address: "0x20000000000003", to_address: "0xffffffffffffffff" },
],
walkCoverage: { unwalked: [{ address: "0x20000000000001" }] },
},
});
});
it("distinguishes unreported pointer coverage from an explicitly empty pointer list", () => {
const source = xrefs();
expect(
parseKunaIncomingReferences(wire(source), input, "0x2000"),
).toMatchObject({
ok: true,
value: { pointerCoverage: null, pointers: null, walkCoverage: null },
});
expect(
parseKunaIncomingReferences(
wire({
...source,
data_refs_coverage: "none",
data_refs_count: 0,
data_refs: [],
}),
input,
"0x2000",
),
).toMatchObject({
ok: true,
value: { pointerCoverage: "none", pointers: [], walkCoverage: null },
});
});
it("rejects partial or inconsistent pointer inventories rather than reporting them empty", () => {
const source = xrefs();
for (const fields of [
{ data_refs: [] },
{ data_refs_count: 0 },
{ data_refs_count: 1, data_refs: [] },
]) {
expect(
parseKunaIncomingReferences(
wire({ ...source, ...fields }),
input,
"0x2000",
),
).toEqual({
ok: false,
error: { code: "xref_count" },
});
}
});
it("preserves unknown coverage for an address outside known sections", () => {
const source = {
...xrefs(),
coverage: {
decoded: false,
section: null,
code_section: null,
executable: null,
unwalked: [],
notes: [],
},
};
expect(
parseKunaIncomingReferences(wire(source), input, "0x2000"),
).toMatchObject({
ok: true,
value: {
walkCoverage: { section: null, code_section: null, executable: null },
},
});
});
it.each([
[
"count",
@@ -178,6 +178,30 @@ it("filters forwarding aliases and reuses exact-address xrefs without inventory
}
});
it("distinguishes exact xrefs from recorded pointer rows to forwarding aliases", async () => {
const { open } = await session("rea-kuna-pointer-alias-", {
version: { stdout: "kuna 1.990\n" },
pointerAliasReferences: true,
});
const client = open();
try {
const answer = await run(client, "xrefs", { address: "0x2000" });
expect(answer.result).toEqual(["0x1004", "0x1100"]);
expect(answer.rawResult).toMatchObject({
references: {
pointers: [{ to_address: "0x2000" }, { to_address: "0x3000" }],
},
});
expect(answer.limitations).toEqual(
expect.arrayContaining([
expect.stringContaining("compare each row's to_address"),
]),
);
} finally {
await client.close();
}
});
it("resolves an xref procedure name without starting the project export", async () => {
const { open, kuna } = await session("rea-kuna-named-xrefs-");
const client = open();