diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index aa54a8c04..b9ff6a23c 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -507,6 +507,33 @@ export class BinarySession return call.finally(() => this.#calls.delete(call)); } + /** Admit one composed request until its result and session bookkeeping settle. */ + withAdmittedAnalysis( + operationName: string, + signal: AbortSignal | undefined, + operation: (analysis: AnalysisOperationPort) => Promise, + ): Promise> { + const generation = this.#transitionGeneration; + const transition = this.#transition; + const call = (async (): Promise> => { + const admission = await this.#waitForTransition( + operationName, + signal, + transition, + ); + if (!admission.ok) return admission; + return ok( + await operation({ + execute: (name, arguments_, options) => + this.#execute(name, arguments_, options, Promise.resolve()), + }), + ); + })(); + // Register synchronously before a lifecycle transition can snapshot calls. + this.#calls.set(call, generation); + return call.finally(() => this.#calls.delete(call)); + } + async #execute( name: Parameters[0], arguments_: Readonly>, diff --git a/src/application/binary/BinarySessionPort.ts b/src/application/binary/BinarySessionPort.ts index fc98c1f16..166479a06 100644 --- a/src/application/binary/BinarySessionPort.ts +++ b/src/application/binary/BinarySessionPort.ts @@ -32,6 +32,12 @@ export interface SavedAnalysisSnapshot { /** Target lifecycle used by CLI and MCP without exposing a concrete provider. */ export interface BinarySessionPort extends AnalysisOperationPort, InvestigationRecordPort { + /** Hold the active target binding through a complete composed operation. */ + withAdmittedAnalysis( + operationName: string, + signal: AbortSignal | undefined, + operation: (analysis: AnalysisOperationPort) => Promise, + ): Promise>; open( path: string, options?: { diff --git a/src/main/transport.ts b/src/main/transport.ts index 729b5d6d1..f8d6014f3 100644 --- a/src/main/transport.ts +++ b/src/main/transport.ts @@ -71,13 +71,16 @@ export const startMcpTransport = async ( serverContext.environment, ); androidProviders.push(android); - return (dependencies.createServer ?? createServer)(session, session, { - logger: serverContext.logger, - environment: serverContext.environment, - delivery: serverContext.delivery, - ...optionalProviders, - androidAnalysis: android, - }); + return (dependencies.createServer ?? createServer)( + { kind: "session", session }, + { + logger: serverContext.logger, + environment: serverContext.environment, + delivery: serverContext.delivery, + ...optionalProviders, + androidAnalysis: android, + }, + ); }, { onerror: () => { diff --git a/src/server/analysisAdmission.ts b/src/server/analysisAdmission.ts new file mode 100644 index 000000000..93e79e2b1 --- /dev/null +++ b/src/server/analysisAdmission.ts @@ -0,0 +1,29 @@ +import type { AnalysisOperationPort } from "../application/AnalysisProvider.js"; +import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; +import { err, ok, type Result } from "../domain/result.js"; + +/** Run a provider-backed tool while its target binding stays admitted. */ +export type WithAdmittedAnalysis = ( + operationName: string, + signal: AbortSignal | undefined, + operation: (analysis: AnalysisOperationPort) => Promise, +) => Promise>; + +/** Select exactly one owner for a server's analysis operation binding. */ +export type ServerAnalysisSource = + | { readonly kind: "session"; readonly session: BinarySessionPort } + | { readonly kind: "fixed"; readonly analysis: AnalysisOperationPort }; + +/** Run against the source's explicit owner, admitting session-scoped work. */ +export const withAdmittedAnalysis = ( + source: ServerAnalysisSource, +): WithAdmittedAnalysis => + source.kind === "session" + ? (operationName, signal, operation) => + source.session.withAdmittedAnalysis(operationName, signal, operation) + : async (operationName, signal, operation) => + signal?.aborted === true + ? err(new AnalysisCancelledError(operationName)) + : ok(await operation(source.analysis)); diff --git a/src/server/createServer.ts b/src/server/createServer.ts index b0d95ecbb..20fae3fd2 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -17,7 +17,6 @@ import { import { parseMcpResponseBudget } from "../config/mcpResponseBudget.js"; import { isAbsolute } from "node:path"; -import type { AnalysisOperationPort } from "../application/AnalysisProvider.js"; import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; import type { BrowserObservationPort } from "../application/BrowserObservationPort.js"; import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js"; @@ -68,6 +67,11 @@ import { registerGuidedPrompts } from "./registerPrompts.js"; import { registerSessionTools } from "./registerSessionTools.js"; import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js"; +import { + withAdmittedAnalysis, + type ServerAnalysisSource, +} from "./analysisAdmission.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; const TARGET_FREE_INSTRUCTIONS = "REA provides reverse-engineering tools for local artifacts, native binaries, managed code, browser pages, and runtimes. Use the tool that directly answers the question; discover targets or inspect inventory only when needed. Tool results include inline Evidence and report their coverage and limitations."; @@ -168,10 +172,11 @@ const createMcpServer = ( * fixed-target seam used by focused tests and embedders. */ export const createServer = ( - analysis: AnalysisOperationPort, - session?: BinarySessionPort, + source: ServerAnalysisSource, options: CreateServerOptions = {}, ): EvidenceMcpServer => { + const session = source.kind === "session" ? source.session : undefined; + 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 }; @@ -194,7 +199,6 @@ export const createServer = ( } = createSessionRecorders(server, session); const toolContext: ServerToolContext = { server, - analysis, session, options: selectedOptions, environment, @@ -203,6 +207,7 @@ export const createServer = ( activeTarget, recordEvidence, recordEvidenceWithUnknown, + withAdmittedAnalysis: withAdmittedAnalysis(source), }; registerBinaryAnalysisTools(toolContext); const previousOnclose = server.server.onclose; @@ -344,39 +349,34 @@ const createSessionRecorders = ( recordEvidence: session === undefined ? undefined - : (evidence: Parameters[0]) => { - const recorded = session.recordEvidence(evidence); - return recorded; - }, + : (evidence: Parameters[0]) => + session.recordEvidence(evidence), recordEvidenceWithUnknown: session === undefined ? undefined : ( evidence: Parameters[0], input: Parameters[1], - ) => { - const recorded = session.recordEvidenceWithUnknown(evidence, input); - return recorded; - }, + ) => session.recordEvidenceWithUnknown(evidence, input), }); interface ServerToolContext extends ReturnType { readonly server: EvidenceMcpServer; - readonly analysis: AnalysisOperationPort; readonly session: BinarySessionPort | undefined; readonly options: CreateServerOptions; readonly environment: Readonly; readonly logger: Logger; + readonly withAdmittedAnalysis: WithAdmittedAnalysis; } const registerBinaryAnalysisTools = ({ server, - analysis, session, logger, activeTarget, recordEvidence, recordEvidenceWithUnknown, + withAdmittedAnalysis, }: ServerToolContext): void => { const recordUnknown = session === undefined @@ -388,9 +388,10 @@ const registerBinaryAnalysisTools = ({ activeTarget, recordEvidence, recordUnknown, + withAdmittedAnalysis, }; - registerOfficialTools(server, analysis, analysisOptions); - registerEnhancedTools(server, analysis, { + registerOfficialTools(server, analysisOptions); + registerEnhancedTools(server, { ...analysisOptions, analysisProfile: session === undefined ? undefined : () => session.analysisProfile(), @@ -403,15 +404,15 @@ const registerBinaryAnalysisTools = ({ ? undefined : (input) => session.recordWorkflowSnapshot(input), }); - const evidenceOptions = { logger, activeTarget, recordEvidence }; - registerNativeTools(server, analysis, evidenceOptions); - registerArtifactTools(server, analysis, { - ...evidenceOptions, - }); - registerManagedTools(server, analysis, { - ...evidenceOptions, - session, - }); + const evidenceOptions = { + logger, + activeTarget, + recordEvidence, + withAdmittedAnalysis, + }; + registerNativeTools(server, evidenceOptions); + registerArtifactTools(server, evidenceOptions); + registerManagedTools(server, evidenceOptions); if (session !== undefined) registerManagedWorkflowTools(server, { logger, diff --git a/src/server/registerArtifactTools.ts b/src/server/registerArtifactTools.ts index 898584567..c3509bae6 100644 --- a/src/server/registerArtifactTools.ts +++ b/src/server/registerArtifactTools.ts @@ -1,7 +1,6 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; -import type { AnalysisOperationPort } from "../application/AnalysisProvider.js"; import { ARTIFACT_TOOL_CONTRACTS } from "../contracts/artifactToolContracts.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import type { Logger } from "pino"; @@ -11,14 +10,14 @@ import { artifactInspectionResultSchema } from "../domain/artifactInspection.js" /** Register deterministic artifact inventory and safe extraction operations. */ export const registerArtifactTools = ( server: EvidenceMcpServer, - analysis: AnalysisOperationPort, options: { readonly logger: Logger; readonly activeTarget: (() => BinaryTarget | undefined) | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis: import("./analysisAdmission.js").WithAdmittedAnalysis; }, ): void => { - registerEvidenceTools(server, analysis, ARTIFACT_TOOL_CONTRACTS, { + registerEvidenceTools(server, ARTIFACT_TOOL_CONTRACTS, { ...options, sourceEvidence: (operation, result) => operation === "inspect_artifact" diff --git a/src/server/registerCloseLifecycleTool.ts b/src/server/registerCloseLifecycleTool.ts index 8f5384380..828a0dfdb 100644 --- a/src/server/registerCloseLifecycleTool.ts +++ b/src/server/registerCloseLifecycleTool.ts @@ -22,23 +22,25 @@ export const registerCloseLifecycleTool = ({ const progress = mcpProgressReporter(context); const snapshotPath = input.snapshot_path; if (snapshotPath === undefined) { - await reportLifecycleStart(progress, closeContract.name); - const closed = await logToolExecution(logger, closeContract.name, () => + // 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 }), ); + await reportLifecycleStart(progress, closeContract.name); + const closed = await closing; await reportLifecycleEnd(progress, closeContract.name, closed.ok); return server.delivery.toCallToolResult(closed, closeContract); } + const closing = logToolExecution(logger, closeContract.name, () => + session.closeWithSnapshot(snapshotPath, input.overwrite, { progress }), + ); await reportLifecycleStart( progress, closeContract.name, "saving snapshot; closing provider", ); - const closed = await logToolExecution(logger, closeContract.name, () => - session.closeWithSnapshot(snapshotPath, input.overwrite, { - progress, - }), - ); + const closed = await closing; await reportLifecycleEnd(progress, closeContract.name, closed.ok); return server.delivery.toCallToolResult( closed.ok ? ok({ ...closed.value }) : closed, diff --git a/src/server/registerEnhancedTools.ts b/src/server/registerEnhancedTools.ts index e972353d6..e1e27c127 100644 --- a/src/server/registerEnhancedTools.ts +++ b/src/server/registerEnhancedTools.ts @@ -28,9 +28,11 @@ import { mcpProgressReporter } from "./mcpProgress.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { executeFunctionAnalysisEvidence } from "../application/FunctionAnalysisEvidence.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; /** Optional session services used by enhanced tool registration. */ export interface EnhancedToolRegistration { + readonly withAdmittedAnalysis: WithAdmittedAnalysis; readonly logger: Logger; readonly activeTarget: (() => BinaryTarget | undefined) | undefined; readonly analysisProfile: @@ -52,7 +54,6 @@ export interface EnhancedToolRegistration { // oxlint-disable-next-line max-lines-per-function -- direct SDK calls retain each schema-handler type correlation. export const registerEnhancedTools = ( server: EvidenceMcpServer, - analysis: AnalysisOperationPort, options: EnhancedToolRegistration, ): void => { const registration = { ...options, delivery: server.delivery }; @@ -76,7 +77,7 @@ export const registerEnhancedTools = ( traceNativeValues.name, toolRegistrationOptions(traceNativeValues), (input, context) => - executeEnhancedTool(analysis, registration, traceNativeValues, { + executeEnhancedTool(registration, traceNativeValues, { validatedCall: { name: "trace_native_values", input }, context, }), @@ -85,7 +86,7 @@ export const registerEnhancedTools = ( nativeDispatchMetadata.name, toolRegistrationOptions(nativeDispatchMetadata), (input, context) => - executeEnhancedTool(analysis, registration, nativeDispatchMetadata, { + executeEnhancedTool(registration, nativeDispatchMetadata, { validatedCall: { name: "inspect_native_dispatch_metadata", input }, context, }), @@ -94,7 +95,7 @@ export const registerEnhancedTools = ( objcClasses.name, toolRegistrationOptions(objcClasses), (input, context) => - executeEnhancedTool(analysis, registration, objcClasses, { + executeEnhancedTool(registration, objcClasses, { validatedCall: { name: "get_objc_classes", input }, context, }), @@ -103,7 +104,7 @@ export const registerEnhancedTools = ( objcProtocols.name, toolRegistrationOptions(objcProtocols), (input, context) => - executeEnhancedTool(analysis, registration, objcProtocols, { + executeEnhancedTool(registration, objcProtocols, { validatedCall: { name: "get_objc_protocols", input }, context, }), @@ -112,7 +113,7 @@ export const registerEnhancedTools = ( batchDecompile.name, toolRegistrationOptions(batchDecompile), (input, context) => - executeEnhancedTool(analysis, registration, batchDecompile, { + executeEnhancedTool(registration, batchDecompile, { validatedCall: { name: "batch_decompile", input }, context, }), @@ -121,7 +122,7 @@ export const registerEnhancedTools = ( callGraph.name, toolRegistrationOptions(callGraph), (input, context) => - executeEnhancedTool(analysis, registration, callGraph, { + executeEnhancedTool(registration, callGraph, { validatedCall: { name: "get_call_graph", input }, context, }), @@ -130,7 +131,7 @@ export const registerEnhancedTools = ( swiftTypes.name, toolRegistrationOptions(swiftTypes), (input, context) => - executeEnhancedTool(analysis, registration, swiftTypes, { + executeEnhancedTool(registration, swiftTypes, { validatedCall: { name: "analyze_swift_types", input }, context, }), @@ -139,7 +140,7 @@ export const registerEnhancedTools = ( xrefsToName.name, toolRegistrationOptions(xrefsToName), (input, context) => - executeEnhancedTool(analysis, registration, xrefsToName, { + executeEnhancedTool(registration, xrefsToName, { validatedCall: { name: "find_xrefs_to_name", input }, context, }), @@ -148,7 +149,7 @@ export const registerEnhancedTools = ( binaryOverview.name, toolRegistrationOptions(binaryOverview), (input, context) => - executeEnhancedTool(analysis, registration, binaryOverview, { + executeEnhancedTool(registration, binaryOverview, { validatedCall: { name: "binary_overview", input }, context, }), @@ -157,7 +158,7 @@ export const registerEnhancedTools = ( analyzeFunction.name, toolRegistrationOptions(analyzeFunction), (input, context) => - executeEnhancedTool(analysis, registration, analyzeFunction, { + executeEnhancedTool(registration, analyzeFunction, { validatedCall: { name: "analyze_function", input }, context, }), @@ -166,7 +167,7 @@ export const registerEnhancedTools = ( inspectNativeApi.name, toolRegistrationOptions(inspectNativeApi), (input, context) => - executeEnhancedTool(analysis, registration, inspectNativeApi, { + executeEnhancedTool(registration, inspectNativeApi, { validatedCall: { name: "inspect_native_api", input }, context, }), @@ -175,7 +176,7 @@ export const registerEnhancedTools = ( traceFeature.name, toolRegistrationOptions(traceFeature), (input, context) => - executeEnhancedTool(analysis, registration, traceFeature, { + executeEnhancedTool(registration, traceFeature, { validatedCall: { name: "trace_feature", input }, context, }), @@ -184,7 +185,7 @@ export const registerEnhancedTools = ( traceCallPath.name, toolRegistrationOptions(traceCallPath), (input, context) => - executeEnhancedTool(analysis, registration, traceCallPath, { + executeEnhancedTool(registration, traceCallPath, { validatedCall: { name: "trace_call_path", input }, context, }), @@ -193,7 +194,7 @@ export const registerEnhancedTools = ( traceNativeUiAction.name, toolRegistrationOptions(traceNativeUiAction), (input, context) => - executeEnhancedTool(analysis, registration, traceNativeUiAction, { + executeEnhancedTool(registration, traceNativeUiAction, { validatedCall: { name: "trace_native_ui_action", input }, context, }), @@ -201,7 +202,6 @@ export const registerEnhancedTools = ( }; const executeEnhancedTool = async ( - analysis: AnalysisOperationPort, registration: EnhancedToolRegistration & { readonly delivery: ToolResultDelivery; }, @@ -211,76 +211,86 @@ const executeEnhancedTool = async ( readonly context: ServerContext; }, ): Promise => { - const { validatedCall, context } = request; - const name = validatedCall.name; - const progress = mcpProgressReporter(context); - await progress.report({ - phase: name, - completed: 0, - total: 1, - message: "started", - }); - const parsedInput = validatedCall.input; - const parameters = jsonParameters(parsedInput); - if (name === "analyze_function") - return executeFunctionTool(analysis, registration, contract, { - context, - parameters, - }); - const services = new EnhancedTools({ - execute: (operation, operationParameters, executionOptions) => - analysis.execute(operation, operationParameters, { - ...executionOptions, - progress, - }), - }); - const result = await logToolExecution(registration.logger, name, () => - services.executeValidated(validatedCall, context.mcpReq.signal), - ); - await progress.report({ - phase: name, - completed: 1, - total: 1, - message: result.ok ? "completed" : "failed", - terminal: true, - }); - if (result.ok) { - const evidence = createWorkflowEvidence({ - target: registration.activeTarget?.(), - operation: name, - parameters, - result: result.value, - upstreamProfile: registration.analysisProfile?.(), - }); - const recorded = registration.recordEvidence?.(evidence); - if (recorded !== undefined && !recorded.ok) - return registration.delivery.toCallToolResult(recorded, contract); - if ( - name !== "trace_native_ui_action" && - registration.allowsSnapshotReplay?.(name) === true - ) { - const workflowRecord = workflowSnapshotRecord(evidence, name); - if (workflowRecord !== undefined) { - const snapshot = registration.recordWorkflowSnapshot?.(workflowRecord); - if (snapshot !== undefined && !snapshot.ok) - return registration.delivery.toCallToolResult(snapshot, contract); + const admitted = await registration.withAdmittedAnalysis( + request.validatedCall.name, + request.context.mcpReq.signal, + async (admittedAnalysis) => { + const { validatedCall, context } = request; + const name = validatedCall.name; + const progress = mcpProgressReporter(context); + await progress.report({ + phase: name, + completed: 0, + total: 1, + message: "started", + }); + const parsedInput = validatedCall.input; + const parameters = jsonParameters(parsedInput); + if (name === "analyze_function") + return executeFunctionTool(admittedAnalysis, registration, contract, { + context, + parameters, + }); + const services = new EnhancedTools({ + execute: (operation, operationParameters, executionOptions) => + admittedAnalysis.execute(operation, operationParameters, { + ...executionOptions, + progress, + }), + }); + const result = await logToolExecution(registration.logger, name, () => + services.executeValidated(validatedCall, context.mcpReq.signal), + ); + await progress.report({ + phase: name, + completed: 1, + total: 1, + message: result.ok ? "completed" : "failed", + terminal: true, + }); + if (result.ok) { + const evidence = createWorkflowEvidence({ + target: registration.activeTarget?.(), + operation: name, + parameters, + result: result.value, + upstreamProfile: registration.analysisProfile?.(), + }); + const recorded = registration.recordEvidence?.(evidence); + if (recorded !== undefined && !recorded.ok) + return registration.delivery.toCallToolResult(recorded, contract); + if ( + name !== "trace_native_ui_action" && + registration.allowsSnapshotReplay?.(name) === true + ) { + const workflowRecord = workflowSnapshotRecord(evidence, name); + if (workflowRecord !== undefined) { + const snapshot = + registration.recordWorkflowSnapshot?.(workflowRecord); + if (snapshot !== undefined && !snapshot.ok) + return registration.delivery.toCallToolResult(snapshot, contract); + } + } + const unknowns = recordWorkflowUnknowns({ + name, + result: result.value, + evidenceId: evidence.evidence_id, + recordUnknown: registration.recordUnknown, + }); + if (!unknowns.ok) + return registration.delivery.toCallToolResult(unknowns, contract); + return registration.delivery.toEvidenceToolResult( + evidence, + contract, + recorded, + ); } - } - const unknowns = recordWorkflowUnknowns({ - name, - result: result.value, - evidenceId: evidence.evidence_id, - recordUnknown: registration.recordUnknown, - }); - if (!unknowns.ok) - return registration.delivery.toCallToolResult(unknowns, contract); - return registration.delivery.toEvidenceToolResult( - evidence, - contract, - recorded, - ); - } - return registration.delivery.toCallToolResult(result, contract); + return registration.delivery.toCallToolResult(result, contract); + }, + ); + return admitted.ok + ? admitted.value + : registration.delivery.toCallToolResult(admitted, contract); }; const executeFunctionTool = async ( diff --git a/src/server/registerEvidenceTools.ts b/src/server/registerEvidenceTools.ts index d03111745..c03079c9c 100644 --- a/src/server/registerEvidenceTools.ts +++ b/src/server/registerEvidenceTools.ts @@ -14,11 +14,16 @@ import { mcpProgressReporter } from "./mcpProgress.js"; import { logToolExecution } from "./toolLogging.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { createArtifactExtractionDestination } from "../application/artifacts/ArtifactExtractionDestination.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; interface EvidenceToolRegistration { readonly logger: Logger; readonly activeTarget: (() => BinaryTarget | undefined) | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis: WithAdmittedAnalysis; + readonly analysisForAdmission?: ( + analysis: AnalysisOperationPort, + ) => AnalysisOperationPort; readonly sourceEvidence?: ( operation: Exclude, result: import("../domain/jsonValue.js").JsonValue, @@ -28,7 +33,6 @@ interface EvidenceToolRegistration { /** Register provider-backed contracts that return atomic Evidence observations. */ export const registerEvidenceTools = ( server: EvidenceMcpServer, - analysis: AnalysisOperationPort, contracts: readonly ToolContract>[], options: EvidenceToolRegistration, ): void => { @@ -37,68 +41,84 @@ export const registerEvidenceTools = ( contract.name, toolRegistrationOptions(contract), async (input, context) => { - const progress = mcpProgressReporter(context); - await progress.report({ - phase: contract.name, - completed: 0, - total: 1, - message: "started", - }); - const parameters = jsonObjectSchema.parse(input); - const executionParameters = - contract.name === "extract_artifact" - ? { - ...parameters, - output_root: createArtifactExtractionDestination(), - } - : parameters; - const execution = await logToolExecution( - options.logger, + const admitted = await options.withAdmittedAnalysis( contract.name, - () => - analysis.execute(contract.name, executionParameters, { - signal: context.mcpReq.signal, - progress, - }), - ); - await progress.report({ - phase: contract.name, - completed: 1, - total: 1, - message: execution.ok ? "completed" : "failed", - terminal: true, - }); - if (!execution.ok) - return server.delivery.toCallToolResult(execution, contract); - const sourceEvidence = - options.sourceEvidence?.(contract.name, execution.value.result) ?? []; - const evidence = createEvidence( - execution.value.subject ?? options.activeTarget?.(), - execution.value.provider, - { - operation: contract.name, - parameters, - result: execution.value.result, - ...(execution.value.analysisProfile === undefined - ? {} - : { analysisProfile: execution.value.analysisProfile }), - rawResult: execution.value.rawResult, - limitations: execution.value.limitations, - locations: execution.value.locations, - evidenceLinks: sourceEvidence.map(({ evidence_id: id }) => id), + context.mcpReq.signal, + async (admittedAnalysis) => { + const analysis = + options.analysisForAdmission?.(admittedAnalysis) ?? + admittedAnalysis; + const progress = mcpProgressReporter(context); + await progress.report({ + phase: contract.name, + completed: 0, + total: 1, + message: "started", + }); + const parameters = jsonObjectSchema.parse(input); + const executionParameters = + contract.name === "extract_artifact" + ? { + ...parameters, + output_root: createArtifactExtractionDestination(), + } + : parameters; + const execution = await logToolExecution( + options.logger, + contract.name, + () => + analysis.execute(contract.name, executionParameters, { + signal: context.mcpReq.signal, + progress, + }), + ); + await progress.report({ + phase: contract.name, + completed: 1, + total: 1, + message: execution.ok ? "completed" : "failed", + terminal: true, + }); + if (!execution.ok) + return server.delivery.toCallToolResult(execution, contract); + const sourceEvidence = + options.sourceEvidence?.(contract.name, execution.value.result) ?? + []; + const evidence = createEvidence( + execution.value.subject ?? options.activeTarget?.(), + execution.value.provider, + { + operation: contract.name, + parameters, + result: execution.value.result, + ...(execution.value.analysisProfile === undefined + ? {} + : { analysisProfile: execution.value.analysisProfile }), + rawResult: execution.value.rawResult, + limitations: execution.value.limitations, + locations: execution.value.locations, + evidenceLinks: sourceEvidence.map(({ evidence_id: id }) => id), + }, + ); + for (const source of sourceEvidence) { + const sourceRecorded = options.recordEvidence?.(source); + if (sourceRecorded !== undefined && !sourceRecorded.ok) + return server.delivery.toCallToolResult( + sourceRecorded, + contract, + ); + } + const recorded = options.recordEvidence?.(evidence); + return server.delivery.toEvidenceToolResult( + evidence, + contract, + recorded, + ); }, ); - for (const source of sourceEvidence) { - const sourceRecorded = options.recordEvidence?.(source); - if (sourceRecorded !== undefined && !sourceRecorded.ok) - return server.delivery.toCallToolResult(sourceRecorded, contract); - } - const recorded = options.recordEvidence?.(evidence); - return server.delivery.toEvidenceToolResult( - evidence, - contract, - recorded, - ); + return admitted.ok + ? admitted.value + : server.delivery.toCallToolResult(admitted, contract); }, ); } diff --git a/src/server/registerManagedTools.ts b/src/server/registerManagedTools.ts index 74b7cac35..f54f2981f 100644 --- a/src/server/registerManagedTools.ts +++ b/src/server/registerManagedTools.ts @@ -2,7 +2,6 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import type { AnalysisOperationPort } from "../application/AnalysisProvider.js"; -import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; import { MANAGED_TOOL_CONTRACTS } from "../contracts/managed/managedToolContracts.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; @@ -15,16 +14,17 @@ import { isManagedToolName } from "../contracts/managed/managedToolContracts.js" /** Register execution-free managed PE/CLI inspection. */ export const registerManagedTools = ( server: EvidenceMcpServer, - analysis: AnalysisOperationPort, options: { readonly logger: Logger; readonly activeTarget: (() => BinaryTarget | undefined) | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; - readonly session: BinarySessionPort | undefined; + readonly withAdmittedAnalysis: import("./analysisAdmission.js").WithAdmittedAnalysis; }, ): void => { let selectedManagedPath: string | undefined; - const managedAnalysis: AnalysisOperationPort = { + const analysisForAdmission = ( + analysis: AnalysisOperationPort, + ): AnalysisOperationPort => ({ execute: async (operation, parameters, executionOptions) => { if (!isManagedToolName(operation)) return analysis.execute(operation, parameters, executionOptions); @@ -57,11 +57,9 @@ export const registerManagedTools = ( selectedManagedPath = requestedPath; return execution; }, - }; - registerEvidenceTools( - server, - managedAnalysis, - MANAGED_TOOL_CONTRACTS, - options, - ); + }); + registerEvidenceTools(server, MANAGED_TOOL_CONTRACTS, { + ...options, + analysisForAdmission, + }); }; diff --git a/src/server/registerNativeTools.ts b/src/server/registerNativeTools.ts index 35d5029a0..ba244a8e3 100644 --- a/src/server/registerNativeTools.ts +++ b/src/server/registerNativeTools.ts @@ -1,7 +1,6 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; -import type { AnalysisOperationPort } from "../application/AnalysisProvider.js"; import { NATIVE_TOOL_CONTRACTS } from "../contracts/native/nativeToolContracts.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import type { Logger } from "pino"; @@ -10,12 +9,13 @@ import { registerEvidenceTools } from "./registerEvidenceTools.js"; /** Register provider-neutral static inspection operations. */ export const registerNativeTools = ( server: EvidenceMcpServer, - analysis: AnalysisOperationPort, options: { readonly logger: Logger; readonly activeTarget: (() => BinaryTarget | undefined) | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis: import("./analysisAdmission.js").WithAdmittedAnalysis; }, -): void => { - registerEvidenceTools(server, analysis, NATIVE_TOOL_CONTRACTS, options); -}; +): void => + registerEvidenceTools(server, NATIVE_TOOL_CONTRACTS, { + ...options, + }); diff --git a/src/server/registerOfficialTools.ts b/src/server/registerOfficialTools.ts index 14c18337d..dcb5b10ab 100644 --- a/src/server/registerOfficialTools.ts +++ b/src/server/registerOfficialTools.ts @@ -17,77 +17,92 @@ 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"; /** Optional session services used by direct tool registration. */ export interface OfficialToolRegistration { readonly logger: Logger; readonly activeTarget: (() => BinaryTarget | undefined) | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis: WithAdmittedAnalysis; } /** Register direct bridge proxies, preserving MCP cancellation and typed errors. */ export const registerOfficialTools = ( server: EvidenceMcpServer, - analysis: AnalysisOperationPort, options: OfficialToolRegistration, ): void => { for (const contract of OFFICIAL_TOOL_CONTRACTS) { - registerOfficialTool(server, analysis, contract, { + registerOfficialTool(server, contract, { logger: options.logger, activeTarget: options.activeTarget, recordEvidence: options.recordEvidence, + withAdmittedAnalysis: options.withAdmittedAnalysis, }); } }; const registerOfficialTool = ( server: EvidenceMcpServer, - analysis: AnalysisOperationPort, contract: (typeof OFFICIAL_TOOL_CONTRACTS)[number], registration: { readonly logger: Logger; readonly activeTarget: (() => BinaryTarget | undefined) | undefined; readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined; + readonly withAdmittedAnalysis: WithAdmittedAnalysis; }, ): void => { server.registerTool( contract.name, toolRegistrationOptions(contract), async (input: unknown, context: ServerContext) => { - const arguments_ = jsonObjectSchema.parse( - contract.inputSchema.parse(input), - ); - const progress = mcpProgressReporter(context); - const result = await runOfficialOperation( - analysis, - contract, - arguments_, - { - logger: registration.logger, - signal: context.mcpReq.signal, - progress, + const admitted = await registration.withAdmittedAnalysis( + contract.name, + context.mcpReq.signal, + async (admittedAnalysis) => { + const arguments_ = jsonObjectSchema.parse( + contract.inputSchema.parse(input), + ); + const progress = mcpProgressReporter(context); + const result = await runOfficialOperation( + admittedAnalysis, + contract, + arguments_, + { + logger: registration.logger, + signal: context.mcpReq.signal, + progress, + }, + ); + if (!result.ok) { + return server.delivery.toCallToolResult(result, contract); + } + const evidence = createEvidence( + result.value.subject ?? registration.activeTarget?.(), + result.value.provider, + { + operation: contract.name, + parameters: arguments_, + result: result.value.result, + ...(result.value.analysisProfile === undefined + ? {} + : { analysisProfile: result.value.analysisProfile }), + rawResult: result.value.rawResult, + limitations: result.value.limitations, + locations: result.value.locations, + }, + ); + const recorded = registration.recordEvidence?.(evidence); + return server.delivery.toEvidenceToolResult( + evidence, + contract, + recorded, + ); }, ); - if (!result.ok) { - return server.delivery.toCallToolResult(result, contract); - } - const evidence = createEvidence( - result.value.subject ?? registration.activeTarget?.(), - result.value.provider, - { - operation: contract.name, - parameters: arguments_, - result: result.value.result, - ...(result.value.analysisProfile === undefined - ? {} - : { analysisProfile: result.value.analysisProfile }), - rawResult: result.value.rawResult, - limitations: result.value.limitations, - locations: result.value.locations, - }, - ); - const recorded = registration.recordEvidence?.(evidence); - return server.delivery.toEvidenceToolResult(evidence, contract, recorded); + return admitted.ok + ? admitted.value + : server.delivery.toCallToolResult(admitted, contract); }, ); }; diff --git a/src/server/registerSessionTools.ts b/src/server/registerSessionTools.ts index d4dc0b48a..5d0b1bf38 100644 --- a/src/server/registerSessionTools.ts +++ b/src/server/registerSessionTools.ts @@ -232,20 +232,36 @@ const registerContextTools = ( navigationContract.name, toolRegistrationOptions(navigationContract), async (input, context) => { - return server.delivery.toCallToolResult( - await getNavigationContext(session, input, context.mcpReq.signal), - navigationContract, + const admitted = await session.withAdmittedAnalysis( + navigationContract.name, + context.mcpReq.signal, + async (analysis) => + server.delivery.toCallToolResult( + await getNavigationContext(analysis, input, context.mcpReq.signal), + navigationContract, + ), ); + return admitted.ok + ? admitted.value + : server.delivery.toCallToolResult(admitted, navigationContract); }, ); server.registerTool( addressContract.name, toolRegistrationOptions(addressContract), async (input, context) => { - return server.delivery.toCallToolResult( - await inspectAddressContext(session, input, context.mcpReq.signal), - addressContract, + const admitted = await session.withAdmittedAnalysis( + addressContract.name, + context.mcpReq.signal, + async (analysis) => + server.delivery.toCallToolResult( + await inspectAddressContext(analysis, input, context.mcpReq.signal), + addressContract, + ), ); + return admitted.ok + ? admitted.value + : server.delivery.toCallToolResult(admitted, addressContract); }, ); }; diff --git a/tests/acceptance/nonBinaryComposition.test.ts b/tests/acceptance/nonBinaryComposition.test.ts index b333db20e..d2409723f 100644 --- a/tests/acceptance/nonBinaryComposition.test.ts +++ b/tests/acceptance/nonBinaryComposition.test.ts @@ -17,11 +17,14 @@ const connect = async (environment: NodeJS.ProcessEnv) => { const session = createTestBinarySession(() => { throw new Error("Target-free analysis must not acquire a deep provider"); }); - const server = createServer(session, session, { - androidAnalysis: createAndroidAnalysisProvider(environment), - firmwareAnalysis: createFirmwareAnalysisProvider(environment), - javascriptRecovery: createJavaScriptRecoveryProvider(environment), - }); + const server = createServer( + { kind: "session", session }, + { + androidAnalysis: createAndroidAnalysisProvider(environment), + firmwareAnalysis: createFirmwareAnalysisProvider(environment), + javascriptRecovery: createJavaScriptRecoveryProvider(environment), + }, + ); const client = new Client({ name: "composition-parity", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/acceptance/webModuleCli.test.ts b/tests/acceptance/webModuleCli.test.ts index 2171d54ca..36675d794 100644 --- a/tests/acceptance/webModuleCli.test.ts +++ b/tests/acceptance/webModuleCli.test.ts @@ -48,9 +48,12 @@ cliTest( const session = createTestBinarySession(() => { throw new Error("no binary acquisition"); }); - const server = createServer(session, session, { - webModuleTrace: createWebModuleTraceService(environment), - }); + const server = createServer( + { kind: "session", session }, + { + webModuleTrace: createWebModuleTraceService(environment), + }, + ); const client = new Client({ name: "module-cli-parity", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/advertisedPatternsVMode.test.ts b/tests/boundary/mcp/advertisedPatternsVMode.test.ts index 595c356fc..024c42d12 100644 --- a/tests/boundary/mcp/advertisedPatternsVMode.test.ts +++ b/tests/boundary/mcp/advertisedPatternsVMode.test.ts @@ -34,7 +34,7 @@ it("advertises portable NUL escapes and patterns that compile in all JS modes", const session = createTestBinarySession(() => { throw new Error("No deep provider may start for a schema projection"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "advertised-patterns", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts index 9d188e6c0..3edeffba7 100644 --- a/tests/boundary/mcp/analysisViewMcp.test.ts +++ b/tests/boundary/mcp/analysisViewMcp.test.ts @@ -27,8 +27,7 @@ const connect = async (binaryLayout?: BinaryLayoutService) => { throw new Error("selected views must not start a deep provider"); }); const server = createServer( - session, - session, + { kind: "session", session }, binaryLayout === undefined ? {} : { binaryLayout }, ); const client = new Client({ name: "analysis-view-mcp", version: "1" }); diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index 7405c30c2..9241bf471 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -26,7 +26,7 @@ it("inspects a standalone keyed archive through MCP with original object identit ), ); const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "keyed-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -119,7 +119,7 @@ it("returns all 520 ZIP file occurrences in one inspect_artifact MCP result", as await writeFile(archive, await writer.close()); const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "artifact-inline-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/artifactMcp.test.ts b/tests/boundary/mcp/artifactMcp.test.ts index 5b786a2dd..ee180a37f 100644 --- a/tests/boundary/mcp/artifactMcp.test.ts +++ b/tests/boundary/mcp/artifactMcp.test.ts @@ -47,7 +47,7 @@ it.each([ } const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "asar-integrity-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -117,7 +117,7 @@ it("extracts an active archive through MCP when requested", async () => { await writeFile(archive, await zip.close()); const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "artifact-extract-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -166,7 +166,7 @@ it("extracts a macOS app bundle through MCP", async () => { await writeFile(join(contents, "MacOS", "Fixture"), header); const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "artifact-extract-app-test", version: "1", @@ -225,7 +225,7 @@ it("records an explicitly continued mismatch, preserves verified siblings, and n await writeFile(archive, bytes); const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "asar-continue-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -326,7 +326,7 @@ it("rejects inline artifact Evidence whose content does not match its ID", async execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "artifact-authority-test", version: "1", @@ -364,7 +364,7 @@ it("compares inline inventory Evidence without prior session calls", async () => execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "artifact-ownership-test", version: "1", @@ -411,7 +411,7 @@ it("returns full artifact graphs inline and compares changed inventories", async ); await writeFile(changedArchive, await changedWriter.close()); const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "artifact-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts b/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts index 4570bbb93..d825d96de 100644 --- a/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts +++ b/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts @@ -21,7 +21,10 @@ it("advertises exact valid schemas and records inline layout without a binary ta const session = createTestBinarySession(() => { throw new Error("deep provider must not start"); }); - const server = createServer(session, session, { binaryLayout: service }); + const server = createServer( + { kind: "session", session }, + { binaryLayout: service }, + ); const client = new Client({ name: "binary-layout-contract", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/browserMcp.test.ts b/tests/boundary/mcp/browserMcp.test.ts index aa3fa853f..8e31e4839 100644 --- a/tests/boundary/mcp/browserMcp.test.ts +++ b/tests/boundary/mcp/browserMcp.test.ts @@ -291,13 +291,16 @@ const connectBrowser = async () => { execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session, { - browserObservation: new CdpBrowserProvider(), - availabilityPolicy: () => ({ - processCaptureEnabled: false, - investigationInputRoots: 0, - }), - }); + const server = createServer( + { kind: "session", session }, + { + browserObservation: new CdpBrowserProvider(), + availabilityPolicy: () => ({ + processCaptureEnabled: false, + investigationInputRoots: 0, + }), + }, + ); const client = new Client({ name: "browser-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/browserScenarioMcp.test.ts b/tests/boundary/mcp/browserScenarioMcp.test.ts index 41736f952..c6bfdd6e1 100644 --- a/tests/boundary/mcp/browserScenarioMcp.test.ts +++ b/tests/boundary/mcp/browserScenarioMcp.test.ts @@ -175,14 +175,17 @@ describe("browser scenario MCP tool", () => { execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(ok(null)), })); - const server = createServer(session, session, { - browserObservation: new CdpBrowserProvider(), - browserScenarioCapture: provider, - availabilityPolicy: () => ({ - processCaptureEnabled: false, - investigationInputRoots: 0, - }), - }); + const server = createServer( + { kind: "session", session }, + { + browserObservation: new CdpBrowserProvider(), + browserScenarioCapture: provider, + availabilityPolicy: () => ({ + processCaptureEnabled: false, + investigationInputRoots: 0, + }), + }, + ); const client = new Client({ name: "browser-scenario-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/bundleComparisonMcp.test.ts b/tests/boundary/mcp/bundleComparisonMcp.test.ts index 0ea1006b3..9fb00e768 100644 --- a/tests/boundary/mcp/bundleComparisonMcp.test.ts +++ b/tests/boundary/mcp/bundleComparisonMcp.test.ts @@ -152,7 +152,7 @@ const connect = async () => { execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "bundle-comparison-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/clientCapabilities.test.ts b/tests/boundary/mcp/clientCapabilities.test.ts index 323f79980..a8acc0048 100644 --- a/tests/boundary/mcp/clientCapabilities.test.ts +++ b/tests/boundary/mcp/clientCapabilities.test.ts @@ -23,7 +23,7 @@ describe("per-request client capability reporting", () => { "reports the negotiated capability %j", async (capabilities, form, url) => { const session = createTestBinarySession(createCacheProvider([])); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "capabilities-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -61,7 +61,7 @@ describe("initialize-scoped client metadata", () => { meta?: Record, ) => { const session = createTestBinarySession(createCacheProvider([])); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client( { name: "initialize-test", version: "2" }, { capabilities }, diff --git a/tests/boundary/mcp/comparisonUnknownsMcp.test.ts b/tests/boundary/mcp/comparisonUnknownsMcp.test.ts index 7e07713fe..bf950f83b 100644 --- a/tests/boundary/mcp/comparisonUnknownsMcp.test.ts +++ b/tests/boundary/mcp/comparisonUnknownsMcp.test.ts @@ -16,7 +16,7 @@ const connect = async () => { const session = createTestBinarySession(() => { throw new Error("Evidence comparison must not launch a provider"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "comparison-unknowns", version: "1" }); onTestFinished(async () => { await Promise.allSettled([client.close(), server.close(), session.close()]); diff --git a/tests/boundary/mcp/dylibResolutionMcp.test.ts b/tests/boundary/mcp/dylibResolutionMcp.test.ts index 34db785e3..4f2d70aab 100644 --- a/tests/boundary/mcp/dylibResolutionMcp.test.ts +++ b/tests/boundary/mcp/dylibResolutionMcp.test.ts @@ -25,7 +25,7 @@ const withClient = async ( verify: (client: Client) => Promise, ): Promise => { const session = createTestBinarySession(new ArtifactProvider(process.env)); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "dylib-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts index efa700ba1..4910fd9f8 100644 --- a/tests/boundary/mcp/electronMcp.test.ts +++ b/tests/boundary/mcp/electronMcp.test.ts @@ -72,13 +72,16 @@ it("exposes endpoint-scoped Electron discovery and inspection as Evidence", asyn execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session, { - electronObservation: new CdpElectronProvider(), - availabilityPolicy: () => ({ - processCaptureEnabled: false, - investigationInputRoots: 1, - }), - }); + const server = createServer( + { kind: "session", session }, + { + electronObservation: new CdpElectronProvider(), + availabilityPolicy: () => ({ + processCaptureEnabled: false, + investigationInputRoots: 1, + }), + }, + ); const client = new Client({ name: "electron-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -197,13 +200,16 @@ it("runs active Electron scenarios with selected paths and inferred working dire execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session, { - electronActiveObservation: provider, - availabilityPolicy: () => ({ - processCaptureEnabled: false, - investigationInputRoots: 0, - }), - }); + const server = createServer( + { kind: "session", session }, + { + electronActiveObservation: provider, + availabilityPolicy: () => ({ + processCaptureEnabled: false, + investigationInputRoots: 0, + }), + }, + ); const client = new Client({ name: "electron-active-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -279,12 +285,15 @@ it("exposes the target-free static JavaScript application workflow", async () => execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session, { - availabilityPolicy: () => ({ - processCaptureEnabled: false, - investigationInputRoots: 1, - }), - }); + const server = createServer( + { kind: "session", session }, + { + availabilityPolicy: () => ({ + processCaptureEnabled: false, + investigationInputRoots: 1, + }), + }, + ); const client = new Client({ name: "electron-static-mcp-test", version: "1", diff --git a/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts b/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts index 958852001..7662c8234 100644 --- a/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts +++ b/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts @@ -186,7 +186,7 @@ afterEach(async () => { }); const connect = async (analysis: AnalysisOperationPort = fixturePort()) => { - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "enhanced-test", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/enhancedToolsHarness.ts b/tests/boundary/mcp/enhancedToolsHarness.ts index 26bb194ab..1fa41151b 100644 --- a/tests/boundary/mcp/enhancedToolsHarness.ts +++ b/tests/boundary/mcp/enhancedToolsHarness.ts @@ -24,7 +24,7 @@ export const inventory = (values: Readonly>) => export const resources: Array<{ close(): Promise }> = []; export const connect = async (analysis: AnalysisOperationPort) => { - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "enhanced-test", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/evidenceBundleInline.test.ts b/tests/boundary/mcp/evidenceBundleInline.test.ts index 597b47a94..adb95ef8f 100644 --- a/tests/boundary/mcp/evidenceBundleInline.test.ts +++ b/tests/boundary/mcp/evidenceBundleInline.test.ts @@ -18,7 +18,7 @@ it("returns the complete session Evidence bundle in the tool result", async () = close: () => Promise.resolve(resultOk(null)), })); expect(session.recordEvidence(evidence).ok).toBe(true); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "inline-evidence-bundle", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/evidenceExportCancellation.test.ts b/tests/boundary/mcp/evidenceExportCancellation.test.ts index 2a7e2c40b..ca084f813 100644 --- a/tests/boundary/mcp/evidenceExportCancellation.test.ts +++ b/tests/boundary/mcp/evidenceExportCancellation.test.ts @@ -28,7 +28,7 @@ it.each([false, true])( }, ); expect(session.recordEvidence(evidence).ok).toBe(true); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "evidence-export-cancellation", version: "1", diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts index d1d7c94bb..d197c2742 100644 --- a/tests/boundary/mcp/evmInterfaceMcp.test.ts +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -20,7 +20,10 @@ it("advertises exact valid schemas and records inline EVM interface evidence wit const session = createTestBinarySession(() => { throw new Error("deep provider must not start"); }); - const server = createServer(session, session, { evmInterface: service }); + const server = createServer( + { kind: "session", session }, + { evmInterface: service }, + ); const client = new Client({ name: "evm-interface-contract", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/firmwareMcp.test.ts b/tests/boundary/mcp/firmwareMcp.test.ts index a18fccdcf..4e3b7761a 100644 --- a/tests/boundary/mcp/firmwareMcp.test.ts +++ b/tests/boundary/mcp/firmwareMcp.test.ts @@ -16,9 +16,12 @@ it("executes named firmware contracts with inline Evidence and no native binary const session = createTestBinarySession(() => { throw new Error("Firmware inspection must not acquire a native provider"); }); - const server = createServer(session, session, { - firmwareAnalysis: fixture.provider, - }); + const server = createServer( + { kind: "session", session }, + { + firmwareAnalysis: fixture.provider, + }, + ); const client = new Client({ name: "firmware-regression", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/functionComparisonMcp.test.ts b/tests/boundary/mcp/functionComparisonMcp.test.ts index e5965a522..429bca334 100644 --- a/tests/boundary/mcp/functionComparisonMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonMcp.test.ts @@ -39,7 +39,7 @@ describe("function comparison reconstruction MCP integration", () => { authority: left.authority, }, ); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "function-verification-test", version: "1", @@ -140,7 +140,7 @@ describe("function comparison MCP integration", () => { expect(session.recordEvidence(FUNCTION_COMPARISON_EXAMPLE.right).ok).toBe( true, ); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "function-comparison-test", version: "1", @@ -196,7 +196,7 @@ describe("function comparison MCP integration", () => { execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "function-link-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -240,7 +240,7 @@ describe("function comparison MCP integration", () => { ); session.recordEvidence(wrong); session.recordEvidence(FUNCTION_COMPARISON_EXAMPLE.right); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "function-authority-test", version: "1", diff --git a/tests/boundary/mcp/ghidraMcpHarness.ts b/tests/boundary/mcp/ghidraMcpHarness.ts index 473179102..cbfbb7019 100644 --- a/tests/boundary/mcp/ghidraMcpHarness.ts +++ b/tests/boundary/mcp/ghidraMcpHarness.ts @@ -52,7 +52,10 @@ export const connectGhidraMcp = async ( const session = composeBinarySession( new AnalysisProviderRegistry([provider]), ); - const server = createServer(session, session, { logger: silentLogger }); + const server = createServer( + { kind: "session", session }, + { logger: silentLogger }, + ); const mcp = new Client({ name, version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/investigationAnalysisMcp.test.ts b/tests/boundary/mcp/investigationAnalysisMcp.test.ts index a9b9d4765..752f81bc3 100644 --- a/tests/boundary/mcp/investigationAnalysisMcp.test.ts +++ b/tests/boundary/mcp/investigationAnalysisMcp.test.ts @@ -324,7 +324,7 @@ const connected = async () => { execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "investigation-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/javascriptAnalysisCancellation.test.ts b/tests/boundary/mcp/javascriptAnalysisCancellation.test.ts index 7325ef763..3325b1706 100644 --- a/tests/boundary/mcp/javascriptAnalysisCancellation.test.ts +++ b/tests/boundary/mcp/javascriptAnalysisCancellation.test.ts @@ -51,7 +51,7 @@ it("stops a cancelled SDK request on the server, retains prior Evidence, and acc }, }, ); - const server = createServer(session, session, { logger }); + const server = createServer({ kind: "session", session }, { logger }); const client = new Client({ name: "javascript-cancellation", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/javascriptRecoveryMcp.test.ts b/tests/boundary/mcp/javascriptRecoveryMcp.test.ts index e22891ae6..4c1bd9809 100644 --- a/tests/boundary/mcp/javascriptRecoveryMcp.test.ts +++ b/tests/boundary/mcp/javascriptRecoveryMcp.test.ts @@ -17,9 +17,12 @@ it.skipIf(process.platform !== "linux" || process.arch !== "x64")( const session = createTestBinarySession(() => { throw new Error("no binary provider should start"); }); - const server = createServer(session, session, { - javascriptRecovery: fixture.provider, - }); + const server = createServer( + { kind: "session", session }, + { + javascriptRecovery: fixture.provider, + }, + ); const client = new Client({ name: "recovery-contract", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts index 9682998e8..77c9d59ec 100644 --- a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts +++ b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts @@ -179,13 +179,16 @@ const createObservationClient = async ( execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session, { - javascriptRuntimeObservation: new V8InspectorProvider(), - availabilityPolicy: () => ({ - processCaptureEnabled: false, - investigationInputRoots: 0, - }), - }); + const server = createServer( + { kind: "session", session }, + { + javascriptRuntimeObservation: new V8InspectorProvider(), + availabilityPolicy: () => ({ + processCaptureEnabled: false, + investigationInputRoots: 0, + }), + }, + ); const client = new Client({ name: "v8-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/keyedArchiveNumberMetadataMcp.test.ts b/tests/boundary/mcp/keyedArchiveNumberMetadataMcp.test.ts index 5d16e87f4..4a40a3064 100644 --- a/tests/boundary/mcp/keyedArchiveNumberMetadataMcp.test.ts +++ b/tests/boundary/mcp/keyedArchiveNumberMetadataMcp.test.ts @@ -27,7 +27,9 @@ mcpTest.for(archiveNumberMetadataCases)( onTestFinished(async () => { await session.close(); }); - const client = await mcp.connect(createServer(session, session)); + const client = await mcp.connect( + createServer({ kind: "session", session }), + ); const opened = await client.callTool({ name: "open_binary", arguments: { path: fixture.path }, diff --git a/tests/boundary/mcp/largeToolResult.test.ts b/tests/boundary/mcp/largeToolResult.test.ts index 8b61f9be6..7453db0d0 100644 --- a/tests/boundary/mcp/largeToolResult.test.ts +++ b/tests/boundary/mcp/largeToolResult.test.ts @@ -49,20 +49,20 @@ const callPseudoCode = async ( recordEvidence, delivery, ); - registerOfficialTools( - server, - { - execute: () => - Promise.resolve( - ok(createAnalysisExecution(payload, provider, { rawResult })), - ), - }, - { - logger: silentLogger, - activeTarget: undefined, - recordEvidence, - }, - ); + registerOfficialTools(server, { + logger: silentLogger, + activeTarget: undefined, + recordEvidence, + withAdmittedAnalysis: async (_operationName, _signal, operation) => + ok( + await operation({ + execute: () => + Promise.resolve( + ok(createAnalysisExecution(payload, provider, { rawResult })), + ), + }), + ), + }); const client = new Client({ name: "complete-evidence-client", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/managedMcp.test.ts b/tests/boundary/mcp/managedMcp.test.ts index 762651708..6a985d7ec 100644 --- a/tests/boundary/mcp/managedMcp.test.ts +++ b/tests/boundary/mcp/managedMcp.test.ts @@ -14,7 +14,6 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js"; import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js"; import type { BinarySession } from "../../../src/application/binary/BinarySession.js"; -import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js"; import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../../src/contracts/managed/managedWorkflowExamples.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js"; @@ -35,8 +34,7 @@ it("runs every managed static inspection independently of an active native targe new ManagedStaticProvider(), ]); const server = createServer( - session, - sessionWithUnrelatedNativeTarget(session), + { kind: "session", session }, { availabilityPolicy: () => ({ processCaptureEnabled: false, @@ -133,8 +131,7 @@ it("opens a managed PE and executes the managed static provider through MCP", as new ManagedStaticProvider(), ]); const server = createServer( - session, - sessionWithUnrelatedNativeTarget(session), + { kind: "session", session }, { availabilityPolicy: () => ({ processCaptureEnabled: false, @@ -288,28 +285,6 @@ const inspectManagedStaticWorkflow = async ( return members; }; -const sessionWithUnrelatedNativeTarget = ( - session: BinarySession, -): BinarySession => { - const nativeTarget: BinaryTarget = { - kind: "executable", - format: "pe", - path: "C:\\Windows\\System32\\notepad.exe", - sha256: "a".repeat(64), - architecture: "x86_64", - availableArchitectures: ["x86_64"], - executableRole: "application", - managed: false, - }; - return new Proxy(session, { - get(target, property, receiver) { - if (property === "activeTarget") return () => nativeTarget; - const value: unknown = Reflect.get(target, property, receiver); - return typeof value === "function" ? value.bind(target) : value; - }, - }); -}; - const methodFrom = (members: Record) => z .array( diff --git a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts index 7c8abe33c..80173eba8 100644 --- a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts +++ b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts @@ -27,7 +27,7 @@ async function connect() { const session = createTestBinarySession(() => { throw new Error("mobile graph projection must not launch a provider"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "mobile-application-graph-test", version: "1", diff --git a/tests/boundary/mcp/nativeCallObservationMcp.test.ts b/tests/boundary/mcp/nativeCallObservationMcp.test.ts index 284d6ce43..cecd3b2f4 100644 --- a/tests/boundary/mcp/nativeCallObservationMcp.test.ts +++ b/tests/boundary/mcp/nativeCallObservationMcp.test.ts @@ -90,7 +90,7 @@ it("routes observe_native_calls through MCP with schema-checked input and output const session = createTestBinarySession( new NativeMacOSProvider({}, new NativeFixtureRunner(), "darwin", tracer), ); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "native-calls-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -233,7 +233,7 @@ it.each([ const session = createTestBinarySession( new NativeMacOSProvider({}, new NativeFixtureRunner(), "darwin", tracer), ); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "native-calls-partial-mcp-test", version: "1", diff --git a/tests/boundary/mcp/nativeDemangleMcp.test.ts b/tests/boundary/mcp/nativeDemangleMcp.test.ts index 75b22572c..71f7732a5 100644 --- a/tests/boundary/mcp/nativeDemangleMcp.test.ts +++ b/tests/boundary/mcp/nativeDemangleMcp.test.ts @@ -40,7 +40,9 @@ describe.skipIf(process.platform !== "darwin")( onTestFinished(async () => { await session.close(); }); - const client = await mcp.connect(createServer(session, session)); + const client = await mcp.connect( + createServer({ kind: "session", session }), + ); const opened = await client.callTool({ name: "open_binary", arguments: { path: target }, diff --git a/tests/boundary/mcp/nativeInputContracts.test.ts b/tests/boundary/mcp/nativeInputContracts.test.ts index 4429f8863..f8a9d183d 100644 --- a/tests/boundary/mcp/nativeInputContracts.test.ts +++ b/tests/boundary/mcp/nativeInputContracts.test.ts @@ -35,7 +35,7 @@ const resources: Array<{ close(): Promise }> = []; const connect = async ( execute: AnalysisOperationPort["execute"], ): Promise => { - const server = createServer({ execute }); + const server = createServer({ kind: "fixed", analysis: { execute } }); const client = new Client({ name: "native-contract-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts b/tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts index 4468491c2..5576fccef 100644 --- a/tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts +++ b/tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts @@ -22,7 +22,7 @@ it("rejects changed signature targets through MCP and accepts reopening the new const session = createTestBinarySession( new NativeMacOSProvider({}, new NativeFixtureRunner(), "darwin"), ); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "signature-binding-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/officialArguments.test.ts b/tests/boundary/mcp/officialArguments.test.ts index 943f6893e..ff0284ac0 100644 --- a/tests/boundary/mcp/officialArguments.test.ts +++ b/tests/boundary/mcp/officialArguments.test.ts @@ -20,7 +20,7 @@ import { } from "../../fixtures/binarySession.js"; const connect = async (analysis: AnalysisOperationPort) => { - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "argument-parity", version: "1.0.0" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/officialTools.test.ts b/tests/boundary/mcp/officialTools.test.ts index 95b741313..70fbf8587 100644 --- a/tests/boundary/mcp/officialTools.test.ts +++ b/tests/boundary/mcp/officialTools.test.ts @@ -72,7 +72,7 @@ afterEach(async () => { }); const connect = async (analysis: AnalysisOperationPort) => { - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "contract-test", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/optionalProviderIsolation.test.ts b/tests/boundary/mcp/optionalProviderIsolation.test.ts index 7ede38874..eb8f64fd1 100644 --- a/tests/boundary/mcp/optionalProviderIsolation.test.ts +++ b/tests/boundary/mcp/optionalProviderIsolation.test.ts @@ -60,10 +60,12 @@ const boot = async (factories: OptionalObservationFactories) => { await run({ env: {}, loadOptionalProviders: async () => loaded, - createServer: (analysis, selectedSession, options) => { - session = selectedSession; + createServer: (source, options) => { + if (source.kind !== "session") + throw new Error("Expected a session-owned server source"); + session = source.session; // A configured policy cannot hide an actual adapter loading failure. - return createServer(analysis, selectedSession, { + return createServer(source, { ...options, availabilityPolicy: () => ({ processCaptureEnabled: true, diff --git a/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts b/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts index 3377277ae..7667b1ffb 100644 --- a/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts +++ b/tests/boundary/mcp/processComparisonDiagnosticsMcp.test.ts @@ -44,7 +44,7 @@ const connect = async () => { const session = createTestBinarySession(() => { throw new Error("Process comparison must not launch a provider"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "process-comparison-test", version: "1" }); onTestFinished(async () => { await Promise.allSettled([client.close(), server.close(), session.close()]); diff --git a/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts b/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts index 9fc252995..fb20dd79b 100644 --- a/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts +++ b/tests/boundary/mcp/processEvidenceIdentityMcp.test.ts @@ -57,7 +57,7 @@ const connectedComparison = async () => { const session = createTestBinarySession(() => { throw new Error("Process comparison must not launch a provider"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "process-evidence-identity", version: "1", diff --git a/tests/boundary/mcp/processTraceAdaptersParity.test.ts b/tests/boundary/mcp/processTraceAdaptersParity.test.ts index 913d640cd..7f9d3d9c3 100644 --- a/tests/boundary/mcp/processTraceAdaptersParity.test.ts +++ b/tests/boundary/mcp/processTraceAdaptersParity.test.ts @@ -126,10 +126,7 @@ describe("declared trace adapter parity", () => { ); expect(session.recordEvidence(left).ok).toBe(true); expect(session.recordEvidence(right).ok).toBe(true); - const server = createServer( - { execute: () => Promise.resolve(ok(null)) }, - session, - ); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "trace-parity", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/progressMcp.test.ts b/tests/boundary/mcp/progressMcp.test.ts index ff0a94216..31c537a92 100644 --- a/tests/boundary/mcp/progressMcp.test.ts +++ b/tests/boundary/mcp/progressMcp.test.ts @@ -12,7 +12,7 @@ describe("ordinary MCP progress and cancellation", () => { const analysis: AnalysisOperationPort = { execute: () => Promise.resolve(observed("0x1000")), }; - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "progress-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -59,7 +59,7 @@ describe("ordinary MCP progress and cancellation", () => { return err(new AnalysisCancelledError(operation)); }, }; - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "cancellation-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/promptMcp.test.ts b/tests/boundary/mcp/promptMcp.test.ts index b8399906a..0f9725c83 100644 --- a/tests/boundary/mcp/promptMcp.test.ts +++ b/tests/boundary/mcp/promptMcp.test.ts @@ -42,7 +42,7 @@ afterEach(async () => { describe("guided prompts over MCP", () => { it("lists and renders all workflows without changing the tool inventory", async () => { const session = fixtureSession(); - const client = await connect(createServer(session, session)); + const client = await connect(createServer({ kind: "session", session })); resources.push(session); const toolsBeforePrompts = (await client.listTools()).tools; @@ -96,7 +96,7 @@ describe("guided prompts over MCP", () => { const session = fixtureSession(() => { executions += 1; }); - const client = await connect(createServer(session, session)); + const client = await connect(createServer({ kind: "session", session })); resources.push(session); for (const contract of PROMPT_CONTRACTS) { @@ -126,7 +126,7 @@ describe("guided prompts over MCP", () => { const session = fixtureSession(() => { executions += 1; }); - const client = await connect(createServer(session, session)); + const client = await connect(createServer({ kind: "session", session })); resources.push(session); for (const contract of PROMPT_CONTRACTS) @@ -180,7 +180,7 @@ it("preserves caller-selected paths and verbatim crash signals", async () => { await writeFile(targetPath, "owned prompt target", "utf8"); await expect(readFile(targetPath.trim(), "utf8")).rejects.toThrow(); const session = fixtureSession(); - const client = await connect(createServer(session, session)); + const client = await connect(createServer({ kind: "session", session })); resources.push(session); for (const [name, arguments_] of [ @@ -234,7 +234,7 @@ describe("guided prompt completion registry", () => { }), ).ok, ).toBe(true); - const client = await connect(createServer(session, session)); + const client = await connect(createServer({ kind: "session", session })); resources.push(session); const result = await client.complete({ @@ -355,7 +355,7 @@ describe("guided prompt completion lifecycle", () => { const session = createTestBinarySession((target) => lifecycleClient(basename(target.path, ".hop"), procedureRequests), ); - const client = await connect(createServer(session, session)); + const client = await connect(createServer({ kind: "session", session })); resources.push(session); expect(await complete(client, "document", "")).toEqual([]); diff --git a/tests/boundary/mcp/providerSelectionMcp.test.ts b/tests/boundary/mcp/providerSelectionMcp.test.ts index 5d30a4e37..1d6cb7b77 100644 --- a/tests/boundary/mcp/providerSelectionMcp.test.ts +++ b/tests/boundary/mcp/providerSelectionMcp.test.ts @@ -43,7 +43,10 @@ describe("provider selection over MCP", () => { candidate("alpha", starts), ]), ); - const server = createServer(session, session, { logger: silentLogger }); + const server = createServer( + { kind: "session", session }, + { logger: silentLogger }, + ); const mcp = new Client({ name: "provider-selection", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/reconstructionCoverageMcp.test.ts b/tests/boundary/mcp/reconstructionCoverageMcp.test.ts index 50dd2508a..cb3605d14 100644 --- a/tests/boundary/mcp/reconstructionCoverageMcp.test.ts +++ b/tests/boundary/mcp/reconstructionCoverageMcp.test.ts @@ -15,7 +15,7 @@ describe("reconstruction coverage MCP", () => { execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "coverage-mcp-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/reconstructionObligationLedgerMcp.test.ts b/tests/boundary/mcp/reconstructionObligationLedgerMcp.test.ts index 79457903e..572b18d63 100644 --- a/tests/boundary/mcp/reconstructionObligationLedgerMcp.test.ts +++ b/tests/boundary/mcp/reconstructionObligationLedgerMcp.test.ts @@ -13,7 +13,7 @@ describe("reconstruction obligation ledger MCP parity", () => { execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "obligation-ledger-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/recordedCrashMcp.test.ts b/tests/boundary/mcp/recordedCrashMcp.test.ts index bdb2b3517..3b809a296 100644 --- a/tests/boundary/mcp/recordedCrashMcp.test.ts +++ b/tests/boundary/mcp/recordedCrashMcp.test.ts @@ -21,7 +21,10 @@ it("advertises exact valid schemas and records inline recorded-core evidence wit const session = createTestBinarySession(() => { throw new Error("deep provider must not start"); }); - const server = createServer(session, session, { recordedCrash: service }); + const server = createServer( + { kind: "session", session }, + { recordedCrash: service }, + ); const client = new Client({ name: "recorded-crash-contract", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts b/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts index 3cf450a7b..ef90cd7e4 100644 --- a/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts +++ b/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts @@ -28,7 +28,7 @@ async function connect() { const session = createTestBinarySession(() => { throw new Error("references must not launch a provider"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "retained-evidence-test", version: "1" }); resources.push(client, server, session); const [a, b] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/serverIdentity.test.ts b/tests/boundary/mcp/serverIdentity.test.ts index aa81a8540..09ace062d 100644 --- a/tests/boundary/mcp/serverIdentity.test.ts +++ b/tests/boundary/mcp/serverIdentity.test.ts @@ -206,7 +206,7 @@ describe("server and catalog identity", () => { describe("live server identity over MCP", () => { it("exposes live identity, a stable catalog, and changing availability", async () => { const session = createTestBinarySession(availabilityProvider()); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client( { name: "identity-test", version: "9" }, { @@ -252,7 +252,7 @@ describe("active-target availability over MCP", () => { const session = createTestBinarySession( availabilityProvider(["inspect_macho", "inspect_artifact"]), ); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "availability-test", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/serverLifecycleMcp.test.ts b/tests/boundary/mcp/serverLifecycleMcp.test.ts index da4520307..6a140befe 100644 --- a/tests/boundary/mcp/serverLifecycleMcp.test.ts +++ b/tests/boundary/mcp/serverLifecycleMcp.test.ts @@ -18,7 +18,7 @@ afterEach(async () => { }); const connect = async (analysis: AnalysisOperationPort) => { - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "integration-test", version: "1.0.0", diff --git a/tests/boundary/mcp/serverUnknownsMcp.test.ts b/tests/boundary/mcp/serverUnknownsMcp.test.ts index fe4dc8499..41fc76c45 100644 --- a/tests/boundary/mcp/serverUnknownsMcp.test.ts +++ b/tests/boundary/mcp/serverUnknownsMcp.test.ts @@ -6,7 +6,6 @@ import { afterEach, expect, it } from "vitest"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import type { AnalysisClient, - AnalysisOperationPort, AnalysisProvider, CapabilityDescriptor, } from "../../../src/application/AnalysisProvider.js"; @@ -62,7 +61,17 @@ const providerWithCapabilities = ( identity: () => identity, capabilities: () => capabilities, createClient: () => ({ - execute: () => Promise.resolve(ok(null)), + execute: (operation) => { + return Promise.resolve( + err( + new AnalysisCapabilityUnavailableError( + "fixture", + operation, + "Decompiler is not installed.", + ), + ), + ); + }, close: () => Promise.resolve(resultOk(null)), }), }; @@ -79,25 +88,10 @@ const structured = (result: CallToolResult): Record => { }; it("does not record capability unavailability without supporting Evidence", async () => { - const received: Array>> = []; - const analysis: AnalysisOperationPort = { - execute: (name, arguments_) => { - received.push(arguments_); - return Promise.resolve( - err( - new AnalysisCapabilityUnavailableError( - "partial", - name, - "Decompiler is not installed.", - ), - ), - ); - }, - }; const session = createTestBinarySession( providerWithCapabilities(["procedure_pseudo_code"]), ); - const server = createServer(analysis, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "unavailable-unknown", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -118,10 +112,9 @@ it("does not record capability unavailability without supporting Evidence", asyn expect(unavailable.isError).toBe(true); expect(structured(unavailable)).toMatchObject({ error: { - category: "unsupported_provider", + category: "unavailable", }, }); - expect(received[0]).toMatchObject({ procedure: "main" }); expect( structured(await client.callTool({ name: "list_unknowns", arguments: {} })), ).toMatchObject({ @@ -166,10 +159,7 @@ it("records capture disagreement as a contradicted unknown", async () => { const rightEvidence = captureEvidence(right); expect(session.recordEvidence(leftEvidence).ok).toBe(true); expect(session.recordEvidence(rightEvidence).ok).toBe(true); - const server = createServer( - { execute: () => Promise.resolve(ok(null)) }, - session, - ); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "comparison-unknown", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/serverWorkflowMcp.test.ts b/tests/boundary/mcp/serverWorkflowMcp.test.ts index aa57b58c4..91a50252a 100644 --- a/tests/boundary/mcp/serverWorkflowMcp.test.ts +++ b/tests/boundary/mcp/serverWorkflowMcp.test.ts @@ -24,7 +24,7 @@ afterEach(async () => { }); const connect = async (analysis: AnalysisOperationPort) => { - const server = createServer(analysis); + const server = createServer({ kind: "fixed", analysis }); const client = new Client({ name: "integration-test", version: "1.0.0", @@ -107,10 +107,7 @@ it("advertises the complete currently available inventory with a session", async close: () => Promise.resolve(resultOk(null)), }) satisfies AnalysisClient, ); - const server = createServer( - { execute: () => Promise.resolve(ok(null)) }, - session, - ); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "integration-test", version: "1.0.0", diff --git a/tests/boundary/mcp/sessionMcp.test.ts b/tests/boundary/mcp/sessionMcp.test.ts index 33eff767a..302f91ad7 100644 --- a/tests/boundary/mcp/sessionMcp.test.ts +++ b/tests/boundary/mcp/sessionMcp.test.ts @@ -168,10 +168,13 @@ describe("target-free MCP lifecycle", () => { close: () => Promise.resolve(), execute: () => Promise.resolve(ok(null)), }; - const server = createServer(session, session, { - androidAnalysis, - logger: silentLogger, - }); + const server = createServer( + { kind: "session", session }, + { + androidAnalysis, + logger: silentLogger, + }, + ); const mcp = new Client({ name: "status-readiness", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -204,7 +207,10 @@ describe("target-free MCP lifecycle", () => { resolveAnalysisProfile: () => Promise.resolve(resultOk({ profile: SNAPSHOT_PROFILE })), }); - const server = createServer(session, session, { logger: silentLogger }); + const server = createServer( + { kind: "session", session }, + { logger: silentLogger }, + ); const mcp = new Client({ name: "replaced-target", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/sessionMcpHarness.ts b/tests/boundary/mcp/sessionMcpHarness.ts index 8e45de5c7..45f298c76 100644 --- a/tests/boundary/mcp/sessionMcpHarness.ts +++ b/tests/boundary/mcp/sessionMcpHarness.ts @@ -44,9 +44,12 @@ export const createSessionMcpHarness = async ( resolveAnalysisProfile: () => Promise.resolve(ok({ profile: SNAPSHOT_PROFILE })), }); - const server = createServer(session, session, { - logger: silentLogger, - }); + const server = createServer( + { kind: "session", session }, + { + logger: silentLogger, + }, + ); const mcp = new Client({ name: "session-test", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/mcp/webModuleTraceMcp.test.ts b/tests/boundary/mcp/webModuleTraceMcp.test.ts index a8ec5a41d..9db421630 100644 --- a/tests/boundary/mcp/webModuleTraceMcp.test.ts +++ b/tests/boundary/mcp/webModuleTraceMcp.test.ts @@ -21,7 +21,10 @@ it("publishes valid SDK schemas and records the named module trace without a bin const session = createTestBinarySession(() => { throw new Error("binary provider must not start"); }); - const server = createServer(session, session, { webModuleTrace: service }); + const server = createServer( + { kind: "session", session }, + { webModuleTrace: service }, + ); const client = new Client({ name: "module-contract", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/webNetworkCaptureMcp.test.ts b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts index b0ec2fb5c..4bdf06f39 100644 --- a/tests/boundary/mcp/webNetworkCaptureMcp.test.ts +++ b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts @@ -34,7 +34,10 @@ it("publishes valid SDK schemas and retains historical inline Evidence with the const session = createTestBinarySession(() => { throw new Error("Binary provider must not start"); }); - const server = createServer(session, session, { webNetworkCapture: service }); + const server = createServer( + { kind: "session", session }, + { webNetworkCapture: service }, + ); const client = new Client({ name: "historical-capture-contract", version: "1", diff --git a/tests/boundary/mcp/webRuntimeMcp.test.ts b/tests/boundary/mcp/webRuntimeMcp.test.ts index f368ee668..4d99fac75 100644 --- a/tests/boundary/mcp/webRuntimeMcp.test.ts +++ b/tests/boundary/mcp/webRuntimeMcp.test.ts @@ -12,9 +12,12 @@ it("advertises actual SDK schemas, instrumentation effects and session-owned inl const session = createTestBinarySession(() => { throw new Error("Binary provider must not start"); }); - const server = createServer(session, session, { - webRuntime: new WebRuntimeService(recordingWebRuntimePort()), - }); + const server = createServer( + { kind: "session", session }, + { + webRuntime: new WebRuntimeService(recordingWebRuntimePort()), + }, + ); const client = new Client({ name: "web-runtime-contract", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/webSourceLocationMcp.test.ts b/tests/boundary/mcp/webSourceLocationMcp.test.ts index 1bbb4bb0a..df40a0aec 100644 --- a/tests/boundary/mcp/webSourceLocationMcp.test.ts +++ b/tests/boundary/mcp/webSourceLocationMcp.test.ts @@ -26,7 +26,10 @@ it("publishes valid SDK schemas and records the named module trace without a bin const session = createTestBinarySession(() => { throw new Error("binary provider must not start"); }); - const server = createServer(session, session, { webSourceLocation: service }); + const server = createServer( + { kind: "session", session }, + { webSourceLocation: service }, + ); const client = new Client({ name: "module-contract", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts b/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts index 9d2635ac7..62369ca4d 100644 --- a/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts +++ b/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts @@ -13,7 +13,10 @@ import { } from "../../../src/application/AnalysisProvider.js"; import type { DirectAnalysisDependencies } from "../../../src/application/DirectAnalysisDependencies.js"; import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js"; -import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { + createDeferred, + createTestBinarySession, +} from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js"; import { readAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js"; @@ -37,6 +40,90 @@ afterEach(async () => { await Promise.all(resources.splice(0).map((resource) => resource.close())); }); +describe("MCP composed workflow target admission", () => { + it("keeps a composed result and snapshot on its admitted target across queued close and open", async () => { + const directory = await createTestTempDirectory( + "rea-mcp-admitted-workflow-", + ); + const firstPath = join(directory, "first.hop"); + const secondPath = join(directory, "second.hop"); + const snapshotPath = join(directory, "first-analysis.json"); + await Promise.all([ + writeFile(firstPath, "first target"), + writeFile(secondPath, "second target"), + ]); + const starts: string[] = []; + const calls: string[] = []; + const entered = createDeferred(); + const release = createDeferred(); + const session = createTestBinarySession( + makeProvider(starts, calls, undefined, { entered, release }), + { resolveAnalysisProfile: () => Promise.resolve(ok({ profile })) }, + ); + const server = createServer( + { kind: "session", session }, + { logger: silentLogger }, + ); + const mcp = new Client({ name: "admitted-workflow", version: "1" }); + const closeStarted = createDeferred(); + mcp.setNotificationHandler("notifications/progress", () => { + closeStarted.resolve(); + }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + resources.push(mcp, server); + await server.connect(serverTransport); + await mcp.connect(clientTransport); + expect( + ( + await mcp.callTool({ + name: "open_binary", + arguments: { path: firstPath }, + }) + ).isError, + ).not.toBe(true); + + const analysisRequest = mcp.callTool({ + name: "binary_overview", + arguments: {}, + }); + await entered.promise; + const closeRequest = mcp.callTool({ + name: "close_binary", + arguments: { snapshot_path: snapshotPath }, + _meta: { progressToken: "close-race" }, + }); + await closeStarted.promise; + const openRequest = mcp.callTool({ + name: "open_binary", + arguments: { path: secondPath }, + }); + release.resolve(); + + const analyzed = await analysisRequest; + expect(analyzed.isError, JSON.stringify(analyzed.content)).not.toBe(true); + const evidence = toolContract("binary_overview").outputSchema.parse( + analyzed.structuredContent, + ); + expect(evidence.analysis_profile).not.toBeNull(); + expect(evidence).toMatchObject({ subject: { local_path: firstPath } }); + const closed = await closeRequest; + expect(closed.isError, JSON.stringify(closed.content)).not.toBe(true); + const snapshot = await readAnalysisSnapshot(snapshotPath); + if (!snapshot.ok) throw snapshot.error; + expect(snapshot.value.workflow_entries).toHaveLength(1); + expect(snapshot.value.evidence_bundle.records).toContainEqual( + expect.objectContaining({ + operation: "binary_overview", + subject: expect.objectContaining({ local_path: firstPath }), + }), + ); + const opened = await openRequest; + expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true); + expect(session.activeTarget()?.path).toBe(secondPath); + }); +}); + describe("MCP composed workflow snapshot replay", () => { it("exports an MCP workflow binding and replays its Evidence without provider startup or calls", async () => { const directory = await createTestTempDirectory( @@ -51,7 +138,10 @@ describe("MCP composed workflow snapshot replay", () => { const session = createTestBinarySession(provider, { resolveAnalysisProfile: () => Promise.resolve(ok({ profile })), }); - const server = createServer(session, session, { logger: silentLogger }); + const server = createServer( + { kind: "session", session }, + { logger: silentLogger }, + ); const mcp = new Client({ name: "workflow-snapshot", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -145,7 +235,10 @@ describe("MCP composed workflow snapshot replay", () => { resolveAnalysisProfile: () => Promise.resolve(ok({ profile })), }, ); - const server = createServer(session, session, { logger: silentLogger }); + const server = createServer( + { kind: "session", session }, + { logger: silentLogger }, + ); const mcp = new Client({ name: "live-workflow-snapshot", version: "1.0.0", @@ -190,6 +283,10 @@ const makeProvider = ( starts: string[], calls: string[], liveOperation?: (typeof operations)[number], + pause?: { + readonly entered: ReturnType>; + readonly release: ReturnType>; + }, ): AnalysisProvider => { const capabilities: CapabilityDescriptor[] = operations.map((operation) => ({ operation, @@ -217,6 +314,10 @@ const makeProvider = ( return { execute: async (operation) => { calls.push(operation); + if (operation === "list_documents" && pause !== undefined) { + pause.entered.resolve(); + await pause.release.promise; + } const result = operation === "health" ? null diff --git a/tests/boundary/process/androidAnalysisMcp.test.ts b/tests/boundary/process/androidAnalysisMcp.test.ts index 1cdc4e4f5..de2827fb8 100644 --- a/tests/boundary/process/androidAnalysisMcp.test.ts +++ b/tests/boundary/process/androidAnalysisMcp.test.ts @@ -34,11 +34,11 @@ it("keeps a replacement server usable after the SDK discards its discovery probe factories.push(factory); return { close: async () => undefined }; }, - createServer: (analysis, binary, options) => { + createServer: (source, options) => { if (options?.androidAnalysis === undefined) throw new Error("Expected owned Android provider"); providers.push(options.androidAnalysis); - return createServer(analysis, binary, options); + return createServer(source, options); }, writeStderr: () => undefined, setExitCode: () => undefined, @@ -83,7 +83,10 @@ it("publishes and executes all APK contracts with inline Evidence and no active const session = createTestBinarySession(() => { throw new Error("APK analysis must not acquire a deep binary provider"); }); - const server = createServer(session, session, { androidAnalysis: provider }); + const server = createServer( + { kind: "session", session }, + { androidAnalysis: provider }, + ); const client = new Client({ name: "android-contract-regression", version: "1", @@ -150,7 +153,10 @@ it("cleans an active engine when the MCP client disconnects", async () => { const session = createTestBinarySession(() => { throw new Error("Unexpected native provider acquisition"); }); - const server = createServer(session, session, { androidAnalysis: provider }); + const server = createServer( + { kind: "session", session }, + { androidAnalysis: provider }, + ); const client = new Client({ name: "android-disconnect-regression", version: "1", diff --git a/tests/boundary/process/processCaptureCancellationAdapters.test.ts b/tests/boundary/process/processCaptureCancellationAdapters.test.ts index c3545576f..7a7159e91 100644 --- a/tests/boundary/process/processCaptureCancellationAdapters.test.ts +++ b/tests/boundary/process/processCaptureCancellationAdapters.test.ts @@ -161,7 +161,7 @@ itWithCaptureCapability( }, }, ); - const server = createServer(session, session, { logger }); + const server = createServer({ kind: "session", session }, { logger }); const client = new Client({ name: "process-settlement-cancellation", version: "1", diff --git a/tests/boundary/process/processFilesystemCoverageAdapters.test.ts b/tests/boundary/process/processFilesystemCoverageAdapters.test.ts index 479380579..aa537514f 100644 --- a/tests/boundary/process/processFilesystemCoverageAdapters.test.ts +++ b/tests/boundary/process/processFilesystemCoverageAdapters.test.ts @@ -21,7 +21,7 @@ const captureViaMcp = async (scenario: Record) => { const session = createTestBinarySession(() => { throw new Error("Process capture must not launch a binary provider"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "filesystem-coverage", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/boundary/process/processSessionMcp.test.ts b/tests/boundary/process/processSessionMcp.test.ts index 3b0a140e5..8b6791df9 100644 --- a/tests/boundary/process/processSessionMcp.test.ts +++ b/tests/boundary/process/processSessionMcp.test.ts @@ -27,9 +27,12 @@ itWithCaptureCapability( "records process residuals linked to capture Evidence", async () => { const session = createTestBinarySession(createCacheProvider([])); - const server = createServer(session, session, { - logger: silentLogger, - }); + const server = createServer( + { kind: "session", session }, + { + logger: silentLogger, + }, + ); const mcp = new Client({ name: "process-unknown", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); diff --git a/tests/boundary/process/processTruncationAdapters.test.ts b/tests/boundary/process/processTruncationAdapters.test.ts index bc317d317..34c1ccb3a 100644 --- a/tests/boundary/process/processTruncationAdapters.test.ts +++ b/tests/boundary/process/processTruncationAdapters.test.ts @@ -40,7 +40,7 @@ const capture = async ( const session = createTestBinarySession(() => { throw new Error("Capture must not launch a binary provider"); }); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "truncation-coverage", version: "1" }); onTestFinished(async () => { await client.close(); diff --git a/tests/composition/analysis-sessions/binarySession.test.ts b/tests/composition/analysis-sessions/binarySession.test.ts index bff71bfd5..c0563e06b 100644 --- a/tests/composition/analysis-sessions/binarySession.test.ts +++ b/tests/composition/analysis-sessions/binarySession.test.ts @@ -793,6 +793,37 @@ describe("typed unavailability without dispatching", () => { expect(clients[0]?.closed).toBe(1); expect(clients[1]?.closed).toBe(1); }); + + it("cancels a composed admission queued behind open without invoking it", async () => { + const [first] = await createBinarySessionTargets(); + const health = createDeferred>(); + const session = createTestBinarySession( + () => new ControllableAnalysisClient(health.promise), + ); + const opening = session.open(first); + const controller = new AbortController(); + let invoked = false; + const admitted = session.withAdmittedAnalysis( + "binary_overview", + controller.signal, + async () => { + invoked = true; + return null; + }, + ); + controller.abort(); + await expect(admitted).resolves.toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCancelledError", + operation: "binary_overview", + }, + }); + expect(invoked).toBe(false); + health.resolve(ok(null)); + expect((await opening).ok).toBe(true); + await session.close(); + }); }); describe("active client replacement", () => { diff --git a/tests/fixtures/applicationMcpHarness.ts b/tests/fixtures/applicationMcpHarness.ts index be5b83124..f9e8f38b1 100644 --- a/tests/fixtures/applicationMcpHarness.ts +++ b/tests/fixtures/applicationMcpHarness.ts @@ -19,7 +19,7 @@ export async function createApplicationMcpHarness(): Promise Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), })); - const server = createServer(session, session); + const server = createServer({ kind: "session", session }); const client = new Client({ name: "application-workflow-test", version: "1", diff --git a/tests/fixtures/contractPresentation.mjs b/tests/fixtures/contractPresentation.mjs index 73b3add91..6f7af51cb 100644 --- a/tests/fixtures/contractPresentation.mjs +++ b/tests/fixtures/contractPresentation.mjs @@ -24,7 +24,7 @@ const { createServer } = await import("../../dist/server/createServer.js"); const config = parseConfig({}); if (!config.ok) throw config.error; const session = createBinarySession(config.value, silentLogger, {}); -const server = createServer(session, session); +const server = createServer({ kind: "session", session }); const client = new Client({ name: "contract-presentation", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { diff --git a/tests/process-global/mcpDeliveryEnvironment.test.ts b/tests/process-global/mcpDeliveryEnvironment.test.ts index 32c9061af..6d8751d5e 100644 --- a/tests/process-global/mcpDeliveryEnvironment.test.ts +++ b/tests/process-global/mcpDeliveryEnvironment.test.ts @@ -51,13 +51,19 @@ it("keeps independent server budgets for successful results and oversized errors }; vi.stubEnv("REA_MCP_MAX_RESPONSE_BYTES", String(largeBudget)); - const small = createServer(analysis, undefined, { - environment: smallEnvironment, - }); + const small = createServer( + { kind: "fixed", analysis }, + { + environment: smallEnvironment, + }, + ); vi.stubEnv("REA_MCP_MAX_RESPONSE_BYTES", String(smallBudget)); - const large = createServer(analysis, undefined, { - environment: largeEnvironment, - }); + const large = createServer( + { kind: "fixed", analysis }, + { + environment: largeEnvironment, + }, + ); smallEnvironment.REA_MCP_MAX_RESPONSE_BYTES = "invalid-after-selection"; largeEnvironment.REA_MCP_MAX_RESPONSE_BYTES = String(smallBudget); vi.stubEnv("REA_MCP_MAX_RESPONSE_BYTES", "invalid-ambient-after-selection"); @@ -161,12 +167,12 @@ it("snapshots the runtime-selected environment before optional loading and reuse selected.REA_BROWSER_EXECUTABLE = "/mutated/browser"; return {}; }, - createServer: (analysis, session, options) => { + createServer: (source, options) => { expect(options?.environment?.REA_BROWSER_EXECUTABLE).toBe( "/selected/browser", ); expect(Object.isFrozen(options?.environment)).toBe(true); - const server = createServer(analysis, session, options); + const server = createServer(source, options); observed.push(server); return server; },