diff --git a/src/cli.ts b/src/cli.ts index cf0a386dae..b9829e68dd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { writeAmpRegistrationBackup } from "./cli/ampRegistrationBackup.js"; +import { registerToolFamilies } from "./contracts/toolFamilyIds.js"; import { registerGpuCommands } from "./cli/gpuCommands.js"; import { registerNativeFunctionMatchingCommands } from "./cli/nativeFunctionMatchingCommands.js"; import { registerCaptureCommands } from "./cli/captureCommands.js"; @@ -170,59 +171,75 @@ export const createCli = ( const cli = timedCli as unknown as CliInstance; registerSetupCommands(cli, logger, environment); - registerGuidanceCommands(cli, logger); - registerGuidedWorkflowCommands(cli, logger); - registerReverseEngineeringCommands(cli, logger, environment); - registerCutterCommands(cli, logger, environment); - registerCoreAnalysisCommands( - cli, - logger, - analysis.runDirectAnalysis, - environment, - ); - registerUtilityCommands(cli, logger, environment, analysis); - registerWasmCommands(cli, logger, environment); - registerApkSignatureCommands(cli, logger, environment); - registerCaptureCommands(cli, logger, environment); - registerSignalCommands(cli, logger, environment); - registerEvmCommands(cli, logger, environment); - registerDesktopApplicationCommands(cli, logger); - registerGoCommands(cli, logger); - registerIl2cppCommands(cli, logger); - registerModelCommands(cli, logger); - registerGpuCommands(cli, logger); - registerRustCommands(cli, logger); - registerPythonPycCommands(cli, logger); - registerErlangCommands(cli, logger); - registerSqliteCommands(cli, logger, environment); - registerArtifactCommands(cli, logger, analysis.runProviderAnalysis); - registerManagedCommands(cli, logger, analysis.runProviderAnalysis); - registerAndroidCommands(cli, logger, environment); - registerApktoolCommands(cli, logger, environment); - registerPcapCommands(cli, logger, environment); - registerFlutterCommands(cli, logger); - registerJebCommands(cli, logger, environment); - registerAdbCommands(cli, logger, environment); - registerFirmwareCommands(cli, logger, environment); - registerBinaryDiagnosticsCommands(cli, logger, environment); - registerAnalysisViewCommands(cli, logger); - registerNativeFunctionMatchingCommands(cli, logger, environment); - registerFridaCommands(cli, logger, fridaInstrumentation); - registerEvidenceCommands(cli, logger); - registerEnvironmentCommands(cli, logger, environment); - registerProcessCommands(cli, logger, environment); - registerBrowserCommands(cli, logger); - registerWebScriptCommands(cli, logger); - registerWebModuleCommands(cli, logger, environment); - registerWebSourceLocationCommands(cli, logger, environment); - registerWebRuntimeCommands(cli, logger); - registerWebNetworkCaptureCommands(cli, logger, environment); - registerJavaScriptRecoveryCommands(cli, logger, environment); - registerAdvancedBrowserCommands(cli, logger); - registerBrowserScenarioCommands(cli, logger, environment); - registerElectronCommands(cli, logger, environment); - registerJavaScriptRuntimeObservationCommands(cli, logger, environment); - registerApplicationCommands(cli, logger); + registerToolFamilies({ + direct: () => + registerCoreAnalysisCommands( + cli, + logger, + analysis.runDirectAnalysis, + environment, + ), + // Enhanced workflows are MCP tools. Their CLI entry points live with core analysis. + enhanced: () => undefined, + native: () => { + registerReverseEngineeringCommands(cli, logger, environment); + registerCutterCommands(cli, logger, environment); + registerUtilityCommands(cli, logger, environment, analysis); + registerBinaryDiagnosticsCommands(cli, logger, environment); + }, + artifact: () => { + registerWasmCommands(cli, logger, environment); + registerApkSignatureCommands(cli, logger, environment); + registerCaptureCommands(cli, logger, environment); + registerSignalCommands(cli, logger, environment); + registerEvmCommands(cli, logger, environment); + registerDesktopApplicationCommands(cli, logger); + registerGoCommands(cli, logger); + registerIl2cppCommands(cli, logger); + registerModelCommands(cli, logger); + registerGpuCommands(cli, logger); + registerRustCommands(cli, logger); + registerPythonPycCommands(cli, logger); + registerErlangCommands(cli, logger); + registerSqliteCommands(cli, logger, environment); + registerArtifactCommands(cli, logger, analysis.runProviderAnalysis); + }, + managed: () => + registerManagedCommands(cli, logger, analysis.runProviderAnalysis), + firmware: () => registerFirmwareCommands(cli, logger, environment), + android: () => registerAndroidCommands(cli, logger, environment), + pcap: () => registerPcapCommands(cli, logger, environment), + flutter: () => registerFlutterCommands(cli, logger), + frida: () => registerFridaCommands(cli, logger, fridaInstrumentation), + apktool: () => registerApktoolCommands(cli, logger, environment), + jeb: () => registerJebCommands(cli, logger, environment), + adb: () => registerAdbCommands(cli, logger, environment), + browser: () => { + registerBrowserCommands(cli, logger); + registerWebRuntimeCommands(cli, logger); + registerWebNetworkCaptureCommands(cli, logger, environment); + registerAdvancedBrowserCommands(cli, logger); + registerBrowserScenarioCommands(cli, logger, environment); + }, + electron: () => registerElectronCommands(cli, logger, environment), + "javascript-runtime": () => + registerJavaScriptRuntimeObservationCommands(cli, logger, environment), + application: () => { + registerGuidanceCommands(cli, logger); + registerGuidedWorkflowCommands(cli, logger); + registerAnalysisViewCommands(cli, logger); + registerEvidenceCommands(cli, logger); + registerEnvironmentCommands(cli, logger, environment); + registerProcessCommands(cli, logger, environment); + registerJavaScriptRecoveryCommands(cli, logger, environment); + registerWebScriptCommands(cli, logger); + registerWebModuleCommands(cli, logger, environment); + registerWebSourceLocationCommands(cli, logger, environment); + registerApplicationCommands(cli, logger); + }, + session: () => + registerNativeFunctionMatchingCommands(cli, logger, environment), + }); presentOmittableDefaults(cli); if (resultOutput !== undefined) streamCliCommandResults(cli, resultOutput); const serveStrict = rejectSurplusCliArguments(cli); diff --git a/src/contracts/toolFamilies.test.ts b/src/contracts/toolFamilies.test.ts index 9d639445a9..2e9b5ce5b2 100644 --- a/src/contracts/toolFamilies.test.ts +++ b/src/contracts/toolFamilies.test.ts @@ -5,7 +5,11 @@ import { contractsForToolFamilies, toolFamilyId, } from "./toolFamilies.js"; -import { TOOL_FAMILY_IDS } from "./toolFamilyIds.js"; +import { + registerToolFamilies, + TOOL_FAMILY_IDS, + type ToolFamilyId, +} from "./toolFamilyIds.js"; import { TOOL_CONTRACTS, toolContract } from "./toolContracts.js"; import type { officialOutputSchemas, @@ -43,6 +47,20 @@ describe("tool families", () => { ]); }); + it("runs registrars in published id order", () => { + const seen: string[] = []; + const registrars = Object.fromEntries( + TOOL_FAMILY_IDS.map((id) => [ + id, + (): void => { + seen.push(id); + }, + ]), + ) as unknown as { readonly [Id in ToolFamilyId]: () => void }; + registerToolFamilies(registrars); + expect(seen).toEqual([...TOOL_FAMILY_IDS]); + }); + it("covers every contract exactly once", () => { const owners = new Map(); for (const { id, contracts } of TOOL_FAMILY_DEFINITIONS) diff --git a/src/contracts/toolFamilies.ts b/src/contracts/toolFamilies.ts index fcb149a8f2..1980cfcad9 100644 --- a/src/contracts/toolFamilies.ts +++ b/src/contracts/toolFamilies.ts @@ -44,7 +44,7 @@ import { ENHANCED_TOOL_CONTRACTS } from "./enhancedToolContracts.js"; import { SESSION_TOOL_CONTRACTS } from "./sessionToolContracts.js"; import { FRIDA_TOOL_CONTRACTS } from "./fridaToolContracts.js"; import { TOOL_CONTRACTS } from "./toolContracts.js"; -import { TOOL_FAMILY_IDS, type ToolFamilyId } from "./toolFamilyIds.js"; +import type { ToolFamilyId } from "./toolFamilyIds.js"; /** One selectable family: a stable selection id and its complete contract set. */ export interface ToolFamilyDefinition { diff --git a/src/contracts/toolFamilyIds.ts b/src/contracts/toolFamilyIds.ts index 1931e947b2..5370c196cb 100644 --- a/src/contracts/toolFamilyIds.ts +++ b/src/contracts/toolFamilyIds.ts @@ -31,3 +31,15 @@ export type ToolFamilyId = (typeof TOOL_FAMILY_IDS)[number]; /** Whether a parsed selection value names a published tool family. */ export const isToolFamilyId = (value: string): value is ToolFamilyId => (TOOL_FAMILY_IDS as readonly string[]).includes(value); + +/** + * Invoke one registrar per published family, in {@link TOOL_FAMILY_IDS} order. + * Both CLI and MCP pass a complete map; a new id fails those call sites. + * This module stays free of contract schemas so CLI startup can walk the list + * without loading every tool definition. + */ +export const registerToolFamilies = (registrars: { + readonly [Id in ToolFamilyId]: () => void; +}): void => { + for (const id of TOOL_FAMILY_IDS) registrars[id](); +}; diff --git a/src/server/createServer.ts b/src/server/createServer.ts index cfd1cd7197..55f7271f79 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -1,7 +1,7 @@ +import { registerToolFamilies } from "../contracts/toolFamilyIds.js"; import { AnalysisSettings } from "../application/AnalysisSettings.js"; import { parseMcpInputBudget } from "../config/mcpInputBudget.js"; import { registerAnalysisSettingsTools } from "./registerAnalysisSettingsTools.js"; -import type { DevicetreeService } from "../application/binaryDiagnostics/DevicetreeService.js"; import { registerDevicetreeTool } from "./registerDevicetreeTool.js"; import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js"; import { createGpuCodeObjectsService } from "../composition/gpu.js"; @@ -42,14 +42,6 @@ import { createBinaryLayoutService, pwntoolsHostSupported, } from "../composition/binaryDiagnostics.js"; -import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js"; -import type { AppleIpsService } from "../application/binaryDiagnostics/AppleIpsService.js"; -import type { KernelModuleService } from "../application/binaryDiagnostics/KernelModuleService.js"; -import type { PeLoadConfigService } from "../application/binaryDiagnostics/PeLoadConfigService.js"; -import type { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js"; -import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js"; -import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js"; -import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js"; import { registerElfNotesTool } from "./registerElfNotesTool.js"; import { registerAppleIpsTool } from "./registerAppleIpsTool.js"; import { registerKernelModuleTool } from "./registerKernelModuleTool.js"; @@ -198,7 +190,6 @@ import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js"; import { withAdmittedAnalysis, type ServerAnalysisSource, - type WithAdmittedAnalysis, } from "./analysisAdmission.js"; import { createReverseEngineeringService } from "../composition/reverseEngineering.js"; import { registerReverseEngineeringTools } from "./registerReverseEngineeringTools.js"; @@ -532,7 +523,6 @@ export const createServer = ( environment.REA_MCP_MAX_INPUT_ELEMENTS, ); server.analysisSettings = analysisSettings; - registerAnalysisSettingsTools(server, analysisSettings); if (options.mcpServer !== undefined) { server.attachRuntime( session === undefined @@ -670,124 +660,14 @@ export const createServer = ( recordEvidenceWithUnknown, } = createSessionRecorders(server, session); const analysisAdmission = withAdmittedAnalysis(source); - const toolContext: ServerToolContext = { - server, - session, - options: selectedOptions, - environment, - logger: toolLogger, - evidenceById, - activeTarget, - recordEvidence, - recordEvidenceWithUnknown, - withAdmittedAnalysis: analysisAdmission, - artifactResources, - goBinary, - rustBinary, - erlangBeam, - il2cppMetadata, - peResources, - nativeExports, - devicetree, - elfNotes, - appleIps, - kernelModule, - peLoadConfig, - peCertificates, - peImports, - electronFuses, - pythonPyc, - webNetworkCapture, - }; - registerDesktopApplicationTool( - server, - desktopApplication, - toolLogger, - recordEvidence, - analysisAdmission, - evidenceById, - ); - registerModelArtifactTools( - server, - modelArtifact, - toolLogger, - recordEvidence, - analysisAdmission, - evidenceById, - ); - registerGpuCodeObjectsTools( - server, - gpuCodeObjects, - toolLogger, - recordEvidence, - analysisAdmission, - evidenceById, - ); - registerBinaryAnalysisTools(toolContext); - registerReverseEngineeringTools( - server, - reverseEngineering, - toolLogger, - delivery, + const recordSessionEvidence = session === undefined ? undefined - : (evidence) => session.recordEvidence(evidence), - analysisAdmission, - session === undefined - ? undefined - : () => session.invalidateSnapshotForExternalMutation(), - session === undefined - ? undefined - : (path, signal, operation, rebindFailure) => - session.withAdmittedExternalMutation( - path, - signal, - operation, - rebindFailure, - ), - evidenceById, - ); - registerFridaTools( - server, - fridaInstrumentation, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - evidenceById, - ); - registerCutterTools( - server, - cutter, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - session, - ); - registerGdbTools( - server, - gdbSessions, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - session, - evidenceById, - ); - registerRizinDebugTools( - server, - rizinDebugSessions, - toolLogger, - delivery, - session === undefined - ? undefined - : (evidence) => session.recordEvidence(evidence), - session, - ); + : ( + evidence: Parameters< + NonNullable["recordEvidence"] + >[0], + ) => session.recordEvidence(evidence); const previousOnclose = server.server.onclose; server.server.onclose = () => { previousOnclose?.(); @@ -808,82 +688,495 @@ export const createServer = ( if (failures.length > 1) throw new AggregateError(failures, "REA server cleanup failed"); }; - registerCaptureTools( - server, - packetCapture, - toolLogger, + const recordUnknown = + session === undefined + ? undefined + : (input: Parameters["recordUnknown"]>[0]) => + session.recordUnknown(input); + const analysisOptions = { + logger: toolLogger, + activeTarget, recordEvidence, - analysisAdmission, - ); - registerSignalTools( - server, - signalRecording, - toolLogger, - recordEvidence, - analysisAdmission, - ); - registerConfiguredAnalysisTools( - toolContext, - android, - wasmArtifact, - apkSignature, - evmInterface, - jeb, - firmwareAnalysis, - webRuntime, - ); - registerAdbTools( - server, - new AdbDeviceAnalysisService(adbDevice), - toolLogger, - recordEvidence, - analysisAdmission, - ); - registerPcapTools( - server, - new PcapAnalysisService(pcap), - toolLogger, - recordEvidence, - analysisAdmission, + recordUnknown, evidenceById, - ); - registerFlutterTools( - server, - new FlutterBuildAnalysisService(flutter), - toolLogger, + withAdmittedAnalysis: analysisAdmission, + }; + const evidenceOptions = { + logger: toolLogger, + activeTarget, recordEvidence, - analysisAdmission, - evidenceById, - ); - registerApktoolTools( - server, - new ApktoolResourceAnalysisService(apktool), - toolLogger, + withAdmittedAnalysis: analysisAdmission, + }; + const common = { + logger: toolLogger, recordEvidence, - analysisAdmission, - ); - registerObservationTools(toolContext); + withAdmittedAnalysis: analysisAdmission, + }; + registerToolFamilies({ + direct: () => registerOfficialTools(server, analysisOptions), + enhanced: () => + registerEnhancedTools(server, { + ...analysisOptions, + analysisProfile: + session === undefined + ? undefined + : (operation) => + session.analysisProfile() ?? session.analysisProfile(operation), + allowsSnapshotReplay: + session === undefined + ? undefined + : (operation) => session.allowsSnapshotReplay(operation), + recordWorkflowSnapshot: + session === undefined + ? undefined + : (input) => session.recordWorkflowSnapshot(input), + }), + native: () => { + registerNativeTools(server, { ...evidenceOptions, evidenceById }); + registerReverseEngineeringTools( + server, + reverseEngineering, + toolLogger, + delivery, + recordSessionEvidence, + analysisAdmission, + session === undefined + ? undefined + : () => session.invalidateSnapshotForExternalMutation(), + session === undefined + ? undefined + : (path, signal, operation, rebindFailure) => + session.withAdmittedExternalMutation( + path, + signal, + operation, + rebindFailure, + ), + evidenceById, + ); + registerCutterTools( + server, + cutter, + toolLogger, + delivery, + recordSessionEvidence, + session, + ); + registerGdbTools( + server, + gdbSessions, + toolLogger, + delivery, + recordSessionEvidence, + session, + evidenceById, + ); + registerRizinDebugTools( + server, + rizinDebugSessions, + toolLogger, + delivery, + recordSessionEvidence, + session, + ); + registerBinaryDiagnosticsTools( + server, + selectedOptions.binaryLayout ?? createBinaryLayoutService(environment), + createByteEntropyService(artifactResources), + toolLogger, + recordEvidence, + evidenceById, + analysisAdmission, + ); + registerPeResourcesTool( + server, + peResources, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerNativeExportsTool( + server, + nativeExports, + toolLogger, + recordEvidence, + evidenceById, + analysisAdmission, + ); + registerDevicetreeTool( + server, + devicetree, + toolLogger, + recordEvidence, + evidenceById, + analysisAdmission, + ); + registerElfNotesTool( + server, + elfNotes, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerAppleIpsTool( + server, + appleIps, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerKernelModuleTool( + server, + kernelModule, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerPeLoadConfigTool( + server, + peLoadConfig, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerPeCertificatesTool( + server, + peCertificates, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerPeImportsTool( + server, + peImports, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerRecordedCrashTools( + server, + selectedOptions.recordedCrash ?? + createRecordedCrashService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + }, + artifact: () => { + registerArtifactTools(server, { ...evidenceOptions, evidenceById }); + registerDesktopApplicationTool( + server, + desktopApplication, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerModelArtifactTools( + server, + modelArtifact, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerGpuCodeObjectsTools( + server, + gpuCodeObjects, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerEvmTools( + server, + evmInterface, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerGoTools( + server, + goBinary, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerIl2cppTools( + server, + il2cppMetadata, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerRustTools( + server, + rustBinary, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerPythonPycTool( + server, + pythonPyc, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerErlangTools( + server, + erlangBeam, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerWasmTools( + server, + wasmArtifact, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerApkSignatureTools( + server, + apkSignature, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + registerSqliteTools( + server, + selectedOptions.sqliteDatabase ?? + createSqliteDatabaseService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerCaptureTools( + server, + packetCapture, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerSignalTools( + server, + signalRecording, + toolLogger, + recordEvidence, + analysisAdmission, + ); + }, + managed: () => { + registerManagedTools(server, { ...evidenceOptions, evidenceById }); + if (session !== undefined) + registerManagedWorkflowTools(server, { + logger: toolLogger, + recordEvidence, + recordEvidenceWithUnknown, + session, + withAdmittedAnalysis: analysisAdmission, + }); + }, + firmware: () => + registerFirmwareTools( + server, + firmwareAnalysis, + toolLogger, + recordEvidence, + analysisAdmission, + ), + android: () => + registerAndroidTools( + server, + new AndroidAnalysisService(android, recordEvidence), + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ), + pcap: () => + registerPcapTools( + server, + new PcapAnalysisService(pcap), + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ), + flutter: () => + registerFlutterTools( + server, + new FlutterBuildAnalysisService(flutter), + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ), + frida: () => + registerFridaTools( + server, + fridaInstrumentation, + toolLogger, + delivery, + recordSessionEvidence, + evidenceById, + ), + apktool: () => + registerApktoolTools( + server, + new ApktoolResourceAnalysisService(apktool), + toolLogger, + recordEvidence, + analysisAdmission, + ), + jeb: () => + registerJebTools( + server, + new JebAnalysisService(jeb), + toolLogger, + recordEvidence, + analysisAdmission, + ), + adb: () => + registerAdbTools( + server, + new AdbDeviceAnalysisService(adbDevice), + toolLogger, + recordEvidence, + analysisAdmission, + ), + browser: () => { + registerBrowserTools(server, { + ...common, + evidenceById, + browser: selectedOptions.browserObservation, + loadFailure: + selectedOptions.optionalProviderLoadFailures?.browserObservation, + }); + registerBrowserScenarioTool(server, { + ...common, + evidenceById, + provider: selectedOptions.browserScenarioCapture, + loadFailure: + selectedOptions.optionalProviderLoadFailures?.browserScenarioCapture, + }); + registerWebRuntimeTools( + server, + webRuntime, + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerWebNetworkCaptureTool( + server, + webNetworkCapture, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); + }, + electron: () => + registerElectronTools(server, { + ...common, + artifactResources, + environment, + evidenceById, + electron: selectedOptions.electronObservation, + electronActive: selectedOptions.electronActiveObservation, + electronFuses, + observationLoadFailure: + selectedOptions.optionalProviderLoadFailures?.electronObservation, + activeLoadFailure: + selectedOptions.optionalProviderLoadFailures + ?.electronActiveObservation, + }), + "javascript-runtime": () => + registerJavaScriptRuntimeObservationTools(server, { + ...common, + runtime: selectedOptions.javascriptRuntimeObservation, + loadFailure: + selectedOptions.optionalProviderLoadFailures + ?.javascriptRuntimeObservation, + }), + application: () => { + registerAnalysisSettingsTools(server, analysisSettings); + registerAnalysisViewTool( + server, + toolLogger, + evidenceById, + recordEvidence, + analysisAdmission, + artifactResources, + ); + registerWebScriptTool(server, { ...common, artifactResources }); + registerWebModuleTool( + server, + selectedOptions.webModuleTrace ?? + createWebModuleTraceService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerWebSourceLocationTool( + server, + selectedOptions.webSourceLocation ?? + createWebSourceLocationService(environment), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerJavaScriptRecoveryTool( + server, + new JavaScriptRecoveryService( + selectedOptions.javascriptRecovery ?? + createJavaScriptRecoveryProvider(environment), + ), + toolLogger, + recordEvidence, + analysisAdmission, + ); + registerApplicationTools(server, { + artifactResources, + evidenceById, + logger: toolLogger, + recordEvidence, + recordEvidenceWithUnknown, + withAdmittedAnalysis: analysisAdmission, + }); + }, + session: () => { + if (session === undefined) return; + registerSessionTools(server, session, toolLogger, { + ...selectedOptions, + artifactResources, + ...(availability === undefined + ? {} + : { availabilityPolicy: availability.policy }), + ...(availability?.evmLimiterAvailability === undefined + ? {} + : { evmLimiterAvailability: availability.evmLimiterAvailability }), + androidAnalysisAvailability: (signal) => + android.inspectAvailability(signal), + adbAnalysisAvailability: (signal) => + adbDevice.inspectAvailability(signal), + apktoolAnalysisAvailability: (signal) => + apktool.inspectAvailability(signal), + jebAnalysisAvailability: (signal) => jeb.inspectAvailability(signal), + startedAt, + }); + }, + }); registerGuidedPrompts(server, analysis, session); - if (session !== undefined) { - registerSessionTools(server, session, toolLogger, { - ...selectedOptions, - artifactResources, - ...(availability === undefined - ? {} - : { availabilityPolicy: availability.policy }), - ...(availability?.evmLimiterAvailability === undefined - ? {} - : { evmLimiterAvailability: availability.evmLimiterAvailability }), - androidAnalysisAvailability: (signal) => - android.inspectAvailability(signal), - adbAnalysisAvailability: (signal) => - adbDevice.inspectAvailability(signal), - apktoolAnalysisAvailability: (signal) => - apktool.inspectAvailability(signal), - jebAnalysisAvailability: (signal) => jeb.inspectAvailability(signal), - startedAt, - }); - } return server; }; @@ -919,263 +1212,6 @@ const selectCompactInputSchemaPresentation = ( return compactPresentationForMcpInputSchemaProfile(configured.value); }; -const registerConfiguredAnalysisTools = ( - { - server, - options, - environment, - logger: toolLogger, - evidenceById, - recordEvidence, - withAdmittedAnalysis, - goBinary, - rustBinary, - erlangBeam, - il2cppMetadata, - peResources, - nativeExports, - devicetree, - elfNotes, - appleIps, - kernelModule, - peLoadConfig, - peCertificates, - peImports, - pythonPyc, - webNetworkCapture, - artifactResources, - }: ServerToolContext, - android: AndroidAnalysisPort, - wasmArtifact: Pick, - apkSignature: Pick, - evmInterface: EvmInterfaceService, - jeb: JebAnalysisPort, - firmwareAnalysis: FirmwareAnalysisService, - webRuntime: WebRuntimeService, -): void => { - registerAndroidTools( - server, - new AndroidAnalysisService(android, recordEvidence), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerJebTools( - server, - new JebAnalysisService(jeb), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerBinaryDiagnosticsTools( - server, - options.binaryLayout ?? createBinaryLayoutService(environment), - createByteEntropyService(artifactResources), - toolLogger, - recordEvidence, - evidenceById, - withAdmittedAnalysis, - ); - registerAnalysisViewTool( - server, - toolLogger, - evidenceById, - recordEvidence, - withAdmittedAnalysis, - artifactResources, - ); - registerPeResourcesTool( - server, - peResources, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerNativeExportsTool( - server, - nativeExports, - toolLogger, - recordEvidence, - evidenceById, - withAdmittedAnalysis, - ); - registerDevicetreeTool( - server, - devicetree, - toolLogger, - recordEvidence, - evidenceById, - withAdmittedAnalysis, - ); - registerElfNotesTool( - server, - elfNotes, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerAppleIpsTool( - server, - appleIps, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerKernelModuleTool( - server, - kernelModule, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerPeLoadConfigTool( - server, - peLoadConfig, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerPeCertificatesTool( - server, - peCertificates, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerPeImportsTool( - server, - peImports, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerGoTools( - server, - goBinary, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerIl2cppTools( - server, - il2cppMetadata, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerRustTools( - server, - rustBinary, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerPythonPycTool( - server, - pythonPyc, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerErlangTools( - server, - erlangBeam, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerWasmTools( - server, - wasmArtifact, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerApkSignatureTools( - server, - apkSignature, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerEvmTools( - server, - evmInterface, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerSqliteTools( - server, - options.sqliteDatabase ?? createSqliteDatabaseService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerRecordedCrashTools( - server, - options.recordedCrash ?? createRecordedCrashService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerFirmwareTools( - server, - firmwareAnalysis, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebModuleTool( - server, - options.webModuleTrace ?? createWebModuleTraceService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebSourceLocationTool( - server, - options.webSourceLocation ?? createWebSourceLocationService(environment), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebRuntimeTools( - server, - webRuntime, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); - registerWebNetworkCaptureTool( - server, - webNetworkCapture, - toolLogger, - recordEvidence, - withAdmittedAnalysis, - evidenceById, - ); - registerJavaScriptRecoveryTool( - server, - new JavaScriptRecoveryService( - options.javascriptRecovery ?? - createJavaScriptRecoveryProvider(environment), - ), - toolLogger, - recordEvidence, - withAdmittedAnalysis, - ); -}; - const createSessionRecorders = ( server: EvidenceMcpServer, session: BinarySessionPort | undefined, @@ -1200,146 +1236,6 @@ const createSessionRecorders = ( ) => session.recordEvidenceWithUnknown(evidence, input), }); -interface ServerToolContext extends ReturnType { - readonly goBinary: GoBinaryService; - readonly rustBinary: RustBinaryService; - readonly erlangBeam: BeamService; - readonly il2cppMetadata: Il2cppMetadataService; - readonly peResources: PeResourcesService; - readonly nativeExports: NativeExportsService; - readonly devicetree: DevicetreeService; - readonly elfNotes: ElfNotesService; - readonly appleIps: AppleIpsService; - readonly kernelModule: KernelModuleService; - readonly peLoadConfig: PeLoadConfigService; - readonly peCertificates: PeCertificatesService; - readonly peImports: PeImportsService; - readonly electronFuses: ElectronFuseService; - readonly pythonPyc: PythonPycService; - readonly webNetworkCapture: WebNetworkCaptureService; - readonly artifactResources: ArtifactResourceScope; - readonly server: EvidenceMcpServer; - readonly session: BinarySessionPort | undefined; - readonly options: CreateServerOptions; - readonly environment: Readonly; - readonly logger: Logger; - readonly withAdmittedAnalysis: WithAdmittedAnalysis; -} - -const registerBinaryAnalysisTools = ({ - server, - session, - logger, - activeTarget, - recordEvidence, - recordEvidenceWithUnknown, - evidenceById, - withAdmittedAnalysis, -}: ServerToolContext): void => { - const recordUnknown = - session === undefined - ? undefined - : (input: Parameters[0]) => - session.recordUnknown(input); - const analysisOptions = { - logger, - activeTarget, - recordEvidence, - recordUnknown, - evidenceById, - withAdmittedAnalysis, - }; - registerOfficialTools(server, analysisOptions); - registerEnhancedTools(server, { - ...analysisOptions, - analysisProfile: - session === undefined - ? undefined - : (operation) => - session.analysisProfile() ?? session.analysisProfile(operation), - allowsSnapshotReplay: - session === undefined - ? undefined - : (operation) => session.allowsSnapshotReplay(operation), - recordWorkflowSnapshot: - session === undefined - ? undefined - : (input) => session.recordWorkflowSnapshot(input), - }); - const evidenceOptions = { - logger, - activeTarget, - recordEvidence, - withAdmittedAnalysis, - }; - registerNativeTools(server, { ...evidenceOptions, evidenceById }); - registerArtifactTools(server, { ...evidenceOptions, evidenceById }); - registerManagedTools(server, { ...evidenceOptions, evidenceById }); - if (session !== undefined) - registerManagedWorkflowTools(server, { - logger, - recordEvidence, - recordEvidenceWithUnknown, - session, - withAdmittedAnalysis, - }); -}; - -const registerObservationTools = ({ - electronFuses, - evidenceById, - server, - options, - logger, - recordEvidence, - recordEvidenceWithUnknown, - withAdmittedAnalysis, - artifactResources, - environment, -}: ServerToolContext): void => { - const common = { logger, recordEvidence, withAdmittedAnalysis }; - registerWebScriptTool(server, { ...common, artifactResources }); - registerBrowserTools(server, { - ...common, - evidenceById, - browser: options.browserObservation, - loadFailure: options.optionalProviderLoadFailures?.browserObservation, - }); - registerBrowserScenarioTool(server, { - ...common, - evidenceById, - provider: options.browserScenarioCapture, - loadFailure: options.optionalProviderLoadFailures?.browserScenarioCapture, - }); - registerElectronTools(server, { - ...common, - artifactResources, - environment, - evidenceById, - electron: options.electronObservation, - electronActive: options.electronActiveObservation, - electronFuses, - observationLoadFailure: - options.optionalProviderLoadFailures?.electronObservation, - activeLoadFailure: - options.optionalProviderLoadFailures?.electronActiveObservation, - }); - registerJavaScriptRuntimeObservationTools(server, { - ...common, - runtime: options.javascriptRuntimeObservation, - loadFailure: - options.optionalProviderLoadFailures?.javascriptRuntimeObservation, - }); - registerApplicationTools(server, { - artifactResources, - evidenceById, - logger, - recordEvidence, - recordEvidenceWithUnknown, - withAdmittedAnalysis, - }); -}; - const selectAdvertisedToolFamilies = ( environment: Readonly, ): ReadonlySet | undefined => { diff --git a/tests/boundary/cli/cliToolFamilyOwners.test.ts b/tests/boundary/cli/cliToolFamilyOwners.test.ts new file mode 100644 index 0000000000..8b7ad988fc --- /dev/null +++ b/tests/boundary/cli/cliToolFamilyOwners.test.ts @@ -0,0 +1,64 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +import { toolFamilyId } from "../../../src/contracts/toolFamilies.js"; + +const SHARED_APPLICATION_TOOLS = [ + ["export_web_scripts", "registerWebScriptCommands", "registerWebScriptTool"], + [ + "trace_web_module_imports", + "registerWebModuleCommands", + "registerWebModuleTool", + ], + [ + "trace_web_source_location", + "registerWebSourceLocationCommands", + "registerWebSourceLocationTool", + ], +] as const; + +const familyBody = (source: string, family: string): string => { + const marker = `${family}: () =>`; + const start = source.indexOf(marker); + const open = source.indexOf("{", start); + let depth = 0; + for (let index = open; index < source.length; index += 1) { + const character = source[index]; + if (character === "{") depth += 1; + else if (character === "}") { + depth -= 1; + if (depth === 0) return source.slice(open, index + 1); + } + } + throw new Error(`unclosed ${family} registrar`); +}; + +describe("CLI and MCP tool family ownership", () => { + const cli = readFileSync("src/cli.ts", "utf8"); + const server = readFileSync("src/server/createServer.ts", "utf8"); + + it("walks family ids without loading the contract tables", () => { + expect(cli).toContain('from "./contracts/toolFamilyIds.js"'); + expect(cli).not.toContain("toolFamilies.js"); + expect(server).toContain('from "../contracts/toolFamilyIds.js"'); + expect(server).not.toContain("toolFamilies.js"); + }); + + it("keeps web script commands in the application family on both adapters", () => { + const cliApplication = familyBody(cli, "application"); + const cliBrowser = familyBody(cli, "browser"); + const serverApplication = familyBody(server, "application"); + const serverBrowser = familyBody(server, "browser"); + for (const [ + tool, + cliRegistrar, + serverRegistrar, + ] of SHARED_APPLICATION_TOOLS) { + expect(toolFamilyId(tool)).toBe("application"); + expect(cliApplication).toContain(cliRegistrar); + expect(cliBrowser).not.toContain(cliRegistrar); + expect(serverApplication).toContain(serverRegistrar); + expect(serverBrowser).not.toContain(serverRegistrar); + } + }); +});