mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
import type { McpServer } from "@modelcontextprotocol/server";
|
|
import { z } from "zod";
|
|
|
|
import { isInputRequiredResult } from "@modelcontextprotocol/server";
|
|
import {
|
|
getNavigationContext,
|
|
inspectAddressContext,
|
|
} from "../application/AnalysisContextQueries.js";
|
|
import { readAnalysisSnapshot } from "../application/AnalysisSnapshotFiles.js";
|
|
import type { BinarySessionPort } from "../application/BinarySession.js";
|
|
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
|
|
import { processCapturePermissionRequest } from "../application/ProcessCapturePermission.js";
|
|
import { createProcessCaptureEvidence } from "../application/ProcessEvidence.js";
|
|
import { captureProcessScenario } from "../application/ProcessHarness.js";
|
|
import { SESSION_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
|
|
import { UnknownRegistryError, type AnalysisError } from "../domain/errors.js";
|
|
import type { Evidence } from "../domain/evidence.js";
|
|
import type { EvidenceFilePolicy } from "../domain/evidenceBundle.js";
|
|
import type {
|
|
ProcessCapture,
|
|
ProcessExecutionPolicy,
|
|
ProcessScenario,
|
|
} from "../domain/processCapture.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import type { Logger } from "../logger.js";
|
|
import { mcpProgressReporter } from "./mcpProgress.js";
|
|
import { projectPermissionFailure } from "../application/PermissionFailure.js";
|
|
import {
|
|
authorizeProcessCaptureWithElicitation,
|
|
type ProcessCaptureElicitation,
|
|
} from "./ProcessCaptureElicitation.js";
|
|
import { registerArtifactComparisonTool } from "./registerArtifactComparisonTool.js";
|
|
import { registerBundleComparisonTool } from "./registerBundleComparisonTool.js";
|
|
import { registerCloseLifecycleTool } from "./registerCloseLifecycleTool.js";
|
|
import { registerFunctionComparisonTool } from "./registerFunctionComparisonTool.js";
|
|
import { registerInvestigationTools } from "./registerInvestigationTools.js";
|
|
import { registerProcessComparisonTool } from "./registerProcessComparisonTool.js";
|
|
import { registerReplayMachineTool } from "./registerReplayMachineTool.js";
|
|
import {
|
|
registerEvidenceTools,
|
|
registerUnknownTools,
|
|
} from "./registerSessionRecordTools.js";
|
|
import { registerSessionStatusTool } from "./registerSessionStatusTool.js";
|
|
import {
|
|
sessionAvailabilityPolicy,
|
|
type SessionAvailability,
|
|
} from "./sessionAvailabilityPolicy.js";
|
|
import {
|
|
DENY_EVIDENCE_FILE_POLICY,
|
|
DENY_PROCESS_POLICY,
|
|
} from "./sessionToolPolicies.js";
|
|
import { logToolExecution } from "./toolLogging.js";
|
|
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
|
import { toCallToolResult } from "./toolResult.js";
|
|
|
|
const recordProcessResidualUnknowns = (
|
|
session: BinarySessionPort,
|
|
scenario: ProcessScenario,
|
|
evidence: Evidence,
|
|
residuals: ProcessCapture["residual_unknowns"],
|
|
): Result<null, AnalysisError> => {
|
|
if (scenario.unknown_registry_approved !== true) return ok(null);
|
|
for (const residual of residuals) {
|
|
const unknown = session.recordUnknown({
|
|
approved: true,
|
|
question: `Was ${residual.scope} behavior fully observed during capture?`,
|
|
severity: "medium",
|
|
domain: `process-${residual.scope}`,
|
|
supporting_evidence_ids: [evidence.evidence_id],
|
|
contradicting_evidence_ids: [],
|
|
required_authority: "controlled-replay",
|
|
required_confidence: "observed",
|
|
required_environment: evidence.environment,
|
|
recommended_probes: [
|
|
{
|
|
operation: "capture_process_scenario",
|
|
rationale:
|
|
"Repeat with a scenario that observes the missing behavior.",
|
|
},
|
|
],
|
|
relationships: [],
|
|
});
|
|
if (
|
|
!unknown.ok &&
|
|
!(
|
|
unknown.error instanceof UnknownRegistryError &&
|
|
unknown.error.reason === "already-exists"
|
|
)
|
|
)
|
|
return unknown;
|
|
}
|
|
return ok(null);
|
|
};
|
|
|
|
interface ProcessToolRegistration {
|
|
readonly server: McpServer;
|
|
readonly session: BinarySessionPort;
|
|
readonly logger: Logger;
|
|
readonly processPolicy: () => ProcessExecutionPolicy;
|
|
readonly captureContract: (typeof SESSION_TOOL_CONTRACTS)[5];
|
|
readonly permissionAuthority?: PermissionAuthority;
|
|
readonly processCaptureElicitation?: ProcessCaptureElicitation;
|
|
}
|
|
|
|
const registerProcessTools = ({
|
|
server,
|
|
session,
|
|
logger,
|
|
processPolicy,
|
|
captureContract,
|
|
permissionAuthority,
|
|
processCaptureElicitation,
|
|
}: ProcessToolRegistration): void => {
|
|
server.registerTool(
|
|
captureContract.name,
|
|
toolRegistrationOptions(captureContract),
|
|
async (input, context) => {
|
|
const scenario = input;
|
|
if (permissionAuthority !== undefined) {
|
|
const request = processCapturePermissionRequest(scenario);
|
|
const authorized =
|
|
processCaptureElicitation === undefined
|
|
? await permissionAuthority.authorize(request, "read")
|
|
: await authorizeProcessCaptureWithElicitation(
|
|
permissionAuthority,
|
|
request,
|
|
context,
|
|
processCaptureElicitation,
|
|
);
|
|
if (isInputRequiredResult(authorized)) return authorized;
|
|
if (!authorized.ok)
|
|
return toCallToolResult(
|
|
err(projectPermissionFailure(authorized.error)),
|
|
captureContract,
|
|
);
|
|
}
|
|
const progress = mcpProgressReporter(context);
|
|
await progress.report({
|
|
phase: captureContract.name,
|
|
completed: 0,
|
|
total: 1,
|
|
message: "started",
|
|
});
|
|
const captured = await logToolExecution(
|
|
logger,
|
|
captureContract.name,
|
|
() =>
|
|
captureProcessScenario(
|
|
scenario,
|
|
processPolicy(),
|
|
context.mcpReq.signal,
|
|
),
|
|
);
|
|
await progress.report({
|
|
phase: captureContract.name,
|
|
completed: 1,
|
|
total: 1,
|
|
message: captured.ok ? "completed" : "failed",
|
|
terminal: true,
|
|
});
|
|
if (!captured.ok) return toCallToolResult(captured, captureContract);
|
|
const evidence = createProcessCaptureEvidence(scenario, captured.value);
|
|
const recorded = session.recordEvidence(evidence);
|
|
if (!recorded.ok) return toCallToolResult(recorded, captureContract);
|
|
const unknowns = recordProcessResidualUnknowns(
|
|
session,
|
|
scenario,
|
|
evidence,
|
|
captured.value.residual_unknowns,
|
|
);
|
|
if (!unknowns.ok) return toCallToolResult(unknowns, captureContract);
|
|
return toCallToolResult(ok(evidence), captureContract);
|
|
},
|
|
);
|
|
};
|
|
|
|
export interface LifecycleToolRegistration {
|
|
readonly server: McpServer;
|
|
readonly session: BinarySessionPort;
|
|
readonly logger: Logger;
|
|
readonly contracts: readonly [
|
|
(typeof SESSION_TOOL_CONTRACTS)[0],
|
|
(typeof SESSION_TOOL_CONTRACTS)[1],
|
|
(typeof SESSION_TOOL_CONTRACTS)[2],
|
|
];
|
|
readonly snapshotFilePolicy: EvidenceFilePolicy;
|
|
readonly startedAt: string;
|
|
readonly availabilityPolicy: () => SessionAvailability;
|
|
readonly permissionAuthority?: PermissionAuthority;
|
|
}
|
|
|
|
const registerLifecycleTools = (
|
|
registration: LifecycleToolRegistration,
|
|
): void => {
|
|
const { server, session, contracts, startedAt, availabilityPolicy } =
|
|
registration;
|
|
registerOpenLifecycleTool(registration);
|
|
registerCloseLifecycleTool(registration);
|
|
registerSessionStatusTool({
|
|
server,
|
|
session,
|
|
contract: contracts[2],
|
|
startedAt,
|
|
availabilityPolicy,
|
|
});
|
|
};
|
|
|
|
const registerOpenLifecycleTool = ({
|
|
server,
|
|
session,
|
|
logger,
|
|
contracts: [openContract],
|
|
snapshotFilePolicy,
|
|
permissionAuthority,
|
|
}: LifecycleToolRegistration): void => {
|
|
server.registerTool(
|
|
openContract.name,
|
|
toolRegistrationOptions(openContract),
|
|
async (input, context) => {
|
|
let snapshot: AnalysisSnapshot | undefined;
|
|
if (input.snapshot_path !== undefined) {
|
|
if (permissionAuthority !== undefined) {
|
|
const authorized = await permissionAuthority.authorize(
|
|
{
|
|
capability: "snapshot_read",
|
|
roots: [input.snapshot_path],
|
|
executables: [],
|
|
environment_names: [],
|
|
network: "none",
|
|
mount: false,
|
|
operation_identity: `open_binary:snapshot:${input.snapshot_path}`,
|
|
},
|
|
"read",
|
|
);
|
|
if (!authorized.ok)
|
|
return toCallToolResult(
|
|
err(projectPermissionFailure(authorized.error)),
|
|
openContract,
|
|
);
|
|
}
|
|
const loaded = await readAnalysisSnapshot(
|
|
input.snapshot_path,
|
|
snapshotFilePolicy,
|
|
);
|
|
if (!loaded.ok) return toCallToolResult(loaded, openContract);
|
|
snapshot = loaded.value;
|
|
}
|
|
const opened = await logToolExecution(logger, openContract.name, () =>
|
|
session.open(input.path, {
|
|
signal: context.mcpReq.signal,
|
|
...(input.provider_id === undefined
|
|
? {}
|
|
: { providerId: input.provider_id }),
|
|
...(snapshot === undefined ? {} : { snapshot }),
|
|
}),
|
|
);
|
|
return opened.ok
|
|
? toCallToolResult(
|
|
{
|
|
ok: true,
|
|
value: {
|
|
path: opened.value.path,
|
|
format: opened.value.format,
|
|
kind: opened.value.kind,
|
|
loaderArgs: z
|
|
.array(z.string())
|
|
.parse(session.openCompatibility().loaderArgs ?? []),
|
|
sha256: opened.value.sha256,
|
|
architecture: opened.value.architecture ?? null,
|
|
},
|
|
},
|
|
openContract,
|
|
)
|
|
: toCallToolResult(opened, openContract);
|
|
},
|
|
);
|
|
};
|
|
|
|
/** Register MCP-only target lifecycle operations on a long-lived session. */
|
|
export interface SessionToolOptions {
|
|
readonly processPolicy?: () => ProcessExecutionPolicy;
|
|
readonly evidenceFilePolicy?: EvidenceFilePolicy;
|
|
readonly analysisSnapshotFilePolicy?: EvidenceFilePolicy;
|
|
readonly startedAt?: string;
|
|
readonly permissionAuthority?: PermissionAuthority;
|
|
readonly processCaptureElicitation?: ProcessCaptureElicitation;
|
|
readonly artifactIntegrityContinueEnabled?: () => boolean;
|
|
readonly availabilityPolicy?: () => SessionAvailability;
|
|
}
|
|
|
|
const registerRecordAndReplayTools = (
|
|
server: McpServer,
|
|
session: BinarySessionPort,
|
|
logger: Logger,
|
|
): void => {
|
|
registerUnknownTools({
|
|
server,
|
|
session,
|
|
contracts: SESSION_TOOL_CONTRACTS,
|
|
});
|
|
registerReplayMachineTool(server, logger);
|
|
};
|
|
|
|
const registerContextTools = (
|
|
server: McpServer,
|
|
session: BinarySessionPort,
|
|
): void => {
|
|
const navigationContract = SESSION_TOOL_CONTRACTS[20];
|
|
const addressContract = SESSION_TOOL_CONTRACTS[21];
|
|
server.registerTool(
|
|
navigationContract.name,
|
|
toolRegistrationOptions(navigationContract),
|
|
async (input, context) => {
|
|
return toCallToolResult(
|
|
await getNavigationContext(session, input, context.mcpReq.signal),
|
|
navigationContract,
|
|
);
|
|
},
|
|
);
|
|
server.registerTool(
|
|
addressContract.name,
|
|
toolRegistrationOptions(addressContract),
|
|
async (input, context) => {
|
|
return toCallToolResult(
|
|
await inspectAddressContext(session, input, context.mcpReq.signal),
|
|
addressContract,
|
|
);
|
|
},
|
|
);
|
|
};
|
|
|
|
export const registerSessionTools = (
|
|
server: McpServer,
|
|
session: BinarySessionPort,
|
|
logger: Logger,
|
|
options: SessionToolOptions = {},
|
|
): void => {
|
|
const processPolicy = options.processPolicy ?? (() => DENY_PROCESS_POLICY);
|
|
const evidenceFilePolicy =
|
|
options.evidenceFilePolicy ?? DENY_EVIDENCE_FILE_POLICY;
|
|
const analysisSnapshotFilePolicy =
|
|
options.analysisSnapshotFilePolicy ?? DENY_EVIDENCE_FILE_POLICY;
|
|
const [
|
|
openContract,
|
|
closeContract,
|
|
statusContract,
|
|
exportContract,
|
|
importContract,
|
|
captureContract,
|
|
compareContract,
|
|
compareArtifactsContract,
|
|
compareFunctionsContract,
|
|
compareBundlesContract,
|
|
changedBehaviorContract,
|
|
callPathContract,
|
|
staticRuntimeContract,
|
|
reconstructionContract,
|
|
] = SESSION_TOOL_CONTRACTS;
|
|
const snapshotContract = SESSION_TOOL_CONTRACTS[19];
|
|
registerLifecycleTools({
|
|
server,
|
|
session,
|
|
logger,
|
|
contracts: [openContract, closeContract, statusContract],
|
|
snapshotFilePolicy: analysisSnapshotFilePolicy,
|
|
startedAt: options.startedAt ?? new Date().toISOString(),
|
|
availabilityPolicy: sessionAvailabilityPolicy(options.availabilityPolicy, {
|
|
processPolicy: processPolicy(),
|
|
evidenceFilePolicy,
|
|
}),
|
|
...(options.permissionAuthority === undefined
|
|
? {}
|
|
: { permissionAuthority: options.permissionAuthority }),
|
|
});
|
|
registerEvidenceTools({
|
|
server,
|
|
session,
|
|
exportContract,
|
|
importContract,
|
|
snapshotContract,
|
|
filePolicy: evidenceFilePolicy,
|
|
...(options.permissionAuthority === undefined
|
|
? {}
|
|
: { permissionAuthority: options.permissionAuthority }),
|
|
});
|
|
registerProcessTools({
|
|
server,
|
|
session,
|
|
logger,
|
|
processPolicy,
|
|
captureContract,
|
|
...(options.permissionAuthority === undefined
|
|
? {}
|
|
: { permissionAuthority: options.permissionAuthority }),
|
|
...(options.processCaptureElicitation === undefined
|
|
? {}
|
|
: { processCaptureElicitation: options.processCaptureElicitation }),
|
|
});
|
|
registerProcessComparisonTool(server, session, compareContract);
|
|
registerArtifactComparisonTool(server, session, compareArtifactsContract);
|
|
registerFunctionComparisonTool(server, session, compareFunctionsContract);
|
|
registerBundleComparisonTool(
|
|
server,
|
|
session,
|
|
compareBundlesContract,
|
|
evidenceFilePolicy,
|
|
);
|
|
const investigationContracts = [
|
|
changedBehaviorContract,
|
|
callPathContract,
|
|
staticRuntimeContract,
|
|
reconstructionContract,
|
|
] as const;
|
|
registerInvestigationTools(server, session, investigationContracts);
|
|
registerRecordAndReplayTools(server, session, logger);
|
|
registerContextTools(server, session);
|
|
};
|