mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
d8c76b6296
Keep failed JSON read handles in the existing server resource scope and fail admission until cleanup succeeds. Thread cancellation through every process CLI input read and replace timer-driven cancellation checks with a shared first-read gate. Classify authenticated standalone Evidence as wrong-kind input while preserving tampered-record integrity failures and actionable messages across ledger, CLI, and MCP consumers. Record unrelated CS_RESTRICT process neighbors as unverified without weakening related-process ownership checks. Update stale ADB fixtures for the actual shell protocol. Validated focused CLI, MCP, filesystem, ownership, and error projection tests; static, docs, and duplication gates pass. The host-level CS_RESTRICT load reproduction was not run. Refs #2050, #2035, #2064.
145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
import { setImmediate } from "node:timers/promises";
|
|
|
|
import {
|
|
prepareSnapshotEntry,
|
|
parsePreparedAnalysisSnapshot,
|
|
type PreparedSnapshotEntry,
|
|
serializeAnalysisSnapshot,
|
|
type AnalysisSnapshot,
|
|
} from "../../domain/analysisSnapshot.js";
|
|
import {
|
|
EvidenceFileError,
|
|
EvidenceIntegrityError,
|
|
} from "../../domain/evidenceErrors.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
import type { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
|
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
|
import { readJsonObjectMembersFile, writeTextFile } from "../JsonFiles.js";
|
|
import { RegularFileCleanupFailure } from "../RegularFileRead.js";
|
|
import type { JsonObjectMemberReader } from "../JsonInputFile.js";
|
|
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
|
|
|
type SnapshotFailure = EvidenceFileError | EvidenceIntegrityError;
|
|
|
|
/** Read and validate an analysis snapshot from the caller's path. */
|
|
export const readAnalysisSnapshot = async (
|
|
path: string,
|
|
signal?: AbortSignal,
|
|
resources?: ArtifactResourceScope,
|
|
): Promise<
|
|
Result<
|
|
AnalysisSnapshot,
|
|
SnapshotFailure | AnalysisResourceConstraintError | AnalysisCancelledError
|
|
>
|
|
> => {
|
|
const entries = snapshotEntryReader("provider");
|
|
const workflows = snapshotEntryReader("workflow");
|
|
const header: Record<string, unknown> = {};
|
|
const metadata = new Map<string, JsonObjectMemberReader>(
|
|
["target", "binding", "evidence_bundle"].map((name) => [
|
|
name,
|
|
{
|
|
mode: "value",
|
|
begin: () => {
|
|
delete header[name];
|
|
},
|
|
accept: (value) => {
|
|
header[name] = value;
|
|
},
|
|
reject: (cause) => {
|
|
throw cause;
|
|
},
|
|
},
|
|
]),
|
|
);
|
|
try {
|
|
const loaded = await readJsonObjectMembersFile(
|
|
path,
|
|
[
|
|
new Map([
|
|
["entries", entries.reader],
|
|
["workflow_entries", workflows.reader],
|
|
]),
|
|
metadata,
|
|
],
|
|
signal,
|
|
resources,
|
|
);
|
|
if (!loaded.ok) return loaded;
|
|
signal?.throwIfAborted();
|
|
const snapshot = parsePreparedAnalysisSnapshot(
|
|
header,
|
|
entries.finish(),
|
|
workflows.finish(),
|
|
);
|
|
// Deliver queued cancellation before handing a complete cache to the session.
|
|
await setImmediate();
|
|
signal?.throwIfAborted();
|
|
return ok(snapshot);
|
|
} catch (cause: unknown) {
|
|
if (cause instanceof RegularFileCleanupFailure) throw cause;
|
|
if (signal?.aborted === true)
|
|
return err(new AnalysisCancelledError("read_evidence_file", { cause }));
|
|
return err(
|
|
new EvidenceIntegrityError(snapshotValidationMessage(cause), { cause }),
|
|
);
|
|
}
|
|
};
|
|
|
|
const snapshotEntryReader = (kind: "provider" | "workflow") => {
|
|
let seen = false;
|
|
let entries: PreparedSnapshotEntry[] = [];
|
|
let failure: { readonly cause: unknown } | undefined;
|
|
const reader: JsonObjectMemberReader = {
|
|
mode: "array",
|
|
begin: () => {
|
|
seen = true;
|
|
entries = [];
|
|
failure = undefined;
|
|
},
|
|
accept: (value) => {
|
|
if (failure === undefined)
|
|
entries.push(prepareSnapshotEntry(value, kind));
|
|
},
|
|
reject: (cause) => {
|
|
failure ??= { cause };
|
|
},
|
|
};
|
|
return {
|
|
reader,
|
|
finish: (): PreparedSnapshotEntry[] => {
|
|
if (!seen)
|
|
throw new TypeError(
|
|
`Analysis snapshot ${kind === "provider" ? "entries" : "workflow_entries"} is missing`,
|
|
);
|
|
if (failure !== undefined) throw failure.cause;
|
|
return entries;
|
|
},
|
|
};
|
|
};
|
|
|
|
/** Atomically write a deterministic analysis snapshot to the caller's path. */
|
|
export const writeAnalysisSnapshot = async (
|
|
snapshot: AnalysisSnapshot,
|
|
path: string,
|
|
overwrite: boolean,
|
|
resources: ArtifactResourceScope,
|
|
): Promise<
|
|
Result<{ readonly path: string; readonly bytes: number }, SnapshotFailure>
|
|
> => {
|
|
let encoded: string;
|
|
try {
|
|
encoded = serializeAnalysisSnapshot(snapshot);
|
|
} catch (cause: unknown) {
|
|
return err(
|
|
new EvidenceIntegrityError(snapshotValidationMessage(cause), { cause }),
|
|
);
|
|
}
|
|
return writeTextFile(encoded, path, overwrite, resources);
|
|
};
|
|
|
|
const snapshotValidationMessage = (cause: unknown): string =>
|
|
cause instanceof TypeError
|
|
? cause.message
|
|
: "Analysis snapshot validation failed";
|