diff --git a/src/application/process/ProcessCli.ts b/src/application/process/ProcessCli.ts index 7c3fa62a48..d69834d786 100644 --- a/src/application/process/ProcessCli.ts +++ b/src/application/process/ProcessCli.ts @@ -9,7 +9,7 @@ import { import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import type { AnalysisErrorProjection } from "../../domain/analysisErrorProjection.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; -import { createEvidence, parseEvidence } from "../../domain/evidence.js"; +import { createEvidence } from "../../domain/evidence.js"; import { describeValidationFailure } from "../../domain/evidenceBundle.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; import { @@ -19,16 +19,15 @@ import { import { processTraceSpecificationSchema } from "../../domain/process/processTraceSpecification.js"; import { processScenarioSchema } from "../../domain/process/processScenario.js"; import { compareProcessCaptures } from "../../domain/process/processComparison.js"; -import { parseProcessCapture } from "../../domain/process/processCaptureParsing.js"; import { createProgressReporter } from "../ProgressReporter.js"; import { captureProcessScenario } from "../../process/capture/ProcessHarness.js"; import { ProcessCaptureResourceScope } from "../../process/capture/ProcessCaptureLifecycle.js"; import { ProcessCaptureError } from "../../process/capture/ProcessCaptureError.js"; +import { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js"; import { - PROCESS_PROVIDER, - isProcessEvidenceProvider, -} from "../../domain/process/processEvidenceProvider.js"; -import { createProcessCaptureEvidence } from "./ProcessEvidence.js"; + createProcessCaptureEvidence, + parseProcessCaptureEvidence, +} from "./ProcessEvidence.js"; import { readJsonFile } from "../JsonFiles.js"; import { RegularFileCleanupFailure } from "../RegularFileRead.js"; @@ -147,8 +146,8 @@ export const compareProcessEvidenceFiles = async ( predicateType: "rea.process-comparison", operation: "compare_process_captures", parameters: { - left_evidence_id: left.id, - right_evidence_id: right.id, + left_evidence_id: left.record.evidence_id, + right_evidence_id: right.record.evidence_id, left_normalization: left.capture.normalization, right_normalization: right.capture.normalization, ...(traceSpecification === undefined @@ -159,8 +158,8 @@ export const compareProcessEvidenceFiles = async ( confidence: "derived", authority: "analyst-inference", limitations: comparison.limitations, - locations: [...left.locations, ...right.locations], - evidenceLinks: [left.id, right.id], + locations: [...left.record.locations, ...right.record.locations], + evidenceLinks: [left.record.evidence_id, right.record.evidence_id], }); } catch (cause: unknown) { if (cause instanceof RegularFileCleanupFailure) throw cause; @@ -169,45 +168,35 @@ export const compareProcessEvidenceFiles = async ( }; const parseCaptureEvidence = (input: unknown, side: "left" | "right") => { - let evidence; - try { - evidence = parseEvidence(input); - } catch (cause: unknown) { - if (cause instanceof z.ZodError) - throw new AnalysisInputError( + const parsed = parseProcessCaptureEvidence(input); + if (parsed.ok) return parsed.value; + const failure = parsed.error; + switch (failure.kind) { + case "invalid-evidence": + if (failure.cause instanceof z.ZodError) + throw new AnalysisInputError( + "compare_process_captures", + { cause: failure.cause }, + projectInputIssues(failure.cause.issues, input), + ); + throw invalidCaptureEvidence(failure.cause, side); + case "wrong-workflow": + throw processInputFailure( "compare_process_captures", - { cause }, - projectInputIssues(cause.issues, input), + "Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.", + [side], + "invalid_value", ); - throw invalidCaptureEvidence(cause, side); - } - if ( - evidence.operation !== "capture_process_scenario" || - evidence.predicate_type !== "rea.process-capture" - ) { - throw processInputFailure( - "compare_process_captures", - `Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.`, - [side], - "invalid_value", - ); - } - if (!isProcessEvidenceProvider(evidence.provider)) - throw new AnalysisInputError("compare_process_captures", undefined, [ - { - path: [side, "provider"], - reason: "invalid_value", - message: `The ${side} Evidence provider ${evidence.provider.id} v${evidence.provider.version} (${evidence.provider.name}) is not the current or supported legacy ${PROCESS_PROVIDER.id} v${PROCESS_PROVIDER.version} process capture provider.`, - }, - ]); - try { - return { - id: evidence.evidence_id, - capture: parseProcessCapture(evidence.normalized_result), - locations: evidence.locations, - }; - } catch (cause: unknown) { - throw invalidCaptureEvidence(cause, side); + case "unsupported-provider": + throw new AnalysisInputError("compare_process_captures", undefined, [ + { + path: [side, "provider"], + reason: "invalid_value", + message: `The ${side} Evidence provider ${failure.provider.id} v${failure.provider.version} (${failure.provider.name}) is not the current or supported legacy ${PROCESS_PROVIDER.id} v${PROCESS_PROVIDER.version} process capture provider.`, + }, + ]); + case "invalid-capture": + throw invalidCaptureEvidence(failure.cause, side); } }; diff --git a/src/application/process/ProcessEvidence.ts b/src/application/process/ProcessEvidence.ts index 13330601ce..b50a6d0f71 100644 --- a/src/application/process/ProcessEvidence.ts +++ b/src/application/process/ProcessEvidence.ts @@ -1,11 +1,79 @@ import { posix, win32 } from "node:path"; import type { JsonValue } from "../../domain/jsonValue.js"; -import { createEvidence } from "../../domain/evidence.js"; -import { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js"; +import { + createEvidence, + parseEvidence, + type Evidence, + type EvidenceProvider, +} from "../../domain/evidence.js"; +import { + PROCESS_PROVIDER, + isProcessEvidenceProvider, +} from "../../domain/process/processEvidenceProvider.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; -import type { ProcessCapture } from "../../domain/process/processCaptureParsing.js"; +import { + parseProcessCapture, + type ProcessCapture, +} from "../../domain/process/processCaptureParsing.js"; import type { ProcessScenario } from "../../domain/process/processScenario.js"; +import { err, ok, type Result } from "../../domain/result.js"; + +const PROCESS_CAPTURE_EVIDENCE = { + operation: "capture_process_scenario", + predicate: "rea.process-capture", +} as const; + +/** Semantic rejection facts; CLI and MCP retain their own error projections. */ +export type ProcessCaptureEvidenceFailure = + | { readonly kind: "invalid-evidence"; readonly cause: unknown } + | { + readonly kind: "wrong-workflow"; + readonly expected: typeof PROCESS_CAPTURE_EVIDENCE; + readonly actual: { + readonly operation: string; + readonly predicate: string; + }; + } + | { + readonly kind: "unsupported-provider"; + readonly provider: EvidenceProvider; + } + | { readonly kind: "invalid-capture"; readonly cause: unknown }; + +/** Authenticate capture Evidence before admitting its workflow, provider and payload. */ +export const parseProcessCaptureEvidence = ( + input: unknown, +): Result< + { readonly record: Evidence; readonly capture: ProcessCapture }, + ProcessCaptureEvidenceFailure +> => { + let record: Evidence; + try { + record = parseEvidence(input); + } catch (cause: unknown) { + return err({ kind: "invalid-evidence", cause }); + } + if ( + record.operation !== PROCESS_CAPTURE_EVIDENCE.operation || + record.predicate_type !== PROCESS_CAPTURE_EVIDENCE.predicate + ) + return err({ + kind: "wrong-workflow", + expected: PROCESS_CAPTURE_EVIDENCE, + actual: { operation: record.operation, predicate: record.predicate_type }, + }); + if (!isProcessEvidenceProvider(record.provider)) + return err({ kind: "unsupported-provider", provider: record.provider }); + try { + return ok({ + record, + capture: parseProcessCapture(record.normalized_result), + }); + } catch (cause: unknown) { + return err({ kind: "invalid-capture", cause }); + } +}; /** Project one process scenario into secret-free Evidence parameters. */ const processEvidenceParameters = ( @@ -28,8 +96,8 @@ export const createProcessCaptureEvidence = ( capture: ProcessCapture, ) => { return createEvidence(undefined, PROCESS_PROVIDER, { - predicateType: "rea.process-capture", - operation: "capture_process_scenario", + predicateType: PROCESS_CAPTURE_EVIDENCE.predicate, + operation: PROCESS_CAPTURE_EVIDENCE.operation, parameters: processEvidenceParameters(scenario, capture.manifest.platform), result: jsonValueSchema.parse(capture), confidence: "observed", diff --git a/src/server/registerProcessComparisonTool.ts b/src/server/registerProcessComparisonTool.ts index aec9e8afef..d27ab9d81f 100644 --- a/src/server/registerProcessComparisonTool.ts +++ b/src/server/registerProcessComparisonTool.ts @@ -8,34 +8,23 @@ import { describeValidationFailure } from "../domain/evidenceBundle.js"; import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; import { createEvidence, - parseEvidence, type Evidence, type EvidenceLocation, } from "../domain/evidence.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; import { compareProcessCaptures } from "../domain/process/processComparison.js"; -import { - parseProcessCapture, - type ProcessCapture, -} from "../domain/process/processCaptureParsing.js"; +import type { ProcessCapture } from "../domain/process/processCaptureParsing.js"; +import { parseProcessCaptureEvidence } from "../application/process/ProcessEvidence.js"; import type { RecordUnknownInput } from "../domain/residualUnknown.js"; -import { err, ok, type Result } from "../domain/result.js"; +import { err, type Result } from "../domain/result.js"; import { recordDerivedEvidence } from "./recordDerivedEvidence.js"; import { recordSessionEvidenceSources } from "./sessionEvidence.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; -import { - PROCESS_PROVIDER, - isProcessEvidenceProvider, -} from "../domain/process/processEvidenceProvider.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", - predicate: "rea.process-capture", -} as const; - const sourceLocations = ( left: readonly EvidenceLocation[] | undefined, right: readonly EvidenceLocation[] | undefined, @@ -53,52 +42,47 @@ const parseCaptureSide = ( { readonly record: Evidence; readonly capture: ProcessCapture }, AnalysisError > => { - let record: Evidence; - try { - record = parseEvidence(value); - } catch (cause: unknown) { - return err( - new EvidenceIntegrityError(`${side} Evidence failed validation`, { - cause, - userMessage: `The ${side} Evidence failed validation (${describeValidationFailure(cause)}). Supply unmodified capture Evidence from capture_process_scenario.`, - }), - ); - } - if ( - record.operation !== PROCESS_CAPTURE_EVIDENCE.operation || - record.predicate_type !== PROCESS_CAPTURE_EVIDENCE.predicate - ) - return err( - new AnalysisInputError(operation, undefined, [ - { - path: [side], - reason: "invalid_value", - message: `Expected process capture Evidence (operation ${PROCESS_CAPTURE_EVIDENCE.operation}, predicate ${PROCESS_CAPTURE_EVIDENCE.predicate}); the ${side} Evidence has operation ${record.operation} and predicate ${record.predicate_type}.`, - }, - ]), - ); - if (!isProcessEvidenceProvider(record.provider)) - return err( - new AnalysisInputError(operation, undefined, [ - { - path: [side, "provider"], - reason: "invalid_value", - message: `The ${side} Evidence provider ${record.provider.id} v${record.provider.version} (${record.provider.name}) is not the current or supported legacy ${PROCESS_PROVIDER.id} v${PROCESS_PROVIDER.version} process capture provider.`, - }, - ]), - ); - try { - return ok({ - record, - capture: parseProcessCapture(record.normalized_result), - }); - } catch (cause: unknown) { - return err( - new EvidenceIntegrityError(`${side} process capture failed validation`, { - cause, - userMessage: `The ${side} Evidence has an invalid process capture result (${describeValidationFailure(cause)}). Recreate the capture with capture_process_scenario.`, - }), - ); + const parsed = parseProcessCaptureEvidence(value); + if (parsed.ok) return parsed; + const failure = parsed.error; + switch (failure.kind) { + case "invalid-evidence": + return err( + new EvidenceIntegrityError(`${side} Evidence failed validation`, { + cause: failure.cause, + userMessage: `The ${side} Evidence failed validation (${describeValidationFailure(failure.cause)}). Supply unmodified capture Evidence from capture_process_scenario.`, + }), + ); + case "wrong-workflow": + return err( + new AnalysisInputError(operation, undefined, [ + { + path: [side], + reason: "invalid_value", + message: `Expected process capture Evidence (operation ${failure.expected.operation}, predicate ${failure.expected.predicate}); the ${side} Evidence has operation ${failure.actual.operation} and predicate ${failure.actual.predicate}.`, + }, + ]), + ); + case "unsupported-provider": + return err( + new AnalysisInputError(operation, undefined, [ + { + path: [side, "provider"], + reason: "invalid_value", + message: `The ${side} Evidence provider ${failure.provider.id} v${failure.provider.version} (${failure.provider.name}) is not the current or supported legacy ${PROCESS_PROVIDER.id} v${PROCESS_PROVIDER.version} process capture provider.`, + }, + ]), + ); + case "invalid-capture": + return err( + new EvidenceIntegrityError( + `${side} process capture failed validation`, + { + cause: failure.cause, + userMessage: `The ${side} Evidence has an invalid process capture result (${describeValidationFailure(failure.cause)}). Recreate the capture with capture_process_scenario.`, + }, + ), + ); } };