fix(mcp): parse adapter inputs exactly once
This commit is contained in:
parent
d1e72207f4
commit
37c0f39954
|
|
@ -72,20 +72,26 @@ export const runCrossVersionInvestigation = async (
|
|||
cause: parsed.error,
|
||||
}),
|
||||
);
|
||||
return runCrossVersionInvestigationValidated(parsed.data, policy, execution);
|
||||
};
|
||||
|
||||
/** Run input already parsed by a trusted adapter boundary. */
|
||||
export const runCrossVersionInvestigationValidated = async (
|
||||
input: CrossVersionInvestigationInput,
|
||||
policy: EvidenceFilePolicy,
|
||||
execution: CrossVersionInvestigationExecution,
|
||||
): Promise<Result<CrossVersionInvestigationOutcome, AnalysisError>> => {
|
||||
const initial = await readInvestigationWorkspace(
|
||||
parsed.data.workspace_path,
|
||||
input.workspace_path,
|
||||
policy,
|
||||
);
|
||||
if (!initial.ok) return initial;
|
||||
const preflight = validateWorkspaceRequest(initial.value, parsed.data);
|
||||
const preflight = validateWorkspaceRequest(initial.value, input);
|
||||
if (!preflight.ok) return preflight;
|
||||
if (isCancelled(execution.signal))
|
||||
return err(new AnalysisCancelledError("find_changed_behavior"));
|
||||
if (parsed.data.replay_run_id !== undefined) {
|
||||
const replay = selectCompletedInvestigationReplay(
|
||||
initial.value,
|
||||
parsed.data,
|
||||
);
|
||||
if (input.replay_run_id !== undefined) {
|
||||
const replay = selectCompletedInvestigationReplay(initial.value, input);
|
||||
if (!replay.ok) return replay;
|
||||
return completeOutcome(
|
||||
replay.value.workspace,
|
||||
|
|
@ -106,7 +112,7 @@ export const runCrossVersionInvestigation = async (
|
|||
});
|
||||
|
||||
const snapshots = await scanVersions(
|
||||
parsed.data,
|
||||
input,
|
||||
execution.inputRoots,
|
||||
execution.signal,
|
||||
execution.integrityContinueEnabled,
|
||||
|
|
@ -119,7 +125,7 @@ export const runCrossVersionInvestigation = async (
|
|||
message: "Version inputs scanned",
|
||||
});
|
||||
return continueInvestigation({
|
||||
input: parsed.data,
|
||||
input,
|
||||
initial: initial.value,
|
||||
snapshots: snapshots.value,
|
||||
policy,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type {
|
|||
AnalysisOperation,
|
||||
AnalysisOperationPort,
|
||||
} from "./AnalysisProvider.js";
|
||||
import type { z } from "zod";
|
||||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
|
||||
import {
|
||||
|
|
@ -37,15 +38,23 @@ type TraceReference = {
|
|||
containing_procedure: JsonValue;
|
||||
};
|
||||
|
||||
/** One enhanced call whose input was parsed at its owning adapter boundary. */
|
||||
export type ValidatedEnhancedCall = {
|
||||
[Name in EnhancedToolName]: {
|
||||
readonly name: Name;
|
||||
readonly input: z.output<(typeof enhancedInputSchemas)[Name]>;
|
||||
};
|
||||
}[EnhancedToolName];
|
||||
|
||||
/**
|
||||
* Composes direct provider operations into bounded reverse-engineering tools.
|
||||
* Inputs are parsed again at this application boundary even though MCP normally
|
||||
* validates them, allowing direct callers to use the same service safely.
|
||||
* Direct callers use `execute`; adapters that own validation use
|
||||
* `executeValidated` so an MCP input is parsed exactly once.
|
||||
*/
|
||||
export class EnhancedTools {
|
||||
constructor(private readonly analysis: AnalysisOperationPort) {}
|
||||
|
||||
/** Parse inputs again at the direct-call boundary, then dispatch exhaustively. */
|
||||
/** Parse an untrusted direct-call input once, then dispatch exhaustively. */
|
||||
execute(
|
||||
name: EnhancedToolName,
|
||||
input: unknown,
|
||||
|
|
@ -55,58 +64,87 @@ export class EnhancedTools {
|
|||
case "swift_classes": {
|
||||
const parsed = enhancedInputSchemas.swift_classes.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#swiftClasses(parsed.data.pattern, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
case "get_objc_classes": {
|
||||
const parsed = enhancedInputSchemas.get_objc_classes.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#objcClasses(parsed.data.pattern, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
case "get_objc_protocols":
|
||||
return this.#objcProtocols(signal);
|
||||
return this.executeValidated({ name, input: {} }, signal);
|
||||
case "batch_decompile": {
|
||||
const parsed = enhancedInputSchemas.batch_decompile.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#batchDecompile(parsed.data.addresses, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
case "get_call_graph": {
|
||||
const parsed = enhancedInputSchemas.get_call_graph.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#callGraph(parsed.data, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
case "analyze_swift_types":
|
||||
return this.#analyzeSwiftTypes(signal);
|
||||
return this.executeValidated({ name, input: {} }, signal);
|
||||
case "find_xrefs_to_name": {
|
||||
const parsed = enhancedInputSchemas.find_xrefs_to_name.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#findXrefs(parsed.data.name, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
case "binary_overview": {
|
||||
const parsed = enhancedInputSchemas.binary_overview.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#binaryOverview(parsed.data, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
case "analyze_function": {
|
||||
const parsed = enhancedInputSchemas.analyze_function.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#analyzeFunction(parsed.data, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
case "trace_feature": {
|
||||
const parsed = enhancedInputSchemas.trace_feature.safeParse(input);
|
||||
return parsed.success
|
||||
? this.#traceFeature(parsed.data, signal)
|
||||
? this.executeValidated({ name, input: parsed.data }, signal)
|
||||
: invalidInput(name, parsed.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Dispatch input already parsed by a trusted adapter boundary. */
|
||||
executeValidated(
|
||||
call: ValidatedEnhancedCall,
|
||||
signal?: AbortSignal,
|
||||
): EnhancedResult {
|
||||
switch (call.name) {
|
||||
case "swift_classes":
|
||||
return this.#swiftClasses(call.input.pattern, signal);
|
||||
case "get_objc_classes":
|
||||
return this.#objcClasses(call.input.pattern, signal);
|
||||
case "get_objc_protocols":
|
||||
return this.#objcProtocols(signal);
|
||||
case "batch_decompile":
|
||||
return this.#batchDecompile(call.input.addresses, signal);
|
||||
case "get_call_graph":
|
||||
return this.#callGraph(call.input, signal);
|
||||
case "analyze_swift_types":
|
||||
return this.#analyzeSwiftTypes(signal);
|
||||
case "find_xrefs_to_name":
|
||||
return this.#findXrefs(call.input.name, signal);
|
||||
case "binary_overview":
|
||||
return this.#binaryOverview(call.input, signal);
|
||||
case "analyze_function":
|
||||
return this.#analyzeFunction(call.input, signal);
|
||||
case "trace_feature":
|
||||
return this.#traceFeature(call.input, signal);
|
||||
}
|
||||
}
|
||||
|
||||
async #analyzeFunction(
|
||||
input: Readonly<Record<string, JsonValue>>,
|
||||
signal?: AbortSignal,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ export const analyzeJavaScriptApplication = async (
|
|||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
const parsed = analyzeJavaScriptApplicationInputSchema.safeParse(rawInput);
|
||||
if (!parsed.success) return err(new AnalysisInputError(OPERATION));
|
||||
return analyzeJavaScriptApplicationValidated(authority, parsed.data, options);
|
||||
};
|
||||
|
||||
/** Analyze input already parsed by a trusted adapter boundary. */
|
||||
export const analyzeJavaScriptApplicationValidated = async (
|
||||
authority: PermissionAuthority | undefined,
|
||||
input: z.output<typeof analyzeJavaScriptApplicationInputSchema>,
|
||||
options: ExecutionOptions = {},
|
||||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
if (authority === undefined)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
|
|
@ -41,12 +50,12 @@ export const analyzeJavaScriptApplication = async (
|
|||
const authorized = await authority.authorize(
|
||||
{
|
||||
capability: "investigation_input",
|
||||
roots: [parsed.data.input_path],
|
||||
roots: [input.input_path],
|
||||
executables: [],
|
||||
environment_names: [],
|
||||
network: "none",
|
||||
mount: false,
|
||||
operation_identity: `${OPERATION}:${parsed.data.input_path}`,
|
||||
operation_identity: `${OPERATION}:${input.input_path}`,
|
||||
},
|
||||
"read",
|
||||
);
|
||||
|
|
@ -55,7 +64,7 @@ export const analyzeJavaScriptApplication = async (
|
|||
authorized.error instanceof PermissionRequiredError
|
||||
? authorized.error
|
||||
: new ArtifactOperationError(OPERATION, "path", {
|
||||
logicalPath: parsed.data.input_path,
|
||||
logicalPath: input.input_path,
|
||||
declaredSha256: null,
|
||||
calculatedSha256: null,
|
||||
unpacked: false,
|
||||
|
|
@ -70,10 +79,10 @@ export const analyzeJavaScriptApplication = async (
|
|||
try {
|
||||
const reconstructed = await reconstructJavaScriptArtifact(
|
||||
{
|
||||
input_path: parsed.data.input_path,
|
||||
format: parsed.data.format,
|
||||
source_map_read_approved: parsed.data.source_map_read_approved,
|
||||
limits: parsed.data.limits,
|
||||
input_path: input.input_path,
|
||||
format: input.format,
|
||||
source_map_read_approved: input.source_map_read_approved,
|
||||
limits: input.limits,
|
||||
},
|
||||
options.signal,
|
||||
);
|
||||
|
|
@ -91,7 +100,7 @@ export const analyzeJavaScriptApplication = async (
|
|||
message: "Application graph and Electron boundaries reconstructed",
|
||||
terminal: true,
|
||||
});
|
||||
return ok(createJavaScriptApplicationEvidence(parsed.data, result));
|
||||
return ok(createJavaScriptApplicationEvidence(input, result));
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof ArtifactReaderFailure)
|
||||
return err(
|
||||
|
|
|
|||
|
|
@ -35,27 +35,35 @@ export const traceApplicationFeatureEvidence = (
|
|||
projectInputIssues(parsed.error.issues, rawInput),
|
||||
),
|
||||
);
|
||||
return traceApplicationFeatureEvidenceValidated(parsed.data);
|
||||
};
|
||||
|
||||
/** Derive one feature trace from input parsed by a trusted adapter. */
|
||||
export const traceApplicationFeatureEvidenceValidated = (
|
||||
input: z.output<typeof traceApplicationFeatureInputSchema>,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
const operation = "trace_application_feature";
|
||||
try {
|
||||
const source = parseApplicationGraphEvidence(parsed.data.application);
|
||||
const source = parseApplicationGraphEvidence(input.application);
|
||||
const nativeEvidence = parseNativeApplicationEvidence(
|
||||
parsed.data.native_observations,
|
||||
input.native_observations,
|
||||
);
|
||||
const result = traceApplicationFeature({
|
||||
sourceEvidenceId: source.evidence.evidence_id,
|
||||
graph: source.graph,
|
||||
nativeEvidence,
|
||||
seed: parsed.data.seed,
|
||||
direction: parsed.data.direction,
|
||||
limits: parsed.data.limits,
|
||||
seed: input.seed,
|
||||
direction: input.direction,
|
||||
limits: input.limits,
|
||||
});
|
||||
return ok(
|
||||
createApplicationFeatureTraceEvidence(
|
||||
{
|
||||
application_evidence_id: source.evidence.evidence_id,
|
||||
native_evidence_ids: nativeEvidence.map(({ evidence_id: id }) => id),
|
||||
seed: parsed.data.seed,
|
||||
direction: parsed.data.direction,
|
||||
limits: parsed.data.limits,
|
||||
seed: input.seed,
|
||||
direction: input.direction,
|
||||
limits: input.limits,
|
||||
},
|
||||
result,
|
||||
),
|
||||
|
|
@ -79,14 +87,22 @@ export const compareApplicationVersionsEvidence = (
|
|||
projectInputIssues(parsed.error.issues, rawInput),
|
||||
),
|
||||
);
|
||||
return compareApplicationVersionsEvidenceValidated(parsed.data);
|
||||
};
|
||||
|
||||
/** Compare versions from input parsed by a trusted adapter. */
|
||||
export const compareApplicationVersionsEvidenceValidated = (
|
||||
input: z.output<typeof compareApplicationVersionsInputSchema>,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
const operation = "compare_application_versions";
|
||||
try {
|
||||
const left = parseApplicationGraphEvidence(parsed.data.left);
|
||||
const right = parseApplicationGraphEvidence(parsed.data.right);
|
||||
const left = parseApplicationGraphEvidence(input.left);
|
||||
const right = parseApplicationGraphEvidence(input.right);
|
||||
const leftNative = parseNativeApplicationEvidence(
|
||||
parsed.data.left_native_observations,
|
||||
input.left_native_observations,
|
||||
);
|
||||
const rightNative = parseNativeApplicationEvidence(
|
||||
parsed.data.right_native_observations,
|
||||
input.right_native_observations,
|
||||
);
|
||||
const result = compareJavaScriptApplicationVersions({
|
||||
left: {
|
||||
|
|
@ -101,7 +117,7 @@ export const compareApplicationVersionsEvidence = (
|
|||
},
|
||||
leftNativeEvidence: leftNative,
|
||||
rightNativeEvidence: rightNative,
|
||||
limits: parsed.data.limits,
|
||||
limits: input.limits,
|
||||
});
|
||||
return ok(
|
||||
createApplicationVersionComparisonEvidence(
|
||||
|
|
@ -112,7 +128,7 @@ export const compareApplicationVersionsEvidence = (
|
|||
right_native_evidence_ids: rightNative.map(
|
||||
({ evidence_id: id }) => id,
|
||||
),
|
||||
limits: parsed.data.limits,
|
||||
limits: input.limits,
|
||||
},
|
||||
result,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -56,6 +56,15 @@ export const runControlledReplay = async (
|
|||
projectInputIssues(parsed.error.issues, rawInput),
|
||||
),
|
||||
);
|
||||
return runControlledReplayValidated(dependencies, parsed.data, options);
|
||||
};
|
||||
|
||||
/** Plan or execute replay input already parsed by a trusted adapter. */
|
||||
export const runControlledReplayValidated = async (
|
||||
dependencies: JavaScriptReplayDependencies,
|
||||
input: z.output<typeof controlledReplayInputSchema>,
|
||||
options: ExecutionOptions = {},
|
||||
): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
if (!dependencies.policy.enabled)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
|
|
@ -74,9 +83,9 @@ export const runControlledReplay = async (
|
|||
);
|
||||
if (isAborted(options.signal))
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
const request = permissionRequest(parsed.data, dependencies.policy);
|
||||
const request = permissionRequest(input, dependencies.policy);
|
||||
const authorized =
|
||||
parsed.data.mode === "plan"
|
||||
input.mode === "plan"
|
||||
? await dependencies.authority.explain(request, "read")
|
||||
: await dependencies.authority.authorize(request, "read");
|
||||
if (!authorized.ok)
|
||||
|
|
@ -90,7 +99,7 @@ export const runControlledReplay = async (
|
|||
let prepared;
|
||||
try {
|
||||
prepared = await prepareReplayPlan(
|
||||
parsed.data,
|
||||
input,
|
||||
dependencies.policy,
|
||||
dependencies.host,
|
||||
);
|
||||
|
|
@ -105,7 +114,7 @@ export const runControlledReplay = async (
|
|||
}
|
||||
if (isAborted(options.signal))
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
if (parsed.data.mode === "plan")
|
||||
if (input.mode === "plan")
|
||||
return ok(
|
||||
asJson({
|
||||
phase: "plan",
|
||||
|
|
@ -114,21 +123,21 @@ export const runControlledReplay = async (
|
|||
evidence: null,
|
||||
}),
|
||||
);
|
||||
if (parsed.data.plan_digest !== prepared.publicPlan.plan_digest)
|
||||
if (input.plan_digest !== prepared.publicPlan.plan_digest)
|
||||
return err(
|
||||
new ReplayPlanStaleError(
|
||||
parsed.data.plan_digest ?? "",
|
||||
input.plan_digest ?? "",
|
||||
prepared.publicPlan.plan_digest,
|
||||
),
|
||||
);
|
||||
let canonicalExportPath: string | undefined;
|
||||
if (parsed.data.reproducer_export !== undefined) {
|
||||
if (parsed.data.reproducer_export.approved !== true)
|
||||
if (input.reproducer_export !== undefined) {
|
||||
if (input.reproducer_export.approved !== true)
|
||||
return err(new AnalysisInputError(`${OPERATION}:reproducer_export`));
|
||||
const exportAuthorized = await dependencies.authority.authorize(
|
||||
{
|
||||
capability: "evidence_write",
|
||||
roots: [parsed.data.reproducer_export.path],
|
||||
roots: [input.reproducer_export.path],
|
||||
executables: [],
|
||||
environment_names: [],
|
||||
network: "none",
|
||||
|
|
@ -146,7 +155,7 @@ export const runControlledReplay = async (
|
|||
}),
|
||||
);
|
||||
canonicalExportPath = exportAuthorized.value.request.roots[0];
|
||||
if (canonicalExportPath !== parsed.data.reproducer_export.path)
|
||||
if (canonicalExportPath !== input.reproducer_export.path)
|
||||
return err(
|
||||
new AnalysisInputError(`${OPERATION}:reproducer_export:path`, {
|
||||
cause: new TypeError(
|
||||
|
|
@ -183,7 +192,7 @@ export const runControlledReplay = async (
|
|||
if (executed.termination === "cancelled")
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
if (
|
||||
parsed.data.reproducer_export !== undefined &&
|
||||
input.reproducer_export !== undefined &&
|
||||
canonicalExportPath !== undefined &&
|
||||
executed.cleanup.state === "complete"
|
||||
) {
|
||||
|
|
@ -191,7 +200,7 @@ export const runControlledReplay = async (
|
|||
schema_version: 1,
|
||||
plan: prepared.publicPlan,
|
||||
result: executed,
|
||||
sources: parsed.data.reproducer_export.include_sources
|
||||
sources: input.reproducer_export.include_sources
|
||||
? {
|
||||
left: prepared.leftSources,
|
||||
...(prepared.rightSources === undefined
|
||||
|
|
|
|||
|
|
@ -27,11 +27,16 @@ export const reconcileJavaScriptRuntimeEvidence = (
|
|||
projectInputIssues(parsed.error.issues, rawInput),
|
||||
),
|
||||
);
|
||||
return reconcileJavaScriptRuntimeEvidenceValidated(parsed.data);
|
||||
};
|
||||
|
||||
/** Reconcile input already parsed by a trusted adapter boundary. */
|
||||
export const reconcileJavaScriptRuntimeEvidenceValidated = (
|
||||
input: z.output<typeof reconcileJavaScriptRuntimeInputSchema>,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
try {
|
||||
const result = reconcileJavaScriptRuntime(parsed.data);
|
||||
return ok(
|
||||
createJavaScriptRuntimeReconciliationEvidence(parsed.data, result),
|
||||
);
|
||||
const result = reconcileJavaScriptRuntime(input);
|
||||
return ok(createJavaScriptRuntimeReconciliationEvidence(input, result));
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
cause instanceof z.ZodError || cause instanceof TypeError
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import type { McpServer } from "@modelcontextprotocol/server";
|
|||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
import {
|
||||
compareApplicationVersionsEvidence,
|
||||
traceApplicationFeatureEvidence,
|
||||
compareApplicationVersionsEvidenceValidated,
|
||||
traceApplicationFeatureEvidenceValidated,
|
||||
} from "../application/JavaScriptApplicationWorkflowService.js";
|
||||
import {
|
||||
runControlledReplay,
|
||||
runControlledReplayValidated,
|
||||
type JavaScriptReplayDependencies,
|
||||
} from "../application/JavaScriptReplayService.js";
|
||||
import { APPLICATION_TOOL_CONTRACTS } from "../contracts/applicationToolContracts.js";
|
||||
|
|
@ -55,7 +55,7 @@ export const registerApplicationTools = (
|
|||
const result = await logToolExecution(
|
||||
options.logger,
|
||||
traceContract.name,
|
||||
() => Promise.resolve(traceApplicationFeatureEvidence(parsed)),
|
||||
() => Promise.resolve(traceApplicationFeatureEvidenceValidated(parsed)),
|
||||
);
|
||||
if (!result.ok) return toCallToolResult(result, traceContract);
|
||||
const sources = [parsed.application, ...parsed.native_observations];
|
||||
|
|
@ -79,7 +79,8 @@ export const registerApplicationTools = (
|
|||
const result = await logToolExecution(
|
||||
options.logger,
|
||||
compareContract.name,
|
||||
() => Promise.resolve(compareApplicationVersionsEvidence(parsed)),
|
||||
() =>
|
||||
Promise.resolve(compareApplicationVersionsEvidenceValidated(parsed)),
|
||||
);
|
||||
if (!result.ok) return toCallToolResult(result, compareContract);
|
||||
const sources = [
|
||||
|
|
@ -117,7 +118,7 @@ export const registerApplicationTools = (
|
|||
options.logger,
|
||||
replayContract.name,
|
||||
() =>
|
||||
runControlledReplay(options.replay, parsed, {
|
||||
runControlledReplayValidated(options.replay, parsed, {
|
||||
signal: context.mcpReq.signal,
|
||||
progress: mcpProgressReporter(context),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import type { McpServer } from "@modelcontextprotocol/server";
|
|||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
|
||||
import { analyzeJavaScriptApplication } from "../application/JavaScriptApplicationService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "../application/JavaScriptRuntimeReconciliationService.js";
|
||||
import { analyzeJavaScriptApplicationValidated } from "../application/JavaScriptApplicationService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidenceValidated } from "../application/JavaScriptRuntimeReconciliationService.js";
|
||||
import {
|
||||
inspectElectronPage,
|
||||
listElectronTargets,
|
||||
|
|
@ -109,10 +109,14 @@ export const registerElectronTools = (
|
|||
options.logger,
|
||||
analyzeContract.name,
|
||||
() =>
|
||||
analyzeJavaScriptApplication(options.permissionAuthority, parsed, {
|
||||
signal: context.mcpReq.signal,
|
||||
progress: mcpProgressReporter(context),
|
||||
}),
|
||||
analyzeJavaScriptApplicationValidated(
|
||||
options.permissionAuthority,
|
||||
parsed,
|
||||
{
|
||||
signal: context.mcpReq.signal,
|
||||
progress: mcpProgressReporter(context),
|
||||
},
|
||||
),
|
||||
);
|
||||
if (!result.ok) return toCallToolResult(result, analyzeContract);
|
||||
return evidenceResult(options, analyzeContract, result.value);
|
||||
|
|
@ -133,7 +137,8 @@ export const registerElectronTools = (
|
|||
const result = await logToolExecution(
|
||||
options.logger,
|
||||
reconcileContract.name,
|
||||
() => Promise.resolve(reconcileJavaScriptRuntimeEvidence(parsed)),
|
||||
() =>
|
||||
Promise.resolve(reconcileJavaScriptRuntimeEvidenceValidated(parsed)),
|
||||
);
|
||||
if (!result.ok) return toCallToolResult(result, reconcileContract);
|
||||
for (const source of [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import type { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { EnhancedTools } from "../application/EnhancedTools.js";
|
||||
import {
|
||||
EnhancedTools,
|
||||
type ValidatedEnhancedCall,
|
||||
} from "../application/EnhancedTools.js";
|
||||
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
|
||||
|
|
@ -13,9 +16,10 @@ import type { Logger } from "../logger.js";
|
|||
import { logToolExecution } from "./toolLogging.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
|
||||
import { UnknownRegistryError } from "../domain/errors.js";
|
||||
import { AnalysisInputError, UnknownRegistryError } from "../domain/errors.js";
|
||||
import { ok, type Result } from "../domain/result.js";
|
||||
import { mcpProgressReporter } from "./mcpProgress.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import {
|
||||
|
|
@ -80,16 +84,12 @@ const registerEnhancedTool = (
|
|||
total: 1,
|
||||
message: "started",
|
||||
});
|
||||
const validatedInput = safeParseToolInput(
|
||||
enhancedInputSchemas[name],
|
||||
input,
|
||||
name,
|
||||
);
|
||||
if (!validatedInput.ok) return toCallToolResult(validatedInput, contract);
|
||||
const parsedInput = jsonObjectSchema.parse(validatedInput.value);
|
||||
const parameters: Record<string, JsonValue> = { ...parsedInput };
|
||||
const validatedCall = parseEnhancedCall(name, input);
|
||||
if (!validatedCall.ok) return toCallToolResult(validatedCall, contract);
|
||||
const parsedInput = validatedCall.value.input;
|
||||
const parameters = jsonParameters(parsedInput);
|
||||
const result = await logToolExecution(registration.logger, name, () =>
|
||||
services.execute(name, input, context.mcpReq.signal),
|
||||
services.executeValidated(validatedCall.value, context.mcpReq.signal),
|
||||
);
|
||||
await progress.report({
|
||||
phase: name,
|
||||
|
|
@ -123,7 +123,7 @@ const registerEnhancedTool = (
|
|||
return toCallToolResult(recorded, contract);
|
||||
const unknowns = recordWorkflowUnknowns({
|
||||
name,
|
||||
input: parsedInput,
|
||||
input: parameters,
|
||||
result: result.value,
|
||||
evidenceId: evidence.evidence_id,
|
||||
recordUnknown: registration.recordUnknown,
|
||||
|
|
@ -138,6 +138,103 @@ const registerEnhancedTool = (
|
|||
);
|
||||
};
|
||||
|
||||
const jsonParameters = (
|
||||
input: Readonly<Record<string, JsonValue | undefined>>,
|
||||
): Record<string, JsonValue> =>
|
||||
Object.fromEntries(
|
||||
Object.entries(input).filter(
|
||||
(entry): entry is [string, JsonValue] => entry[1] !== undefined,
|
||||
),
|
||||
);
|
||||
|
||||
const parseEnhancedCall = (
|
||||
name: ValidatedEnhancedCall["name"],
|
||||
input: unknown,
|
||||
): Result<ValidatedEnhancedCall, AnalysisInputError> => {
|
||||
switch (name) {
|
||||
case "swift_classes": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.swift_classes,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "get_objc_classes": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.get_objc_classes,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "get_objc_protocols": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.get_objc_protocols,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "batch_decompile": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.batch_decompile,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "get_call_graph": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.get_call_graph,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "analyze_swift_types": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.analyze_swift_types,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "find_xrefs_to_name": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.find_xrefs_to_name,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "binary_overview": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.binary_overview,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "analyze_function": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.analyze_function,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
case "trace_feature": {
|
||||
const parsed = safeParseToolInput(
|
||||
enhancedInputSchemas.trace_feature,
|
||||
input,
|
||||
name,
|
||||
);
|
||||
return parsed.ok ? ok({ name, input: parsed.value }) : parsed;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface WorkflowUnknownInput {
|
||||
readonly name: string;
|
||||
readonly input: Readonly<Record<string, JsonValue>>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
import { runCrossVersionInvestigation } from "../application/CrossVersionInvestigation.js";
|
||||
import { runCrossVersionInvestigationValidated } from "../application/CrossVersionInvestigation.js";
|
||||
import type { ToolContract } from "../contracts/toolContracts.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import { safeParseToolInput } from "./toolInputValidation.js";
|
||||
|
|
@ -133,7 +133,7 @@ const registerChangedBehavior = (
|
|||
operation: contract.name,
|
||||
});
|
||||
}
|
||||
const investigated = await runCrossVersionInvestigation(
|
||||
const investigated = await runCrossVersionInvestigationValidated(
|
||||
investigationRun,
|
||||
policies.evidenceFiles,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/server";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AnalysisOperationPort } from "../src/application/AnalysisProvider.js";
|
||||
import { EnhancedTools } from "../src/application/EnhancedTools.js";
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
SESSION_TOOL_CONTRACTS,
|
||||
TOOL_CONTRACTS,
|
||||
} from "../src/contracts/toolContracts.js";
|
||||
import { enhancedInputSchemas } from "../src/contracts/enhancedInputs.js";
|
||||
import { AnalysisOutputError } from "../src/domain/errors.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../src/domain/jsonValue.js";
|
||||
import { err } from "../src/domain/result.js";
|
||||
|
|
@ -144,6 +145,7 @@ const emptyBounded = () => ({
|
|||
const resources: Array<{ close(): Promise<void> }> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await Promise.all(
|
||||
resources.splice(0).map(async (resource) => resource.close()),
|
||||
);
|
||||
|
|
@ -613,6 +615,16 @@ describe("enhanced MCP tools", () => {
|
|||
|
||||
it("returns correction-only structured validation issues", async () => {
|
||||
const client = await connect();
|
||||
const traceParser = vi.spyOn(
|
||||
enhancedInputSchemas.trace_feature,
|
||||
"safeParse",
|
||||
);
|
||||
await client.callTool({
|
||||
name: "trace_feature",
|
||||
arguments: { query: "needle", max_operations: 1 },
|
||||
});
|
||||
expect(traceParser).toHaveBeenCalledTimes(1);
|
||||
traceParser.mockClear();
|
||||
const misspelled = await client.callTool({
|
||||
name: "trace_feature",
|
||||
arguments: { query: "needle", max_operatons: 1 },
|
||||
|
|
@ -629,6 +641,7 @@ describe("enhanced MCP tools", () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
expect(traceParser).not.toHaveBeenCalled();
|
||||
const nestedMisspelled = await client.callTool({
|
||||
name: "analyze_function",
|
||||
arguments: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue