import { z } from "zod"; import { jsonValueSchema } from "../jsonValue.js"; import { normalizationSchema } from "./processScenario.js"; import { collectProcessCaptureIssues } from "./processCaptureValidation.js"; export * from "./processScenario.js"; /** Normalized raw PTY chunk, preserving transport-level output differences. */ export interface TerminalFrame { readonly sequence: number; readonly at_ms: number; readonly data: string; } /** Serialized terminal state after interpreting control and resize sequences. */ export interface RenderedTerminalFrame { readonly sequence: number; readonly at_ms: number; readonly columns: number; readonly rows: number; readonly cursor_x: number; readonly cursor_y: number; readonly active_buffer: "normal" | "alternate"; readonly lines: readonly string[]; readonly serialized_state: string; } /** Scheduled terminal interaction with its observed dispatch outcome. */ export interface InteractionEvent { readonly sequence: number; readonly scheduled_at_ms: number; readonly dispatched_at_ms: number; readonly type: "input" | "resize" | "signal"; readonly data: string; readonly outcome: "dispatched" | "target_exited" | "failed"; } /** One filesystem state used for before/after comparison. */ interface FileStateIdentity { readonly path: string; readonly mode: number; readonly size: number; } export type FileState = FileStateIdentity & ( | { readonly type: "file"; readonly sha256: string | null; readonly symlink_target: null; } | { readonly type: "symlink"; readonly sha256: null; readonly symlink_target: string; } | { readonly type: "directory" | "other"; readonly sha256: null; readonly symlink_target: null; } ); interface FileEffectIdentity { readonly path: string; } type FileEffect = FileEffectIdentity & ( | { readonly status: "created"; readonly before: null; readonly after: FileState; } | { readonly status: "deleted"; readonly before: FileState; readonly after: null; } | { readonly status: "modified" | "unchanged"; readonly before: FileState; readonly after: FileState; } ); /** A sampled owned-process observation; sampling cannot prove syscall completeness. */ export interface ProcessSample { readonly at_ms: number; readonly pid: number; readonly parent_pid: number; readonly command: string; readonly process_group_id: number | null; readonly session_id: number | null; } /** Initial or final filesystem snapshot selected for observation. */ export interface FilesystemCheckpoint { readonly name: string; readonly at_ms: number; readonly files: readonly FileState[]; readonly effects: readonly FileEffect[]; readonly truncated: boolean; } /** Capture collection whose members participate in global observation order. */ export const PROCESS_CAPTURE_EVENT_COLLECTIONS = [ "frames", "rendered_frames", "interaction_events", "lifecycle", "process_samples", "filesystem_checkpoints", ] as const; export type ProcessCaptureEventCollection = (typeof PROCESS_CAPTURE_EVENT_COLLECTIONS)[number]; /** One reference from global observation order into a capture collection. */ export interface ProcessCaptureEventJournalEntry { readonly capture_order: number; readonly collection: ProcessCaptureEventCollection; readonly index: number; } /** Shared observation callback used by every process-capture producer. */ export type RecordProcessCaptureEvent = ( collection: ProcessCaptureEventCollection, index: number, ) => void; /** Observed process-tree settlement and the cleanup required by that state. */ export type ProcessSettlement = | VerifiedProcessSettlement | { readonly state: "quiesced"; readonly elapsed_ms: number; readonly cleanup_outcome: "failed"; }; export type VerifiedProcessSettlement = | { readonly state: "quiesced"; readonly elapsed_ms: number; readonly cleanup_outcome: "not_required"; } | { readonly state: "alive_at_deadline" | "unverifiable"; readonly elapsed_ms: number; readonly cleanup_outcome: "cleaned" | "failed"; }; /** * Process capture observation set. * * `truncated` and `residual_unknowns` are semantic evidence: consumers must not * infer equivalence from matching bounded observations when either is present. */ export interface UnverifiedProcessCapture { readonly manifest: { readonly rea_version: string; readonly provider_version: string; readonly platform: string; readonly architecture: string; readonly pty_backend: "node-pty"; readonly started_at: string; readonly completed_at: string; readonly scenario: Readonly>; readonly comparison_contract: Readonly>; readonly full_scenario_sha256: string; readonly comparison_contract_sha256: string; /** Digest of the selected file sampled before spawn, when readable. */ readonly selected_executable_sha256: string | null; /** Digest associated with the launch only when path metadata stayed stable across spawn. */ readonly executable_sha256: string | null; /** * Digest recorded by the original v3 format before selected and launch * executable digests were distinguished. Kept only on migrated captures. */ readonly legacy_executable_sha256?: string | undefined; readonly executable_identity: { readonly state: "path_metadata_unchanged" | "unknown"; readonly reason: string | null; }; readonly normalization_sha256: string; }; readonly normalization: z.infer; readonly frames: readonly TerminalFrame[]; readonly rendered_frames: readonly RenderedTerminalFrame[]; readonly interaction_events: readonly InteractionEvent[]; readonly exit: { readonly code: number | null; readonly signal: number | null; readonly reason: "exited" | "timeout" | "idle_timeout"; }; readonly settlement: VerifiedProcessSettlement; readonly process_samples: readonly ProcessSample[]; readonly filesystem_checkpoints: readonly FilesystemCheckpoint[]; /** * Global observation order across independently recorded collections. * * Absence is accepted for captures written before this journal existed. */ readonly event_journal?: readonly ProcessCaptureEventJournalEntry[]; readonly files_before: readonly FileState[]; readonly files_after: readonly FileState[]; readonly filesystem_effects: readonly FileEffect[]; readonly truncated: boolean; readonly limitations: readonly string[]; readonly residual_unknowns: readonly { readonly scope: | "terminal" | "interaction" | "exit" | "process" | "filesystem" | "cleanup" | "network" | "environment"; readonly reason: string; }[]; readonly cleanup: { readonly owned_process_group: "verified"; readonly temporary_root: "removed"; }; } const fileStateShape = { path: z.string(), mode: z.number().int().nonnegative(), size: z.number().int().nonnegative(), }; /** A resource cleanup result retained when observations cannot be verified. */ export interface ProcessCaptureResourceCleanup { readonly state: "cleaned" | "failed" | "unverified" | "not_required"; readonly reason: string | null; } export interface ProcessCaptureCleanupReport { readonly owned_process_group: ProcessCaptureResourceCleanup; readonly terminal_renderer: ProcessCaptureResourceCleanup; readonly temporary_root: ProcessCaptureResourceCleanup; } /** One observation field that may be unavailable when capture completion fails. */ export type PartialProcessObservationField = | { readonly state: "available"; readonly value: T } | { readonly state: "unavailable"; readonly reason: string }; /** Snapshot facts available before filesystem comparison can be completed. */ /** Snapshot facts available before filesystem comparison can be completed. */ export interface PartialFilesystemSnapshot { readonly files: readonly FileState[]; readonly truncated: boolean; } /** Available and unavailable fields when a run never becomes a full capture. */ /** Available and unavailable fields when a run never becomes a full capture. */ export interface IncompleteProcessCaptureObservations { readonly target_pid: PartialProcessObservationField; readonly frames: PartialProcessObservationField; readonly rendered_frames: PartialProcessObservationField< readonly RenderedTerminalFrame[] >; readonly interaction_events: PartialProcessObservationField< readonly InteractionEvent[] >; readonly exit: PartialProcessObservationField<{ readonly code: number | null; readonly signal: number | null; readonly reason: "exited" | "timeout" | "idle_timeout" | "cancelled"; }>; readonly settlement: PartialProcessObservationField<{ readonly state: "quiesced" | "alive_at_deadline" | "unverifiable"; readonly elapsed_ms: number; }>; readonly process_samples: PartialProcessObservationField< readonly ProcessSample[] >; readonly filesystem_snapshots: { readonly before: PartialProcessObservationField; readonly after: PartialProcessObservationField; }; readonly event_journal: PartialProcessObservationField< readonly ProcessCaptureEventJournalEntry[] >; readonly manifest: PartialProcessObservationField< UnverifiedProcessCapture["manifest"] >; } const fileStateSchema = z.discriminatedUnion("type", [ z.object({ ...fileStateShape, type: z.literal("file"), sha256: z .string() .regex(/^[a-f0-9]{64}$/u) .nullable(), symlink_target: z.null(), }), z.object({ ...fileStateShape, type: z.literal("symlink"), sha256: z.null(), symlink_target: z.string(), }), z.object({ ...fileStateShape, type: z.enum(["directory", "other"]), sha256: z.null(), symlink_target: z.null(), }), ]); const fileEffectSchema = z.discriminatedUnion("status", [ z.object({ path: z.string(), status: z.literal("created"), before: z.null(), after: fileStateSchema, }), z.object({ path: z.string(), status: z.literal("deleted"), before: fileStateSchema, after: z.null(), }), z.object({ path: z.string(), status: z.enum(["modified", "unchanged"]), before: fileStateSchema, after: fileStateSchema, }), ]); /** Exact serialized shape of a process capture. */ const processSettlementSchema = z.discriminatedUnion("state", [ z.object({ state: z.literal("quiesced"), elapsed_ms: z.number().int().nonnegative(), cleanup_outcome: z.enum(["not_required", "failed"]), }), z.object({ state: z.enum(["alive_at_deadline", "unverifiable"]), elapsed_ms: z.number().int().nonnegative(), cleanup_outcome: z.enum(["cleaned", "failed"]), }), ]); const processCaptureShapeSchema = z.strictObject({ manifest: z.strictObject({ rea_version: z.string().min(1), provider_version: z.string().min(1), platform: z.string().min(1), architecture: z.string().min(1), pty_backend: z.literal("node-pty"), started_at: z.iso.datetime(), completed_at: z.iso.datetime(), scenario: z.record(z.string(), jsonValueSchema), comparison_contract: z.record(z.string(), jsonValueSchema), full_scenario_sha256: z.string().regex(/^[a-f0-9]{64}$/u), comparison_contract_sha256: z.string().regex(/^[a-f0-9]{64}$/u), selected_executable_sha256: z .string() .regex(/^[a-f0-9]{64}$/u) .nullable(), executable_sha256: z .string() .regex(/^[a-f0-9]{64}$/u) .nullable(), legacy_executable_sha256: z .string() .regex(/^[a-f0-9]{64}$/u) .optional(), executable_identity: z.strictObject({ state: z.enum(["path_metadata_unchanged", "unknown"]), reason: z.string().nullable(), }), normalization_sha256: z.string().regex(/^[a-f0-9]{64}$/u), }), normalization: normalizationSchema, frames: z.array( z.object({ sequence: z.number().int().nonnegative(), at_ms: z.number().int().nonnegative(), data: z.string(), }), ), rendered_frames: z.array( z.object({ sequence: z.number().int().nonnegative(), at_ms: z.number().int().nonnegative(), columns: z.number().int().positive(), rows: z.number().int().positive(), cursor_x: z.number().int().nonnegative(), cursor_y: z.number().int().nonnegative(), active_buffer: z.enum(["normal", "alternate"]), lines: z.array(z.string()), serialized_state: z.string(), }), ), interaction_events: z.array( z.object({ sequence: z.number().int().nonnegative(), scheduled_at_ms: z.number().int().nonnegative(), dispatched_at_ms: z.number().int().nonnegative(), type: z.enum(["input", "resize", "signal"]), data: z.string(), outcome: z.enum(["dispatched", "target_exited", "failed"]), }), ), exit: z.object({ code: z.number().int().nullable(), signal: z.number().int().nullable(), reason: z.enum(["exited", "timeout", "idle_timeout"]), }), settlement: z.discriminatedUnion("state", [ z.object({ state: z.literal("quiesced"), elapsed_ms: z.number().int().nonnegative(), cleanup_outcome: z.literal("not_required"), }), z.object({ state: z.enum(["alive_at_deadline", "unverifiable"]), elapsed_ms: z.number().int().nonnegative(), cleanup_outcome: z.enum(["cleaned", "failed"]), }), ]), process_samples: z.array( z.object({ at_ms: z.number().int().nonnegative(), pid: z.number().int().positive(), parent_pid: z.number().int().nonnegative(), command: z.string(), process_group_id: z.number().int().positive().nullable(), session_id: z.number().int().nonnegative().nullable(), }), ), filesystem_checkpoints: z.array( z.object({ name: z.enum(["before", "after_settlement"]), at_ms: z.number().int().nonnegative(), files: z.array(fileStateSchema), effects: z.array(fileEffectSchema), truncated: z.boolean(), }), ), event_journal: z .array( z.object({ capture_order: z.number().int().nonnegative(), collection: z.enum(PROCESS_CAPTURE_EVENT_COLLECTIONS), index: z.number().int().nonnegative(), }), ) .default([]), files_before: z.array(fileStateSchema), files_after: z.array(fileStateSchema), filesystem_effects: z.array(fileEffectSchema), truncated: z.boolean(), limitations: z.array(z.string()), residual_unknowns: z.array( z.object({ scope: z.enum([ "terminal", "interaction", "exit", "process", "filesystem", "cleanup", "network", "environment", ]), reason: z.string(), }), ), cleanup: z.object({ owned_process_group: z.literal("verified"), temporary_root: z.literal("removed"), }), }); const processCleanupResourceSchema = z.strictObject({ state: z.enum(["cleaned", "failed", "unverified", "not_required"]), reason: z.string().nullable(), }); const partialObservationFieldSchema = ( value: Schema, ) => z.discriminatedUnion("state", [ z.strictObject({ state: z.literal("available"), value }), z.strictObject({ state: z.literal("unavailable"), reason: z.string() }), ]); const incompleteProcessCaptureObservationsSchema = z.strictObject({ target_pid: partialObservationFieldSchema(z.number().int().positive()), frames: partialObservationFieldSchema(processCaptureShapeSchema.shape.frames), rendered_frames: partialObservationFieldSchema( processCaptureShapeSchema.shape.rendered_frames, ), interaction_events: partialObservationFieldSchema( processCaptureShapeSchema.shape.interaction_events, ), exit: partialObservationFieldSchema( z.strictObject({ code: z.number().int().nullable(), signal: z.number().int().nullable(), reason: z.enum(["exited", "timeout", "idle_timeout", "cancelled"]), }), ), settlement: partialObservationFieldSchema( z.strictObject({ state: z.enum(["quiesced", "alive_at_deadline", "unverifiable"]), elapsed_ms: z.number().int().nonnegative(), }), ), process_samples: partialObservationFieldSchema( processCaptureShapeSchema.shape.process_samples, ), filesystem_snapshots: z.strictObject({ before: partialObservationFieldSchema( z.strictObject({ files: z.array(fileStateSchema), truncated: z.boolean(), }), ), after: partialObservationFieldSchema( z.strictObject({ files: z.array(fileStateSchema), truncated: z.boolean(), }), ), }), event_journal: partialObservationFieldSchema( processCaptureShapeSchema.shape.event_journal, ), manifest: partialObservationFieldSchema( processCaptureShapeSchema.shape.manifest, ), }); const partialCleanupReportSchema = z.strictObject({ owned_process_group: processCleanupResourceSchema, terminal_renderer: processCleanupResourceSchema, temporary_root: processCleanupResourceSchema, }); const partialObservationMetadataSchema = z.strictObject({ cleanup: partialCleanupReportSchema, execution_failure: z.string().nullable(), }); const completedCaptureObservationSchema = z.strictObject({ capture: processCaptureShapeSchema .omit({ cleanup: true }) .extend({ settlement: processSettlementSchema }), }); const incompleteCaptureObservationSchema = z.strictObject({ observations: incompleteProcessCaptureObservationsSchema, }); /** Validated, non-comparable process observations attached to cleanup errors. */ export const partialProcessCaptureObservationSchema = z .union([ completedCaptureObservationSchema.extend( partialObservationMetadataSchema.shape, ), incompleteCaptureObservationSchema.extend( partialObservationMetadataSchema.shape, ), ]) .superRefine((partial, context) => { if ( !Object.values(partial.cleanup).some( ({ state }) => state === "failed" || state === "unverified", ) ) context.addIssue({ code: "custom", path: ["cleanup"], message: "partial observations require incomplete resource cleanup", }); if ("observations" in partial) { const { observations } = partial; if ( ![ observations.frames, observations.interaction_events, observations.process_samples, observations.filesystem_snapshots.before, observations.event_journal, ].some(({ state }) => state === "available") ) context.addIssue({ code: "custom", path: ["observations"], message: "incomplete observations contain no collected evidence", }); return; } const { capture } = partial; const settlement = capture.settlement.state === "quiesced" ? { ...capture.settlement, cleanup_outcome: "not_required" as const, } : { ...capture.settlement, cleanup_outcome: "cleaned" as const }; const validationCapture: UnverifiedProcessCapture = { ...capture, settlement, cleanup: { owned_process_group: "verified", temporary_root: "removed", }, }; for (const issue of collectProcessCaptureIssues(validationCapture)) context.addIssue({ code: "custom", path: ["capture", ...issue.path.split(".")], message: issue.message, }); }); /** One collected payload: completed capture data or explicit unavailable fields. */ export type PartialProcessCaptureObservation = z.infer< typeof partialProcessCaptureObservationSchema >; /** Exact serialized shape plus all process-capture semantic invariants. */ export const processCaptureSchema = processCaptureShapeSchema .superRefine((capture, context) => { if (capture.settlement.cleanup_outcome === "failed") context.addIssue({ code: "custom", path: ["settlement", "cleanup_outcome"], message: "verified captures cannot contain failed cleanup", }); for (const issue of collectProcessCaptureIssues(capture)) context.addIssue({ code: "custom", path: issue.path.split("."), message: issue.message, }); }) .describe( "The capture must preserve its canonical scenario, comparison, and normalization SHA-256 commitments; ordered capture timestamps and contiguous sequence numbers; before and final filesystem snapshots with truncation propagated; and exit-code consistency with deadline termination. When parsing older input without an event journal, REA supplies an empty journal; empty journals are valid. A non-empty journal must reference every captured observation exactly once with unique in-range references. These cross-field invariants are checked by REA after capture.", ); export { parseProcessCapture } from "./processCaptureParsing.js"; export type { ProcessCapture } from "./processCaptureParsing.js"; export { compareProcessCaptures, comparisonStatusSchema, deriveProcessComparisonStatus, PROCESS_COMPARISON_DIMENSIONS, processCaptureComparisonSchema, } from "./processComparison.js";