fix(discovery): report debugger start host restrictions (#7965)

This commit is contained in:
morluto authored and GitHub committed 2026-10-29 09:20:33 +08:00
1 parent 70a8325c38
commit 3536ea2f49
7 files changed
+109 -2

No files matched your search

@@ -1,3 +1,4 @@
import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js";
import { describe, expect, it } from "vitest";
@@ -887,6 +888,32 @@ it("keeps independent native sessions discoverable without an unrelated binary t
);
});
it.each(["darwin", "linux", "win32"] as const)(
"reports the persistent debugger start host restriction on %s",
(platform) => {
const entries = buildCapabilityInventory(status(), {
...enabledPolicy,
hostUnsupported: {
persistentDebugger: !persistentDebuggerHostSupported(platform),
},
});
for (const name of ["start_gdb_session", "start_rizin_debug_session"])
expect(entries.find((entry) => entry.name === name)).toMatchObject(
platform === "linux"
? { available: true, reason: "available" }
: {
available: false,
reason: "unsupported_host",
remediation: expect.stringContaining("Linux"),
},
);
for (const name of ["gdb_session_status", "close_rizin_debug_session"])
expect(entries.find((entry) => entry.name === name)).toMatchObject({
available: true,
});
},
);
describe("summary tool availability", () => {
it("summarizes every tool's current availability without static catalog flags", () => {
const full = buildCapabilityInventory(status(), enabledPolicy);
+8
View File
@@ -71,6 +71,7 @@ export const HOST_RESTRICTED_FEATURES = [
"firmwareInspection",
"firmwareExtraction",
"javascriptRecovery",
"persistentDebugger",
] as const;
export type HostRestrictedFeature = (typeof HOST_RESTRICTED_FEATURES)[number];
@@ -444,6 +445,13 @@ const workflowAvailabilityFor = ({
kind,
policy,
}: AvailabilityContext): Availability | null => {
if (name === "start_gdb_session" || name === "start_rizin_debug_session")
return hostRestrictedAvailability(
policy,
"persistentDebugger",
true,
"Run persistent debugger sessions on Linux with /proc descriptor readback.",
);
if (
name === "inspect_desktop_application" ||
name === "inspect_go_binary" ||
+2 -1
View File
@@ -1,3 +1,4 @@
import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
import { ABORTED, waitForAbortable } from "../process/AbortablePromise.js";
import { snapshotGdbCommandRecords } from "./GdbCommandRecords.js";
import { randomUUID } from "node:crypto";
@@ -639,7 +640,7 @@ export class GdbSessionManager {
? cancelledWithCallerReason("start_gdb_session", signal)
: new AnalysisCancelledError("start_gdb_session"),
);
if (this.#platform !== "linux")
if (!persistentDebuggerHostSupported(this.#platform))
return err(
new AnalysisCapabilityUnavailableError(
"gdb",
+4
View File
@@ -0,0 +1,4 @@
/** Both persistent debugger managers require Linux procfs descriptor readback. */
export const persistentDebuggerHostSupported = (
platform: NodeJS.Platform,
): boolean => platform === "linux";
+2 -1
View File
@@ -1,3 +1,4 @@
import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
import { randomUUID } from "node:crypto";
import { isUtf8 } from "node:buffer";
import { open, readlink, realpath, stat } from "node:fs/promises";
@@ -361,7 +362,7 @@ export class RizinDebugSessionManager {
> {
if (signal?.aborted)
return err(rizinCancellation("start_rizin_debug_session", signal));
if (this.#platform !== "linux")
if (!persistentDebuggerHostSupported(this.#platform))
return err(
new AnalysisCapabilityUnavailableError(
"rizin",
+2
View File
@@ -205,6 +205,7 @@ import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js";
import { CutterBridgeService } from "../cutter/CutterBridgeService.js";
import { registerCutterTools } from "./registerCutterTools.js";
import { GdbSessionManager } from "../gdb/GdbSessionManager.js";
import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
import { registerGdbTools } from "./registerGdbTools.js";
import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
import { registerRizinDebugTools } from "./registerRizinDebugTools.js";
@@ -299,6 +300,7 @@ const installSessionToolAvailability = (
const basePolicy = sessionAvailabilityPolicy(options.availabilityPolicy, {
optionalProviderLoadFailures: options.optionalProviderLoadFailures,
hostUnsupported: {
persistentDebugger: !persistentDebuggerHostSupported(platform),
pcapCapture: outsideVerifiedHost(
options.pcapAnalysis,
verifiedHost.pcapCapture,
@@ -0,0 +1,64 @@
import { expect, it } from "vitest";
import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import { sessionOutputSchemas } from "../../../src/contracts/toolOutputSchemaGroups.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
it("reports debugger start host restrictions without requiring an analysis target", async () => {
const session = createTestBinarySession(() => {
throw new Error("discovery must not start binary analysis");
});
const { client } = await createApplicationMcpHarness({ session });
const entries = async (detail: "summary" | "complete") => {
const response = await client.callTool({
name: "binary_session",
arguments: { detail },
});
expect(response.isError).not.toBe(true);
const parsed = sessionOutputSchemas.binary_session.parse(
response.structuredContent,
).result;
expect(parsed.open).toBe(false);
return expandToolAvailability(parsed.tool_availability);
};
for (const detail of ["summary", "complete"] as const) {
const tools = await entries(detail);
for (const name of ["start_gdb_session", "start_rizin_debug_session"]) {
expect(tools.find((tool) => tool.name === name)).toMatchObject(
process.platform === "linux"
? { available: true, reason: "available" }
: {
available: false,
reason: "unsupported_host",
remediation: expect.stringContaining("Linux"),
},
);
}
expect(
tools.find((tool) => tool.name === "gdb_session_status"),
).toMatchObject({ available: true });
}
if (process.platform !== "linux") {
for (const [name, args] of [
["start_gdb_session", {}],
[
"start_rizin_debug_session",
{ path: "./absent.bin", inferior_tty: "/dev/null" },
],
] as const) {
const response = await client.callTool({ name, arguments: args });
expect(response.isError).toBe(true);
expect(parseMcpToolError(response)).toMatchObject({
error: { code: "capability_unavailable" },
});
}
const after = await entries("complete");
for (const name of ["start_gdb_session", "start_rizin_debug_session"])
expect(after.find((tool) => tool.name === name)).toMatchObject({
available: false,
reason: "unsupported_host",
});
}
});