Compare commits

...
6 changed files with 223 additions and 33 deletions

No files matched your search

+48 -6
View File
@@ -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),
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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[];
+25
View File
@@ -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.`;
+71 -25
View File
@@ -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();