Files
rea/src/application/EnhancedLiteralTracing.ts
T
Mykola Dementiev 5f8af4ee5f feat(ida): resolve containing procedures and report the admitted program as the document (#2374)
* test(mcp): expect trace_feature to report references the provider truncated

A provider that reports a capped xrefs read makes the trace truncated with a residual unknown for that target. An explicit complete report and a provider that reports nothing leave the trace as before.

* fix(mcp): report references a provider truncated in trace_feature

Executions gain an optional flag for a capped xrefs read, set by the IDA adapter from the upstream cap. trace_feature marks the trace truncated with a residual unknown for that target only when the provider reports a truncation; a provider that reports nothing keeps its previous result. The trace_feature workflow profile is revised so earlier results are not replayed.

* refactor(ida): report reference truncation whenever the runner observed an xrefs cap

* test(mcp): cover session forwarding, per-target state, native reports and failures of the trace coverage

* test(mcp): format the session forwarding test

* test(ida): expect headless IDA to resolve containing procedures from lookups and segment facts

Cover positive and negative containment, segment boundaries including addresses above 2^53, failures of the lookup and of the segment survey, cancellation, input validation and the admitted program as the only document.

* test(ida): expect feature tracing and address context to work on headless IDA

Run trace_feature, including a capped reference read, and inspect_address_context with and without an explicit document through the IDA session client.

* test(mcp): expect trace_feature availability to follow the search operations it calls

* fix(mcp): derive trace_feature availability from the search operations it calls

* feat(ida): resolve containing procedures and report the admitted program as the document

The modern headless profile resolves an address through the function lookup and, when no function contains it, tells a mapped address from an unmapped one using the database-scoped minimal survey. A survey that is missing, truncated or inconsistent fails the read. list_documents reports the single admitted program. The attached profiles do not declare either operation.

* docs(ida): describe containing procedures, the admitted document and truncated feature traces

* test(ida): verify containing procedures, the admitted document and feature tracing on real IDA

* docs(testing): list the headless IDA workflows the real lane verifies

* test(ida): make each survey failure case isolate a single defect

* test(ida): pin large addresses, segment order, document identity, attached refusals and each trace_feature prerequisite

* test(mcp): expect a repeated reference source to be resolved once in trace_feature

* fix(mcp): resolve the containing procedure of a repeated reference source once

* test(ida): trace the fixture procedure and derive probe addresses from its facts in the real lane

* test(process): format processCaptureLifecycle.test.ts after the merge

* test(ida): declare the alignment each synthetic universal slice offset satisfies

* refactor(ida): move the real lane's feature trace assertions into a helper

* test(ida): expect the real lane's feature trace check to accept references to every matched address

* fix(ida): check each trace reference against its own matched address in the real lane

* test(ida): pin that the new operations reject a document other than the admitted program

* refactor(ida): move the real lane's mapped non-function probe into a helper

* test(ida): expect the real lane's mapped data probe to skip strings inside functions

* fix(ida): try the target's strings in order for the mapped non-function probe

* test(ida): declare the real lane helper types for the verifier tests

* test(ida): expect the real lane to reject a listed string that resolves as unmapped

* fix(ida): fail the real lane when a listed string resolves as unmapped
2026-10-13 20:56:20 +08:00

199 lines
5.6 KiB
TypeScript

import { z } from "zod";
import type {
AnalysisExecution,
AnalysisOperation,
} from "./AnalysisProvider.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import type {
EnhancedResult,
TraceMatch,
TraceReference,
} from "./EnhancedToolTypes.js";
type AnalysisCall = (
name: AnalysisOperation,
arguments_: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal,
) => Promise<Result<JsonValue, AnalysisError>>;
type ObserveCall = (
name: AnalysisOperation,
arguments_: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal,
) => Promise<Result<AnalysisExecution, AnalysisError>>;
type LiteralSearchSource = readonly [
"search_strings" | "search_procedures",
"string" | "procedure",
];
interface ReferenceTraceRequest {
readonly matches: readonly TraceMatch[];
readonly signal?: AbortSignal;
}
const LITERAL_SEARCH_SOURCES: readonly LiteralSearchSource[] = [
["search_strings", "string"],
["search_procedures", "procedure"],
];
export interface LiteralTraceInput {
readonly query: string;
readonly case_sensitive: boolean;
}
/** Trace matching strings and procedures through provider xrefs. */
export const traceLiteralFeature = async (
call: AnalysisCall,
observe: ObserveCall,
input: LiteralTraceInput,
signal?: AbortSignal,
): EnhancedResult => {
const searched = await literalMatches(
call,
input,
LITERAL_SEARCH_SOURCES,
signal,
);
if (!searched.ok) return searched;
const traced = await traceReferences(call, observe, {
matches: searched.value.matches,
...(signal === undefined ? {} : { signal }),
});
if (!traced.ok) return traced;
return ok({
query: input.query,
search_mode: "literal",
matches: searched.value.matches,
references: traced.value.references,
truncated: traced.value.residual.length > 0,
residual_unknowns: traced.value.residual,
});
};
const literalMatches = async (
call: AnalysisCall,
input: LiteralTraceInput,
sources: readonly LiteralSearchSource[],
signal?: AbortSignal,
): Promise<Result<{ matches: TraceMatch[] }, AnalysisError>> => {
const matches: TraceMatch[] = [];
const matched = new Set<string>();
for (const [tool, type] of sources) {
const result = await call(
tool,
{
pattern: input.query,
mode: "literal",
case_sensitive: input.case_sensitive,
},
signal,
);
if (!result.ok) return result;
const items = z
.array(z.object({ address: z.string(), value: z.string() }))
.safeParse(result.value);
if (!items.success)
return err(
new AnalysisOutputError(
tool,
"provider returned an invalid search result",
),
);
addPageMatches({
items: items.data.map(({ address, value: name }) => ({
address,
name,
})),
type,
matches,
matched,
});
}
return ok({ matches });
};
const addPageMatches = (input: {
readonly items: readonly {
readonly address: string;
readonly name: string;
}[];
readonly type: TraceMatch["type"];
readonly matches: TraceMatch[];
readonly matched: Set<string>;
}): void => {
const { items, type, matches, matched } = input;
for (const item of items) {
const key = `${type}\u0000${item.address}`;
if (matched.has(key)) continue;
matches.push({ type, address: item.address, value: item.name });
matched.add(key);
}
};
const traceReferences = async (
call: AnalysisCall,
observe: ObserveCall,
request: ReferenceTraceRequest,
): Promise<
Result<{ references: TraceReference[]; residual: string[] }, AnalysisError>
> => {
const references: TraceReference[] = [];
const residual: string[] = [];
// A source address is resolved once however many matches reference it.
const containing = new Map<string, JsonValue>();
for (const match of request.matches) {
const observed = await observe(
"xrefs",
{ address: match.address },
request.signal,
);
if (!observed.ok) return observed;
// Only an explicit producer report of truncation is a coverage gap; a
// provider that reports nothing keeps its previous, unqualified result.
const unknown = `References to ${match.address} were truncated by the provider; additional references may exist.`;
if (
observed.value.referencesTruncated === true &&
!residual.includes(unknown)
)
residual.push(unknown);
const sources = observed.value.result;
if (!Array.isArray(sources))
return err(
new AnalysisOutputError(
"xrefs",
"provider returned a non-array result",
),
);
for (const source of sources) {
if (typeof source !== "string")
return err(
new AnalysisOutputError(
"xrefs",
"provider returned a non-address value",
),
);
let containingProcedure = containing.get(source);
if (containingProcedure === undefined) {
const resolved = await call(
"resolve_containing_procedure",
{ address: source },
request.signal,
);
if (!resolved.ok) return resolved;
containingProcedure = resolved.value;
containing.set(source, containingProcedure);
}
references.push({
target_address: match.address,
source_address: source,
containing_procedure: containingProcedure,
});
}
}
return ok({ references, residual });
};