mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(mcp): admit composed analysis through result bookkeeping
This commit is contained in:
@@ -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
@@ -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: () => {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user