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