mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b163a039 | ||
|
|
fa3652bb09 | ||
|
|
65939bc4ad | ||
|
|
1d56162834 | ||
|
|
f07595fa1a | ||
|
|
479e9b7168 |
No files matched your search
@@ -28,6 +28,10 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
|
||||
import {
|
||||
KUNA_XREF_MISSING_NAME_REMEDIATION,
|
||||
missingKunaXrefNameMessage,
|
||||
} from "../dist/kuna/KunaSelectorGuidance.js";
|
||||
import {
|
||||
requireMcpOperationResult,
|
||||
requireMcpToolError,
|
||||
@@ -233,13 +237,13 @@ const openServer = async (provider, kunaSettings = {}) => {
|
||||
structured: result.structuredContent,
|
||||
};
|
||||
};
|
||||
const callError = async (name, arguments_ = {}) => {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 3_600_000 },
|
||||
const callError = async (name, arguments_ = {}) =>
|
||||
requireMcpToolError(
|
||||
await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 3_600_000 },
|
||||
),
|
||||
);
|
||||
return requireMcpToolError(result);
|
||||
};
|
||||
const close = async (priorFailure) => {
|
||||
clearInterval(timer);
|
||||
await sample();
|
||||
@@ -409,6 +413,44 @@ const verifyKuna = async (
|
||||
}),
|
||||
});
|
||||
const listed = await cold.call("list_procedures");
|
||||
if (verifyNamedTrace) {
|
||||
const missing = await cold.callError("xrefs", {
|
||||
address: "rea_missing_xref_inventory_name",
|
||||
});
|
||||
assert.equal(missing.code, "invalid_request");
|
||||
assert.equal(
|
||||
missing.message,
|
||||
missingKunaXrefNameMessage("rea_missing_xref_inventory_name"),
|
||||
);
|
||||
assert.equal(
|
||||
missing.remediation.action,
|
||||
KUNA_XREF_MISSING_NAME_REMEDIATION,
|
||||
);
|
||||
assert.ok(missing.message.includes("rea_missing_xref_inventory_name"));
|
||||
assert.equal(missing.recovery, undefined);
|
||||
const procedure = await cold.callError("procedure_address", {
|
||||
procedure: "rea_missing_procedure_name",
|
||||
});
|
||||
assert.equal(procedure.code, "invalid_request");
|
||||
assert.equal(procedure.recovery, undefined);
|
||||
assert.match(procedure.remediation.action, /search_procedures/u);
|
||||
assert.ok(
|
||||
procedure.details.issues.some(
|
||||
(issue) => issue.observed?.procedure === "rea_missing_procedure_name",
|
||||
),
|
||||
);
|
||||
const missingAddress = await cold.callError("procedure_address", {
|
||||
procedure: "0x00000001",
|
||||
});
|
||||
assert.equal(missingAddress.code, "invalid_request");
|
||||
assert.equal(missingAddress.recovery, undefined);
|
||||
assert.match(missingAddress.remediation.action, /list_procedures/u);
|
||||
assert.ok(
|
||||
missingAddress.details.issues.some(
|
||||
(issue) => issue.observed?.procedure === "0x00000001",
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const [name, address] of functions) {
|
||||
assert.ok(
|
||||
listed.value.some((entry) => entry.address === address),
|
||||
|
||||
@@ -71,7 +71,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
|
||||
),
|
||||
enhanced(
|
||||
"find_xrefs_to_name",
|
||||
"Resolve an exact name against the bound provider's name inventory and return a resolved or unresolved result. Unresolved names use the stable name_not_found reason; this xref workflow returns address-only projections.",
|
||||
"Resolve an exact name against the bound provider's name inventory and return a resolved or unresolved result. Unresolved names use the stable name_not_found reason; this xref workflow returns address-only projections. Kuna lacks this general name inventory; xrefs can instead resolve names present in Kuna's inventory, but unlisted names may still exist in the target.",
|
||||
enhancedInputSchemas.find_xrefs_to_name,
|
||||
),
|
||||
enhanced(
|
||||
|
||||
@@ -261,7 +261,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. 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.",
|
||||
"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. Kuna also accepts names in its own inventory, including data names; an unlisted name may still exist in the target. 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[];
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/** Recovery for an xref name absent from Kuna's own inventory. */
|
||||
export const KUNA_XREF_MISSING_NAME_REMEDIATION =
|
||||
"Inspect target symbols or exports where available, verify address coordinates, and call xrefs with an address. Absence from Kuna's inventory does not establish absence from the target.";
|
||||
|
||||
/** Recovery for a name with multiple observed inventory entries. */
|
||||
export const KUNA_XREF_AMBIGUOUS_NAME_REMEDIATION =
|
||||
"Select one of the reported addresses and call xrefs again.";
|
||||
|
||||
export const KUNA_PROCEDURE_MISSING_NAME_REMEDIATION =
|
||||
"Use search_procedures or list_procedures to inspect Kuna's current inventory. An absent name does not establish that the function is absent from the target.";
|
||||
|
||||
export const KUNA_PROCEDURE_MISSING_ADDRESS_REMEDIATION =
|
||||
"Use list_procedures or search_procedures to inspect Kuna's current inventory and verify the selected address. An absent entry does not establish that the function is absent from the target; REA cannot use it as a Kuna procedure in this session.";
|
||||
|
||||
export const KUNA_PROCEDURE_AMBIGUOUS_NAME_REMEDIATION =
|
||||
"Select one of the reported exact addresses and retry the operation.";
|
||||
|
||||
export const missingKunaXrefNameMessage = (selector: string): string =>
|
||||
`Kuna found no inventory entry named ${selector}.`;
|
||||
|
||||
export const ambiguousKunaNameMessage = (
|
||||
selector: string,
|
||||
addresses: readonly string[],
|
||||
): string =>
|
||||
`${selector} names ${String(addresses.length)} Kuna entries (${addresses.join(", ")}); select one by address.`;
|
||||
@@ -95,6 +95,15 @@ import { KunaReferenceQueries } from "./KunaReferenceQueries.js";
|
||||
import { KunaQueryWork } from "./KunaQueryWork.js";
|
||||
import { KunaStringQueries } from "./KunaStringQueries.js";
|
||||
import { normalizeKunaAddress } from "./KunaAddress.js";
|
||||
import {
|
||||
ambiguousKunaNameMessage,
|
||||
KUNA_PROCEDURE_AMBIGUOUS_NAME_REMEDIATION,
|
||||
KUNA_PROCEDURE_MISSING_ADDRESS_REMEDIATION,
|
||||
KUNA_PROCEDURE_MISSING_NAME_REMEDIATION,
|
||||
KUNA_XREF_AMBIGUOUS_NAME_REMEDIATION,
|
||||
KUNA_XREF_MISSING_NAME_REMEDIATION,
|
||||
missingKunaXrefNameMessage,
|
||||
} from "./KunaSelectorGuidance.js";
|
||||
import type { RegexMatchingDeadline } from "../config/regexMatchingDeadline.js";
|
||||
import type { RegexWorkerHeapRequest } from "../config/regexWorkerHeapRequest.js";
|
||||
import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js";
|
||||
@@ -1139,12 +1148,23 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
const entry = index.byAddress.get(address);
|
||||
if (entry === undefined)
|
||||
return err(
|
||||
new AnalysisInputError(operation, {
|
||||
userMessage: `${address} is not a procedure entry Kuna found; list_procedures shows the entries it found.`,
|
||||
conformsToInputContract: true,
|
||||
remediationAction:
|
||||
"Inspect list_procedures and select a discovered entry if it answers your question. An unlisted address is not proof of a function; REA cannot use it as a Kuna procedure in this session. Do not repeat the unchanged call.",
|
||||
}),
|
||||
new AnalysisInputError(
|
||||
operation,
|
||||
{
|
||||
userMessage: `${address} is not a procedure entry Kuna found; list_procedures shows the entries it found.`,
|
||||
remediationAction: KUNA_PROCEDURE_MISSING_ADDRESS_REMEDIATION,
|
||||
conformsToInputContract: true,
|
||||
},
|
||||
[
|
||||
{
|
||||
path: [field],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"The address was not found in Kuna's procedure inventory.",
|
||||
observed: { [field]: selector },
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
return refused(entry) ? dataRefusal(entry) : selected(entry);
|
||||
}
|
||||
@@ -1154,26 +1174,52 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
return selected(matches[0]);
|
||||
if (matches.length === 0 && named.length === 1 && named[0] !== undefined)
|
||||
return dataRefusal(named[0]);
|
||||
const missing = named.length === 0;
|
||||
const ambiguous = matches.length > 1;
|
||||
const procedureLookupFailure =
|
||||
field === "procedure" && (missing || ambiguous);
|
||||
const message = missing
|
||||
? field === "address"
|
||||
? missingKunaXrefNameMessage(selector)
|
||||
: `Kuna found no procedure named ${selector}; search_procedures finds similar names.`
|
||||
: matches.length === 0
|
||||
? `${selector} names ${String(named.length)} Kuna entries that lie in Mach-O data regions (${named.map(({ address }) => address).join(", ")}); they are data, not procedures.`
|
||||
: ambiguousKunaNameMessage(
|
||||
selector,
|
||||
matches.map(({ address }) => address),
|
||||
);
|
||||
const remediationAction =
|
||||
field === "address"
|
||||
? missing
|
||||
? KUNA_XREF_MISSING_NAME_REMEDIATION
|
||||
: KUNA_XREF_AMBIGUOUS_NAME_REMEDIATION
|
||||
: procedureLookupFailure
|
||||
? missing
|
||||
? KUNA_PROCEDURE_MISSING_NAME_REMEDIATION
|
||||
: KUNA_PROCEDURE_AMBIGUOUS_NAME_REMEDIATION
|
||||
: matches.length === 0
|
||||
? "Inspect the data addresses with address_name or xrefs, or select a procedure entry from list_procedures. Do not repeat the unchanged call."
|
||||
: undefined;
|
||||
return err(
|
||||
new AnalysisInputError(operation, {
|
||||
conformsToInputContract: true,
|
||||
remediationAction:
|
||||
matches.length === 0 && named.length === 0
|
||||
? field === "procedure"
|
||||
? "Use search_procedures or list_procedures to inspect Kuna's discovered entries, then select a matching entry. Do not repeat the unchanged call."
|
||||
: "Inspect the selected target for the intended address and use that address explicitly. Do not repeat the unchanged call."
|
||||
: matches.length === 0
|
||||
? "Inspect the data addresses with address_name or xrefs, or select a procedure entry from list_procedures. Do not repeat the unchanged call."
|
||||
: "Select the intended entry by its exact address. Do not repeat the unchanged call.",
|
||||
userMessage:
|
||||
matches.length === 0 && named.length === 0
|
||||
? field === "procedure"
|
||||
? `Kuna found no procedure named ${selector}; search_procedures finds similar names.`
|
||||
: `Kuna found no inventory entry named ${selector}.`
|
||||
: matches.length === 0
|
||||
? `${selector} names ${String(named.length)} Kuna entries that lie in Mach-O data regions (${named.map(({ address }) => address).join(", ")}); they are data, not procedures.`
|
||||
: `${selector} names ${String(matches.length)} Kuna entries (${matches.map(({ address }) => address).join(", ")}); select one by address.`,
|
||||
}),
|
||||
new AnalysisInputError(
|
||||
operation,
|
||||
{
|
||||
userMessage: message,
|
||||
...(remediationAction === undefined
|
||||
? {}
|
||||
: { remediationAction, conformsToInputContract: true }),
|
||||
},
|
||||
procedureLookupFailure
|
||||
? [
|
||||
{
|
||||
path: [field],
|
||||
reason: "invalid_value",
|
||||
message: "The selector did not identify one procedure entry.",
|
||||
observed: { procedure: selector },
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { expect, it as test } from "vitest";
|
||||
|
||||
import { functionDossierSchema } from "../../../../src/domain/native/functionDossier.js";
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import {
|
||||
ambiguousKunaNameMessage,
|
||||
KUNA_XREF_AMBIGUOUS_NAME_REMEDIATION,
|
||||
KUNA_XREF_MISSING_NAME_REMEDIATION,
|
||||
missingKunaXrefNameMessage,
|
||||
} from "../../../../src/kuna/KunaSelectorGuidance.js";
|
||||
import { fakeKunaCalls } from "./fakeKuna.js";
|
||||
import {
|
||||
MACHO_FUNCTIONS,
|
||||
@@ -165,6 +172,76 @@ it("resolves a data symbol's name for xrefs, which accept data addresses", async
|
||||
}
|
||||
});
|
||||
|
||||
it("guides valid ambiguous and missing procedure selectors without schema repair", async () => {
|
||||
const { open } = await machoSession("rea-kuna-procedure-recovery-");
|
||||
const client = open();
|
||||
try {
|
||||
for (const [procedure, expected] of [
|
||||
["puts", "address"],
|
||||
["missing_procedure", "search_procedures"],
|
||||
["0x00000001", "list_procedures"],
|
||||
] as const) {
|
||||
const result = await client.execute("procedure_address", { procedure });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected unresolved procedure");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.recovery).toBeUndefined();
|
||||
expect(projected.remediation.action).toContain(expected);
|
||||
expect(projected.details?.issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ observed: { procedure } }),
|
||||
]),
|
||||
);
|
||||
if (procedure === "puts") {
|
||||
expect(projected.message).toContain("0x100000480");
|
||||
expect(projected.message).toContain("0x100004000");
|
||||
}
|
||||
}
|
||||
expect(
|
||||
(await run(client, "procedure_address", { procedure: "helper" })).result,
|
||||
).toBe("0x100000440");
|
||||
} finally {
|
||||
expect(await client.close()).toEqual({ ok: true, value: null });
|
||||
}
|
||||
});
|
||||
|
||||
it("reports missing and ambiguous xref names within Kuna's inventory", async () => {
|
||||
const { open } = await machoSession("rea-kuna-macho-xref-recovery-");
|
||||
const client = open();
|
||||
try {
|
||||
const missing = await client.execute("xrefs", { address: "missing_data" });
|
||||
expect(missing.ok).toBe(false);
|
||||
if (missing.ok) throw new Error("Expected missing inventory name");
|
||||
expect(projectAnalysisError(missing.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
message: missingKunaXrefNameMessage("missing_data"),
|
||||
remediation: { action: KUNA_XREF_MISSING_NAME_REMEDIATION },
|
||||
});
|
||||
expect(projectAnalysisError(missing.error).recovery).toBeUndefined();
|
||||
expect(projectAnalysisError(missing.error).message).toContain(
|
||||
"missing_data",
|
||||
);
|
||||
|
||||
const ambiguous = await client.execute("xrefs", { address: "puts" });
|
||||
expect(ambiguous.ok).toBe(false);
|
||||
if (ambiguous.ok) throw new Error("Expected ambiguous inventory name");
|
||||
expect(projectAnalysisError(ambiguous.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
message: ambiguousKunaNameMessage("puts", ["0x100000480", "0x100004000"]),
|
||||
remediation: { action: KUNA_XREF_AMBIGUOUS_NAME_REMEDIATION },
|
||||
});
|
||||
expect(projectAnalysisError(ambiguous.error).message).toContain(
|
||||
"0x100000480",
|
||||
);
|
||||
expect(projectAnalysisError(ambiguous.error).message).toContain(
|
||||
"0x100004000",
|
||||
);
|
||||
expect(projectAnalysisError(ambiguous.error).recovery).toBeUndefined();
|
||||
} finally {
|
||||
expect(await client.close()).toEqual({ ok: true, value: null });
|
||||
}
|
||||
});
|
||||
|
||||
it("classifies the published export's inventory with the section table it read", async () => {
|
||||
const { kuna, open } = await machoSession("rea-kuna-macho-warm-");
|
||||
const cold = open();
|
||||
|
||||
Reference in new issue
Block a user