mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
feat(mcp): page retained incoming reference addresses (#8591)
This commit is contained in:
1 parent
75c3cab540
commit
55078b9f57
8 files changed
+97
-50
No files matched your search
@@ -292,6 +292,12 @@ limitation and reason IDs index that page's `items.messages` dictionary. Expand
|
||||
those references to recover exact text, repeated messages, and absent versus
|
||||
empty optional fields. Failed nodes retain their original error.
|
||||
|
||||
Retained `xrefs` Evidence also supports `native-observation` address pages and
|
||||
`native-context` source caveats. `xrefs` still returns complete Evidence by
|
||||
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.
|
||||
|
||||
These views reference complete source context through `source_context.digest`
|
||||
and caveat counts. Read it using the same parent with
|
||||
`view: {"kind":"native-context","facet":"overview"}`. The other facets are
|
||||
|
||||
@@ -121,53 +121,56 @@ const graph = (): NativeCallGraph =>
|
||||
limitations: ["Graph caveat", "Graph caveat"],
|
||||
});
|
||||
|
||||
it("returns useful caller/callee facts with exact source context identity and partial coverage", async () => {
|
||||
const source = parent("procedure_callees", [
|
||||
"ram:0x0010",
|
||||
"ram:0x0010",
|
||||
"ram:0x0020",
|
||||
]);
|
||||
const before = JSON.stringify(source);
|
||||
const summary = await project(source, { kind: "summary" });
|
||||
expect(summary.value).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_evidence_id: source.evidence_id,
|
||||
source_provider: source.provider,
|
||||
source_parameters: source.parameters,
|
||||
source_analysis_profile_digest: source.analysis_profile?.digest,
|
||||
source_context: {
|
||||
evidence_limitation_count: 1,
|
||||
result_limitation_count: null,
|
||||
},
|
||||
summary: { observations: 3 },
|
||||
items: {
|
||||
type: "addresses",
|
||||
rows: [
|
||||
{ source_index: 0, address: "ram:0x0010" },
|
||||
{ source_index: 1, address: "ram:0x0010" },
|
||||
{ source_index: 2, address: "ram:0x0020" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(summary.evidence.relationship_coverage).toEqual(
|
||||
source.relationship_coverage,
|
||||
);
|
||||
const page = await project(source, {
|
||||
kind: "native-observation",
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
});
|
||||
expect(page.value).toMatchObject({
|
||||
items: { rows: [{ source_index: 1, address: "ram:0x0010" }] },
|
||||
coverage: { total: 3, next_offset: 2, exhausted: false },
|
||||
});
|
||||
expect(page.value).toHaveProperty(
|
||||
"source_context.digest",
|
||||
contextDigest(summary.value),
|
||||
);
|
||||
expect(JSON.stringify(summary.value)).not.toContain('"analysis_profile":');
|
||||
expect(JSON.stringify(source)).toBe(before);
|
||||
});
|
||||
it.each(["procedure_callees", "xrefs"])(
|
||||
"pages %s addresses with exact source context and duplicate identity",
|
||||
async (operation) => {
|
||||
const source = parent(operation, [
|
||||
"ram:0x0010",
|
||||
"ram:0x0010",
|
||||
"ram:0x0020",
|
||||
]);
|
||||
const before = JSON.stringify(source);
|
||||
const summary = await project(source, { kind: "summary" });
|
||||
expect(summary.value).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_evidence_id: source.evidence_id,
|
||||
source_provider: source.provider,
|
||||
source_parameters: source.parameters,
|
||||
source_analysis_profile_digest: source.analysis_profile?.digest,
|
||||
source_context: {
|
||||
evidence_limitation_count: 1,
|
||||
result_limitation_count: null,
|
||||
},
|
||||
summary: { observations: 3 },
|
||||
items: {
|
||||
type: "addresses",
|
||||
rows: [
|
||||
{ source_index: 0, address: "ram:0x0010" },
|
||||
{ source_index: 1, address: "ram:0x0010" },
|
||||
{ source_index: 2, address: "ram:0x0020" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(summary.evidence.relationship_coverage).toEqual(
|
||||
source.relationship_coverage,
|
||||
);
|
||||
const page = await project(source, {
|
||||
kind: "native-observation",
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
});
|
||||
expect(page.value).toMatchObject({
|
||||
items: { rows: [{ source_index: 1, address: "ram:0x0010" }] },
|
||||
coverage: { total: 3, next_offset: 2, exhausted: false },
|
||||
});
|
||||
expect(page.value).toHaveProperty(
|
||||
"source_context.digest",
|
||||
contextDigest(summary.value),
|
||||
);
|
||||
expect(JSON.stringify(summary.value)).not.toContain('"analysis_profile":');
|
||||
expect(JSON.stringify(source)).toBe(before);
|
||||
},
|
||||
);
|
||||
|
||||
it("pages complete context fields and changes its digest when source profile or caveats change", async () => {
|
||||
const source = parent("procedure_callers", []);
|
||||
|
||||
@@ -260,7 +260,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
official(
|
||||
"xrefs",
|
||||
"Return analyzed references to a code or data address. Hopper may default to its current cursor; headless providers such as Ghidra require an explicit address. Use to connect strings, globals, selectors, and functions; bare addresses are untyped and indirect references may be incomplete. Use inspect_native_instruction at the target address for typed incoming references where supported.",
|
||||
"Return analyzed references to a code or data address. Hopper may default to its current cursor; headless providers such as Ghidra require an explicit address. Use to connect strings, globals, selectors, and functions; bare addresses are untyped and indirect references may be incomplete. Page a retained result with inspect_analysis_view and native-observation; use native-context for source caveats. Use inspect_native_instruction at the target address for typed incoming references where supported.",
|
||||
z.object({ document, address: optionalAddress }),
|
||||
),
|
||||
] as const satisfies readonly ToolContract[];
|
||||
|
||||
@@ -382,6 +382,7 @@ export const nativeObservationSummaryViewResultSchemas = {
|
||||
procedure_address: nativeObservationSummaryFor("procedure_address"),
|
||||
procedure_callees: nativeObservationSummaryFor("procedure_callees"),
|
||||
procedure_callers: nativeObservationSummaryFor("procedure_callers"),
|
||||
xrefs: nativeObservationSummaryFor("xrefs"),
|
||||
resolve_containing_procedure: nativeObservationSummaryFor(
|
||||
"resolve_containing_procedure",
|
||||
),
|
||||
@@ -393,6 +394,7 @@ const nativeObservationSummaryViewResultSchema = z.discriminatedUnion(
|
||||
nativeObservationSummaryViewResultSchemas.procedure_address,
|
||||
nativeObservationSummaryViewResultSchemas.procedure_callees,
|
||||
nativeObservationSummaryViewResultSchemas.procedure_callers,
|
||||
nativeObservationSummaryViewResultSchemas.xrefs,
|
||||
nativeObservationSummaryViewResultSchemas.resolve_containing_procedure,
|
||||
nativeObservationSummaryViewResultSchemas.get_call_graph,
|
||||
],
|
||||
@@ -412,6 +414,7 @@ const nativeObservationPageViewResultSchema = z.discriminatedUnion(
|
||||
nativeObservationPageFor("procedure_address"),
|
||||
nativeObservationPageFor("procedure_callees"),
|
||||
nativeObservationPageFor("procedure_callers"),
|
||||
nativeObservationPageFor("xrefs"),
|
||||
nativeObservationPageFor("resolve_containing_procedure"),
|
||||
nativeObservationPageFor("get_call_graph"),
|
||||
],
|
||||
|
||||
@@ -158,6 +158,7 @@ export const nativeObservationItemsSchemas = {
|
||||
procedure_address: addresses,
|
||||
procedure_callees: addresses,
|
||||
procedure_callers: addresses,
|
||||
xrefs: addresses,
|
||||
resolve_containing_procedure: resolutions,
|
||||
get_call_graph: nativeGraphViewItemsSchema,
|
||||
};
|
||||
|
||||
@@ -45,7 +45,8 @@ const readSource = (
|
||||
): Result<NativeObservation, AnalysisError> => {
|
||||
switch (parent.operation) {
|
||||
case "procedure_callees":
|
||||
case "procedure_callers": {
|
||||
case "procedure_callers":
|
||||
case "xrefs": {
|
||||
const read = readAddresses(parent);
|
||||
return read.ok
|
||||
? ok({ operation: parent.operation, result: read.value })
|
||||
@@ -138,6 +139,7 @@ const observationRows = (
|
||||
};
|
||||
case "procedure_callees":
|
||||
case "procedure_callers":
|
||||
case "xrefs":
|
||||
return {
|
||||
parent_operation: source.operation,
|
||||
total: source.result.length,
|
||||
|
||||
@@ -66,6 +66,7 @@ export const NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS = [
|
||||
export const NATIVE_OBSERVATION_OPERATIONS = [
|
||||
...NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS,
|
||||
"get_call_graph",
|
||||
"xrefs",
|
||||
] as const;
|
||||
export type NativeObservationOperation =
|
||||
(typeof NATIVE_OBSERVATION_OPERATIONS)[number];
|
||||
@@ -74,6 +75,7 @@ export const nativeObservationSourceSchemas = {
|
||||
procedure_address: z.string(),
|
||||
procedure_callees: nativeAddressListSchema,
|
||||
procedure_callers: nativeAddressListSchema,
|
||||
xrefs: nativeAddressListSchema,
|
||||
resolve_containing_procedure: containingProcedureResolutionSchema,
|
||||
get_call_graph: nativeCallGraphSchema,
|
||||
};
|
||||
|
||||
@@ -119,6 +119,33 @@ it("delivers native primitive and graph summaries with exact retained parents an
|
||||
);
|
||||
expect(rejected.isError).toBe(true);
|
||||
}
|
||||
const references = await call("xrefs", { address: "0x401000" });
|
||||
const source = {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: references.evidence_id,
|
||||
};
|
||||
const before = harness.calls.length;
|
||||
const page = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: { kind: "native-observation", offset: 0, limit: 1 },
|
||||
});
|
||||
expect(page.normalized_result).toMatchObject({
|
||||
parent_evidence_id: references.evidence_id,
|
||||
parent_operation: "xrefs",
|
||||
items: {
|
||||
type: "addresses",
|
||||
rows: [{ source_index: 0, address: "0x401001" }],
|
||||
},
|
||||
coverage: { total: 1, exhausted: true },
|
||||
});
|
||||
const caveats = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: { kind: "native-context", facet: "limitations" },
|
||||
});
|
||||
expect(caveats.normalized_result).toMatchObject({
|
||||
parent_operation: "xrefs",
|
||||
});
|
||||
expect(harness.calls).toHaveLength(before);
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
@@ -177,9 +204,12 @@ it("follows native overflow continuations through MCP without repeating provider
|
||||
for (const name of [
|
||||
...NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS,
|
||||
"get_call_graph",
|
||||
"xrefs",
|
||||
] as const) {
|
||||
const input =
|
||||
name === "get_call_graph" || name === "resolve_containing_procedure"
|
||||
name === "get_call_graph" ||
|
||||
name === "resolve_containing_procedure" ||
|
||||
name === "xrefs"
|
||||
? { address: "0x1000" }
|
||||
: { procedure: "fixture_main" };
|
||||
const complete = await client.callTool({ name, arguments: input });
|
||||
|
||||
Reference in new issue
Block a user