[GH-ISSUE #3930] [Bug] Explicit Ghidra profile refusal omits provider-selection diagnostics #1537

Closed
opened 2026-10-16 21:06:51 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3930

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Provider or capability discovery

Observed behavior

Observed through the registered MCP tool, the response has code unsupported_target, operation resolve_analysis_profile and reason REA_GHIDRA_LANGUAGE_ID cannot override the admitted raw-binary import. Its details omit selection_reason, requested_provider_id, candidate_ids and rejections. The provider client-start count stays zero.

The initial verification ran this refusal once alongside two passing controls. Reporting unsupported during initial provider support inspection returned capability_unavailable with target_unsupported selection details. Removing the language override from the same ARM raw input opened successfully without starting a provider client. The standalone reproduction above also failed at the same caller-visible assertion. These observations concern simulated installation readiness and the production provider's profile refusal, not real Ghidra analysis.

Reproduction

From the baseline checkout with its locked dependencies already installed, save this temporary test as tests/boundary/mcp/profileRefusal.test.ts. It uses production configuration parsing, raw target admission, provider profile resolution, session routing and MCP error delivery. The existing installation-discovery fake supplies readiness without launching an engine.

import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
import { parseConfig } from "../../../src/config/parseConfig.js";
import { GhidraProvider } from "../../../src/ghidra/GhidraProvider.js";
import { silentLogger } from "../../../src/logger.js";
import { createServer } from "../../../src/server/createServer.js";
import { ghidraInstallationHost } from "./ghidraMcpHarness.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";

it("retains typed selection details when the selected provider refuses a raw profile", async () => {
  const directory = await mkdtemp(join(process.env.REA_VERIFY_ROOT!, "case-"));
  const path = join(directory, "image.bin");
  await writeFile(path, Buffer.from([0, 0, 160, 225]));
  const config = parseConfig({
    GHIDRA_INSTALL_DIR: "/opt/ghidra_12.1.4_PUBLIC",
    REA_GHIDRA_LANGUAGE_ID: "ARM:LE:32:v7",
  });
  if (!config.ok) throw config.error;
  let clients = 0;
  const provider = new GhidraProvider(config.value, silentLogger, {}, ghidraInstallationHost(), () => {
    clients++;
    throw new Error("Unexpected engine startup");
  });
  expect(provider.inspectAvailability().status).toBe("available");
  const session = composeBinarySession(new AnalysisProviderRegistry([provider]));
  const server = createServer({ kind: "session", session }, { logger: silentLogger });
  const mcp = new Client({ name: "profile-refusal-repro", version: "1.0.0" });
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  try {
    await server.connect(serverTransport);
    await mcp.connect(clientTransport);
    const result = await mcp.callTool({ name: "open_binary", arguments: {
      path, format: "raw-binary", architecture: "arm", entry_point: "0x8000000", provider_id: "ghidra",
    } });
    expect(clients).toBe(0);
    const { error } = parseMcpToolError(result);
    expect(error).toMatchObject({ code: "capability_unavailable", details: {
      selection_reason: "target_unsupported", requested_provider_id: "ghidra",
      candidate_ids: ["ghidra"], rejections: expect.arrayContaining([expect.objectContaining({ provider_id: "ghidra" })]),
    } });
  } finally {
    await Promise.allSettled([mcp.close(), server.close()]);
    await session.close();
    await rm(directory, { recursive: true, force: true });
  }
});

Run the focused test with an isolated writable directory and home:

export REA_VERIFY_ROOT="$(mktemp -d)"
mkdir "$REA_VERIFY_ROOT/home"
HOME="$REA_VERIFY_ROOT/home" npx vitest run tests/boundary/mcp/profileRefusal.test.ts
rm tests/boundary/mcp/profileRefusal.test.ts
rm -rf "$REA_VERIFY_ROOT"

The contract assertion fails because the response code is unsupported_target and its details contain only operation, path and reason. The test closes the MCP endpoints and session and removes its binary fixture.

Expected behavior

An explicitly selected provider that cannot support the requested target returns typed provider-selection diagnostics. For this unsupported choice, the caller should receive capability_unavailable with details.selection_reason equal to target_unsupported, details.requested_provider_id equal to ghidra, candidate IDs and the provider's rejection reason. The refusal reason must remain visible.

Impact and scope

The refused call still reports a readable reason, but clients lose the documented selected-provider, candidate and rejection fields for this bounded raw-target profile refusal. No false success or data loss was observed. Removing the language override allowed the identical raw ARM input to open in the control. That workaround does not restore diagnostics for the refused selection.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64. Simulated Linux x64 installation discovery reported Ghidra 12.1.4 and Java 21.0.11; no real Ghidra engine ran.

Target and artifact context

Four-byte synthetic ARM raw file, format: raw-binary, architecture: arm, entry_point: 0x8000000, explicit provider_id: ghidra, with REA_GHIDRA_LANGUAGE_ID=ARM:LE:32:v7.

Interface

MCP

Earliest failure stage

Setup or provider admission

Relevant output or evidence

  • Expected source: docs/mcp-contracts.md:382-394 promises typed selection details for unsupported explicit choices; docs/raw-binary.md:15-38 documents this MCP input; src/application/binary/AnalysisProviderRegistry.test.ts:178-197 and :223-230 assert typed unsupported selection; src/domain/analysisErrorProjection.ts:106-109 and :350-366 define the projected selection code and details.
  • Failure source: The executed registered MCP call returns unsupported_target without selection details. src/server/registerSessionTools.ts:174-218 forwards these caller options into session routing; src/application/binary/SessionProviderRouter.ts:73-87 calls selection. src/ghidra/GhidraProvider.ts:154-217 accepts this raw ARM target, and src/ghidra/GhidraAnalysisProfile.ts:110-120 refuses the override. src/application/binary/AnalysisProviderEvaluation.ts:57-77 records the target rejection and profile error, while src/application/binary/AnalysisProviderRegistry.ts:251-252 returns the profile error before building the selection diagnostic.

The focused existing registry and MCP selection tests also passed, with 10 tests across two files. Their success does not cover this profile-refusal case. The source locators explain the observed route; real installation discovery and engine analysis were not tested.

At current upstream main 22ec8bdb2f7d4626587f23b1eed4a207df600386, the registry/profile refusal code remains unchanged. The only cited-file change adds launch_rejection to a different provider-launch error branch in analysisErrorProjection.ts; it does not change this refusal path.

Typed outcome or recovery expected

The reproduction must reject the same explicit raw-target choice with capability_unavailable, retain the original refusal reason in the candidate rejections, and return all selection identity fields. It must keep the provider client-start count at zero. Keep the passing controls: initial unsupported inspection must retain typed selection details, and the same raw target without an override must still open. Genuine adapter and cleanup errors must retain their existing diagnostics rather than becoming generic unsupported choices.

Analysis or suspected cause

Source-derived: src/application/binary/AnalysisProviderEvaluation.ts:57-77 records both target rejection and profile error. Explicit selection returns that profile error at src/application/binary/AnalysisProviderRegistry.ts:251-252 before building the documented selection diagnostic.

Additional context

Production configuration parsing, raw target admission, Ghidra profile resolution, session routing and MCP projection ran. Only installation readiness was simulated. Existing registry and MCP selection tests passed all 10 cases, which do not cover this profile-refusal branch.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3930 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Provider or capability discovery ### Observed behavior Observed through the registered MCP tool, the response has code `unsupported_target`, operation `resolve_analysis_profile` and reason `REA_GHIDRA_LANGUAGE_ID cannot override the admitted raw-binary import.` Its details omit `selection_reason`, `requested_provider_id`, `candidate_ids` and `rejections`. The provider client-start count stays zero. The initial verification ran this refusal once alongside two passing controls. Reporting unsupported during initial provider support inspection returned `capability_unavailable` with `target_unsupported` selection details. Removing the language override from the same ARM raw input opened successfully without starting a provider client. The standalone reproduction above also failed at the same caller-visible assertion. These observations concern simulated installation readiness and the production provider's profile refusal, not real Ghidra analysis. ### Reproduction From the baseline checkout with its locked dependencies already installed, save this temporary test as `tests/boundary/mcp/profileRefusal.test.ts`. It uses production configuration parsing, raw target admission, provider profile resolution, session routing and MCP error delivery. The existing installation-discovery fake supplies readiness without launching an engine. ```ts import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js"; import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js"; import { parseConfig } from "../../../src/config/parseConfig.js"; import { GhidraProvider } from "../../../src/ghidra/GhidraProvider.js"; import { silentLogger } from "../../../src/logger.js"; import { createServer } from "../../../src/server/createServer.js"; import { ghidraInstallationHost } from "./ghidraMcpHarness.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; it("retains typed selection details when the selected provider refuses a raw profile", async () => { const directory = await mkdtemp(join(process.env.REA_VERIFY_ROOT!, "case-")); const path = join(directory, "image.bin"); await writeFile(path, Buffer.from([0, 0, 160, 225])); const config = parseConfig({ GHIDRA_INSTALL_DIR: "/opt/ghidra_12.1.4_PUBLIC", REA_GHIDRA_LANGUAGE_ID: "ARM:LE:32:v7", }); if (!config.ok) throw config.error; let clients = 0; const provider = new GhidraProvider(config.value, silentLogger, {}, ghidraInstallationHost(), () => { clients++; throw new Error("Unexpected engine startup"); }); expect(provider.inspectAvailability().status).toBe("available"); const session = composeBinarySession(new AnalysisProviderRegistry([provider])); const server = createServer({ kind: "session", session }, { logger: silentLogger }); const mcp = new Client({ name: "profile-refusal-repro", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await mcp.connect(clientTransport); const result = await mcp.callTool({ name: "open_binary", arguments: { path, format: "raw-binary", architecture: "arm", entry_point: "0x8000000", provider_id: "ghidra", } }); expect(clients).toBe(0); const { error } = parseMcpToolError(result); expect(error).toMatchObject({ code: "capability_unavailable", details: { selection_reason: "target_unsupported", requested_provider_id: "ghidra", candidate_ids: ["ghidra"], rejections: expect.arrayContaining([expect.objectContaining({ provider_id: "ghidra" })]), } }); } finally { await Promise.allSettled([mcp.close(), server.close()]); await session.close(); await rm(directory, { recursive: true, force: true }); } }); ``` Run the focused test with an isolated writable directory and home: ```sh export REA_VERIFY_ROOT="$(mktemp -d)" mkdir "$REA_VERIFY_ROOT/home" HOME="$REA_VERIFY_ROOT/home" npx vitest run tests/boundary/mcp/profileRefusal.test.ts rm tests/boundary/mcp/profileRefusal.test.ts rm -rf "$REA_VERIFY_ROOT" ``` The contract assertion fails because the response code is `unsupported_target` and its details contain only `operation`, `path` and `reason`. The test closes the MCP endpoints and session and removes its binary fixture. ### Expected behavior An explicitly selected provider that cannot support the requested target returns typed provider-selection diagnostics. For this unsupported choice, the caller should receive `capability_unavailable` with `details.selection_reason` equal to `target_unsupported`, `details.requested_provider_id` equal to `ghidra`, candidate IDs and the provider's rejection reason. The refusal reason must remain visible. ### Impact and scope The refused call still reports a readable reason, but clients lose the documented selected-provider, candidate and rejection fields for this bounded raw-target profile refusal. No false success or data loss was observed. Removing the language override allowed the identical raw ARM input to open in the control. That workaround does not restore diagnostics for the refused selection. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64. Simulated Linux x64 installation discovery reported Ghidra 12.1.4 and Java 21.0.11; no real Ghidra engine ran. ### Target and artifact context Four-byte synthetic ARM raw file, `format: raw-binary`, `architecture: arm`, `entry_point: 0x8000000`, explicit `provider_id: ghidra`, with `REA_GHIDRA_LANGUAGE_ID=ARM:LE:32:v7`. ### Interface MCP ### Earliest failure stage Setup or provider admission ### Relevant output or evidence - Expected source: `docs/mcp-contracts.md:382-394` promises typed selection details for unsupported explicit choices; `docs/raw-binary.md:15-38` documents this MCP input; `src/application/binary/AnalysisProviderRegistry.test.ts:178-197` and `:223-230` assert typed unsupported selection; `src/domain/analysisErrorProjection.ts:106-109` and `:350-366` define the projected selection code and details. - Failure source: The executed registered MCP call returns `unsupported_target` without selection details. `src/server/registerSessionTools.ts:174-218` forwards these caller options into session routing; `src/application/binary/SessionProviderRouter.ts:73-87` calls selection. `src/ghidra/GhidraProvider.ts:154-217` accepts this raw ARM target, and `src/ghidra/GhidraAnalysisProfile.ts:110-120` refuses the override. `src/application/binary/AnalysisProviderEvaluation.ts:57-77` records the target rejection and profile error, while `src/application/binary/AnalysisProviderRegistry.ts:251-252` returns the profile error before building the selection diagnostic. The focused existing registry and MCP selection tests also passed, with 10 tests across two files. Their success does not cover this profile-refusal case. The source locators explain the observed route; real installation discovery and engine analysis were not tested. At current upstream main `22ec8bdb2f7d4626587f23b1eed4a207df600386`, the registry/profile refusal code remains unchanged. The only cited-file change adds `launch_rejection` to a different provider-launch error branch in `analysisErrorProjection.ts`; it does not change this refusal path. ### Typed outcome or recovery expected The reproduction must reject the same explicit raw-target choice with `capability_unavailable`, retain the original refusal reason in the candidate rejections, and return all selection identity fields. It must keep the provider client-start count at zero. Keep the passing controls: initial unsupported inspection must retain typed selection details, and the same raw target without an override must still open. Genuine adapter and cleanup errors must retain their existing diagnostics rather than becoming generic unsupported choices. ### Analysis or suspected cause Source-derived: `src/application/binary/AnalysisProviderEvaluation.ts:57-77` records both target rejection and profile error. Explicit selection returns that profile error at `src/application/binary/AnalysisProviderRegistry.ts:251-252` before building the documented selection diagnostic. ### Additional context Production configuration parsing, raw target admission, Ghidra profile resolution, session routing and MCP projection ran. Only installation readiness was simulated. Existing registry and MCP selection tests passed all 10 cases, which do not cover this profile-refusal branch.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Confirmed: when a profile refusal is returned as unsupported_target, omitting selection_reason, requested_provider_id, candidate_ids and rejections leaves callers unable to tell why the Ghidra profile was refused (e.g. raw-binary admission vs. language override) or what to change. Refusal responses should carry the provider-selection diagnostics so configuration errors are actionable.

<!-- gh-comment-id:6186298089 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed: when a profile refusal is returned as unsupported_target, omitting selection_reason, requested_provider_id, candidate_ids and rejections leaves callers unable to tell why the Ghidra profile was refused (e.g. raw-binary admission vs. language override) or what to change. Refusal responses should carry the provider-selection diagnostics so configuration errors are actionable.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1537
No description provided.