fix(application): accept alternative document selection in binary_ove… (#2680)

* fix(application): accept alternative document selection in binary_overview (#2460)

Update binary_overview capability negotiation so that either current_document or list_documents satisfies the document identity requirement, matching resolveAnalysisDocument runtime semantics. When a provider declares neither document capability, resolveAnalysisDocument reports AnalysisCapabilityUnavailableError for current_document rather than leaking the internal list_documents fallback. Preserve list_segments as an independently mandatory requirement.

* fix(application): retain document fallback provider errors

Return the fallback producer error unchanged, retaining operation identity, retry advice, diagnostics and cleanup instead of fabricating missing capabilities.

Source inspection and git diff --check; tests/builds/typechecks/CI reruns skipped at explicit user request.

* test(application): retain document fallback error expectations

Composition and MCP controls now expect the original list_documents producer error and reason.

Source inspection and git diff --check only. Tests, builds and typechecks skipped by explicit user request. GitHub review writes paused globally.

* fix(ida): assert attached-mode routing failure

---------

Co-authored-by: yashas1624 <yashas1624@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
yashas s 2026-10-14 00:41:02 +05:30 committed by GitHub
parent 0c6c571a01
commit 23dc8fbf83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 227 additions and 21 deletions

View File

@ -498,6 +498,106 @@ it("reports host denial before target mismatch but keeps composed workflow prere
).toBe("target_required");
});
describe("binary_overview capability requirements", () => {
it("admits a provider with current_document and other mandatory capabilities", () => {
const capabilities = [
"list_segments",
"current_document",
"list_procedures",
"list_strings",
].map((operation) => ({ operation, available: true, reason: null }));
expect(
entry(
"binary_overview",
status({ open: true, kind: "executable", capabilities }),
),
).toMatchObject({
available: true,
reason: "available",
remediation: null,
});
});
it("admits a provider with list_documents and other mandatory capabilities", () => {
const capabilities = [
"list_segments",
"list_documents",
"list_procedures",
"list_strings",
].map((operation) => ({ operation, available: true, reason: null }));
expect(
entry(
"binary_overview",
status({ open: true, kind: "executable", capabilities }),
),
).toMatchObject({
available: true,
reason: "available",
remediation: null,
});
});
it("reports document selection remediation when neither current_document nor list_documents is declared", () => {
const capabilities = [
"list_segments",
"list_procedures",
"list_strings",
].map((operation) => ({ operation, available: true, reason: null }));
expect(
entry(
"binary_overview",
status({ open: true, kind: "executable", capabilities }),
),
).toMatchObject({
available: false,
reason: "provider_missing",
remediation:
"Configure a provider for one of: current_document, list_documents.",
});
});
it("reports missing segment enumeration independently when segments are missing", () => {
const capabilities = [
"current_document",
"list_procedures",
"list_strings",
].map((operation) => ({ operation, available: true, reason: null }));
expect(
entry(
"binary_overview",
status({ open: true, kind: "executable", capabilities }),
),
).toMatchObject({
available: false,
reason: "provider_missing",
remediation: "Configure a provider for required operation list_segments.",
});
});
it("reports provider_unavailable when declared document selection capability is unavailable", () => {
const capabilities = [
{ operation: "list_segments", available: true, reason: null },
{
operation: "current_document",
available: false,
reason: "Document view is closed",
},
{ operation: "list_procedures", available: true, reason: null },
{ operation: "list_strings", available: true, reason: null },
];
expect(
entry(
"binary_overview",
status({ open: true, kind: "executable", capabilities }),
),
).toMatchObject({
available: false,
reason: "provider_unavailable",
remediation: "Document view is closed",
});
});
});
describe("trace_feature requirements", () => {
const capabilities = (operations: readonly string[]) =>
operations.map((operation) => ({

View File

@ -98,7 +98,9 @@ type AvailabilityContext = {
readonly policy: AvailabilityPolicy;
};
const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
const ENHANCED_REQUIREMENTS: Readonly<
Record<string, readonly (string | readonly string[])[]>
> = {
trace_native_values: ["analyze_function", "resolve_native_call_targets"],
trace_native_ui_action: [
"decode_interface_builder",
@ -114,7 +116,7 @@ const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
find_xrefs_to_name: ["list_names", "xrefs"],
binary_overview: [
"list_segments",
"list_documents",
["current_document", "list_documents"],
"list_procedures",
"list_strings",
],
@ -642,27 +644,62 @@ const composedAvailability = (
};
const composedAvailabilityFor = (
requirements: readonly string[],
requirements: readonly (string | readonly string[])[],
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
const missing = requirements.find((operation) => !descriptors.has(operation));
if (missing !== undefined)
return {
reason: "provider_missing",
remediation: `Configure a provider for required operation ${missing}.`,
};
const unavailable = requirements
.map((operation) => descriptors.get(operation))
.find((descriptor) => descriptor?.available === false);
if (unavailable !== undefined)
return {
reason:
unavailable.availability_code === "unsupported_host"
? "unsupported_host"
: "provider_unavailable",
remediation:
unavailable.reason ?? "Choose another target or configured provider.",
};
for (const requirement of requirements) {
if (typeof requirement === "string") {
if (!descriptors.has(requirement))
return {
reason: "provider_missing",
remediation: `Configure a provider for required operation ${requirement}.`,
};
} else {
const declared = requirement.some((operation) =>
descriptors.has(operation),
);
if (!declared)
return {
reason: "provider_missing",
remediation: `Configure a provider for one of: ${requirement.join(", ")}.`,
};
}
}
for (const requirement of requirements) {
if (typeof requirement === "string") {
const descriptor = descriptors.get(requirement);
if (descriptor?.available === false)
return {
reason:
descriptor.availability_code === "unsupported_host"
? "unsupported_host"
: "provider_unavailable",
remediation:
descriptor.reason ??
"Choose another target or configured provider.",
};
} else {
const anyAvailable = requirement.some(
(operation) => descriptors.get(operation)?.available === true,
);
if (!anyAvailable) {
const unavailable = requirement
.map((operation) => descriptors.get(operation))
.find((descriptor) => descriptor?.available === false);
if (unavailable !== undefined)
return {
reason:
unavailable.availability_code === "unsupported_host"
? "unsupported_host"
: "provider_unavailable",
remediation:
unavailable.reason ??
"Choose another target or configured provider.",
};
}
}
}
return { reason: "available", remediation: null };
};

View File

@ -313,4 +313,40 @@ describe("overview document identity through MCP", () => {
.isError,
).toBe(true);
});
it("reports document selection unavailable when neither current-document nor list-documents is supported", async () => {
const client = await connect({
execute: (name) => {
if (name === "current_document" || name === "list_documents")
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
"fixture",
name,
"provider lacks document selection",
),
),
);
if (name === "list_segments") return Promise.resolve(ok([]));
if (name === "list_procedures" || name === "list_strings")
return Promise.resolve(ok([]));
throw new Error(`Unexpected analysis operation: ${name}`);
},
});
const result = await client.callTool({
name: "binary_overview",
arguments: {},
});
expect(result.isError).toBe(true);
expect(parseMcpToolError(result).error).toMatchObject({
code: "capability_unavailable",
details: {
provider_id: "fixture",
operation: "list_documents",
reason: expect.stringContaining(
"provider lacks document selection",
),
},
});
});
});

View File

@ -14,6 +14,7 @@ import {
import { createEvidence } from "../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
import { EnhancedTools } from "../../src/application/EnhancedTools.js";
import { IDA_PROVIDER_IDENTITY } from "../../src/ida/IdaProviderCapabilities.js";
const roots: string[] = [];
@ -183,3 +184,35 @@ describe("IDA provider composition", () => {
expect(producer.closes).toBe(1);
});
});
describe("IDA binary_overview execution", () => {
it("reports document selection unavailable when binary_overview executes against an IDA session", async () => {
const { root, target } = await createIdaTarget();
roots.push(root);
const path = join(root, "registration.json");
await writeFile(path, JSON.stringify({ command: "fixture" }));
const config = parseConfig({ REA_IDA_MCP_CONFIG: path });
if (!config.ok) throw config.error;
const producer = new RecordingIdaMcp(target);
const session = createTestBinarySession(
new IdaProvider(config.value, {}, () => producer),
);
expect((await session.open(target.path)).ok).toBe(true);
const tools = new EnhancedTools(session);
const overview = await tools.execute("binary_overview", {});
expect(overview).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCapabilityUnavailableError",
providerId: "ida",
operation: "list_documents",
message: expect.stringContaining(
"operation is not declared by this provider",
),
},
});
const procedures = await session.execute("list_procedures", {});
expect(procedures.ok).toBe(true);
expect((await session.close()).ok).toBe(true);
});
});