fix(kuna): say why Kuna cannot answer instead of a generic message (#4113)

Kuna capability gaps such as "no code at this address" or "no function node
at this address" were built without a user message, so MCP and the CLI
showed only "This analysis is unavailable for the current target. Choose
another analysis or target." and kept the reason in details. One Kuna
helper now presents the reason as the message for every such gap, and the
unsupported-operation reason names call relationships and xrefs, which Kuna
now answers.

Refs #1957

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-15 07:37:20 -07:00
committed by GitHub
parent cd64e24ff2
commit f7250fe35f
6 changed files with 68 additions and 42 deletions
+2 -3
View File
@@ -4,7 +4,6 @@ import { join } from "node:path";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
@@ -19,6 +18,7 @@ import {
} from "./KunaProjectFiles.js";
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** The exact producing range for one facet, including a failed C block. */
export interface KunaProjectFileSpan {
@@ -134,8 +134,7 @@ export class KunaProjectReader {
const span = index.value.code.get(address);
if (span === undefined)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
kunaUnavailable(
operation,
`Kuna's export lists no code at ${address}; import slots and data entries have no listing.`,
),
+4 -19
View File
@@ -8,10 +8,7 @@ import type {
ProviderAvailability,
ProviderTargetSupport,
} from "../application/AnalysisProvider.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../domain/analysisErrorCore.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import {
createAnalysisProfile,
type AnalysisProfileCommitment,
@@ -33,6 +30,7 @@ import {
import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js";
import { KunaRunSlots } from "./KunaRunSlots.js";
import { KunaSessionClient } from "./KunaSessionClient.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** Architectures REA verified Kuna on: x86-64 and AArch64 ELF and Mach-O, plus ARM Thumb. */
const VERIFIED_ARCHITECTURES: ReadonlySet<string> = new Set([
@@ -169,14 +167,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
if (!installation.ok)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
"open_binary",
installation.error.reason,
{ userMessage: installation.error.reason },
),
);
return err(kunaUnavailable("open_binary", installation.error.reason));
const { value } = installation;
return ok({
profile: createAnalysisProfile(
@@ -218,13 +209,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
if (!configuration.ok)
return {
execute: async (operation) =>
err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
operation,
configuration.error.reason,
),
),
err(kunaUnavailable(operation, configuration.error.reason)),
close: async () => ok(null),
};
return new KunaSessionClient(target, profile, {
+2 -3
View File
@@ -1,7 +1,6 @@
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import type { JsonValue } from "../domain/jsonValue.js";
@@ -21,6 +20,7 @@ import {
} from "./KunaReferenceFiles.js";
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
import type { KunaPendingQuery, KunaQueryWork } from "./KunaQueryWork.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** Charge raw JSON bytes conservatively; retained projections are smaller. */
const XREF_CACHE_BYTES = 16 * 1024 * 1024;
@@ -158,8 +158,7 @@ export class KunaReferenceQueries {
const node = graph.nodes.get(address);
if (node === undefined)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
kunaUnavailable(
operation,
`Kuna's graph has no function node at ${address}; its relationships are unknown.`,
),
+4 -17
View File
@@ -69,6 +69,7 @@ import {
KunaProjectReader,
type KunaProjectFileSpan,
} from "./KunaProjectReader.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** How often a request waiting on the project export reports that it still waits. */
const WAIT_PROGRESS_INTERVAL_MS = 10_000;
@@ -278,13 +279,7 @@ export class KunaSessionClient implements AnalysisClient {
options: ExecutionOptions = {},
): Promise<Result<AnalysisExecution, AnalysisError>> {
if (this.#closing)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
operation,
"This Kuna session is closed.",
),
);
return err(kunaUnavailable(operation, "This Kuna session is closed."));
return this.#own(
this.#execute(operation, parameters, {
...options,
@@ -312,8 +307,7 @@ export class KunaSessionClient implements AnalysisClient {
return err(new AnalysisCancelledError(operation));
if (operation !== "health" && !isKunaOperation(operation))
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
kunaUnavailable(
operation,
"Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.",
),
@@ -1229,14 +1223,7 @@ export class KunaSessionClient implements AnalysisClient {
this.#installation ??= inspectKunaInstallation(this.#environment).then(
(inspected): Result<KunaInstallation, AnalysisError> => {
if (!inspected.ok)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
"health",
inspected.error.reason,
{ userMessage: inspected.error.reason },
),
);
return err(kunaUnavailable("health", inspected.error.reason));
const committed = this.#profile?.parameters["kuna_sha256"];
if (committed !== undefined && committed !== inspected.value.sha256)
return err(
+18
View File
@@ -0,0 +1,18 @@
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js";
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
/**
* A Kuna capability gap whose reason is what the caller needs to read. Without
* a user message the projection shows only "choose another analysis or
* target", which hides facts such as "no code at this address".
*/
export const kunaUnavailable = (
operation: string,
reason: string,
options: AnalysisErrorOptions = {},
): AnalysisCapabilityUnavailableError =>
new AnalysisCapabilityUnavailableError(KUNA_PROVIDER_ID, operation, reason, {
...options,
userMessage: reason,
});
@@ -80,3 +80,41 @@ it("reports a graph beyond REA's buffer once per session instead of rebuilding i
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("says why Kuna cannot answer instead of a generic unavailable message", async () => {
const { open } = await session("rea-kuna-unavailable-reasons-");
const client = open();
try {
const cases = [
{
operation: "procedure_assembly",
parameters: { procedure: "0x2000" },
message:
"Kuna's export lists no code at 0x2000; import slots and data entries have no listing.",
},
{
operation: "procedure_callers",
parameters: { procedure: "0x1004" },
message:
"Kuna's graph has no function node at 0x1004; its relationships are unknown.",
},
{
operation: "list_segments",
parameters: {},
message:
"Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.",
},
] as const;
for (const { operation, parameters, message } of cases) {
const result = await client.execute(operation, parameters);
if (result.ok) throw new Error(`${operation} unexpectedly succeeded`);
expect(projectAnalysisError(result.error)).toMatchObject({
code: "capability_unavailable",
message,
retryable: false,
});
}
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});