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:
parent
0c6c571a01
commit
23dc8fbf83
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue