Files
rea/src/application/binary/AnalysisSnapshotFiles.ts
T
morluto d8c76b6296 fix(io): retain cleanup ownership and honor cancellation
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.
2026-10-12 14:12:44 +08:00

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";