fix(native): preserve function byte source qualifications (#8606)
* fix(native): preserve function byte source qualifications * fix(native): recover oversized inline matching sources * test(native): verify matching byte source recovery --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
4ec2726b1b
commit
2b740cab36
7 files changed
+524
-3
No files matched your search
@@ -46,6 +46,18 @@ fingerprint and the same known architecture. Extents are never guessed from the
|
||||
next entry, and relocations are not normalized. A larger mapped read can serve
|
||||
several observed bodies; only needed ranges need to be captured.
|
||||
|
||||
For a matched function, its `evidence_links` include the selected byte reads
|
||||
that covered its body. Request the `function-matches` view with
|
||||
`collection: "byte-sources"` to page the selected `read_bytes` source IDs,
|
||||
sides and limitations; intersect those IDs with the function's links. This
|
||||
distinguishes, for example, Ghidra initialized-memory bytes from file bytes.
|
||||
Identical overlapping reads retain the selected covering source, not every
|
||||
corroborating read. Pages preserve the original source limitations and may end
|
||||
early when the response budget is reached. If one source is too large for a
|
||||
page, the error identifies its Evidence and the next offset; inspect the
|
||||
original matching Evidence or export it if retained, then resume at that
|
||||
offset. Older matching Evidence may not record this source selection.
|
||||
|
||||
This first slice reuses existing range observations. Stripped builds can require
|
||||
many lightweight range queries when those facts are not retained yet. Matching
|
||||
does not hide that acquisition cost or silently start those queries. Full
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { readFunctionInventory } from "../../domain/native/functionInventory.js";
|
||||
import {
|
||||
nativeFunctionMatchingSchema,
|
||||
nativeFunctionByteSourceGuidance,
|
||||
NATIVE_FUNCTION_MATCHING_PREDICATE,
|
||||
} from "../../domain/native/functionMatching.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -94,6 +95,38 @@ const matchWithAdmission = async (
|
||||
rightFunctions,
|
||||
{ identicalBytesComparable: compatible, checkpoint },
|
||||
);
|
||||
const byteSources: {
|
||||
side: "left" | "right";
|
||||
evidence_id: string;
|
||||
limitations: string[];
|
||||
}[] = [];
|
||||
for (const [side, functions, observations] of [
|
||||
["left", leftFunctions, leftObservations],
|
||||
["right", rightFunctions, rightObservations],
|
||||
] as const) {
|
||||
const used = new Set<string>();
|
||||
for (const item of functions)
|
||||
for (const id of item.member.evidence_links) {
|
||||
if (!used.has(id)) await checkpoint(128 + id.length * 4);
|
||||
used.add(id);
|
||||
}
|
||||
for (const observation of observations) {
|
||||
if (
|
||||
observation.operation !== "read_bytes" ||
|
||||
!used.has(observation.evidence_id)
|
||||
)
|
||||
continue;
|
||||
used.delete(observation.evidence_id);
|
||||
await checkpoint(512 + observation.evidence_id.length * 8);
|
||||
for (const limitation of observation.limitations)
|
||||
await checkpoint(128 + limitation.length * 8);
|
||||
byteSources.push({
|
||||
side,
|
||||
evidence_id: observation.evidence_id,
|
||||
limitations: [...observation.limitations],
|
||||
});
|
||||
}
|
||||
}
|
||||
const limitations = [
|
||||
...new Set([
|
||||
...left.limitations.map((x) => `Left: ${x}`),
|
||||
@@ -101,6 +134,7 @@ const matchWithAdmission = async (
|
||||
"Coverage is restricted to the supplied provider-observed inventories; missing/truncated inventory facts never prove absence from either binary.",
|
||||
"Only unique exact symbols and bytes are paired. Compatible observed CFG/call/string fingerprints propose unresolved candidates, never identity or equivalence. Missing structural coverage stays unknown.",
|
||||
"Name or byte correspondence is not behavioral equivalence; use analyze_function for selected pairs, then compare_functions. Existing dossier pairs include directly usable retained-reference inputs.",
|
||||
...(byteSources.length === 0 ? [] : [nativeFunctionByteSourceGuidance]),
|
||||
...(!compatible
|
||||
? [
|
||||
"Identical-byte matching unavailable: input architectures are missing or differ.",
|
||||
@@ -141,6 +175,8 @@ const matchWithAdmission = async (
|
||||
).length,
|
||||
},
|
||||
summary,
|
||||
byte_sources_count: byteSources.length,
|
||||
byte_sources: byteSources,
|
||||
matches,
|
||||
limitations,
|
||||
});
|
||||
|
||||
@@ -118,7 +118,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
),
|
||||
session(
|
||||
"match_native_functions",
|
||||
"Match two retained list_procedures inventories by unique exact reported symbols, then unique identical observed function bytes. Optional same-context list_names/procedure_info/read_bytes/analyze_function Evidence supplies symbol origin, exact bodies, bytes and dossier links as supplied static facts. Optional compatible CFG/call/string fingerprints from supplied dossiers propose unresolved groups only. Duplicate candidates remain unresolved groups; only_left/right mean unpaired within supplied observations, never proof of addition/removal. Default summary retains complete Evidence for filtered, selected-column, paged function-matches views. Complete detail returns every group. Existing dossier pairs provide direct retained-reference compare_functions inputs.",
|
||||
"Match two retained list_procedures inventories by unique exact reported symbols, then unique identical observed function bytes. Optional same-context list_names/procedure_info/read_bytes/analyze_function Evidence supplies symbol origin, exact bodies, bytes and dossier links as supplied static facts. Optional compatible CFG/call/string fingerprints from supplied dossiers propose unresolved groups only. Duplicate candidates remain unresolved groups; only_left/right mean unpaired within supplied observations, never proof of addition/removal. Default summary retains complete Evidence for filtered, selected-column, paged function-matches views. The byte-sources collection identifies selected read_bytes Evidence and its limitations; intersect its IDs with each function's evidence_links to identify its sources. Complete detail returns every group. Existing dossier pairs provide direct retained-reference compare_functions inputs.",
|
||||
matchNativeFunctionsInputSchema,
|
||||
),
|
||||
session(
|
||||
|
||||
@@ -1129,6 +1129,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
artifact: z.null(),
|
||||
view: nativeFunctionMatchingViewRequestSchema,
|
||||
items: nativeFunctionMatchingViewItemsSchema,
|
||||
byte_limited: z.boolean().optional(),
|
||||
parent_operation: z.literal("match_native_functions"),
|
||||
}),
|
||||
z.strictObject({
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
nativeFunctionMatchingSchema,
|
||||
nativeFunctionMatchSchema,
|
||||
nativeFunctionMatchMemberSchema,
|
||||
nativeFunctionByteSourceSchema,
|
||||
nativeFunctionByteSourceGuidance,
|
||||
nativeMatchStateSchema,
|
||||
type NativeFunctionMatching,
|
||||
} from "../native/functionMatching.js";
|
||||
@@ -23,6 +25,8 @@ import {
|
||||
type SummaryAnalysisView,
|
||||
} from "./analysisViewShape.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { jsonValueByteLength } from "../jsonValueByteLength.js";
|
||||
import { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
|
||||
|
||||
const matchRow = nativeFunctionMatchSchema.extend({
|
||||
left_count: z.number().int().nonnegative(),
|
||||
@@ -56,9 +60,18 @@ export const nativeFunctionMatchingViewRequestSchema = z.discriminatedUnion(
|
||||
names: z.array(z.string()).optional(),
|
||||
fields: z.array(functionRow.keyof()).optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("function-matches"),
|
||||
collection: z.literal("byte-sources"),
|
||||
sides: z.array(z.enum(["left", "right"])).optional(),
|
||||
evidence_ids: z.array(z.string()).optional(),
|
||||
offset: z.number().int().nonnegative().default(0),
|
||||
limit: z.number().int().positive().default(64),
|
||||
}),
|
||||
],
|
||||
);
|
||||
export const nativeFunctionMatchingViewItemsSchema = z.union([
|
||||
z.array(nativeFunctionByteSourceSchema),
|
||||
z.array(
|
||||
matchRow.partial().required({
|
||||
match_id: true,
|
||||
@@ -93,8 +106,14 @@ interface Checked {
|
||||
}
|
||||
const checked = new WeakMap<object, Checked>();
|
||||
const readReport = createAnalysisViewResultReader(nativeFunctionMatchingSchema);
|
||||
// Leave room for the Evidence envelope and duplicated MCP text/structured output.
|
||||
const BYTE_SOURCE_PAGE_BYTES = 1024 * 1024;
|
||||
const BYTE_SOURCE_PROJECTION_BYTES =
|
||||
BYTE_SOURCE_PAGE_BYTES -
|
||||
(jsonValueByteLength({ view_digest: "0".repeat(64) }) - 1);
|
||||
|
||||
type Request = z.output<typeof nativeFunctionMatchingViewRequestSchema>;
|
||||
type MatchRequest = Exclude<Request, { collection: "byte-sources" }>;
|
||||
const checkedReport = (
|
||||
parent: AnalysisViewParent,
|
||||
): Result<Checked, AnalysisError> => {
|
||||
@@ -134,6 +153,7 @@ const sharedContext = (
|
||||
[...parent.limitations, ...report.limitations],
|
||||
parent.evidenceId,
|
||||
"limitations",
|
||||
report.byte_sources_count ? [nativeFunctionByteSourceGuidance] : [],
|
||||
),
|
||||
unknowns: [
|
||||
"Unpaired observations and page exhaustion do not establish binary-wide function absence or behavioral equivalence.",
|
||||
@@ -141,7 +161,7 @@ const sharedContext = (
|
||||
});
|
||||
const selected = (values: readonly string[] | undefined) =>
|
||||
values === undefined ? undefined : new Set(values);
|
||||
const filterRows = (entry: Checked, view: Request): readonly number[] => {
|
||||
const filterRows = (entry: Checked, view: MatchRequest): readonly number[] => {
|
||||
const key = JSON.stringify([
|
||||
view.collection,
|
||||
view.states,
|
||||
@@ -188,7 +208,7 @@ const filterRows = (entry: Checked, view: Request): readonly number[] => {
|
||||
};
|
||||
const selectedItems = (
|
||||
entry: Checked,
|
||||
view: Request,
|
||||
view: MatchRequest,
|
||||
rows: readonly number[],
|
||||
) => {
|
||||
const indices = rows.slice(view.offset, view.offset + view.limit);
|
||||
@@ -283,6 +303,7 @@ export const projectNativeFunctionMatchingView = (
|
||||
const {
|
||||
matches: _matches,
|
||||
limitations: _limitations,
|
||||
byte_sources: _byteSources,
|
||||
...context
|
||||
} = entry.report;
|
||||
const shared = sharedContext(parent, entry.report);
|
||||
@@ -294,6 +315,102 @@ export const projectNativeFunctionMatchingView = (
|
||||
summary: context,
|
||||
coverage: completeWithinViewCoverage(1, 1),
|
||||
});
|
||||
if (view.collection === "byte-sources") {
|
||||
if (entry.report.byte_sources === undefined)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
{
|
||||
path: ["view"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"This historical report did not record byte-source qualifications; inspect its linked Evidence.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const sides = selected(view.sides),
|
||||
ids = selected(view.evidence_ids);
|
||||
const items: z.output<typeof nativeFunctionByteSourceSchema>[] = [];
|
||||
const fixed = {
|
||||
kind: "function-matches" as const,
|
||||
view,
|
||||
...shared,
|
||||
...context,
|
||||
};
|
||||
let bytes = jsonValueByteLength(
|
||||
{
|
||||
...fixed,
|
||||
items: [],
|
||||
byte_limited: true,
|
||||
coverage: pageViewCoverage(
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
),
|
||||
} as JsonValue,
|
||||
BYTE_SOURCE_PROJECTION_BYTES,
|
||||
);
|
||||
const tooLarge = (source?: { evidence_id: string; next_offset: number }) =>
|
||||
err(
|
||||
new AnalysisResourceConstraintError(
|
||||
"inspect_analysis_view",
|
||||
"transport",
|
||||
"The selected byte-source view exceeds the projection allowance.",
|
||||
{
|
||||
parent_evidence_id: parent.evidenceId,
|
||||
limit_bytes: BYTE_SOURCE_PAGE_BYTES,
|
||||
...(source === undefined
|
||||
? {}
|
||||
: {
|
||||
source_evidence_id: source.evidence_id,
|
||||
next_offset: source.next_offset,
|
||||
}),
|
||||
},
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction:
|
||||
source === undefined
|
||||
? "Narrow the selection or inspect the original matching Evidence."
|
||||
: "Inspect the original matching Evidence, or export it if retained; use next_offset to continue after the oversized source.",
|
||||
},
|
||||
),
|
||||
);
|
||||
if (bytes > BYTE_SOURCE_PROJECTION_BYTES) return tooLarge();
|
||||
let total = 0,
|
||||
byteLimited = false;
|
||||
for (const row of entry.report.byte_sources) {
|
||||
if (
|
||||
(sides !== undefined && !sides.has(row.side)) ||
|
||||
(ids !== undefined && !ids.has(row.evidence_id))
|
||||
)
|
||||
continue;
|
||||
if (total >= view.offset && items.length < view.limit && !byteLimited) {
|
||||
const item = row;
|
||||
const size =
|
||||
jsonValueByteLength(
|
||||
item,
|
||||
Math.max(0, BYTE_SOURCE_PROJECTION_BYTES - bytes),
|
||||
) + 1;
|
||||
if (bytes + size > BYTE_SOURCE_PROJECTION_BYTES) {
|
||||
if (items.length === 0)
|
||||
return tooLarge({
|
||||
evidence_id: row.evidence_id,
|
||||
next_offset: total + 1,
|
||||
});
|
||||
byteLimited = true;
|
||||
} else {
|
||||
items.push(item);
|
||||
bytes += size;
|
||||
}
|
||||
}
|
||||
total++;
|
||||
}
|
||||
return ok({
|
||||
...fixed,
|
||||
items,
|
||||
byte_limited: byteLimited,
|
||||
coverage: pageViewCoverage(view.offset, items.length, total),
|
||||
});
|
||||
}
|
||||
const rows = filterRows(entry, view),
|
||||
items = selectedItems(entry, view, rows);
|
||||
return ok({
|
||||
|
||||
@@ -26,6 +26,13 @@ export const nativeFunctionMatchMemberSchema = z.strictObject({
|
||||
evidence_links: z.array(evidenceId).min(1),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
export const nativeFunctionByteSourceSchema = z.strictObject({
|
||||
side: z.enum(["left", "right"]),
|
||||
evidence_id: evidenceId,
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
export const nativeFunctionByteSourceGuidance =
|
||||
"A function's selected byte sources are its evidence_links listed in the function-matches byte-sources view. Inspect their limitations before interpreting the byte basis.";
|
||||
export const nativeFunctionMatchSchema = z.strictObject({
|
||||
match_id: prefixedDigestSchema("match"),
|
||||
state: nativeMatchStateSchema,
|
||||
@@ -68,15 +75,21 @@ export const nativeFunctionMatchingContextSchema = z.strictObject({
|
||||
right_structural_candidates: z.number().int().nonnegative(),
|
||||
}),
|
||||
summary: nativeFunctionMatchingSummarySchema,
|
||||
byte_sources_count: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
export const nativeFunctionMatchingSchema = nativeFunctionMatchingContextSchema
|
||||
.extend({
|
||||
matches: z.array(nativeFunctionMatchSchema),
|
||||
limitations: z.array(z.string()),
|
||||
byte_sources: z.array(nativeFunctionByteSourceSchema).optional(),
|
||||
})
|
||||
.superRefine((report, context) => {
|
||||
const seen = new Set<string>();
|
||||
const members = { left: new Set<string>(), right: new Set<string>() };
|
||||
const byteReferences = {
|
||||
left: new Set<string>(),
|
||||
right: new Set<string>(),
|
||||
};
|
||||
const fingerprints = { left: 0, right: 0 };
|
||||
const candidates = { left: 0, right: 0 };
|
||||
const counts = { matched: 0, only_left: 0, only_right: 0, unresolved: 0 };
|
||||
@@ -119,6 +132,7 @@ export const nativeFunctionMatchingSchema = nativeFunctionMatchingContextSchema
|
||||
"Function occurrence identity, provenance or uniqueness disagrees with its selected inventory",
|
||||
});
|
||||
members[side].add(member.function_id);
|
||||
for (const id of member.evidence_links) byteReferences[side].add(id);
|
||||
if (member.body_sha256 !== null) fingerprints[side]++;
|
||||
if (member.structural_candidate_sha256 !== null) candidates[side]++;
|
||||
}
|
||||
@@ -170,6 +184,31 @@ export const nativeFunctionMatchingSchema = nativeFunctionMatchingContextSchema
|
||||
message: "Match state contradicts its occurrences",
|
||||
});
|
||||
}
|
||||
if (
|
||||
(report.byte_sources === undefined) !==
|
||||
(report.byte_sources_count === undefined) ||
|
||||
(report.byte_sources !== undefined &&
|
||||
report.byte_sources_count !== report.byte_sources.length)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Byte-source count disagrees with recorded sources",
|
||||
});
|
||||
if (report.byte_sources !== undefined) {
|
||||
const recorded = { left: new Set<string>(), right: new Set<string>() };
|
||||
for (const source of report.byte_sources) {
|
||||
if (
|
||||
recorded[source.side].has(source.evidence_id) ||
|
||||
!byteReferences[source.side].has(source.evidence_id)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"Byte source is duplicated or not used on its recorded side",
|
||||
});
|
||||
recorded[source.side].add(source.evidence_id);
|
||||
}
|
||||
}
|
||||
if (
|
||||
report.source_coverage.status !==
|
||||
(counts.only_left + counts.only_right + counts.unresolved > 0
|
||||
|
||||
@@ -653,3 +653,319 @@ cliTest(
|
||||
},
|
||||
60000,
|
||||
);
|
||||
|
||||
cliTest.for([{ profile: "full" }, { profile: "compact" }] as const)(
|
||||
"ties byte qualifications to selected observations through paged and offline MCP ($profile)",
|
||||
{ timeout: 60000 },
|
||||
async ({ profile }) => {
|
||||
const left = nativeMatchingInventory("1", [
|
||||
{ address: "0x10", value: "left" },
|
||||
]);
|
||||
const right = nativeMatchingInventory("2", [
|
||||
{ address: "0x20", value: "right" },
|
||||
]);
|
||||
const [leftInfo] = nativeMatchingDetails("1", "0x10", "c3");
|
||||
const [rightInfo] = nativeMatchingDetails("2", "0x20", "c3");
|
||||
const read = (side: "1" | "2", address: string, limitation: string) =>
|
||||
createEvidence(nativeMatchingTarget(side), nativeMatchingProvider, {
|
||||
operation: "read_bytes",
|
||||
parameters: { address, length: 1 },
|
||||
result: {
|
||||
address,
|
||||
requested_bytes: 1,
|
||||
returned_bytes: 1,
|
||||
bytes_hex: "c3",
|
||||
complete: true,
|
||||
},
|
||||
limitations: [limitation],
|
||||
});
|
||||
const first = read("1", "0x10", "First selected source limitation");
|
||||
const duplicate = read("1", "0x10", "Alternative source limitation");
|
||||
const unused = read("1", "0x99", "Unused source limitation");
|
||||
const other = read("2", "0x20", "Right source limitation");
|
||||
const reference = (evidence_id: string) => ({
|
||||
kind: "retained-evidence",
|
||||
evidence_id,
|
||||
});
|
||||
const { call } = await connectLocalToolsMcp({
|
||||
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
|
||||
});
|
||||
const execute = async (
|
||||
firstRead: typeof first,
|
||||
secondRead: typeof first,
|
||||
) => {
|
||||
const reply = await call("match_native_functions", {
|
||||
left,
|
||||
right,
|
||||
left_observations: [leftInfo, firstRead, firstRead, secondRead, unused],
|
||||
right_observations: [rightInfo, other],
|
||||
detail: "complete",
|
||||
});
|
||||
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
||||
return parseEvidence(reply.structuredContent);
|
||||
};
|
||||
const evidence = await execute(first, duplicate);
|
||||
const report = nativeFunctionMatchingSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
const match = report.matches.find(
|
||||
(item) => item.basis === "identical-bytes",
|
||||
);
|
||||
expect(match?.left[0]?.evidence_links).toContain(first.evidence_id);
|
||||
expect(match?.left[0]?.evidence_links).not.toContain(duplicate.evidence_id);
|
||||
expect(match?.right[0]?.evidence_links).toContain(other.evidence_id);
|
||||
expect(report.byte_sources).toEqual([
|
||||
{
|
||||
side: "left",
|
||||
evidence_id: first.evidence_id,
|
||||
limitations: ["First selected source limitation"],
|
||||
},
|
||||
{
|
||||
side: "right",
|
||||
evidence_id: other.evidence_id,
|
||||
limitations: ["Right source limitation"],
|
||||
},
|
||||
]);
|
||||
const alternate = nativeFunctionMatchingSchema.parse(
|
||||
(await execute(duplicate, first)).normalized_result,
|
||||
);
|
||||
expect(
|
||||
alternate.matches.find((item) => item.basis === "identical-bytes")
|
||||
?.left[0]?.evidence_links,
|
||||
).toContain(duplicate.evidence_id);
|
||||
expect(alternate.byte_sources?.map((item) => item.evidence_id)).toEqual([
|
||||
duplicate.evidence_id,
|
||||
other.evidence_id,
|
||||
]);
|
||||
const sourcePage = async (caller: typeof call, offset: number) => {
|
||||
const reply = await caller("inspect_analysis_view", {
|
||||
source: reference(evidence.evidence_id),
|
||||
view: {
|
||||
kind: "function-matches",
|
||||
collection: "byte-sources",
|
||||
offset,
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
||||
return parseEvidence(reply.structuredContent).normalized_result;
|
||||
};
|
||||
expect(await sourcePage(call, 0)).toMatchObject({
|
||||
items: [
|
||||
{
|
||||
side: "left",
|
||||
evidence_id: first.evidence_id,
|
||||
limitations: ["First selected source limitation"],
|
||||
},
|
||||
],
|
||||
coverage: { total: 2, next_offset: 1 },
|
||||
});
|
||||
expect(await sourcePage(call, 1)).toMatchObject({
|
||||
items: [
|
||||
{
|
||||
side: "right",
|
||||
evidence_id: other.evidence_id,
|
||||
limitations: ["Right source limitation"],
|
||||
},
|
||||
],
|
||||
coverage: { total: 2, next_offset: null },
|
||||
});
|
||||
const root = await createTestTempDirectory("rea-match-byte-sources-");
|
||||
const path = join(root, "bundle.json");
|
||||
const exported = await call("export_evidence_bundle", { path });
|
||||
expect(exported.isError, JSON.stringify(exported)).not.toBe(true);
|
||||
const offline = await connectLocalToolsMcp({
|
||||
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
|
||||
});
|
||||
const imported = await offline.call("import_evidence_bundle", { path });
|
||||
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
||||
expect(await sourcePage(offline.call, 0)).toMatchObject({
|
||||
items: [{ evidence_id: first.evidence_id }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"bounds byte-source pages and recovers an oversized source",
|
||||
{ timeout: 60000 },
|
||||
async () => {
|
||||
const left = nativeMatchingInventory("1", [
|
||||
{ address: "0x10", value: "left" },
|
||||
]);
|
||||
const right = nativeMatchingInventory("2", [
|
||||
{ address: "0x20", value: "right" },
|
||||
]);
|
||||
const [info] = nativeMatchingDetails("1", "0x10", "00".repeat(256));
|
||||
if (info === undefined) throw new Error("Missing synthetic body");
|
||||
const reads = Array.from({ length: 256 }, (_, index) => {
|
||||
const address = `0x${(16 + index).toString(16)}`;
|
||||
return createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, {
|
||||
operation: "read_bytes",
|
||||
parameters: { address, length: 1 },
|
||||
result: {
|
||||
address,
|
||||
requested_bytes: 1,
|
||||
returned_bytes: 1,
|
||||
bytes_hex: "00",
|
||||
complete: true,
|
||||
},
|
||||
limitations: [
|
||||
`${index}:` +
|
||||
"x".repeat(index === 0 ? 1_100_000 : index === 1 ? 6000 : 4500),
|
||||
],
|
||||
});
|
||||
});
|
||||
const reference = (evidence_id: string) => ({
|
||||
kind: "retained-evidence",
|
||||
evidence_id,
|
||||
});
|
||||
const root = await createTestTempDirectory("rea-match-large-source-");
|
||||
const path = join(root, "inputs.json");
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify(createEvidenceBundle([left, right, info, ...reads])),
|
||||
);
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const imported = await call("import_evidence_bundle", { path });
|
||||
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
||||
const result = await call("match_native_functions", {
|
||||
left: reference(left.evidence_id),
|
||||
right: reference(right.evidence_id),
|
||||
left_observations: [info, ...reads].map((record) =>
|
||||
reference(record.evidence_id),
|
||||
),
|
||||
detail: "summary",
|
||||
});
|
||||
expect(result.isError, JSON.stringify(result)).not.toBe(true);
|
||||
const parent = parseEvidence(result.structuredContent)
|
||||
.normalized_result as { parent_evidence_id: string };
|
||||
const oversized = await call("inspect_analysis_view", {
|
||||
source: reference(parent.parent_evidence_id),
|
||||
view: { kind: "function-matches", collection: "byte-sources" },
|
||||
});
|
||||
expect(parseMcpToolError(oversized).error).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: {
|
||||
resource: "transport",
|
||||
reported_limits: {
|
||||
source_evidence_id: reads[0]!.evidence_id,
|
||||
next_offset: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
let offset = 1;
|
||||
const ids: string[] = [reads[0]!.evidence_id];
|
||||
let limited = false;
|
||||
while (true) {
|
||||
const reply = await call("inspect_analysis_view", {
|
||||
source: reference(parent.parent_evidence_id),
|
||||
view: {
|
||||
kind: "function-matches",
|
||||
collection: "byte-sources",
|
||||
offset,
|
||||
limit: 1000,
|
||||
},
|
||||
});
|
||||
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
||||
const page = parseEvidence(reply.structuredContent).normalized_result as {
|
||||
byte_limited: boolean;
|
||||
items: { evidence_id: string; limitations: string[] }[];
|
||||
coverage: { total: number; next_offset: number | null };
|
||||
};
|
||||
limited ||= page.byte_limited;
|
||||
if (offset === 1)
|
||||
expect(page.items[0]?.limitations).toEqual(reads[1]!.limitations);
|
||||
ids.push(...page.items.map((item) => item.evidence_id));
|
||||
expect(page.coverage.total).toBe(256);
|
||||
if (page.coverage.next_offset === null) break;
|
||||
expect(page.coverage.next_offset).toBe(ids.length);
|
||||
offset = page.coverage.next_offset;
|
||||
}
|
||||
expect(limited).toBe(true);
|
||||
expect(ids).toEqual(reads.map((record) => record.evidence_id));
|
||||
const original = await call("get_evidence_bundle", {
|
||||
detail: "records",
|
||||
filters: { evidence_id: reads[0]!.evidence_id },
|
||||
});
|
||||
expect(original.isError, JSON.stringify(original)).not.toBe(true);
|
||||
expect(JSON.stringify(original.structuredContent)).toContain(
|
||||
reads[0]!.limitations[0],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"oversized inline matching Evidence points back to the caller's original record",
|
||||
{ timeout: 60000 },
|
||||
async () => {
|
||||
const left = nativeMatchingInventory("1", [
|
||||
{ address: "0x10", value: "left" },
|
||||
]);
|
||||
const right = nativeMatchingInventory("2", [
|
||||
{ address: "0x20", value: "right" },
|
||||
]);
|
||||
const [info] = nativeMatchingDetails("1", "0x10", "c3");
|
||||
if (info === undefined) throw new Error("Missing synthetic body");
|
||||
const read = createEvidence(
|
||||
nativeMatchingTarget("1"),
|
||||
nativeMatchingProvider,
|
||||
{
|
||||
operation: "read_bytes",
|
||||
parameters: { address: "0x10", length: 1 },
|
||||
result: {
|
||||
address: "0x10",
|
||||
requested_bytes: 1,
|
||||
returned_bytes: 1,
|
||||
bytes_hex: "c3",
|
||||
complete: true,
|
||||
},
|
||||
limitations: ["x".repeat(1_100_000)],
|
||||
},
|
||||
);
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const match = await call("match_native_functions", {
|
||||
left,
|
||||
right,
|
||||
left_observations: [info, read],
|
||||
detail: "complete",
|
||||
});
|
||||
expect(match.isError, JSON.stringify(match)).not.toBe(true);
|
||||
const matchingEvidence = parseEvidence(match.structuredContent);
|
||||
const offline = await connectLocalToolsMcp();
|
||||
const page = await offline.call("inspect_analysis_view", {
|
||||
source: { kind: "inline", evidence: matchingEvidence },
|
||||
view: { kind: "function-matches", collection: "byte-sources" },
|
||||
});
|
||||
const error = parseMcpToolError(page).error;
|
||||
expect(error).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: {
|
||||
reported_limits: {
|
||||
source_evidence_id: read.evidence_id,
|
||||
next_offset: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
const report = nativeFunctionMatchingSchema.parse(
|
||||
matchingEvidence.normalized_result,
|
||||
);
|
||||
expect(report.byte_sources).toEqual([
|
||||
{
|
||||
side: "left",
|
||||
evidence_id: read.evidence_id,
|
||||
limitations: read.limitations,
|
||||
},
|
||||
]);
|
||||
const lookup = await offline.call("get_evidence_bundle", {
|
||||
detail: "records",
|
||||
filters: { evidence_id: read.evidence_id },
|
||||
});
|
||||
expect(lookup.isError, JSON.stringify(lookup)).not.toBe(true);
|
||||
expect(
|
||||
(lookup.structuredContent as { result: { records: unknown[] } }).result
|
||||
.records,
|
||||
).toEqual([]);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user