fix(ida): report missing tools when a compatibility profile mismatches (#2307)
A profile mismatch reached the caller as a bare AnalysisProtocolError, which projects to `unreadable_output` with a generic message. The error already named every missing tool, but none of that detail survived, so the only way to identify the cause was to log the JSON-RPC traffic and notice the exchange stopping after `tools/list`. Throw a ProviderAdapterError instead, carrying the mode, the expected profile and the missing tool names through the diagnostics channel that already surfaces in `details`. Document which upstream versions each profile accepts, since upstream `main` now serves neither. Refs #2301 Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
1 parent
48a683ac8c
commit
beca34f522
4 files changed
+112
-6
No files matched your search
@@ -4,6 +4,15 @@ REA adapts an existing IDA MCP server into its native analysis tools. Existing I
|
||||
|
||||
Choose one lifecycle mode in an upstream MCP JSON registration. Set `REA_IDA_MCP_CONFIG` to that file, then select `ida` once through `REA_ANALYSIS_PROVIDER`, the CLI's `--provider ida`, or MCP's `open_binary.provider_id`. No provider startup occurs during discovery.
|
||||
|
||||
## Supported upstream versions
|
||||
|
||||
Each mode accepts one upstream tool profile, checked against `tools/list` before any analysis runs. Upstream's `main` branch has since moved to 2.0.0, which renamed the GUI tools and dropped `idb_close`, so it serves neither profile. Installing from that branch fails at startup with the missing tool names in the error diagnostics.
|
||||
|
||||
| Mode | Upstream profile | Known working |
|
||||
| -------- | ------------------- | ------------------------------------------------------ |
|
||||
| attached | legacy GUI tools | 1.4.0 |
|
||||
| headless | database supervisor | builds serving `idb_open`, `idb_list`, and `idb_close` |
|
||||
|
||||
## Analyze an already-open GUI target
|
||||
|
||||
The attached profile supports upstream **1.4.0 legacy tools**, including `get_metadata`, `list_functions`, and `decompile_function`. Copy the upstream registration's `command`, `args`, and optional `env` into a JSON file. An existing `mcpServers` object with an `ida-pro-mcp` entry is also accepted. For example:
|
||||
|
||||
@@ -31,6 +31,7 @@ const LIVE_PEER = {
|
||||
const fixture = async (
|
||||
mode: "attached" | "headless" = "attached",
|
||||
timeoutMs = 1000,
|
||||
protocol?: "native",
|
||||
) => {
|
||||
const { root, target } = await createIdaTarget();
|
||||
roots.push(root);
|
||||
@@ -41,6 +42,7 @@ const fixture = async (
|
||||
args: [],
|
||||
env: {},
|
||||
mode,
|
||||
...(protocol === undefined ? {} : { protocol }),
|
||||
timeoutMs,
|
||||
workspaceRoot: root,
|
||||
},
|
||||
@@ -709,6 +711,74 @@ describe("headless IDA non-busy health payloads", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("IDA compatibility profile mismatch", () => {
|
||||
it("reports the native profile for an incompatible catalog", async () => {
|
||||
const { producer, client } = await fixture("attached", 1000, "native");
|
||||
producer.tools = [];
|
||||
const result = await client.execute("health", {});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected incompatible native catalog");
|
||||
expect(projectAnalysisError(result.error).details).toMatchObject({
|
||||
diagnostics: {
|
||||
mode: "attached",
|
||||
expected_profile: "native",
|
||||
missing_tools: expect.arrayContaining(["decompile"]),
|
||||
},
|
||||
});
|
||||
expect(producer.calls).toEqual([]);
|
||||
expect((await client.close()).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("names every missing tool so the caller can identify the upstream profile", async () => {
|
||||
const { producer, client } = await fixture("headless");
|
||||
producer.tools = [
|
||||
"idb_open",
|
||||
"idb_list",
|
||||
"server_health",
|
||||
"list_funcs",
|
||||
"lookup_funcs",
|
||||
"find_regex",
|
||||
"decompile",
|
||||
"disasm",
|
||||
"callees",
|
||||
"xrefs_to",
|
||||
];
|
||||
const result = await client.execute("health", {});
|
||||
expect(result.ok, "an incomplete profile cannot serve analysis").toBe(
|
||||
false,
|
||||
);
|
||||
if (result.ok) return;
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(
|
||||
projected.details,
|
||||
"the absent tool and the expected profile are both reported",
|
||||
).toMatchObject({
|
||||
provider_id: "ida",
|
||||
diagnostics: {
|
||||
mode: "headless",
|
||||
expected_profile: "database-supervisor",
|
||||
missing_tools: ["idb_close"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the legacy profile when an attached producer serves modern tools", async () => {
|
||||
const { producer, client } = await fixture("attached");
|
||||
producer.tools = ["server_health", "lookup_funcs", "decompile", "disasm"];
|
||||
const result = await client.execute("health", {});
|
||||
expect(result.ok, "an incomplete profile cannot serve analysis").toBe(
|
||||
false,
|
||||
);
|
||||
if (result.ok) return;
|
||||
expect(
|
||||
projectAnalysisError(result.error).details,
|
||||
"attached mode names the legacy profile it expected",
|
||||
).toMatchObject({
|
||||
diagnostics: { mode: "attached", expected_profile: "legacy-1.4" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("headless IDA lifecycle boundaries", () => {
|
||||
it("never closes an unowned worker even when open echoes the requested private identity", async () => {
|
||||
const { producer, client } = await fixture("headless");
|
||||
|
||||
@@ -483,10 +483,27 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
? legacyTools
|
||||
: modernTools;
|
||||
const missing = expected.filter((name) => !tools.has(name));
|
||||
if (missing.length > 0)
|
||||
throw new AnalysisProtocolError(
|
||||
`IDA MCP ${this.config.protocol === "native" ? "native attached" : this.config.mode} compatibility profile is missing tools: ${missing.join(", ")}. Native attached mode requires the native IDA MCP tool catalog; legacy attached mode requires the legacy 1.4 profile; headless mode requires the database supervisor profile. See docs/ida-provider.md.`,
|
||||
);
|
||||
if (missing.length > 0) {
|
||||
const summary = `IDA MCP ${this.config.protocol === "native" ? "native attached" : this.config.mode} compatibility profile is missing tools: ${missing.join(", ")}. Native attached mode requires the native IDA MCP tool catalog; legacy attached mode requires the legacy 1.4 profile; headless mode requires the database supervisor profile. See docs/ida-provider.md.`;
|
||||
// The caller cannot act on a profile mismatch it never sees, so the
|
||||
// missing names travel as adapter diagnostics rather than a bare message.
|
||||
throw new ProviderAdapterError("ida", "initialize", {
|
||||
userMessage: summary,
|
||||
diagnostics: {
|
||||
reason: summary,
|
||||
mode: this.config.mode,
|
||||
expected_profile:
|
||||
this.config.protocol === "native"
|
||||
? "native"
|
||||
: this.config.mode === "attached"
|
||||
? "legacy-1.4"
|
||||
: "database-supervisor",
|
||||
missing_tools: [...missing],
|
||||
recovery:
|
||||
"Install an IDA MCP build that serves the selected profile; see docs/ida-provider.md.",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (this.config.mode === "headless") {
|
||||
this.#workspace = await IdaWorkspace.allocate(
|
||||
this.target,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
|
||||
import { IdaProvider } from "../../src/ida/IdaProvider.js";
|
||||
import { createIdaTarget, RecordingIdaMcp } from "../fixtures/idaMcp.js";
|
||||
import { createTestBinarySession } from "../fixtures/binarySession.js";
|
||||
@@ -157,12 +158,21 @@ describe("IDA provider composition", () => {
|
||||
expect(analyzed).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisProtocolError",
|
||||
message: expect.stringContaining(
|
||||
_tag: "ProviderAdapterError",
|
||||
userMessage: expect.stringContaining(
|
||||
"headless compatibility profile is missing tools: idb_open",
|
||||
),
|
||||
},
|
||||
});
|
||||
if (analyzed.ok) throw new Error("Expected incompatible IDA profile");
|
||||
expect(projectAnalysisError(analyzed.error).details).toMatchObject({
|
||||
provider_id: "ida",
|
||||
diagnostics: {
|
||||
mode: "headless",
|
||||
expected_profile: "database-supervisor",
|
||||
missing_tools: expect.arrayContaining(["idb_open"]),
|
||||
},
|
||||
});
|
||||
expect(producer.connects).toBe(1);
|
||||
expect((await session.close()).ok).toBe(true);
|
||||
expect(producer.closes).toBe(1);
|
||||
|
||||
Reference in new issue
Block a user