diff --git a/docs/pcap-analysis.md b/docs/pcap-analysis.md index b65607e55e..2beafbd00a 100644 --- a/docs/pcap-analysis.md +++ b/docs/pcap-analysis.md @@ -30,6 +30,8 @@ Evidence. Capture inspection requires Linux with util-linux prlimit or Windows with owned Job Objects. macOS capture inspection returns a capability gap because the new native memory boundary is unverified; its version probes remain available. +`binary_session` advertises those operations separately: unsupported capture +hosts do not disable the client probe or remove either tool from the catalog. A selected tool directory or unreadable tool is a configuration capability refusal naming its selected path, before any capture bytes are opened. Tools and installed libraries/plugins are caller-trusted. An empty private diff --git a/src/application/CapabilityInventory.test.ts b/src/application/CapabilityInventory.test.ts index f8c6282bf3..3000b87e27 100644 --- a/src/application/CapabilityInventory.test.ts +++ b/src/application/CapabilityInventory.test.ts @@ -1,3 +1,4 @@ +import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js"; import { describe, expect, it } from "vitest"; import { @@ -815,3 +816,30 @@ describe("host-restricted integrations", () => { }, ); }); + +it.each(["darwin", "linux", "win32"])( + "distinguishes pcap capture host support from its target-free client probe on %s", + (platform) => { + const tools = buildCapabilityInventory( + { open: false, capabilities: [] }, + { + processCaptureEnabled: true, + hostUnsupported: { pcapCapture: !pcapCaptureHostSupported(platform) }, + }, + ); + expect( + tools.find(({ name }) => name === "inspect_pcap_client"), + ).toMatchObject({ available: true, reason: "available" }); + expect( + tools.find(({ name }) => name === "inspect_pcap_capture"), + ).toMatchObject( + platform === "darwin" + ? { + available: false, + reason: "unsupported_host", + remediation: expect.stringContaining("native memory bound"), + } + : { available: true, reason: "available" }, + ); + }, +); diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index 36966c5a7e..afdebe921f 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -1,3 +1,4 @@ +import { PCAP_CAPTURE_UNSUPPORTED_HOST_REASON } from "../domain/pcap/pcapAnalysis.js"; import { DEFAULT_CALL_RELATIONSHIP_DIRECTION } from "../contracts/enhancedInputs.js"; import { FIRMWARE_TOOL_REQUIREMENTS } from "../contracts/firmware/firmwareToolRequirements.js"; import { CALL_RELATIONSHIP_TRAVERSALS } from "./CallRelationshipTraversal.js"; @@ -56,6 +57,7 @@ type ProviderDescriptor = ProviderCapability; /** Optional integrations whose verified hosts exclude some platforms. */ export const HOST_RESTRICTED_FEATURES = [ "packetCapture", + "pcapCapture", "signalRecording", "evmInterface", "binaryLayout", @@ -602,6 +604,13 @@ const workflowAvailabilityFor = ({ remediation: "Run process capture on a supported Linux or macOS host.", }; if (kind === "application") return { reason: "available", remediation: null }; + if (name === "inspect_pcap_capture") + return hostRestrictedAvailability( + policy, + "pcapCapture", + true, + PCAP_CAPTURE_UNSUPPORTED_HOST_REASON, + ); if (kind === "pcap-provider" || kind === "flutter-provider") return { reason: "available", remediation: null }; const electron = electronProviderAvailability(name, kind, policy); diff --git a/src/domain/pcap/pcapAnalysis.ts b/src/domain/pcap/pcapAnalysis.ts index 09d6898164..8231d11518 100644 --- a/src/domain/pcap/pcapAnalysis.ts +++ b/src/domain/pcap/pcapAnalysis.ts @@ -2,6 +2,12 @@ import { z } from "zod"; import { localPathStringSchema } from "../localPath.js"; import { digestSchema } from "../digests.js"; +/** Summary capture admission and discovery share this native-bound host profile. */ +export const pcapCaptureHostSupported = (platform: string): boolean => + platform === "linux" || platform === "win32"; +export const PCAP_CAPTURE_UNSUPPORTED_HOST_REASON = + "Packet summaries require an enforceable native memory bound: Linux with util-linux prlimit, or Windows with owned Job Objects. This host's capture boundary remains unsupported."; + /** * Caller intent for packet-capture analysis through caller-supplied tshark * and capinfos. REA never installs Wireshark or its tools. diff --git a/src/pcap/PcapNativeBound.ts b/src/pcap/PcapNativeBound.ts index 75ef19baec..24a8bf4a08 100644 --- a/src/pcap/PcapNativeBound.ts +++ b/src/pcap/PcapNativeBound.ts @@ -1,3 +1,7 @@ +import { + pcapCaptureHostSupported, + PCAP_CAPTURE_UNSUPPORTED_HOST_REASON, +} from "../domain/pcap/pcapAnalysis.js"; import { access, realpath } from "node:fs/promises"; import { constants } from "node:fs"; import { isAbsolute } from "node:path"; @@ -32,9 +36,8 @@ export const preparePcapNativeBound = async ( ], }; } - if (process.platform !== "linux") { - const reason = - "Packet summaries require an enforceable native memory bound: Linux with util-linux prlimit, or Windows with owned Job Objects. This host's capture boundary remains unsupported."; + if (!pcapCaptureHostSupported(process.platform)) { + const reason = PCAP_CAPTURE_UNSUPPORTED_HOST_REASON; throw new AnalysisCapabilityUnavailableError( "pcap", "inspect_pcap_capture", diff --git a/src/pcap/PcapProvider.ts b/src/pcap/PcapProvider.ts index 57a6fc8c34..690f3246b2 100644 --- a/src/pcap/PcapProvider.ts +++ b/src/pcap/PcapProvider.ts @@ -8,6 +8,7 @@ import { } from "../application/AnalysisProvider.js"; import { pcapResultSchemas, + pcapCaptureHostSupported, type PcapRequest, type PcapOperation, } from "../domain/pcap/pcapAnalysis.js"; @@ -505,7 +506,7 @@ export class PcapProvider { ): Promise { try { const { client } = await this.client("inspect_pcap_client", signal); - if (process.platform !== "linux" && process.platform !== "win32") + if (!pcapCaptureHostSupported(process.platform)) return { status: "unavailable", code: "unsupported_host", diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 8b4e78f8de..c2a558249d 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -1,3 +1,4 @@ +import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js"; import { createGpuCodeObjectsService } from "../composition/gpu.js"; import { registerGpuCodeObjectsTools } from "./registerGpuCodeObjectsTools.js"; import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; @@ -276,6 +277,7 @@ const installSessionToolAvailability = ( // Each provider owns its verified host; discovery and execution share it. const verifiedHost = { packetCapture: tsharkHostSupported(platform), + pcapCapture: pcapCaptureHostSupported(platform), signalRecording: sigrokHostSupported(platform), evmInterface: evmHostSupported(platform, arch), pwntools: pwntoolsHostSupported(platform, arch), @@ -291,6 +293,10 @@ const installSessionToolAvailability = ( const basePolicy = sessionAvailabilityPolicy(options.availabilityPolicy, { optionalProviderLoadFailures: options.optionalProviderLoadFailures, hostUnsupported: { + pcapCapture: outsideVerifiedHost( + options.pcapAnalysis, + verifiedHost.pcapCapture, + ), packetCapture: outsideVerifiedHost( options.packetCapture, verifiedHost.packetCapture, diff --git a/tests/boundary/mcp/packetCaptureAvailability.test.ts b/tests/boundary/mcp/packetCaptureAvailability.test.ts index 6002dc57e1..c8d9ea7a45 100644 --- a/tests/boundary/mcp/packetCaptureAvailability.test.ts +++ b/tests/boundary/mcp/packetCaptureAvailability.test.ts @@ -18,6 +18,7 @@ const linux = process.platform === "linux"; const packetCaptureAvailability = async ( environment: Readonly>, + name = "inspect_packet_capture", ): Promise | undefined> => { const session = createTestBinarySession(() => { throw new Error("binary analysis must not start during status discovery"); @@ -44,7 +45,7 @@ const packetCaptureAvailability = async ( .object({ result: sessionProvider }) .parse(response.structuredContent).result; return expandToolAvailability(parsed.tool_availability).find( - ({ name }) => name === "inspect_packet_capture", + (tool) => tool.name === name, ); } finally { await Promise.allSettled([client.close(), server.close(), session.close()]); @@ -106,3 +107,20 @@ it("reports packet tools unavailable when no absolute PATH entry holds TShark", reason: linux ? "provider_missing" : "unsupported_host", }); }); + +it("advertises pcap capture host support separately from the always available client probe", async () => { + expect( + await packetCaptureAvailability({}, "inspect_pcap_client"), + ).toMatchObject({ available: true, reason: "available" }); + expect( + await packetCaptureAvailability({}, "inspect_pcap_capture"), + ).toMatchObject( + process.platform === "linux" || process.platform === "win32" + ? { available: true, reason: "available" } + : { + available: false, + reason: "unsupported_host", + remediation: expect.stringContaining("native memory bound"), + }, + ); +});