fix(mcp): parse adapter inputs exactly once

This commit is contained in:
morluto
2026-07-16 19:51:25 +08:00
parent d1e72207f4
commit 37c0f39954
11 changed files with 288 additions and 89 deletions
+21 -12
View File
@@ -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