mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(evidence): centralize authentication failure ownership
This commit is contained in:
@@ -3,7 +3,6 @@ import { dispatchQueuedInterrupts } from "../../process/queuedInterrupts.js";
|
||||
import { readImmutableEvidenceBundle } from "../EvidenceBundleFiles.js";
|
||||
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ANALYSIS_VIEW_PROVIDER } from "../InvestigationProviders.js";
|
||||
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
|
||||
@@ -17,11 +16,8 @@ import {
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
type Evidence,
|
||||
} from "../../domain/evidence.js";
|
||||
import { safeParseEvidence } from "../../domain/evidenceValidation.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
inspectAnalysisViewInputSchema,
|
||||
projectAnalysisView,
|
||||
@@ -335,30 +331,20 @@ const authenticateEvidence = (
|
||||
path: readonly (string | number)[],
|
||||
input: unknown,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
try {
|
||||
return ok(parseEvidence(value));
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
cause.issues.map((issue) => ({
|
||||
...issue,
|
||||
path: [...path, ...issue.path],
|
||||
})),
|
||||
input,
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
if (cause instanceof TypeError)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
cause.message,
|
||||
cause.message.length > 0 ? { userMessage: cause.message } : undefined,
|
||||
),
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
const parsed = safeParseEvidence(value);
|
||||
if (parsed.ok) return parsed;
|
||||
const cause = parsed.error;
|
||||
return err(
|
||||
analysisInputErrorFromIssues(
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
cause.issues.map((issue) => ({
|
||||
...issue,
|
||||
path: [...path, ...issue.path],
|
||||
})),
|
||||
input,
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const createAnalysisViewEvidence = (
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { requireAuthenticationHeadroom } from "../JsonAuthenticationHeadroom.js";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { parseEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
EvidenceFileError,
|
||||
EvidenceIntegrityError,
|
||||
} from "../../domain/evidenceErrors.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { safeParseEvidence } from "../../domain/evidenceValidation.js";
|
||||
import { EvidenceFileError } from "../../domain/evidenceErrors.js";
|
||||
import { projectInputIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { readJsonFile } from "../JsonFiles.js";
|
||||
|
||||
@@ -51,26 +48,16 @@ export const readEvidenceFileSource = async (
|
||||
description: "Insufficient heap headroom to authenticate the Evidence file",
|
||||
});
|
||||
if (!headroom.ok) return headroom;
|
||||
try {
|
||||
return ok(parseEvidence(loaded.value));
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(
|
||||
const parsed = safeParseEvidence(loaded.value);
|
||||
return parsed.ok
|
||||
? parsed
|
||||
: err(
|
||||
new EvidenceFileError("read", "wrong-kind", {
|
||||
path,
|
||||
cause,
|
||||
contentIssues: projectInputIssues(cause.issues, loaded.value),
|
||||
cause: parsed.error,
|
||||
contentIssues: projectInputIssues(parsed.error.issues, loaded.value),
|
||||
userMessage:
|
||||
"The selected file does not contain valid Evidence. Repair or recreate the file and try again.",
|
||||
}),
|
||||
);
|
||||
if (cause instanceof TypeError)
|
||||
return err(
|
||||
new EvidenceIntegrityError(cause.message, {
|
||||
cause,
|
||||
userMessage: cause.message,
|
||||
}),
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
AnalysisInputError,
|
||||
type AnalysisInputIssue,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { immutableEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { safeParseImmutableEvidence } from "../../domain/evidenceValidation.js";
|
||||
import { parseExactJson } from "../../domain/exactJsonNumber.js";
|
||||
import { projectInputIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { immutableJsonSnapshotMember } from "../../domain/immutableJson.js";
|
||||
@@ -136,20 +137,18 @@ export const parseEvidenceFile = (
|
||||
role: "manifest" | "evidence" | "state",
|
||||
value: unknown,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
try {
|
||||
for (const key of ["parameters", "raw_result", "normalized_result"])
|
||||
immutableJsonSnapshotMember(value, key);
|
||||
return ok(immutableEvidence(value));
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
return err(
|
||||
schemaInputError(
|
||||
operation,
|
||||
{ prefix: [role], cause, value },
|
||||
`The ${role} file is not authenticated REA Evidence JSON.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const key of ["parameters", "raw_result", "normalized_result"])
|
||||
immutableJsonSnapshotMember(value, key);
|
||||
const parsed = safeParseImmutableEvidence(value);
|
||||
return parsed.ok
|
||||
? parsed
|
||||
: err(
|
||||
schemaInputError(
|
||||
operation,
|
||||
{ prefix: [role], cause: parsed.error, value },
|
||||
`The ${role} file is not authenticated REA Evidence JSON.`,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
/** A schema refusal of `value`, found at `prefix`; issue paths extend the prefix and the schema failure is retained. */
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { UnknownRegistryError } from "../../domain/unknownRegistryError.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { readImmutableEvidence } from "../../domain/evidenceValidation.js";
|
||||
import { safeParseImmutableEvidence } from "../../domain/evidenceValidation.js";
|
||||
import {
|
||||
createResidualUnknown,
|
||||
updateResidualUnknown,
|
||||
@@ -482,12 +482,12 @@ const readLedgerEvidence = (
|
||||
input: unknown,
|
||||
message: string,
|
||||
): Result<Evidence, EvidenceIntegrityError> => {
|
||||
const checked = readImmutableEvidence(input);
|
||||
const checked = safeParseImmutableEvidence(input);
|
||||
return checked.ok
|
||||
? checked
|
||||
: err(
|
||||
new EvidenceIntegrityError(message, {
|
||||
cause: checked.error.cause,
|
||||
cause: checked.error,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,8 @@ import {
|
||||
JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
|
||||
MANAGED_WORKFLOW_PROVIDER,
|
||||
} from "../InvestigationProviders.js";
|
||||
import {
|
||||
isImmutableEvidence,
|
||||
parseEvidence,
|
||||
type Evidence,
|
||||
} from "../../domain/evidence.js";
|
||||
import { isImmutableEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { safeParseEvidence } from "../../domain/evidenceValidation.js";
|
||||
import {
|
||||
type AnalysisInputIssue,
|
||||
AnalysisInputError,
|
||||
@@ -355,12 +352,9 @@ const parseEvidenceInput = (
|
||||
input: unknown,
|
||||
path: readonly (string | number)[],
|
||||
): Evidence => {
|
||||
try {
|
||||
return parseEvidence(input);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError) throw zodInputFailure(cause, input, path);
|
||||
throw cause;
|
||||
}
|
||||
const parsed = safeParseEvidence(input);
|
||||
if (parsed.ok) return parsed.value;
|
||||
throw zodInputFailure(parsed.error, input, path);
|
||||
};
|
||||
|
||||
const parseCallerValue = <Value>(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
import { posix, win32 } from "node:path";
|
||||
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
type Evidence,
|
||||
type EvidenceProvider,
|
||||
} from "../../domain/evidence.js";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
safeParseProcessCapture,
|
||||
type ProcessCapture,
|
||||
} from "../../domain/process/processCaptureParsing.js";
|
||||
import { safeParseEvidence } from "../../domain/evidenceValidation.js";
|
||||
import type { ProcessScenario } from "../../domain/process/processScenario.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
|
||||
@@ -49,14 +49,10 @@ export const parseProcessCaptureEvidence = (
|
||||
{ readonly record: Evidence; readonly capture: ProcessCapture },
|
||||
ProcessCaptureEvidenceFailure
|
||||
> => {
|
||||
let record: Evidence;
|
||||
try {
|
||||
record = parseEvidence(input);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError)
|
||||
return err({ kind: "invalid-evidence", cause });
|
||||
throw cause;
|
||||
}
|
||||
const parsedEvidence = safeParseEvidence(input);
|
||||
if (!parsedEvidence.ok)
|
||||
return err({ kind: "invalid-evidence", cause: parsedEvidence.error });
|
||||
const record = parsedEvidence.value;
|
||||
if (
|
||||
record.operation !== PROCESS_CAPTURE_EVIDENCE.operation ||
|
||||
record.predicate_type !== PROCESS_CAPTURE_EVIDENCE.predicate
|
||||
|
||||
@@ -22,10 +22,10 @@ import {
|
||||
artifactManifestId,
|
||||
occurrenceIdForLocation,
|
||||
} from "./artifactIdentity.js";
|
||||
import { parseEvidence, type Evidence } from "./evidence.js";
|
||||
import type { Evidence } from "./evidence.js";
|
||||
import { z } from "zod";
|
||||
import { EvidenceIntegrityError } from "./evidenceErrors.js";
|
||||
import { readEvidence } from "./evidenceValidation.js";
|
||||
import { readEvidence, safeParseEvidence } from "./evidenceValidation.js";
|
||||
import {
|
||||
artifactInspectionResultSchema,
|
||||
type ArtifactInspectionResult,
|
||||
@@ -191,24 +191,23 @@ const parseInspectionInventoryPayload = (
|
||||
);
|
||||
const sourceEvidence =
|
||||
immutableJsonSnapshotMember(sealedStep, "evidence") ?? step.evidence;
|
||||
let parsedEvidence: Evidence;
|
||||
try {
|
||||
parsedEvidence = parseEvidence(sourceEvidence);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
const parsedEvidence = safeParseEvidence(sourceEvidence);
|
||||
if (!parsedEvidence.ok)
|
||||
throw Object.assign(
|
||||
new z.ZodError(
|
||||
cause.issues.map((issue) => ({
|
||||
parsedEvidence.error.issues.map((issue) => ({
|
||||
...issue,
|
||||
path: ["substeps", index, "evidence", ...issue.path],
|
||||
})),
|
||||
),
|
||||
{ cause },
|
||||
{ cause: parsedEvidence.error },
|
||||
);
|
||||
}
|
||||
const evidence = canBorrowParsedJsonSnapshot(sourceEvidence, parsedEvidence)
|
||||
const evidence = canBorrowParsedJsonSnapshot(
|
||||
sourceEvidence,
|
||||
parsedEvidence.value,
|
||||
)
|
||||
? sourceEvidence
|
||||
: parsedEvidence;
|
||||
: parsedEvidence.value;
|
||||
immutableJsonSnapshotMember(evidence, "normalized_result");
|
||||
if (step.evidence_id !== evidence.evidence_id)
|
||||
throw new ArtifactInventoryValidationError(
|
||||
|
||||
@@ -5,27 +5,42 @@ import { EvidenceIntegrityError } from "./evidenceErrors.js";
|
||||
import { describeValidationFailure } from "./validationFailure.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
|
||||
/** Expected authentication failures, independent of a caller's error contract. */
|
||||
export const safeParseEvidence = (
|
||||
input: unknown,
|
||||
): Result<Evidence, z.ZodError> => authenticateEvidence(input, parseEvidence);
|
||||
|
||||
/** Authenticate and seal source records while retaining typed validation failures. */
|
||||
export const safeParseImmutableEvidence = (
|
||||
input: unknown,
|
||||
): Result<Evidence, z.ZodError> =>
|
||||
authenticateEvidence(input, immutableEvidence);
|
||||
|
||||
/** Authenticate one source record, preserving unexpected implementation failures. */
|
||||
export const readEvidence = (
|
||||
input: unknown,
|
||||
): Result<Evidence, EvidenceIntegrityError> =>
|
||||
authenticateEvidence(input, parseEvidence);
|
||||
): Result<Evidence, EvidenceIntegrityError> => {
|
||||
const parsed = safeParseEvidence(input);
|
||||
return parsed.ok ? parsed : err(integrityFailure(parsed.error));
|
||||
};
|
||||
|
||||
/** Authenticate and seal source records retained independently of mutable input. */
|
||||
export const readImmutableEvidence = (
|
||||
input: unknown,
|
||||
): Result<Evidence, EvidenceIntegrityError> =>
|
||||
authenticateEvidence(input, immutableEvidence);
|
||||
): Result<Evidence, EvidenceIntegrityError> => {
|
||||
const parsed = safeParseImmutableEvidence(input);
|
||||
return parsed.ok ? parsed : err(integrityFailure(parsed.error));
|
||||
};
|
||||
|
||||
const authenticateEvidence = (
|
||||
input: unknown,
|
||||
parse: (input: unknown) => Evidence,
|
||||
): Result<Evidence, EvidenceIntegrityError> => {
|
||||
): Result<Evidence, z.ZodError> => {
|
||||
try {
|
||||
return ok(parse(input));
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
return err(integrityFailure(cause));
|
||||
return err(cause);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { AnalysisInputError } from "../analysisErrorCore.js";
|
||||
import { projectInputIssues } from "../inputIssueProjection.js";
|
||||
import { parseEvidence, type Evidence } from "../evidence.js";
|
||||
import type { Evidence } from "../evidence.js";
|
||||
import { safeParseEvidence } from "../evidenceValidation.js";
|
||||
|
||||
const OPERATION = "reconcile_javascript_runtime";
|
||||
|
||||
@@ -23,13 +24,9 @@ export const authenticateRuntimeEvidence = (
|
||||
input: unknown,
|
||||
path: readonly (string | number)[],
|
||||
): Evidence => {
|
||||
try {
|
||||
return parseEvidence(input);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError)
|
||||
throw inputErrorAtPath(cause.issues, input, path, cause);
|
||||
throw cause;
|
||||
}
|
||||
const parsed = safeParseEvidence(input);
|
||||
if (parsed.ok) return parsed.value;
|
||||
throw inputErrorAtPath(parsed.error.issues, input, path, parsed.error);
|
||||
};
|
||||
|
||||
/** Represent one cross-field caller constraint as a standard input issue. */
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
|
||||
import { createApplicationMcpHarness } from "../fixtures/applicationMcpHarness.js";
|
||||
import {
|
||||
analysisViewCaptureEvidence,
|
||||
environmentManifestCaptureFixture,
|
||||
} from "../fixtures/analysisView.js";
|
||||
import { createTestBinarySession } from "../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
const unexpectedFailure = (
|
||||
kind: "TypeError" | "Error",
|
||||
source: "inline" | "evidence-file",
|
||||
): Error => {
|
||||
const message = `injected ${kind} during ${source} Evidence authentication`;
|
||||
return kind === "TypeError" ? new TypeError(message) : new Error(message);
|
||||
};
|
||||
|
||||
const manifestView = {
|
||||
kind: "environment-manifest",
|
||||
template_rules: ["opaque-token", "sibling-pair-variance"],
|
||||
} as const;
|
||||
|
||||
const createOwnedHarness = async () => {
|
||||
const harness = await createApplicationMcpHarness({
|
||||
session: createTestBinarySession(() => {
|
||||
throw new Error("stored Evidence views must not launch a provider");
|
||||
}),
|
||||
});
|
||||
onTestFinished(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await harness.close();
|
||||
});
|
||||
return harness;
|
||||
};
|
||||
|
||||
it.each(
|
||||
(["inline", "evidence-file"] as const).flatMap((source) =>
|
||||
(["TypeError", "Error"] as const).map(
|
||||
(failureKind) => [source, failureKind] as const,
|
||||
),
|
||||
),
|
||||
)(
|
||||
"preserves an unexpected parser failure from %s input (%s) without retaining a view",
|
||||
async (sourceKind, failureKind) => {
|
||||
const harness = await createOwnedHarness();
|
||||
const parent = analysisViewCaptureEvidence(
|
||||
environmentManifestCaptureFixture(),
|
||||
);
|
||||
expect(harness.session.recordEvidence(parent).ok).toBe(true);
|
||||
const before = harness.session.exportEvidenceBundle();
|
||||
|
||||
let source: Record<string, unknown>;
|
||||
if (sourceKind === "inline") {
|
||||
source = { kind: "inline", evidence: parent };
|
||||
} else {
|
||||
const root = await createTestTempDirectory("rea-view-auth-failure-");
|
||||
const path = join(root, "parent.json");
|
||||
await writeFile(path, JSON.stringify(parent));
|
||||
source = { kind: "evidence-file", path };
|
||||
}
|
||||
|
||||
const evidenceModule = await import("../../src/domain/evidence.js");
|
||||
const failure = unexpectedFailure(failureKind, sourceKind);
|
||||
vi.spyOn(evidenceModule, "parseEvidence").mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
|
||||
const response = await harness.client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: { source, view: manifestView },
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
isError: true,
|
||||
content: [{ type: "text", text: failure.message }],
|
||||
});
|
||||
expect(response.structuredContent).toBeUndefined();
|
||||
expect(harness.session.exportEvidenceBundle()).toEqual(before);
|
||||
},
|
||||
);
|
||||
|
||||
it("uses retained Evidence without reparsing its parent", async () => {
|
||||
const harness = await createOwnedHarness();
|
||||
const parent = analysisViewCaptureEvidence(
|
||||
environmentManifestCaptureFixture(),
|
||||
);
|
||||
expect(harness.session.recordEvidence(parent).ok).toBe(true);
|
||||
|
||||
const evidenceModule = await import("../../src/domain/evidence.js");
|
||||
const originalParse = evidenceModule.parseEvidence;
|
||||
vi.spyOn(evidenceModule, "parseEvidence").mockImplementation((input) => {
|
||||
if (
|
||||
typeof input === "object" &&
|
||||
input !== null &&
|
||||
"evidence_id" in input &&
|
||||
input.evidence_id === parent.evidence_id
|
||||
)
|
||||
throw new TypeError("retained source parent was reparsed");
|
||||
return originalParse(input);
|
||||
});
|
||||
|
||||
const response = await harness.client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: manifestView,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.isError).not.toBe(true);
|
||||
expect(response.structuredContent).toBeDefined();
|
||||
expect(harness.session.exportEvidenceBundle().records).toHaveLength(2);
|
||||
});
|
||||
Reference in New Issue
Block a user