From c1b5ccf0a42ee44bde46241b15039bf07f24b0b1 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:03:24 +0800 Subject: [PATCH] refactor(server): admit complete evidence workflows and share handlers Validation: integrated changes passed formatting, typecheck, lint, resolved module-boundary verification and git diff --check. Tests and real-provider workflows were not run. --- src/server/admittedToolOperation.ts | 22 +++ src/server/contractToolHandler.ts | 54 +++++- src/server/createServer.ts | 106 ++++++++--- src/server/registerAdbTools.ts | 3 + src/server/registerAnalysisViewTool.ts | 24 ++- src/server/registerAndroidTools.ts | 3 + src/server/registerApktoolTools.ts | 3 + .../compareExportShapes.ts | 76 ++++---- .../compareSourceToBundle.ts | 71 ++++--- .../compareVersions.ts | 86 +++++---- .../registerApplicationTools/coverage.ts | 22 ++- .../obligationLedger.ts | 58 +++--- .../projectMobileApplicationGraph.ts | 46 +++-- .../registerApplicationTools/traceFeature.ts | 63 ++++--- .../traceSemantics.ts | 61 +++--- src/server/registerApplicationTools/types.ts | 2 + src/server/registerArtifactComparisonTool.ts | 106 ++++++----- src/server/registerBinaryDiagnosticsTools.ts | 55 ++++-- src/server/registerBrowserScenarioTool.ts | 37 ++-- src/server/registerBrowserTools.ts | 43 +++-- src/server/registerBundleComparisonTool.ts | 95 +++++++--- src/server/registerCloseLifecycleTool.ts | 49 ++++- src/server/registerElectronTools.ts | 43 +++-- src/server/registerEvmTools.ts | 24 ++- src/server/registerFirmwareTools.ts | 31 ++-- src/server/registerFunctionComparisonTool.ts | 152 ++++++++------- src/server/registerJavaScriptRecoveryTool.ts | 24 ++- ...gisterJavaScriptRuntimeObservationTools.ts | 37 ++-- src/server/registerJebTools.ts | 3 + .../compareManagedMembers.ts | 127 +++++++------ .../importManagedReconstruction.ts | 81 ++++---- .../projectManagedApplicationGraph.ts | 110 ++++++----- .../registerManagedWorkflowTools/types.ts | 2 + .../verifyManagedNativeBoundaries.ts | 173 +++++++++--------- src/server/registerPeResourcesTool.ts | 23 ++- src/server/registerProcessComparisonTool.ts | 137 ++++++++------ src/server/registerRecordedCrashTools.ts | 24 ++- src/server/registerSessionTools.ts | 134 ++++++++++---- src/server/registerWebModuleTool.ts | 24 ++- src/server/registerWebNetworkCaptureTool.ts | 24 ++- src/server/registerWebRuntimeTools.ts | 75 +++++--- src/server/registerWebScriptTool.ts | 40 ++-- src/server/registerWebSourceLocationTool.ts | 24 ++- 43 files changed, 1482 insertions(+), 915 deletions(-) create mode 100644 src/server/admittedToolOperation.ts diff --git a/src/server/admittedToolOperation.ts b/src/server/admittedToolOperation.ts new file mode 100644 index 000000000..3a81bd53b --- /dev/null +++ b/src/server/admittedToolOperation.ts @@ -0,0 +1,22 @@ +import type { + ServerContext, + CallToolResult, +} from "@modelcontextprotocol/server"; + +import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; + +/** Run one complete tool workflow inside session admission when supplied. */ +export const runAdmittedToolOperation = async ( + server: EvidenceMcpServer, + admission: WithAdmittedAnalysis | undefined, + operationName: string, + signal: ServerContext["mcpReq"]["signal"], + operation: () => Promise, +): Promise => { + if (admission === undefined) return operation(); + const admitted = await admission(operationName, signal, operation); + return admitted.ok + ? admitted.value + : server.delivery.toErrorToolResult(admitted.error); +}; diff --git a/src/server/contractToolHandler.ts b/src/server/contractToolHandler.ts index 6bf95d3e5..ea3e8935b 100644 --- a/src/server/contractToolHandler.ts +++ b/src/server/contractToolHandler.ts @@ -9,6 +9,8 @@ import type { Evidence } from "../domain/evidence.js"; import type { Result } from "../domain/result.js"; import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import { logToolExecution } from "./toolLogging.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; /** Application service executing one named contract operation. */ export interface ContractToolService { @@ -29,19 +31,51 @@ export const createContractToolHandler = service: ContractToolService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ) => - (contract: ToolContract) => { + (contract: ToolContract) => + createEvidenceToolHandler( + server, + (input, options) => service.execute(contract.name, input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + )(contract); + +/** Bind a named Evidence-producing operation to shared logging and delivery. */ +export const createEvidenceToolHandler = + ( + server: EvidenceMcpServer, + operation: ( + input: unknown, + options: ExecutionOptions, + ) => Promise>, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, + ) => + (contract: ToolContract) => { const name = contract.name; return async (input: unknown, context: ServerContext) => { - const result = await logToolExecution(logger, name, () => - service.execute(name, input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, + const executeAndRecord = async () => { + const result = await logToolExecution(logger, name, () => + operation(input, { signal: context.mcpReq.signal }), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }; + return runAdmittedToolOperation( + server, + withAdmittedAnalysis, + name, + context.mcpReq.signal, + executeAndRecord, ); }; }; diff --git a/src/server/createServer.ts b/src/server/createServer.ts index ef4a8b0b6..3692cb4e1 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -204,7 +204,23 @@ export const createServer = ( const analysis = source.kind === "session" ? source.session : source.analysis; const environment = snapshotEnvironment(options.environment ?? process.env); const delivery = selectToolResultDelivery(environment, options.delivery); - const selectedOptions = { ...options, environment, delivery }; + const evmInterface = + options.evmInterface ?? createEvmInterfaceService(environment); + const webSourceLocation = + options.webSourceLocation ?? createWebSourceLocationService(environment); + const javascriptRecovery = + options.javascriptRecovery ?? createJavaScriptRecoveryProvider(environment); + const firmwareAnalysis = new FirmwareAnalysisService( + options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment), + ); + const selectedOptions = { + ...options, + environment, + delivery, + evmInterface, + webSourceLocation, + javascriptRecovery, + }; const startedAt = new Date().toISOString(); const logger = options.logger ?? silentLogger; const server = createMcpServer( @@ -220,9 +236,34 @@ export const createServer = ( const jeb = options.jebAnalysis ?? createJebAnalysisProvider(environment); const adbDevice = options.adbDeviceAnalysis ?? createAdbDeviceAnalysisProvider(environment); + let providerClose: Promise | undefined; + const closeProviders = (): Promise => { + providerClose ??= Promise.allSettled([ + android.close(), + apktool.close(), + jeb.close(), + evmInterface.close(), + webSourceLocation.close(), + javascriptRecovery.close?.() ?? Promise.resolve(), + firmwareAnalysis.close(), + options.browserScenarioCapture?.close?.() ?? Promise.resolve(), + ]) + .then((results) => { + const failures = results.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (failures.length > 0) + throw new AggregateError(failures, "REA provider cleanup failed"); + }) + .catch((cause: unknown) => { + providerClose = undefined; + throw cause; + }); + return providerClose; + }; const availability = installSessionToolAvailability( session, - selectedOptions, + options, environment, ); const toolLogger = logger.child({ layer: "server" }); @@ -232,6 +273,7 @@ export const createServer = ( recordEvidence, recordEvidenceWithUnknown, } = createSessionRecorders(server, session); + const analysisAdmission = withAdmittedAnalysis(source); const toolContext: ServerToolContext = { server, session, @@ -242,54 +284,39 @@ export const createServer = ( activeTarget, recordEvidence, recordEvidenceWithUnknown, - withAdmittedAnalysis: withAdmittedAnalysis(source), + withAdmittedAnalysis: analysisAdmission, }; registerBinaryAnalysisTools(toolContext); const previousOnclose = server.server.onclose; server.server.onclose = () => { previousOnclose?.(); - void android.close().catch((cause: unknown) => { + void closeProviders().catch((cause: unknown) => { logger.error( { error: cause instanceof Error ? cause.message : String(cause) }, - "Android provider cleanup failed", - ); - }); - void apktool.close().catch((cause: unknown) => { - logger.error( - { error: cause instanceof Error ? cause.message : String(cause) }, - "Apktool provider cleanup failed", - ); - }); - void jeb.close().catch((cause: unknown) => { - logger.error( - { error: cause instanceof Error ? cause.message : String(cause) }, - "JEB provider cleanup failed", + "Provider cleanup failed during server shutdown", ); }); }; const closeServer = server.close.bind(server); server.close = async () => { - const results = await Promise.allSettled([ - closeServer(), - android.close(), - apktool.close(), - jeb.close(), - ]); + const results = await Promise.allSettled([closeServer(), closeProviders()]); for (const result of results) if (result.status === "rejected") throw result.reason; }; - registerConfiguredAnalysisTools(toolContext, android, jeb); + registerConfiguredAnalysisTools(toolContext, android, jeb, firmwareAnalysis); registerAdbTools( server, new AdbDeviceAnalysisService(adbDevice), toolLogger, recordEvidence, + analysisAdmission, ); registerApktoolTools( server, new ApktoolResourceAnalysisService(apktool), toolLogger, recordEvidence, + analysisAdmission, ); registerObservationTools(toolContext); registerGuidedPrompts(server, analysis, session); @@ -339,21 +366,25 @@ const registerConfiguredAnalysisTools = ( logger: toolLogger, evidenceById, recordEvidence, + withAdmittedAnalysis, }: ServerToolContext, android: AndroidAnalysisPort, jeb: JebAnalysisPort, + firmwareAnalysis: FirmwareAnalysisService, ): void => { registerAndroidTools( server, new AndroidAnalysisService(android), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerJebTools( server, new JebAnalysisService(jeb), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerBinaryDiagnosticsTools( server, @@ -361,57 +392,70 @@ const registerConfiguredAnalysisTools = ( toolLogger, recordEvidence, evidenceById, + withAdmittedAnalysis, + ); + registerAnalysisViewTool( + server, + toolLogger, + evidenceById, + recordEvidence, + withAdmittedAnalysis, ); - registerAnalysisViewTool(server, toolLogger, evidenceById, recordEvidence); registerPeResourcesTool( server, createPeResourcesService(), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerEvmTools( server, options.evmInterface ?? createEvmInterfaceService(environment), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerRecordedCrashTools( server, options.recordedCrash ?? createRecordedCrashService(environment), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerFirmwareTools( server, - new FirmwareAnalysisService( - options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment), - ), + firmwareAnalysis, toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerWebModuleTool( server, options.webModuleTrace ?? createWebModuleTraceService(environment), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerWebSourceLocationTool( server, options.webSourceLocation ?? createWebSourceLocationService(environment), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerWebRuntimeTools( server, options.webRuntime ?? createWebRuntimeService(), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerWebNetworkCaptureTool( server, options.webNetworkCapture ?? createWebNetworkCaptureService(environment), toolLogger, recordEvidence, + withAdmittedAnalysis, ); registerJavaScriptRecoveryTool( server, @@ -421,6 +465,7 @@ const registerConfiguredAnalysisTools = ( ), toolLogger, recordEvidence, + withAdmittedAnalysis, ); }; @@ -509,6 +554,7 @@ const registerBinaryAnalysisTools = ({ recordEvidence, recordEvidenceWithUnknown, session, + withAdmittedAnalysis, }); }; @@ -519,8 +565,9 @@ const registerObservationTools = ({ logger, recordEvidence, recordEvidenceWithUnknown, + withAdmittedAnalysis, }: ServerToolContext): void => { - const common = { logger, recordEvidence }; + const common = { logger, recordEvidence, withAdmittedAnalysis }; registerWebScriptTool(server, common); registerBrowserTools(server, { ...common, @@ -553,5 +600,6 @@ const registerObservationTools = ({ logger, recordEvidence, recordEvidenceWithUnknown, + withAdmittedAnalysis, }); }; diff --git a/src/server/registerAdbTools.ts b/src/server/registerAdbTools.ts index a60edffc6..65020fdc7 100644 --- a/src/server/registerAdbTools.ts +++ b/src/server/registerAdbTools.ts @@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { createContractToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; /** Bind ADB handlers to their exact named contracts through shared workflows. */ export const registerAdbTools = ( @@ -12,12 +13,14 @@ export const registerAdbTools = ( service: AdbDeviceAnalysisService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const handler = createContractToolHandler( server, service, logger, recordEvidence, + withAdmittedAnalysis, ); for (const contract of [ toolContract("inspect_adb_client"), diff --git a/src/server/registerAnalysisViewTool.ts b/src/server/registerAnalysisViewTool.ts index 216c9e9ec..ebd204edd 100644 --- a/src/server/registerAnalysisViewTool.ts +++ b/src/server/registerAnalysisViewTool.ts @@ -5,7 +5,8 @@ import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind selected-view projection to its named contract and session Evidence owner. */ @@ -14,22 +15,19 @@ export const registerAnalysisViewTool = ( logger: Logger, evidenceById?: EvidenceLookup, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("inspect_analysis_view"); + const handler = createEvidenceToolHandler( + server, + (input) => Promise.resolve(inspectAnalysisView(input, evidenceById)), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input) => { - const result = await logToolExecution(logger, contract.name, () => - Promise.resolve(inspectAnalysisView(input, evidenceById)), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + handler(contract), ); }; diff --git a/src/server/registerAndroidTools.ts b/src/server/registerAndroidTools.ts index 8fd4ca04e..887e263dd 100644 --- a/src/server/registerAndroidTools.ts +++ b/src/server/registerAndroidTools.ts @@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { createContractToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; /** Bind Android handlers to their exact named contracts through shared workflows. */ export const registerAndroidTools = ( @@ -12,12 +13,14 @@ export const registerAndroidTools = ( service: AndroidAnalysisService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const handler = createContractToolHandler( server, service, logger, recordEvidence, + withAdmittedAnalysis, ); const packageContract = toolContract("inspect_android_package"); server.registerTool( diff --git a/src/server/registerApktoolTools.ts b/src/server/registerApktoolTools.ts index 4b2b850d9..b776a34af 100644 --- a/src/server/registerApktoolTools.ts +++ b/src/server/registerApktoolTools.ts @@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { createContractToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; /** Bind Apktool handlers to their exact named contracts. */ export const registerApktoolTools = ( @@ -12,12 +13,14 @@ export const registerApktoolTools = ( service: ApktoolResourceAnalysisService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const handler = createContractToolHandler( server, service, logger, recordEvidence, + withAdmittedAnalysis, ); for (const contract of [ toolContract("inspect_apktool_client"), diff --git a/src/server/registerApplicationTools/compareExportShapes.ts b/src/server/registerApplicationTools/compareExportShapes.ts index de811fd86..738040229 100644 --- a/src/server/registerApplicationTools/compareExportShapes.ts +++ b/src/server/registerApplicationTools/compareExportShapes.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; @@ -20,36 +21,49 @@ export const registerCompareJavaScriptExportShapesTool = ( server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input) => { - const resolved = resolvePairedEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const parsed = resolved.value; - const result = await logToolExecution(options.logger, contract.name, () => - Promise.resolve(compareJavaScriptExportShapesEvidenceValidated(parsed)), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordSessionEvidenceSources(options.recordEvidence, [ - parsed.left, - parsed.right, - ]); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, contract); - const comparison = javaScriptExportShapeComparisonResultSchema.parse( - result.value.normalized_result, - ); - const unknown = - comparison.summary.unknown > 0 || - comparison.coverage.status !== "complete-within-inputs"; - return recordResult( - { ...options, delivery: server.delivery }, - contract, - result.value, - unknown ? "javascript-export-shape" : undefined, - ); - }, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const resolved = resolvePairedEvidenceRequest( + input, + options.evidenceById, + ); + if (!resolved.ok) + return server.delivery.toCallToolResult(resolved, contract); + const parsed = resolved.value; + const result = await logToolExecution( + options.logger, + contract.name, + () => + Promise.resolve( + compareJavaScriptExportShapesEvidenceValidated(parsed), + ), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = recordSessionEvidenceSources( + options.recordEvidence, + [parsed.left, parsed.right], + ); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, contract); + const comparison = javaScriptExportShapeComparisonResultSchema.parse( + result.value.normalized_result, + ); + const unknown = + comparison.summary.unknown > 0 || + comparison.coverage.status !== "complete-within-inputs"; + return recordResult( + { ...options, delivery: server.delivery }, + contract, + result.value, + unknown ? "javascript-export-shape" : undefined, + ); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/compareSourceToBundle.ts b/src/server/registerApplicationTools/compareSourceToBundle.ts index d3bfde679..5ca65ee1d 100644 --- a/src/server/registerApplicationTools/compareSourceToBundle.ts +++ b/src/server/registerApplicationTools/compareSourceToBundle.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; @@ -20,34 +21,46 @@ export const registerCompareSourceToBundleTool = ( server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input) => { - const resolved = resolveApplicationEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const parsed = resolved.value; - const result = await logToolExecution(options.logger, contract.name, () => - Promise.resolve(compareSourceToBundleEvidenceValidated(parsed)), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordSessionEvidenceSources(options.recordEvidence, [ - parsed.application, - ]); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, contract); - const comparison = sourceToBundleComparisonResultSchema.parse( - result.value.normalized_result, - ); - return recordResult( - { ...options, delivery: server.delivery }, - contract, - result.value, - comparison.summary.unknown > 0 - ? "source-to-bundle-comparison" - : undefined, - ); - }, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const resolved = resolveApplicationEvidenceRequest( + input, + options.evidenceById, + ); + if (!resolved.ok) + return server.delivery.toCallToolResult(resolved, contract); + const parsed = resolved.value; + const result = await logToolExecution( + options.logger, + contract.name, + () => + Promise.resolve(compareSourceToBundleEvidenceValidated(parsed)), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = recordSessionEvidenceSources( + options.recordEvidence, + [parsed.application], + ); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, contract); + const comparison = sourceToBundleComparisonResultSchema.parse( + result.value.normalized_result, + ); + return recordResult( + { ...options, delivery: server.delivery }, + contract, + result.value, + comparison.summary.unknown > 0 + ? "source-to-bundle-comparison" + : undefined, + ); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/compareVersions.ts b/src/server/registerApplicationTools/compareVersions.ts index dd9f6c6ff..749279140 100644 --- a/src/server/registerApplicationTools/compareVersions.ts +++ b/src/server/registerApplicationTools/compareVersions.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { resolvePairedEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; @@ -20,44 +21,53 @@ export const registerCompareApplicationVersionsTool = ( server.registerTool( compareContract.name, toolRegistrationOptions(compareContract), - async (input) => { - const resolved = resolvePairedEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, compareContract); - const parsed = resolved.value; - const result = await logToolExecution( - options.logger, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, compareContract.name, - () => - Promise.resolve(compareApplicationVersionsEvidenceValidated(parsed)), - ); - if (!result.ok) - return server.delivery.toCallToolResult(result, compareContract); - const sources = [ - parsed.left, - parsed.right, - ...parsed.left_native_observations, - ...parsed.right_native_observations, - ]; - const recorded = recordSessionEvidenceSources( - options.recordEvidence, - sources, - ); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, compareContract); - const comparison = applicationVersionComparisonResultSchema.parse( - result.value.normalized_result, - ); - const unknown = comparison.summary.unknown > 0; - return recordResult( - { ...options, delivery: server.delivery }, - compareContract, - result.value, - unknown ? "application-version-comparison" : undefined, - ); - }, + context.mcpReq.signal, + async () => { + const resolved = resolvePairedEvidenceRequest( + input, + options.evidenceById, + ); + if (!resolved.ok) + return server.delivery.toCallToolResult(resolved, compareContract); + const parsed = resolved.value; + const result = await logToolExecution( + options.logger, + compareContract.name, + () => + Promise.resolve( + compareApplicationVersionsEvidenceValidated(parsed), + ), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, compareContract); + const sources = [ + parsed.left, + parsed.right, + ...parsed.left_native_observations, + ...parsed.right_native_observations, + ]; + const recorded = recordSessionEvidenceSources( + options.recordEvidence, + sources, + ); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, compareContract); + const comparison = applicationVersionComparisonResultSchema.parse( + result.value.normalized_result, + ); + const unknown = comparison.summary.unknown > 0; + return recordResult( + { ...options, delivery: server.delivery }, + compareContract, + result.value, + unknown ? "application-version-comparison" : undefined, + ); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/coverage.ts b/src/server/registerApplicationTools/coverage.ts index 6063c025b..3b986da1b 100644 --- a/src/server/registerApplicationTools/coverage.ts +++ b/src/server/registerApplicationTools/coverage.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { evaluateReconstructionCoverage } from "../../application/ReconstructionCoverageService.js"; import { applicationToolContract } from "../../contracts/applicationToolContracts.js"; @@ -16,11 +17,20 @@ export const registerCoverageTools = ( server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input) => { - const result = await logToolExecution(options.logger, contract.name, () => - Promise.resolve(evaluateReconstructionCoverage(input)), - ); - return server.delivery.toCallToolResult(result, contract); - }, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution( + options.logger, + contract.name, + () => Promise.resolve(evaluateReconstructionCoverage(input)), + ); + return server.delivery.toCallToolResult(result, contract); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/obligationLedger.ts b/src/server/registerApplicationTools/obligationLedger.ts index 3b0e8fc36..5899d1798 100644 --- a/src/server/registerApplicationTools/obligationLedger.ts +++ b/src/server/registerApplicationTools/obligationLedger.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; import { @@ -23,27 +24,40 @@ export const registerReconstructionObligationLedgerTool = ( server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input) => { - const resolved = resolveReconstructionObligationLedgerRequest(input); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const result = await logToolExecution(options.logger, contract.name, () => - Promise.resolve( - buildReconstructionObligationLedgerEvidenceValidated(resolved.value), - ), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordSessionEvidenceSources( - options.recordEvidence, - resolved.value.evidence_bundle.records, - ); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, contract); - return recordResult( - { ...options, delivery: server.delivery }, - contract, - result.value, - ); - }, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const resolved = resolveReconstructionObligationLedgerRequest(input); + if (!resolved.ok) + return server.delivery.toCallToolResult(resolved, contract); + const result = await logToolExecution( + options.logger, + contract.name, + () => + Promise.resolve( + buildReconstructionObligationLedgerEvidenceValidated( + resolved.value, + ), + ), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = recordSessionEvidenceSources( + options.recordEvidence, + resolved.value.evidence_bundle.records, + ); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, contract); + return recordResult( + { ...options, delivery: server.delivery }, + contract, + result.value, + ); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/projectMobileApplicationGraph.ts b/src/server/registerApplicationTools/projectMobileApplicationGraph.ts index 55e0687f2..f82ff6405 100644 --- a/src/server/registerApplicationTools/projectMobileApplicationGraph.ts +++ b/src/server/registerApplicationTools/projectMobileApplicationGraph.ts @@ -1,4 +1,6 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import type { ServerContext } from "@modelcontextprotocol/server"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { projectAndroidApplicationEvidence } from "../../application/android/AndroidApplicationService.js"; import { projectAppleApplicationEvidence } from "../../application/apple/AppleApplicationService.js"; @@ -44,23 +46,33 @@ const registerProjection = (context: { server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input: unknown) => { - const result = await logToolExecution(options.logger, contract.name, () => - Promise.resolve(project(input)), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recordedSources = recordSessionEvidenceSources( - options.recordEvidence, - inventoryEvidenceSources(input), - ); - if (!recordedSources.ok) - return server.delivery.toCallToolResult(recordedSources, contract); - return recordResult( - { ...options, delivery: server.delivery }, - contract, - result.value, - ); - }, + async (input: unknown, request: ServerContext) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + request.mcpReq.signal, + async () => { + const result = await logToolExecution( + options.logger, + contract.name, + () => Promise.resolve(project(input)), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recordedSources = recordSessionEvidenceSources( + options.recordEvidence, + inventoryEvidenceSources(input), + ); + if (!recordedSources.ok) + return server.delivery.toCallToolResult(recordedSources, contract); + return recordResult( + { ...options, delivery: server.delivery }, + contract, + result.value, + ); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/traceFeature.ts b/src/server/registerApplicationTools/traceFeature.ts index 119ce4355..859a3795e 100644 --- a/src/server/registerApplicationTools/traceFeature.ts +++ b/src/server/registerApplicationTools/traceFeature.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; @@ -19,33 +20,41 @@ export const registerTraceFeatureTool = ( server.registerTool( traceContract.name, toolRegistrationOptions(traceContract), - async (input) => { - const resolved = resolveApplicationEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, traceContract); - const parsed = resolved.value; - const result = await logToolExecution( - options.logger, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, traceContract.name, - () => Promise.resolve(traceApplicationFeatureEvidenceValidated(parsed)), - ); - if (!result.ok) - return server.delivery.toCallToolResult(result, traceContract); - const sources = [parsed.application, ...parsed.native_observations]; - const recorded = recordSessionEvidenceSources( - options.recordEvidence, - sources, - ); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, traceContract); - return recordResult( - { ...options, delivery: server.delivery }, - traceContract, - result.value, - ); - }, + context.mcpReq.signal, + async () => { + const resolved = resolveApplicationEvidenceRequest( + input, + options.evidenceById, + ); + if (!resolved.ok) + return server.delivery.toCallToolResult(resolved, traceContract); + const parsed = resolved.value; + const result = await logToolExecution( + options.logger, + traceContract.name, + () => + Promise.resolve(traceApplicationFeatureEvidenceValidated(parsed)), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, traceContract); + const sources = [parsed.application, ...parsed.native_observations]; + const recorded = recordSessionEvidenceSources( + options.recordEvidence, + sources, + ); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, traceContract); + return recordResult( + { ...options, delivery: server.delivery }, + traceContract, + result.value, + ); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/traceSemantics.ts b/src/server/registerApplicationTools/traceSemantics.ts index c8f75878f..7f907a272 100644 --- a/src/server/registerApplicationTools/traceSemantics.ts +++ b/src/server/registerApplicationTools/traceSemantics.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; @@ -19,28 +20,42 @@ export const registerTraceJavaScriptSemanticsTool = ( server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input) => { - const resolved = resolveApplicationEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const parsed = resolved.value; - const result = await logToolExecution(options.logger, contract.name, () => - Promise.resolve(traceJavaScriptSemanticsEvidenceValidated(parsed)), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordSessionEvidenceSources(options.recordEvidence, [ - parsed.application, - ]); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, contract); - return recordResult( - { ...options, delivery: server.delivery }, - contract, - result.value, - ); - }, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const resolved = resolveApplicationEvidenceRequest( + input, + options.evidenceById, + ); + if (!resolved.ok) + return server.delivery.toCallToolResult(resolved, contract); + const parsed = resolved.value; + const result = await logToolExecution( + options.logger, + contract.name, + () => + Promise.resolve( + traceJavaScriptSemanticsEvidenceValidated(parsed), + ), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = recordSessionEvidenceSources( + options.recordEvidence, + [parsed.application], + ); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, contract); + return recordResult( + { ...options, delivery: server.delivery }, + contract, + result.value, + ); + }, + ), ); }; diff --git a/src/server/registerApplicationTools/types.ts b/src/server/registerApplicationTools/types.ts index 82c5c6ad4..27fbf70c7 100644 --- a/src/server/registerApplicationTools/types.ts +++ b/src/server/registerApplicationTools/types.ts @@ -4,9 +4,11 @@ import type { EvidenceUnknownWriter, } from "../../application/investigation/InvestigationRecordPort.js"; import type { Logger } from "pino"; +import type { WithAdmittedAnalysis } from "../analysisAdmission.js"; /** Shared services for registering JavaScript application graph workflows. */ export interface ApplicationToolRegistration { + readonly withAdmittedAnalysis?: WithAdmittedAnalysis; readonly logger: Logger; readonly evidenceById: EvidenceReader["evidenceById"] | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; diff --git a/src/server/registerArtifactComparisonTool.ts b/src/server/registerArtifactComparisonTool.ts index 7352430ca..f9839ac1d 100644 --- a/src/server/registerArtifactComparisonTool.ts +++ b/src/server/registerArtifactComparisonTool.ts @@ -10,6 +10,8 @@ import { recordDerivedEvidence } from "./recordDerivedEvidence.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; import { ARTIFACT_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { withAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; /** Register Evidence-backed deterministic artifact comparison. */ export const registerArtifactComparisonTool = ( @@ -17,54 +19,70 @@ export const registerArtifactComparisonTool = ( session: BinarySessionPort, contract: ReturnType>, ): void => { + const admission = withAdmittedAnalysis({ kind: "session", session }); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const computed = await runDerivedOperation(context, contract.name, () => { - const left = parseEvidence(input.left); - const right = parseEvidence(input.right); - return { left, right, comparison: compareArtifacts(left, right) }; - }); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const { left, right, comparison } = computed.value; - const sources = [left, right]; - for (const source of sources) { - const recordedSource = session.recordEvidence(source); - if (!recordedSource.ok) - return server.delivery.toCallToolResult(recordedSource, contract); - } - const leftEvidenceIds = [left.evidence_id]; - const rightEvidenceIds = [right.evidence_id]; - const evidence = createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, { - predicateType: "rea.artifact-comparison", - operation: contract.name, - parameters: { - left_evidence_ids: leftEvidenceIds, - right_evidence_ids: rightEvidenceIds, + async (input, context) => + runAdmittedToolOperation( + server, + admission, + contract.name, + context.mcpReq.signal, + async () => { + const computed = await runDerivedOperation( + context, + contract.name, + () => { + const left = parseEvidence(input.left); + const right = parseEvidence(input.right); + return { left, right, comparison: compareArtifacts(left, right) }; + }, + ); + if (!computed.ok) + return server.delivery.toCallToolResult(computed, contract); + const { left, right, comparison } = computed.value; + const sources = [left, right]; + for (const source of sources) { + const recordedSource = session.recordEvidence(source); + if (!recordedSource.ok) + return server.delivery.toCallToolResult(recordedSource, contract); + } + const leftEvidenceIds = [left.evidence_id]; + const rightEvidenceIds = [right.evidence_id]; + const evidence = createEvidence( + undefined, + ARTIFACT_COMPARISON_PROVIDER, + { + predicateType: "rea.artifact-comparison", + operation: contract.name, + parameters: { + left_evidence_ids: leftEvidenceIds, + right_evidence_ids: rightEvidenceIds, + }, + result: jsonValueSchema.parse(comparison), + confidence: "derived", + authority: "analyst-inference", + limitations: comparison.limitations, + evidenceLinks: [...leftEvidenceIds, ...rightEvidenceIds], + }, + ); + return server.delivery.toEvidenceToolResult( + evidence, + contract, + recordDerivedEvidence( + session, + evidence, + artifactUnknownInput( + left, + right, + comparison.status, + evidence.evidence_id, + ), + ), + ); }, - result: jsonValueSchema.parse(comparison), - confidence: "derived", - authority: "analyst-inference", - limitations: comparison.limitations, - evidenceLinks: [...leftEvidenceIds, ...rightEvidenceIds], - }); - return server.delivery.toEvidenceToolResult( - evidence, - contract, - recordDerivedEvidence( - session, - evidence, - artifactUnknownInput( - left, - right, - comparison.status, - evidence.evidence_id, - ), - ), - ); - }, + ), ); }; diff --git a/src/server/registerBinaryDiagnosticsTools.ts b/src/server/registerBinaryDiagnosticsTools.ts index 95d268428..99ca2cfd4 100644 --- a/src/server/registerBinaryDiagnosticsTools.ts +++ b/src/server/registerBinaryDiagnosticsTools.ts @@ -8,6 +8,8 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; /** Bind offline layout inspection to its named contract and session Evidence owner. */ export const registerBinaryDiagnosticsTools = ( @@ -16,30 +18,43 @@ export const registerBinaryDiagnosticsTools = ( logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], evidenceById?: EvidenceLookup, + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("inspect_binary_layout"); server.registerTool( contract.name, toolRegistrationOptions(contract), - async ({ detail, ...request }, context) => { - const result = await logToolExecution(logger, contract.name, async () => { - const inspected = await service.inspect(request, { - signal: context.mcpReq.signal, - }); - return detail === "summary" && inspected.ok - ? summarizeRetainedAnalysis(inspected.value, { - recordEvidence, - evidenceById, - }) - : inspected; - }); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + async ({ detail, ...request }, context) => + runAdmittedToolOperation( + server, + withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution( + logger, + contract.name, + async () => { + const inspected = await service.inspect(request, { + signal: context.mcpReq.signal, + }); + return detail === "summary" && inspected.ok + ? summarizeRetainedAnalysis(inspected.value, { + recordEvidence, + evidenceById, + }) + : inspected; + }, + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }, + ), ); }; diff --git a/src/server/registerBrowserScenarioTool.ts b/src/server/registerBrowserScenarioTool.ts index 6e710cd5d..a22d86cba 100644 --- a/src/server/registerBrowserScenarioTool.ts +++ b/src/server/registerBrowserScenarioTool.ts @@ -13,12 +13,15 @@ import { browserScenarioSchema } from "../domain/browserScenario.js"; import type { Logger } from "pino"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; interface BrowserScenarioToolRegistration { readonly logger: Logger; readonly loadFailure?: OptionalProviderLoadFailure | undefined; readonly provider: BrowserScenarioCapturePort | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis?: WithAdmittedAnalysis; } /** Register the browser scenario tool with execution-time provider diagnostics. */ @@ -37,17 +40,29 @@ export const registerBrowserScenarioTool = ( err(optionalProviderUnavailable(options.loadFailure, contract.name)), contract, ); - const result = await logToolExecution(options.logger, contract.name, () => - captureBrowserScenario(options.provider, scenario, { - signal: context.mcpReq.signal, - }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = options.recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, + return runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution( + options.logger, + contract.name, + () => + captureBrowserScenario(options.provider, scenario, { + signal: context.mcpReq.signal, + }), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = options.recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }, ); }, ); diff --git a/src/server/registerBrowserTools.ts b/src/server/registerBrowserTools.ts index 80cbaa4e3..1a3e691d3 100644 --- a/src/server/registerBrowserTools.ts +++ b/src/server/registerBrowserTools.ts @@ -31,12 +31,15 @@ import type { Logger } from "pino"; import { mcpProgressReporter } from "./mcpProgress.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; interface BrowserToolRegistration { readonly logger: Logger; readonly loadFailure?: OptionalProviderLoadFailure | undefined; readonly browser: BrowserObservationPort | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis?: WithAdmittedAnalysis; } interface BrowserToolContext { @@ -50,7 +53,7 @@ export const registerBrowserTools = ( server: EvidenceMcpServer, options: BrowserToolRegistration, ): void => { - const registration = { ...options, delivery: server.delivery }; + const registration = { ...options, delivery: server.delivery, server }; const listContract = toolContract("list_browser_targets"); const inspectContract = toolContract("inspect_web_page"); const analyzeContract = toolContract("analyze_web_bundle"); @@ -179,7 +182,10 @@ export const registerBrowserTools = ( }; const runBrowserTool = async ( - options: BrowserToolRegistration & { readonly delivery: ToolResultDelivery }, + options: BrowserToolRegistration & { + readonly delivery: ToolResultDelivery; + readonly server: EvidenceMcpServer; + }, contract: ToolContract, request: { readonly input: Input; readonly context: ServerContext }, execute: ( @@ -193,17 +199,26 @@ const runBrowserTool = async ( err(optionalProviderUnavailable(options.loadFailure, contract.name)), contract, ); - const result = await logToolExecution(options.logger, contract.name, () => - execute(input, { - signal: context.mcpReq.signal, - progress: mcpProgressReporter(context), - }), - ); - if (!result.ok) return options.delivery.toCallToolResult(result, contract); - const recorded = options.recordEvidence?.(result.value); - return options.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, + return runAdmittedToolOperation( + options.server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution(options.logger, contract.name, () => + execute(input, { + signal: context.mcpReq.signal, + progress: mcpProgressReporter(context), + }), + ); + if (!result.ok) + return options.delivery.toCallToolResult(result, contract); + const recorded = options.recordEvidence?.(result.value); + return options.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }, ); }; diff --git a/src/server/registerBundleComparisonTool.ts b/src/server/registerBundleComparisonTool.ts index c5b03e612..bedb65c44 100644 --- a/src/server/registerBundleComparisonTool.ts +++ b/src/server/registerBundleComparisonTool.ts @@ -7,8 +7,11 @@ import { compareBundles } from "../domain/bundleComparison.js"; import { createEvidence } from "../domain/evidence.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; +import { recordSessionEvidenceSources } from "./sessionEvidence.js"; import { BUNDLE_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { withAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; /** Register canonical Evidence bundle comparison. */ export const registerBundleComparisonTool = ( @@ -16,37 +19,73 @@ export const registerBundleComparisonTool = ( session: BinarySessionPort, contract: ReturnType>, ): void => { + const admission = withAdmittedAnalysis({ kind: "session", session }); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const [left, right] = await Promise.all([ - readEvidenceBundle(input.left_bundle_path), - readEvidenceBundle(input.right_bundle_path), - ]); - if (!left.ok) return server.delivery.toCallToolResult(left, contract); - if (!right.ok) return server.delivery.toCallToolResult(right, contract); - const computed = await runDerivedOperation(context, contract.name, () => - compareBundles(left.value, right.value, input.record_pairs), - ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const comparison = computed.value; - const evidence = createEvidence(undefined, BUNDLE_COMPARISON_PROVIDER, { - predicateType: "rea.bundle-comparison", - operation: contract.name, - parameters: { - left_bundle_sha256: comparison.left_bundle_sha256, - right_bundle_sha256: comparison.right_bundle_sha256, - record_pairs: input.record_pairs, + async (input, context) => + runAdmittedToolOperation( + server, + admission, + contract.name, + context.mcpReq.signal, + async () => { + const [left, right] = await Promise.all([ + readEvidenceBundle(input.left_bundle_path), + readEvidenceBundle(input.right_bundle_path), + ]); + if (!left.ok) return server.delivery.toCallToolResult(left, contract); + if (!right.ok) + return server.delivery.toCallToolResult(right, contract); + const computed = await runDerivedOperation( + context, + contract.name, + () => compareBundles(left.value, right.value, input.record_pairs), + ); + if (!computed.ok) + return server.delivery.toCallToolResult(computed, contract); + const comparison = computed.value; + const sourceRecords = [ + ...new Map( + [...left.value.records, ...right.value.records].map((record) => [ + record.evidence_id, + record, + ]), + ).values(), + ]; + const recordedSources = recordSessionEvidenceSources( + (evidence) => session.recordEvidence(evidence), + sourceRecords, + ); + if (!recordedSources.ok) + return server.delivery.toCallToolResult(recordedSources, contract); + const evidence = createEvidence( + undefined, + BUNDLE_COMPARISON_PROVIDER, + { + predicateType: "rea.bundle-comparison", + operation: contract.name, + parameters: { + left_bundle_sha256: comparison.left_bundle_sha256, + right_bundle_sha256: comparison.right_bundle_sha256, + record_pairs: input.record_pairs, + }, + result: jsonValueSchema.parse(comparison), + confidence: "derived", + authority: "analyst-inference", + limitations: comparison.limitations, + evidenceLinks: sourceRecords.map( + ({ evidence_id }) => evidence_id, + ), + }, + ); + const recorded = session.recordEvidence(evidence); + return server.delivery.toEvidenceToolResult( + evidence, + contract, + recorded, + ); }, - result: jsonValueSchema.parse(comparison), - confidence: "derived", - authority: "analyst-inference", - limitations: comparison.limitations, - }); - const recorded = session.recordEvidence(evidence); - return server.delivery.toEvidenceToolResult(evidence, contract, recorded); - }, + ), ); }; diff --git a/src/server/registerCloseLifecycleTool.ts b/src/server/registerCloseLifecycleTool.ts index 828a0dfdb..1c7edcec0 100644 --- a/src/server/registerCloseLifecycleTool.ts +++ b/src/server/registerCloseLifecycleTool.ts @@ -3,6 +3,8 @@ import { reportLifecycleStart, } from "./lifecycleProgress.js"; import { ok } from "../domain/result.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import type { Result } from "../domain/result.js"; import { mcpProgressReporter } from "./mcpProgress.js"; import type { LifecycleToolRegistration } from "./registerSessionTools.js"; import { logToolExecution } from "./toolLogging.js"; @@ -14,7 +16,46 @@ export const registerCloseLifecycleTool = ({ session, logger, closeContract, + closeProcessResources, }: LifecycleToolRegistration): void => { + const closeWithResources = async ( + closeSession: () => Promise>, + ): Promise> => { + const [sessionClose, resourceClose] = await Promise.allSettled([ + closeSession(), + closeProcessResources(), + ]); + if (sessionClose.status === "rejected") { + if (resourceClose.status === "rejected") + logger.warn( + { + error: + resourceClose.reason instanceof Error + ? resourceClose.reason.message + : String(resourceClose.reason), + }, + "Process capture cleanup threw while closing the session", + ); + else if (!resourceClose.value.ok) + logger.warn( + { error: resourceClose.value.error.message }, + "Process capture cleanup failed while closing the session", + ); + throw sessionClose.reason; + } + if (resourceClose.status === "rejected") throw resourceClose.reason; + const closed = sessionClose.value; + const resources = resourceClose.value; + if (!closed.ok) { + if (!resources.ok) + logger.warn( + { error: resources.error.message }, + "Process capture cleanup failed while closing the session", + ); + return closed; + } + return resources.ok ? closed : resources; + }; server.registerTool( closeContract.name, toolRegistrationOptions(closeContract), @@ -25,7 +66,7 @@ export const registerCloseLifecycleTool = ({ // Enqueue the lifecycle transition before awaiting progress transport so // a later open request cannot overtake this close. const closing = logToolExecution(logger, closeContract.name, () => - session.close({ progress }), + closeWithResources(() => session.close({ progress })), ); await reportLifecycleStart(progress, closeContract.name); const closed = await closing; @@ -33,7 +74,11 @@ export const registerCloseLifecycleTool = ({ return server.delivery.toCallToolResult(closed, closeContract); } const closing = logToolExecution(logger, closeContract.name, () => - session.closeWithSnapshot(snapshotPath, input.overwrite, { progress }), + closeWithResources(() => + session.closeWithSnapshot(snapshotPath, input.overwrite, { + progress, + }), + ), ); await reportLifecycleStart( progress, diff --git a/src/server/registerElectronTools.ts b/src/server/registerElectronTools.ts index 3cab83ef8..2d128943c 100644 --- a/src/server/registerElectronTools.ts +++ b/src/server/registerElectronTools.ts @@ -30,6 +30,8 @@ import type { Logger } from "pino"; import { mcpProgressReporter } from "./mcpProgress.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; interface ElectronToolRegistration { readonly logger: Logger; @@ -39,6 +41,7 @@ interface ElectronToolRegistration { readonly electronActive: ElectronActiveObservationPort | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; readonly evidenceById: EvidenceLookup | undefined; + readonly withAdmittedAnalysis?: WithAdmittedAnalysis; } interface ElectronToolContext { @@ -52,7 +55,7 @@ export const registerElectronTools = ( server: EvidenceMcpServer, options: ElectronToolRegistration, ): void => { - const registration = { ...options, delivery: server.delivery }; + const registration = { ...options, delivery: server.delivery, server }; const listContract = toolContract("list_electron_targets"); const inspectContract = toolContract("inspect_electron_page"); const analyzeContract = toolContract("analyze_javascript_application"); @@ -139,7 +142,10 @@ export const registerElectronTools = ( }; const runElectronTool = async ( - options: ElectronToolRegistration & { readonly delivery: ToolResultDelivery }, + options: ElectronToolRegistration & { + readonly delivery: ToolResultDelivery; + readonly server: EvidenceMcpServer; + }, contract: ToolContract, request: { readonly input: Input; @@ -163,17 +169,26 @@ const runElectronTool = async ( err(optionalProviderUnavailable(failure, contract.name)), contract, ); - const result = await logToolExecution(options.logger, contract.name, () => - execute(input, { - signal: context.mcpReq.signal, - progress: mcpProgressReporter(context), - }), - ); - if (!result.ok) return options.delivery.toCallToolResult(result, contract); - const recorded = options.recordEvidence?.(result.value); - return options.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, + return runAdmittedToolOperation( + options.server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution(options.logger, contract.name, () => + execute(input, { + signal: context.mcpReq.signal, + progress: mcpProgressReporter(context), + }), + ); + if (!result.ok) + return options.delivery.toCallToolResult(result, contract); + const recorded = options.recordEvidence?.(result.value); + return options.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }, ); }; diff --git a/src/server/registerEvmTools.ts b/src/server/registerEvmTools.ts index 5ae594375..a2562329d 100644 --- a/src/server/registerEvmTools.ts +++ b/src/server/registerEvmTools.ts @@ -4,7 +4,8 @@ import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind the named EVM contract to shared validation and session Evidence recording. */ @@ -13,22 +14,19 @@ export const registerEvmTools = ( service: EvmInterfaceService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("inspect_evm_interface"); + const handler = createEvidenceToolHandler( + server, + (input, options) => service.inspect(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.inspect(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + handler(contract), ); }; diff --git a/src/server/registerFirmwareTools.ts b/src/server/registerFirmwareTools.ts index 5484d9681..7a1a95800 100644 --- a/src/server/registerFirmwareTools.ts +++ b/src/server/registerFirmwareTools.ts @@ -1,12 +1,11 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; -import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; -import type { ServerContext } from "@modelcontextprotocol/server"; import type { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js"; import { toolContract } from "../contracts/toolContracts.js"; -import type { ToolContract } from "../contracts/toolContractTypes.js"; import type { FirmwareOperation } from "../domain/firmware/firmwareAnalysis.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { createContractToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind firmware handlers to their exact named schemas. */ @@ -15,23 +14,15 @@ export const registerFirmwareTools = ( service: FirmwareAnalysisService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { - const handler = - (contract: ToolContract) => - async (input: unknown, context: ServerContext) => { - const result = await logToolExecution(logger, contract.name, () => - service.execute(contract.name, input, { - signal: context.mcpReq.signal, - }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }; + const handler = createContractToolHandler( + server, + service, + logger, + recordEvidence, + withAdmittedAnalysis, + ); const inspect = toolContract("inspect_firmware_regions"); server.registerTool( inspect.name, diff --git a/src/server/registerFunctionComparisonTool.ts b/src/server/registerFunctionComparisonTool.ts index 12f5a3357..b968972ed 100644 --- a/src/server/registerFunctionComparisonTool.ts +++ b/src/server/registerFunctionComparisonTool.ts @@ -16,6 +16,8 @@ import { recordSessionEvidenceSources } from "./sessionEvidence.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; import { FUNCTION_COMPARISON_PROVIDER } from "../application/InvestigationProviders.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { withAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; import { compareFunctions } from "../domain/functionComparison.js"; /** Register explicit Evidence-backed function comparison. */ @@ -24,74 +26,96 @@ export const registerFunctionComparisonTool = ( session: BinarySessionPort, contract: ReturnType>, ): void => { + const admission = withAdmittedAnalysis({ kind: "session", session }); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - let leftEvidence: Evidence; - let rightEvidence: Evidence; - try { - leftEvidence = parseEvidence(input.left); - rightEvidence = parseEvidence(input.right); - } catch (cause: unknown) { - return server.delivery.toCallToolResult( - err( - new EvidenceIntegrityError( - cause instanceof Error ? cause.message : "Invalid Evidence", - ), - ), - contract, - ); - } - if ( - leftEvidence.operation !== "analyze_function" || - rightEvidence.operation !== "analyze_function" || - leftEvidence.predicate_type !== "rea.analysis" || - rightEvidence.predicate_type !== "rea.analysis" - ) - return server.delivery.toCallToolResult( - err(new EvidenceIntegrityError("Expected analyze_function Evidence")), - contract, - ); - const leftIds = [leftEvidence.evidence_id]; - const rightIds = [rightEvidence.evidence_id]; - const computed = await runDerivedOperation(context, contract.name, () => - compareFunctions(leftEvidence, rightEvidence), - ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const comparison = computed.value; - const recordedSources = recordSessionEvidenceSources( - (evidence) => session.recordEvidence(evidence), - [leftEvidence, rightEvidence], - ); - if (!recordedSources.ok) - return server.delivery.toCallToolResult(recordedSources, contract); - const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, { - predicateType: "rea.function-comparison", - operation: contract.name, - parameters: { - left_evidence_id: leftEvidence.evidence_id, - right_evidence_id: rightEvidence.evidence_id, + async (input, context) => + runAdmittedToolOperation( + server, + admission, + contract.name, + context.mcpReq.signal, + async () => { + let leftEvidence: Evidence; + let rightEvidence: Evidence; + try { + leftEvidence = parseEvidence(input.left); + rightEvidence = parseEvidence(input.right); + } catch (cause: unknown) { + return server.delivery.toCallToolResult( + err( + new EvidenceIntegrityError( + cause instanceof Error ? cause.message : "Invalid Evidence", + ), + ), + contract, + ); + } + if ( + leftEvidence.operation !== "analyze_function" || + rightEvidence.operation !== "analyze_function" || + leftEvidence.predicate_type !== "rea.analysis" || + rightEvidence.predicate_type !== "rea.analysis" + ) + return server.delivery.toCallToolResult( + err( + new EvidenceIntegrityError( + "Expected analyze_function Evidence", + ), + ), + contract, + ); + const leftIds = [leftEvidence.evidence_id]; + const rightIds = [rightEvidence.evidence_id]; + const computed = await runDerivedOperation( + context, + contract.name, + () => compareFunctions(leftEvidence, rightEvidence), + ); + if (!computed.ok) + return server.delivery.toCallToolResult(computed, contract); + const comparison = computed.value; + const recordedSources = recordSessionEvidenceSources( + (evidence) => session.recordEvidence(evidence), + [leftEvidence, rightEvidence], + ); + if (!recordedSources.ok) + return server.delivery.toCallToolResult(recordedSources, contract); + const evidence = createEvidence( + undefined, + FUNCTION_COMPARISON_PROVIDER, + { + predicateType: "rea.function-comparison", + operation: contract.name, + parameters: { + left_evidence_id: leftEvidence.evidence_id, + right_evidence_id: rightEvidence.evidence_id, + }, + result: jsonValueSchema.parse(comparison), + confidence: "derived", + authority: "analyst-inference", + limitations: comparison.limitations, + evidenceLinks: [...leftIds, ...rightIds], + }, + ); + const recorded = recordDerivedEvidence( + session, + evidence, + functionUnknownInput({ + status: comparison.status, + leftIds, + rightIds, + comparisonEvidenceId: evidence.evidence_id, + }), + ); + return server.delivery.toEvidenceToolResult( + evidence, + contract, + recorded, + ); }, - result: jsonValueSchema.parse(comparison), - confidence: "derived", - authority: "analyst-inference", - limitations: comparison.limitations, - evidenceLinks: [...leftIds, ...rightIds], - }); - const recorded = recordDerivedEvidence( - session, - evidence, - functionUnknownInput({ - status: comparison.status, - leftIds, - rightIds, - comparisonEvidenceId: evidence.evidence_id, - }), - ); - return server.delivery.toEvidenceToolResult(evidence, contract, recorded); - }, + ), ); }; diff --git a/src/server/registerJavaScriptRecoveryTool.ts b/src/server/registerJavaScriptRecoveryTool.ts index ce742a2ba..ef0a450a6 100644 --- a/src/server/registerJavaScriptRecoveryTool.ts +++ b/src/server/registerJavaScriptRecoveryTool.ts @@ -4,7 +4,8 @@ import type { JavaScriptRecoveryService } from "../application/javascript/JavaSc import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind optional recovery to its exact named contract without acquiring an engine. */ @@ -13,22 +14,19 @@ export const registerJavaScriptRecoveryTool = ( service: JavaScriptRecoveryService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("recover_javascript_sources"); + const handler = createEvidenceToolHandler( + server, + (input, options) => service.recover(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.recover(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + handler(contract), ); }; diff --git a/src/server/registerJavaScriptRuntimeObservationTools.ts b/src/server/registerJavaScriptRuntimeObservationTools.ts index dce046f7e..3b6bd8b70 100644 --- a/src/server/registerJavaScriptRuntimeObservationTools.ts +++ b/src/server/registerJavaScriptRuntimeObservationTools.ts @@ -22,12 +22,15 @@ import { observeJavaScriptRuntimeInputSchema } from "../domain/javascript/javasc import type { Logger } from "pino"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; interface RuntimeToolRegistration { readonly logger: Logger; readonly loadFailure?: OptionalProviderLoadFailure | undefined; readonly runtime: JavaScriptRuntimeObservationPort | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis?: WithAdmittedAnalysis; } /** Register passive Inspector tools even when policy keeps them unavailable. */ @@ -35,7 +38,7 @@ export const registerJavaScriptRuntimeObservationTools = ( server: EvidenceMcpServer, options: RuntimeToolRegistration, ): void => { - const registration = { ...options, delivery: server.delivery }; + const registration = { ...options, delivery: server.delivery, server }; const listContract = toolContract("list_javascript_runtime_targets"); const observeContract = toolContract("observe_javascript_runtime"); server.registerTool( @@ -67,7 +70,10 @@ export const registerJavaScriptRuntimeObservationTools = ( }; const runRuntimeTool = async ( - options: RuntimeToolRegistration & { readonly delivery: ToolResultDelivery }, + options: RuntimeToolRegistration & { + readonly delivery: ToolResultDelivery; + readonly server: EvidenceMcpServer; + }, contract: ToolContract, request: { readonly input: Input; readonly context: ServerContext }, execute: ( @@ -81,14 +87,23 @@ const runRuntimeTool = async ( err(optionalProviderUnavailable(options.loadFailure, contract.name)), contract, ); - const result = await logToolExecution(options.logger, contract.name, () => - execute(input, context.mcpReq.signal), - ); - if (!result.ok) return options.delivery.toCallToolResult(result, contract); - const recorded = options.recordEvidence?.(result.value); - return options.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, + return runAdmittedToolOperation( + options.server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution(options.logger, contract.name, () => + execute(input, context.mcpReq.signal), + ); + if (!result.ok) + return options.delivery.toCallToolResult(result, contract); + const recorded = options.recordEvidence?.(result.value); + return options.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }, ); }; diff --git a/src/server/registerJebTools.ts b/src/server/registerJebTools.ts index 16f719485..96bd63723 100644 --- a/src/server/registerJebTools.ts +++ b/src/server/registerJebTools.ts @@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { createContractToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; /** Bind JEB handlers to their exact named contracts through shared workflows. */ export const registerJebTools = ( @@ -12,12 +13,14 @@ export const registerJebTools = ( service: JebAnalysisService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const handler = createContractToolHandler( server, service, logger, recordEvidence, + withAdmittedAnalysis, ); const clientContract = toolContract("inspect_jeb_client"); server.registerTool( diff --git a/src/server/registerManagedWorkflowTools/compareManagedMembers.ts b/src/server/registerManagedWorkflowTools/compareManagedMembers.ts index a357382a6..cea75f457 100644 --- a/src/server/registerManagedWorkflowTools/compareManagedMembers.ts +++ b/src/server/registerManagedWorkflowTools/compareManagedMembers.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; import { compareManagedMembersEvidenceValidated } from "../../application/managed/ManagedMemberComparisonService.js"; @@ -19,61 +20,77 @@ export const registerCompareManagedMembers = ( server.registerTool( compareContract.name, toolRegistrationOptions(compareContract), - async (input) => { - const leftResolved = resolveManagedEvidence(input.left); - const rightResolved = resolveManagedEvidence(input.right); - if (!leftResolved.ok) - return server.delivery.toCallToolResult(leftResolved, compareContract); - if (!rightResolved.ok) - return server.delivery.toCallToolResult(rightResolved, compareContract); - const [left] = leftResolved.value; - const [right] = rightResolved.value; - if (left === undefined || right === undefined) - throw new TypeError("Managed comparison Evidence resolution failed"); - const parsed = { left, right }; - const result = await logToolExecution( - options.logger, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, compareContract.name, - () => Promise.resolve(compareManagedMembersEvidenceValidated(parsed)), - ); - if (!result.ok) - return server.delivery.toCallToolResult(result, compareContract); - const recorded = recordSessionEvidenceSources(options.recordEvidence, [ - parsed.left, - parsed.right, - ]); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, compareContract); - const comparison = managedMemberComparisonResultSchema.parse( - result.value.normalized_result, - ); - const unknown = comparison.summary.unknown > 0; - const output = unknown - ? options.recordEvidenceWithUnknown?.(result.value, { - question: - "Which managed members remain unmatched or ambiguous across these versions?", - severity: "medium", - domain: "managed-member-comparison", - supporting_evidence_ids: [result.value.evidence_id], - contradicting_evidence_ids: [], - required_authority: "shipped-artifact", - required_confidence: "observed", - required_environment: null, - recommended_probes: [ - { - operation: "inspect_managed_members", - rationale: - "Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved members must remain unknown.", - }, - ], - relationships: [], - }) - : options.recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - compareContract, - output, - ); - }, + context.mcpReq.signal, + async () => { + const leftResolved = resolveManagedEvidence(input.left); + const rightResolved = resolveManagedEvidence(input.right); + if (!leftResolved.ok) + return server.delivery.toCallToolResult( + leftResolved, + compareContract, + ); + if (!rightResolved.ok) + return server.delivery.toCallToolResult( + rightResolved, + compareContract, + ); + const [left] = leftResolved.value; + const [right] = rightResolved.value; + if (left === undefined || right === undefined) + throw new TypeError( + "Managed comparison Evidence resolution failed", + ); + const parsed = { left, right }; + const result = await logToolExecution( + options.logger, + compareContract.name, + () => + Promise.resolve(compareManagedMembersEvidenceValidated(parsed)), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, compareContract); + const recorded = recordSessionEvidenceSources( + options.recordEvidence, + [parsed.left, parsed.right], + ); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, compareContract); + const comparison = managedMemberComparisonResultSchema.parse( + result.value.normalized_result, + ); + const unknown = comparison.summary.unknown > 0; + const output = unknown + ? options.recordEvidenceWithUnknown?.(result.value, { + question: + "Which managed members remain unmatched or ambiguous across these versions?", + severity: "medium", + domain: "managed-member-comparison", + supporting_evidence_ids: [result.value.evidence_id], + contradicting_evidence_ids: [], + required_authority: "shipped-artifact", + required_confidence: "observed", + required_environment: null, + recommended_probes: [ + { + operation: "inspect_managed_members", + rationale: + "Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved members must remain unknown.", + }, + ], + relationships: [], + }) + : options.recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + compareContract, + output, + ); + }, + ), ); }; diff --git a/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts b/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts index 934e18be8..0381164fb 100644 --- a/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts +++ b/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { importManagedReconstructionEvidenceValidated } from "../../application/managed/ManagedReconstructionService.js"; import { logToolExecution } from "../toolLogging.js"; @@ -19,39 +20,53 @@ export const registerImportManagedReconstruction = ( server.registerTool( reconstructionContract.name, toolRegistrationOptions(reconstructionContract), - async (input) => { - const resolved = resolveManagedEvidence(input.static_members); - if (!resolved.ok) - return server.delivery.toCallToolResult( - resolved, - reconstructionContract, - ); - const staticMembers = resolved.value[0]; - if (staticMembers === undefined) - throw new TypeError( - "Managed reconstruction Evidence resolution failed", - ); - const parsed = { ...input, static_members: staticMembers }; - const result = await logToolExecution( - options.logger, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, reconstructionContract.name, - () => - Promise.resolve(importManagedReconstructionEvidenceValidated(parsed)), - ); - if (!result.ok) - return server.delivery.toCallToolResult(result, reconstructionContract); - const recordedSource = options.recordEvidence?.(parsed.static_members); - if (recordedSource !== undefined && !recordedSource.ok) - return server.delivery.toCallToolResult( - recordedSource, - reconstructionContract, - ); - const recorded = options.recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - reconstructionContract, - recorded, - ); - }, + context.mcpReq.signal, + async () => { + const resolved = resolveManagedEvidence(input.static_members); + if (!resolved.ok) + return server.delivery.toCallToolResult( + resolved, + reconstructionContract, + ); + const staticMembers = resolved.value[0]; + if (staticMembers === undefined) + throw new TypeError( + "Managed reconstruction Evidence resolution failed", + ); + const parsed = { ...input, static_members: staticMembers }; + const result = await logToolExecution( + options.logger, + reconstructionContract.name, + () => + Promise.resolve( + importManagedReconstructionEvidenceValidated(parsed), + ), + ); + if (!result.ok) + return server.delivery.toCallToolResult( + result, + reconstructionContract, + ); + const recordedSource = options.recordEvidence?.( + parsed.static_members, + ); + if (recordedSource !== undefined && !recordedSource.ok) + return server.delivery.toCallToolResult( + recordedSource, + reconstructionContract, + ); + const recorded = options.recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + reconstructionContract, + recorded, + ); + }, + ), ); }; diff --git a/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts b/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts index 82fe8dc33..10b850cfc 100644 --- a/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts +++ b/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; import { projectManagedApplicationGraphEvidence } from "../../application/managed/ManagedApplicationGraphService.js"; @@ -25,52 +26,69 @@ export const registerProjectManagedApplicationGraph = ( server.registerTool( graphContract.name, toolRegistrationOptions(graphContract), - async (input) => { - const managedArtifact = - input.managed_artifact === undefined - ? undefined - : resolveManagedArtifactEvidence(input.managed_artifact); - if (managedArtifact !== undefined && !managedArtifact.ok) - return server.delivery.toCallToolResult(managedArtifact, graphContract); - const managedMembers = - input.managed_members === undefined - ? undefined - : resolveManagedEvidence(input.managed_members); - if (managedMembers !== undefined && !managedMembers.ok) - return server.delivery.toCallToolResult(managedMembers, graphContract); - const managedBoundaries = - input.managed_native_boundaries === undefined - ? undefined - : resolveManagedBoundaryEvidence(input.managed_native_boundaries); - if (managedBoundaries !== undefined && !managedBoundaries.ok) - return server.delivery.toCallToolResult( - managedBoundaries, - graphContract, - ); - const parsed = { - managed_artifact: managedArtifact?.value[0], - managed_members: managedMembers?.value[0], - managed_native_boundaries: managedBoundaries?.value[0], - }; - const result = await logToolExecution( - options.logger, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, graphContract.name, - () => Promise.resolve(projectManagedApplicationGraphEvidence(parsed)), - ); - if (!result.ok) - return server.delivery.toCallToolResult(result, graphContract); - const recordedSources = recordSessionEvidenceSources( - options.recordEvidence, - sourceEvidence(parsed), - ); - if (!recordedSources.ok) - return server.delivery.toCallToolResult(recordedSources, graphContract); - const recorded = options.recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - graphContract, - recorded, - ); - }, + context.mcpReq.signal, + async () => { + const managedArtifact = + input.managed_artifact === undefined + ? undefined + : resolveManagedArtifactEvidence(input.managed_artifact); + if (managedArtifact !== undefined && !managedArtifact.ok) + return server.delivery.toCallToolResult( + managedArtifact, + graphContract, + ); + const managedMembers = + input.managed_members === undefined + ? undefined + : resolveManagedEvidence(input.managed_members); + if (managedMembers !== undefined && !managedMembers.ok) + return server.delivery.toCallToolResult( + managedMembers, + graphContract, + ); + const managedBoundaries = + input.managed_native_boundaries === undefined + ? undefined + : resolveManagedBoundaryEvidence(input.managed_native_boundaries); + if (managedBoundaries !== undefined && !managedBoundaries.ok) + return server.delivery.toCallToolResult( + managedBoundaries, + graphContract, + ); + const parsed = { + managed_artifact: managedArtifact?.value[0], + managed_members: managedMembers?.value[0], + managed_native_boundaries: managedBoundaries?.value[0], + }; + const result = await logToolExecution( + options.logger, + graphContract.name, + () => + Promise.resolve(projectManagedApplicationGraphEvidence(parsed)), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, graphContract); + const recordedSources = recordSessionEvidenceSources( + options.recordEvidence, + sourceEvidence(parsed), + ); + if (!recordedSources.ok) + return server.delivery.toCallToolResult( + recordedSources, + graphContract, + ); + const recorded = options.recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + graphContract, + recorded, + ); + }, + ), ); }; diff --git a/src/server/registerManagedWorkflowTools/types.ts b/src/server/registerManagedWorkflowTools/types.ts index ab864c475..f66ab56fc 100644 --- a/src/server/registerManagedWorkflowTools/types.ts +++ b/src/server/registerManagedWorkflowTools/types.ts @@ -4,9 +4,11 @@ import type { } from "../../application/investigation/InvestigationRecordPort.js"; import type { BinarySessionPort } from "../../application/binary/BinarySessionPort.js"; import type { Logger } from "pino"; +import type { WithAdmittedAnalysis } from "../analysisAdmission.js"; /** Shared services for registering managed-code workflow tools. */ export interface ManagedWorkflowToolRegistration { + readonly withAdmittedAnalysis?: WithAdmittedAnalysis; readonly logger: Logger; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; readonly recordEvidenceWithUnknown: diff --git a/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts b/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts index 7496de428..02559c680 100644 --- a/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts +++ b/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts @@ -1,4 +1,5 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; +import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { recordSessionEvidenceSources } from "../sessionEvidence.js"; import { verifyManagedNativeBoundariesEvidence } from "../../application/managed/ManagedNativeVerificationService.js"; @@ -24,88 +25,96 @@ export const registerVerifyManagedNativeBoundaries = ( server.registerTool( nativeVerificationContract.name, toolRegistrationOptions(nativeVerificationContract), - async (input) => { - const managedBoundaries = resolveManagedBoundaryEvidence( - input.managed_boundaries, - ); - if (!managedBoundaries.ok) - return server.delivery.toCallToolResult( - managedBoundaries, - nativeVerificationContract, - ); - const nativeObservations = resolveNativeEvidence( - input.native_observations, - ); - if (!nativeObservations.ok) - return server.delivery.toCallToolResult( - nativeObservations, - nativeVerificationContract, - ); - const managedBoundary = managedBoundaries.value[0]; - if (managedBoundary === undefined) - throw new TypeError("Managed boundary Evidence resolution failed"); - const parsed = { - ...input, - managed_boundaries: managedBoundary, - native_observations: nativeObservations.value, - }; - const result = await logToolExecution( - options.logger, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, nativeVerificationContract.name, - () => Promise.resolve(verifyManagedNativeBoundariesEvidence(parsed)), - ); - if (!result.ok) - return server.delivery.toCallToolResult( - result, - nativeVerificationContract, - ); - const recordedSources = recordSessionEvidenceSources( - options.recordEvidence, - [parsed.managed_boundaries, ...parsed.native_observations], - ); - if (!recordedSources.ok) - return server.delivery.toCallToolResult( - recordedSources, - nativeVerificationContract, - ); - const verification = managedNativeVerificationResultSchema.parse( - result.value.normalized_result, - ); - const unknown = - verification.summary.unresolved > 0 || - verification.summary.contradicted > 0 || - verification.summary.native_body_unresolved > 0; - const output = unknown - ? options.recordEvidenceWithUnknown?.(result.value, { - question: - "Which managed/native boundaries remain unresolved or contradicted by the supplied native Evidence?", - severity: "medium", - domain: "managed-native-verification", - supporting_evidence_ids: [result.value.evidence_id], - contradicting_evidence_ids: [], - required_authority: "shipped-artifact", - required_confidence: "observed", - required_environment: null, - recommended_probes: [ - { - operation: "inspect_managed_native_boundaries", - rationale: - "Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved declarations must remain unknown.", - }, - { - operation: "analyze_function", - rationale: - "Analyze the provider-resolved native function candidate for the declared export.", - }, - ], - relationships: [], - }) - : options.recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - nativeVerificationContract, - output, - ); - }, + context.mcpReq.signal, + async () => { + const managedBoundaries = resolveManagedBoundaryEvidence( + input.managed_boundaries, + ); + if (!managedBoundaries.ok) + return server.delivery.toCallToolResult( + managedBoundaries, + nativeVerificationContract, + ); + const nativeObservations = resolveNativeEvidence( + input.native_observations, + ); + if (!nativeObservations.ok) + return server.delivery.toCallToolResult( + nativeObservations, + nativeVerificationContract, + ); + const managedBoundary = managedBoundaries.value[0]; + if (managedBoundary === undefined) + throw new TypeError("Managed boundary Evidence resolution failed"); + const parsed = { + ...input, + managed_boundaries: managedBoundary, + native_observations: nativeObservations.value, + }; + const result = await logToolExecution( + options.logger, + nativeVerificationContract.name, + () => + Promise.resolve(verifyManagedNativeBoundariesEvidence(parsed)), + ); + if (!result.ok) + return server.delivery.toCallToolResult( + result, + nativeVerificationContract, + ); + const recordedSources = recordSessionEvidenceSources( + options.recordEvidence, + [parsed.managed_boundaries, ...parsed.native_observations], + ); + if (!recordedSources.ok) + return server.delivery.toCallToolResult( + recordedSources, + nativeVerificationContract, + ); + const verification = managedNativeVerificationResultSchema.parse( + result.value.normalized_result, + ); + const unknown = + verification.summary.unresolved > 0 || + verification.summary.contradicted > 0 || + verification.summary.native_body_unresolved > 0; + const output = unknown + ? options.recordEvidenceWithUnknown?.(result.value, { + question: + "Which managed/native boundaries remain unresolved or contradicted by the supplied native Evidence?", + severity: "medium", + domain: "managed-native-verification", + supporting_evidence_ids: [result.value.evidence_id], + contradicting_evidence_ids: [], + required_authority: "shipped-artifact", + required_confidence: "observed", + required_environment: null, + recommended_probes: [ + { + operation: "inspect_managed_native_boundaries", + rationale: + "Review the inspection Evidence coverage and limit diagnostics; this operation has no page override, so unresolved declarations must remain unknown.", + }, + { + operation: "analyze_function", + rationale: + "Analyze the provider-resolved native function candidate for the declared export.", + }, + ], + relationships: [], + }) + : options.recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + nativeVerificationContract, + output, + ); + }, + ), ); }; diff --git a/src/server/registerPeResourcesTool.ts b/src/server/registerPeResourcesTool.ts index 81e6fd852..9958be47b 100644 --- a/src/server/registerPeResourcesTool.ts +++ b/src/server/registerPeResourcesTool.ts @@ -3,7 +3,8 @@ import type { PeResourcesService } from "../application/binaryDiagnostics/PeReso import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Register the explicit-file PE resource workflow and retain its complete Evidence. */ @@ -12,21 +13,19 @@ export const registerPeResourcesTool = ( service: PeResourcesService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("inspect_pe_resources"); + const handler = createEvidenceToolHandler( + server, + (input, options) => service.inspect(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.inspect(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recordEvidence?.(result.value), - ); - }, + handler(contract), ); }; diff --git a/src/server/registerProcessComparisonTool.ts b/src/server/registerProcessComparisonTool.ts index 2769d27ce..ce5feb597 100644 --- a/src/server/registerProcessComparisonTool.ts +++ b/src/server/registerProcessComparisonTool.ts @@ -25,6 +25,8 @@ import { recordSessionEvidenceSources } from "./sessionEvidence.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { withAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; const PROCESS_CAPTURE_EVIDENCE = { operation: "capture_process_scenario", @@ -94,72 +96,87 @@ export const registerProcessComparisonTool = ( contract: ReturnType>, now: () => number = Date.now, ): void => { + const admission = withAdmittedAnalysis({ kind: "session", session }); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const left = parseCaptureSide(contract.name, "left", input.left); - if (!left.ok) return server.delivery.toCallToolResult(left, contract); - const right = parseCaptureSide(contract.name, "right", input.right); - if (!right.ok) return server.delivery.toCallToolResult(right, contract); - const { record: leftRecord, capture: leftCapture } = left.value; - const { record: rightRecord, capture: rightCapture } = right.value; - const computed = await runDerivedOperation(context, contract.name, () => - compareProcessCaptures(leftCapture, rightCapture, { - ...(input.max_capture_age_ms === undefined - ? {} - : { maxCaptureAgeMs: input.max_capture_age_ms }), - ...(input.trace_spec === undefined - ? {} - : { traceSpecification: input.trace_spec }), - now, - }), - ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const comparison = computed.value; - const evidence = createEvidence(undefined, PROCESS_PROVIDER, { - predicateType: "rea.process-comparison", - operation: contract.name, - parameters: { - left_evidence_id: leftRecord.evidence_id, - right_evidence_id: rightRecord.evidence_id, - left_normalization: leftCapture.normalization, - right_normalization: rightCapture.normalization, - ...(input.trace_spec === undefined - ? {} - : { trace_spec: jsonValueSchema.parse(input.trace_spec) }), - }, - result: jsonValueSchema.parse(comparison), - confidence: "derived", - authority: "analyst-inference", - limitations: comparison.limitations, - locations: sourceLocations(leftRecord.locations, rightRecord.locations), - evidenceLinks: [leftRecord.evidence_id, rightRecord.evidence_id], - }); - const recordedSources = recordSessionEvidenceSources( - (evidence) => session.recordEvidence(evidence), - [leftRecord, rightRecord], - ); - if (!recordedSources.ok) - return server.delivery.toCallToolResult(recordedSources, contract); - return server.delivery.toEvidenceToolResult( - evidence, - contract, - recordDerivedEvidence( - session, - evidence, - comparisonUnknownInput( - { + async (input, context) => + runAdmittedToolOperation( + server, + admission, + contract.name, + context.mcpReq.signal, + async () => { + const left = parseCaptureSide(contract.name, "left", input.left); + if (!left.ok) return server.delivery.toCallToolResult(left, contract); + const right = parseCaptureSide(contract.name, "right", input.right); + if (!right.ok) + return server.delivery.toCallToolResult(right, contract); + const { record: leftRecord, capture: leftCapture } = left.value; + const { record: rightRecord, capture: rightCapture } = right.value; + const computed = await runDerivedOperation( + context, + contract.name, + () => + compareProcessCaptures(leftCapture, rightCapture, { + ...(input.max_capture_age_ms === undefined + ? {} + : { maxCaptureAgeMs: input.max_capture_age_ms }), + ...(input.trace_spec === undefined + ? {} + : { traceSpecification: input.trace_spec }), + now, + }), + ); + if (!computed.ok) + return server.delivery.toCallToolResult(computed, contract); + const comparison = computed.value; + const evidence = createEvidence(undefined, PROCESS_PROVIDER, { + predicateType: "rea.process-comparison", + operation: contract.name, + parameters: { left_evidence_id: leftRecord.evidence_id, right_evidence_id: rightRecord.evidence_id, - comparison_evidence_id: evidence.evidence_id, + left_normalization: leftCapture.normalization, + right_normalization: rightCapture.normalization, + ...(input.trace_spec === undefined + ? {} + : { trace_spec: jsonValueSchema.parse(input.trace_spec) }), }, - comparison, - ), - ), - ); - }, + result: jsonValueSchema.parse(comparison), + confidence: "derived", + authority: "analyst-inference", + limitations: comparison.limitations, + locations: sourceLocations( + leftRecord.locations, + rightRecord.locations, + ), + evidenceLinks: [leftRecord.evidence_id, rightRecord.evidence_id], + }); + const recordedSources = recordSessionEvidenceSources( + (evidence) => session.recordEvidence(evidence), + [leftRecord, rightRecord], + ); + if (!recordedSources.ok) + return server.delivery.toCallToolResult(recordedSources, contract); + return server.delivery.toEvidenceToolResult( + evidence, + contract, + recordDerivedEvidence( + session, + evidence, + comparisonUnknownInput( + { + left_evidence_id: leftRecord.evidence_id, + right_evidence_id: rightRecord.evidence_id, + comparison_evidence_id: evidence.evidence_id, + }, + comparison, + ), + ), + ); + }, + ), ); }; diff --git a/src/server/registerRecordedCrashTools.ts b/src/server/registerRecordedCrashTools.ts index 4775afa68..104c355c6 100644 --- a/src/server/registerRecordedCrashTools.ts +++ b/src/server/registerRecordedCrashTools.ts @@ -4,7 +4,8 @@ import type { RecordedCrashService } from "../application/binaryDiagnostics/Reco import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind recorded crash inspection to its named contract and session Evidence owner. */ @@ -13,22 +14,19 @@ export const registerRecordedCrashTools = ( service: RecordedCrashService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("inspect_recorded_crash"); + const handler = createEvidenceToolHandler( + server, + (input, options) => service.inspect(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.inspect(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + handler(contract), ); }; diff --git a/src/server/registerSessionTools.ts b/src/server/registerSessionTools.ts index da36c8767..3541af850 100644 --- a/src/server/registerSessionTools.ts +++ b/src/server/registerSessionTools.ts @@ -8,13 +8,15 @@ import { readAnalysisSnapshot } from "../application/binary/AnalysisSnapshotFile import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; import { createProcessCaptureEvidence } from "../application/process/ProcessEvidence.js"; import { captureProcessScenario } from "../process/capture/ProcessHarness.js"; +import { ProcessCaptureResourceScope } from "../process/capture/ProcessCaptureLifecycle.js"; +import { ProcessCaptureError } from "../process/capture/ProcessCaptureError.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js"; import { UnknownRegistryError } from "../domain/unknownRegistryError.js"; import { type AnalysisError } from "../domain/analysisErrorBase.js"; import type { Evidence } from "../domain/evidence.js"; import type { ProcessCapture } from "../domain/process/processCaptureParsing.js"; -import { ok, type Result } from "../domain/result.js"; +import { err, ok, type Result } from "../domain/result.js"; import type { Logger } from "pino"; import type { ProviderAvailability } from "../application/AnalysisProvider.js"; import { mcpProgressReporter } from "./mcpProgress.js"; @@ -33,6 +35,8 @@ import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js"; import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { withAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; const recordProcessResidualUnknowns = ( session: BinarySessionPort, @@ -77,6 +81,7 @@ interface ProcessToolRegistration { readonly captureContract: ReturnType< typeof toolContract<"capture_process_scenario"> >; + readonly resourceScope: ProcessCaptureResourceScope; } const registerProcessTools = ({ @@ -84,45 +89,55 @@ const registerProcessTools = ({ session, logger, captureContract, + resourceScope, }: ProcessToolRegistration): void => { + const admission = withAdmittedAnalysis({ kind: "session", session }); server.registerTool( captureContract.name, toolRegistrationOptions(captureContract), - async (input, context) => { - const progress = mcpProgressReporter(context); - const captured = await logToolExecution( - logger, + async (input, context) => + runAdmittedToolOperation( + server, + admission, captureContract.name, - () => - captureProcessScenario( - input, - context.mcpReq.signal, - process.platform, - process.env, - undefined, - undefined, - progress, - ), - ); - if (!captured.ok) - return server.delivery.toCallToolResult(captured, captureContract); - const evidence = createProcessCaptureEvidence(input, captured.value); - const recorded = session.recordEvidence(evidence); - if (!recorded.ok) - return server.delivery.toCallToolResult(recorded, captureContract); - const unknowns = recordProcessResidualUnknowns( - session, - evidence, - captured.value.residual_unknowns, - ); - if (!unknowns.ok) - return server.delivery.toCallToolResult(unknowns, captureContract); - return server.delivery.toEvidenceToolResult( - evidence, - captureContract, - recorded, - ); - }, + context.mcpReq.signal, + async () => { + const progress = mcpProgressReporter(context); + const captured = await logToolExecution( + logger, + captureContract.name, + () => + captureProcessScenario( + input, + context.mcpReq.signal, + process.platform, + process.env, + undefined, + undefined, + progress, + resourceScope, + ), + ); + if (!captured.ok) + return server.delivery.toCallToolResult(captured, captureContract); + const evidence = createProcessCaptureEvidence(input, captured.value); + const recorded = session.recordEvidence(evidence); + if (!recorded.ok) + return server.delivery.toCallToolResult(recorded, captureContract); + const unknowns = recordProcessResidualUnknowns( + session, + evidence, + captured.value.residual_unknowns, + ); + if (!unknowns.ok) + return server.delivery.toCallToolResult(unknowns, captureContract); + return server.delivery.toEvidenceToolResult( + evidence, + captureContract, + recorded, + ); + }, + ), ); }; @@ -138,6 +153,7 @@ export interface LifecycleToolRegistration { readonly androidAnalysisAvailability: ( signal: AbortSignal, ) => Promise; + readonly closeProcessResources: () => Promise>; } const registerLifecycleTools = ( @@ -279,6 +295,7 @@ export const registerSessionTools = ( const compareFunctionsContract = toolContract("compare_functions"); const compareBundlesContract = toolContract("compare_bundles"); const snapshotContract = toolContract("get_evidence_bundle"); + const processResources = new ProcessCaptureResourceScope(); registerLifecycleTools({ server, session, @@ -293,6 +310,27 @@ export const registerSessionTools = ( ), androidAnalysisAvailability: options.androidAnalysisAvailability ?? missingAndroidAvailability, + closeProcessResources: async () => { + try { + await processResources.retryCleanup(); + return ok(null); + } catch (cause: unknown) { + return err( + new ProcessCaptureError( + "process capture cleanup remains incomplete", + { + cause, + reason: "cleanup_incomplete", + cleanupResources: ["process_capture_resources"], + executionFailure: + cause instanceof Error + ? cause.message || cause.name + : String(cause), + }, + ), + ); + } + }, }); registerEvidenceTools({ server, @@ -306,7 +344,33 @@ export const registerSessionTools = ( session, logger, captureContract, + resourceScope: processResources, }); + const closeServer = server.close.bind(server); + server.close = async () => { + const results = await Promise.allSettled([ + closeServer(), + processResources.close(), + ]); + const failures = results.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (failures.length > 0) + throw new AggregateError(failures, "REA session cleanup failed"); + }; + const previousOnclose = server.server.onclose; + server.server.onclose = () => { + try { + previousOnclose?.(); + } finally { + void processResources.close().catch((cause: unknown) => { + logger.error( + { error: cause instanceof Error ? cause.message : String(cause) }, + "Process capture cleanup failed during server shutdown", + ); + }); + } + }; registerProcessComparisonTool(server, session, compareContract); registerArtifactComparisonTool(server, session, compareArtifactsContract); registerFunctionComparisonTool(server, session, compareFunctionsContract); diff --git a/src/server/registerWebModuleTool.ts b/src/server/registerWebModuleTool.ts index 5d8adcf45..c26a647d7 100644 --- a/src/server/registerWebModuleTool.ts +++ b/src/server/registerWebModuleTool.ts @@ -4,7 +4,8 @@ import type { WebModuleTraceService } from "../application/WebModuleTraceService import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind the module relationship workflow to its exact canonical contract. */ @@ -13,22 +14,19 @@ export const registerWebModuleTool = ( service: WebModuleTraceService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("trace_web_module_imports"); + const handler = createEvidenceToolHandler( + server, + (input, options) => service.trace(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.trace(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + handler(contract), ); }; diff --git a/src/server/registerWebNetworkCaptureTool.ts b/src/server/registerWebNetworkCaptureTool.ts index d8222961b..9f0c9dec8 100644 --- a/src/server/registerWebNetworkCaptureTool.ts +++ b/src/server/registerWebNetworkCaptureTool.ts @@ -4,7 +4,8 @@ import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureS import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind historical inspection to its named contract and caller-owned Evidence writer. */ @@ -13,6 +14,7 @@ export const registerWebNetworkCaptureTool = ( service: WebNetworkCaptureService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("inspect_web_network_capture"); const registration = toolRegistrationOptions(contract); @@ -26,20 +28,16 @@ export const registerWebNetworkCaptureTool = ( validate: (value: unknown) => ({ value }), }, }; + const handler = createEvidenceToolHandler( + server, + (input, options) => service.inspect(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, { ...registration, inputSchema }, - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.inspect(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + handler(contract), ); }; diff --git a/src/server/registerWebRuntimeTools.ts b/src/server/registerWebRuntimeTools.ts index 5179dfee7..b9e210703 100644 --- a/src/server/registerWebRuntimeTools.ts +++ b/src/server/registerWebRuntimeTools.ts @@ -7,6 +7,8 @@ import type { Logger } from "pino"; import { mcpProgressReporter } from "./mcpProgress.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; /** Bind distinct runtime operations to named contracts and the session's Evidence owner. */ export const registerWebRuntimeTools = ( @@ -14,44 +16,59 @@ export const registerWebRuntimeTools = ( service: WebRuntimeService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const execution = toolContract("observe_web_execution"); const listeners = toolContract("inspect_web_event_listeners"); server.registerTool( execution.name, toolRegistrationOptions(execution), - async (input, context) => { - const result = await logToolExecution(logger, execution.name, () => - service.observe(input, { - signal: context.mcpReq.signal, - progress: mcpProgressReporter(context), - }), - ); - if (!result.ok) - return server.delivery.toCallToolResult(result, execution); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - execution, - recorded, - ); - }, + async (input, context) => + runAdmittedToolOperation( + server, + withAdmittedAnalysis, + execution.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution(logger, execution.name, () => + service.observe(input, { + signal: context.mcpReq.signal, + progress: mcpProgressReporter(context), + }), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, execution); + const recorded = recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + execution, + recorded, + ); + }, + ), ); server.registerTool( listeners.name, toolRegistrationOptions(listeners), - async (input, context) => { - const result = await logToolExecution(logger, listeners.name, () => - service.inspect(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) - return server.delivery.toCallToolResult(result, listeners); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - listeners, - recorded, - ); - }, + async (input, context) => + runAdmittedToolOperation( + server, + withAdmittedAnalysis, + listeners.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution(logger, listeners.name, () => + service.inspect(input, { signal: context.mcpReq.signal }), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, listeners); + const recorded = recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + listeners, + recorded, + ); + }, + ), ); }; diff --git a/src/server/registerWebScriptTool.ts b/src/server/registerWebScriptTool.ts index 60fffbdd8..37a4bc835 100644 --- a/src/server/registerWebScriptTool.ts +++ b/src/server/registerWebScriptTool.ts @@ -6,6 +6,8 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { runAdmittedToolOperation } from "./admittedToolOperation.js"; /** Register local script export without requiring a live browser provider. */ export const registerWebScriptTool = ( @@ -13,23 +15,37 @@ export const registerWebScriptTool = ( options: { readonly logger: Logger; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis?: WithAdmittedAnalysis; }, ): void => { const contract = toolContract("export_web_scripts"); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(options.logger, contract.name, () => - exportWebScriptsValidated(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = options.recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + async (input, context) => + runAdmittedToolOperation( + server, + options.withAdmittedAnalysis, + contract.name, + context.mcpReq.signal, + async () => { + const result = await logToolExecution( + options.logger, + contract.name, + () => + exportWebScriptsValidated(input, { + signal: context.mcpReq.signal, + }), + ); + if (!result.ok) + return server.delivery.toCallToolResult(result, contract); + const recorded = options.recordEvidence?.(result.value); + return server.delivery.toEvidenceToolResult( + result.value, + contract, + recorded, + ); + }, + ), ); }; diff --git a/src/server/registerWebSourceLocationTool.ts b/src/server/registerWebSourceLocationTool.ts index 19368141b..c295757c7 100644 --- a/src/server/registerWebSourceLocationTool.ts +++ b/src/server/registerWebSourceLocationTool.ts @@ -4,7 +4,8 @@ import type { WebSourceLocationService } from "../application/WebSourceLocationS import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import { logToolExecution } from "./toolLogging.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind source point tracing to its exact named contract and session Evidence owner. */ @@ -13,22 +14,19 @@ export const registerWebSourceLocationTool = ( service: WebSourceLocationService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, ): void => { const contract = toolContract("trace_web_source_location"); + const handler = createEvidenceToolHandler( + server, + (input, options) => service.trace(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); server.registerTool( contract.name, toolRegistrationOptions(contract), - async (input, context) => { - const result = await logToolExecution(logger, contract.name, () => - service.trace(input, { signal: context.mcpReq.signal }), - ); - if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidence?.(result.value); - return server.delivery.toEvidenceToolResult( - result.value, - contract, - recorded, - ); - }, + handler(contract), ); };