fix(mcp): admit composed analysis through result bookkeeping

This commit is contained in:
morluto
2026-10-10 00:35:17 +08:00
parent e64678e8f9
commit e11f4d371a
80 changed files with 774 additions and 471 deletions
+27
View File
@@ -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<Value>(
operationName: string,
signal: AbortSignal | undefined,
operation: (analysis: AnalysisOperationPort) => Promise<Value>,
): Promise<Result<Value, AnalysisError>> {
const generation = this.#transitionGeneration;
const transition = this.#transition;
const call = (async (): Promise<Result<Value, AnalysisError>> => {
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<AnalysisOperationPort["execute"]>[0],
arguments_: Readonly<Record<string, JsonValue>>,
@@ -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<Value>(
operationName: string,
signal: AbortSignal | undefined,
operation: (analysis: AnalysisOperationPort) => Promise<Value>,
): Promise<Result<Value, AnalysisError>>;
open(
path: string,
options?: {
+10 -7
View File
@@ -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: () => {
+29
View File
@@ -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 = <Value>(
operationName: string,
signal: AbortSignal | undefined,
operation: (analysis: AnalysisOperationPort) => Promise<Value>,
) => Promise<Result<Value, AnalysisError>>;
/** 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));
+26 -25
View File
@@ -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<typeof session.recordEvidence>[0]) => {
const recorded = session.recordEvidence(evidence);
return recorded;
},
: (evidence: Parameters<typeof session.recordEvidence>[0]) =>
session.recordEvidence(evidence),
recordEvidenceWithUnknown:
session === undefined
? undefined
: (
evidence: Parameters<typeof session.recordEvidenceWithUnknown>[0],
input: Parameters<typeof session.recordEvidenceWithUnknown>[1],
) => {
const recorded = session.recordEvidenceWithUnknown(evidence, input);
return recorded;
},
) => session.recordEvidenceWithUnknown(evidence, input),
});
interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly server: EvidenceMcpServer;
readonly analysis: AnalysisOperationPort;
readonly session: BinarySessionPort | undefined;
readonly options: CreateServerOptions;
readonly environment: Readonly<NodeJS.ProcessEnv>;
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,
+2 -3
View File
@@ -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"
+9 -7
View File
@@ -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,
+95 -85
View File
@@ -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<CallToolResult> => {
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 (
+80 -60
View File
@@ -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<AnalysisOperation, "health">,
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<Exclude<AnalysisOperation, "health">>[],
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);
},
);
}
+9 -11
View File
@@ -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,
});
};
+5 -5
View File
@@ -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,
});
+50 -35
View File
@@ -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);
},
);
};
+22 -6
View File
@@ -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);
},
);
};
@@ -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();
+6 -3
View File
@@ -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();
@@ -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();
+1 -2
View File
@@ -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" });
@@ -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();
+7 -7
View File
@@ -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();
@@ -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();
+10 -7
View File
@@ -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();
+11 -8
View File
@@ -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();
@@ -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();
@@ -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<string, unknown>,
) => {
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 },
@@ -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()]);
@@ -25,7 +25,7 @@ const withClient = async (
verify: (client: Client) => Promise<void>,
): Promise<void> => {
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();
+29 -20
View File
@@ -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",
@@ -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();
+1 -1
View File
@@ -24,7 +24,7 @@ export const inventory = (values: Readonly<Record<string, string>>) =>
export const resources: Array<{ close(): Promise<void> }> = [];
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();
@@ -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();
@@ -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",
+4 -1
View File
@@ -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();
+6 -3
View File
@@ -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();
@@ -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",
+4 -1
View File
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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();
@@ -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 },
+14 -14
View File
@@ -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();
+2 -27
View File
@@ -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<string, unknown>) =>
z
.array(
@@ -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",
@@ -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",
+3 -1
View File
@@ -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 },
@@ -35,7 +35,7 @@ const resources: Array<{ close(): Promise<void> }> = [];
const connect = async (
execute: AnalysisOperationPort["execute"],
): Promise<Client> => {
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();
@@ -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();
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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();
@@ -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,
@@ -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()]);
@@ -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",
@@ -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();
+2 -2
View File
@@ -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();
+6 -6
View File
@@ -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([]);
@@ -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();
@@ -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();
@@ -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();
+4 -1
View File
@@ -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();
@@ -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();
+2 -2
View File
@@ -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();
@@ -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",
+14 -24
View File
@@ -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<string, unknown> => {
};
it("does not record capability unavailability without supporting Evidence", async () => {
const received: Array<Readonly<Record<string, unknown>>> = [];
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();
+2 -5
View File
@@ -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",
+11 -5
View File
@@ -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();
+6 -3
View File
@@ -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();
+4 -1
View File
@@ -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();
@@ -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",
+6 -3
View File
@@ -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();
@@ -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();
@@ -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<void>();
const release = createDeferred<void>();
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<void>();
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<typeof createDeferred<void>>;
readonly release: ReturnType<typeof createDeferred<void>>;
},
): 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
@@ -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",
@@ -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",
@@ -21,7 +21,7 @@ const captureViaMcp = async (scenario: Record<string, unknown>) => {
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();
@@ -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();
@@ -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();
@@ -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<ReturnType<typeof ok>>();
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", () => {
+1 -1
View File
@@ -19,7 +19,7 @@ export async function createApplicationMcpHarness(): Promise<ApplicationMcpHarne
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: "application-workflow-test",
version: "1",
+1 -1
View File
@@ -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 {
@@ -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;
},