mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 15:26:25 +00:00
Remove REA permission grants, scope ceilings, approval confirmations, and inert settings. Preserve selected local evidence, accept caller-chosen commands and paths, and report actual runtime failures. Harden provider results, schema serialization, snapshot evidence binding, archive paths, source maps, native screenshots, managed EH parsing, and cancellation cleanup. Regenerate tool metadata and document direct invocation. BREAKING CHANGE: Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
186 lines
6.3 KiB
TypeScript
186 lines
6.3 KiB
TypeScript
import type { IPty } from "@lydell/node-pty";
|
|
|
|
import type {
|
|
InteractionEvent,
|
|
ProcessCapture,
|
|
ProcessReactiveControlRecord,
|
|
ProcessSample,
|
|
ProcessScenario,
|
|
ProtocolEvent,
|
|
TerminalFrame,
|
|
} from "../domain/processCapture.js";
|
|
import type { ProcessReactiveInput } from "../domain/processReactiveRuntime.js";
|
|
import { ProcessCaptureError } from "./ProcessCaptureError.js";
|
|
import type { ProcessCaptureJournal } from "./ProcessCaptureJournal.js";
|
|
import type { ProcessCheckpoints } from "./ProcessCheckpoints.js";
|
|
import type { CommandShimReplay } from "./CommandShimReplay.js";
|
|
import { ProcessReactiveCoordinator } from "./ProcessReactiveCoordinator.js";
|
|
import {
|
|
executeProcessReactiveEffects,
|
|
type ProcessReactiveTerminal,
|
|
unsupportedProcessReactiveFeatures,
|
|
} from "./ProcessReactiveEffects.js";
|
|
import { subscribeProcessReactiveObservations } from "./ProcessReactiveObservations.js";
|
|
import {
|
|
normalizeProcessSamples,
|
|
normalizeProcessShimEvent,
|
|
normalizeProtocolEvents,
|
|
} from "./ProcessNormalization.js";
|
|
import type { TerminalRenderer } from "./TerminalRenderer.js";
|
|
|
|
/** Coordinator and subscription owned by one live process capture. */
|
|
export interface ProcessReactiveHarness {
|
|
readonly coordinator: ProcessReactiveCoordinator;
|
|
readonly controls: readonly ProcessReactiveControlRecord[];
|
|
readonly unsubscribe: () => void;
|
|
}
|
|
|
|
const reactiveTerminalFor = (
|
|
terminal: IPty | undefined,
|
|
): ProcessReactiveTerminal | undefined => {
|
|
if (terminal === undefined) return undefined;
|
|
return {
|
|
write: (data) => terminal.write(data),
|
|
resize: (columns, rows) => terminal.resize(columns, rows),
|
|
closeInput: () => {
|
|
const end: unknown = Reflect.get(terminal, "end");
|
|
if (typeof end !== "function")
|
|
throw new ProcessCaptureError(
|
|
"PTY backend does not expose an stdin-close operation",
|
|
);
|
|
Reflect.apply(end, terminal, []);
|
|
},
|
|
kill: (signal) => terminal.kill(signal),
|
|
};
|
|
};
|
|
|
|
const controlKind = (
|
|
input: ProcessReactiveInput,
|
|
): ProcessReactiveControlRecord["kind"] | undefined =>
|
|
input.kind === "observation" ? undefined : input.kind;
|
|
|
|
/** Reject reactive declarations the current process adapter cannot execute. */
|
|
export const assertSupportedReactiveScenario = (
|
|
scenario: ProcessScenario,
|
|
): void => {
|
|
if (scenario.reactive === null) return;
|
|
const unsupported = unsupportedProcessReactiveFeatures(scenario.reactive);
|
|
if (unsupported.length === 0) return;
|
|
throw new ProcessCaptureError(
|
|
`reactive scenario requests unsupported features: ${unsupported.join(", ")}`,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Attach the serialized reactive runtime once the target PID is available.
|
|
*
|
|
* Subscription replays journal entries admitted during launcher startup before
|
|
* observing new records, so PID-dependent normalization cannot drop them.
|
|
*/
|
|
export const startProcessReactiveHarness = (options: {
|
|
readonly scenario: ProcessScenario;
|
|
readonly terminal: () => IPty | undefined;
|
|
readonly renderer: TerminalRenderer;
|
|
readonly checkpoints: ProcessCheckpoints;
|
|
readonly shimReplay: CommandShimReplay;
|
|
readonly started: number;
|
|
readonly capture: {
|
|
readonly interactions: InteractionEvent[];
|
|
readonly journal: ProcessCaptureJournal;
|
|
readonly frames: readonly TerminalFrame[];
|
|
readonly processSamples: readonly ProcessSample[];
|
|
readonly protocolEvents: () => readonly ProtocolEvent[];
|
|
readonly temporaryRoot: string;
|
|
};
|
|
}): ProcessReactiveHarness | undefined => {
|
|
if (options.scenario.reactive === null) return undefined;
|
|
const controls: ProcessReactiveControlRecord[] = [];
|
|
const coordinator = new ProcessReactiveCoordinator({
|
|
scenario: options.scenario.reactive,
|
|
executor: {
|
|
execute: (actions, signal) =>
|
|
executeProcessReactiveEffects(
|
|
{
|
|
scenario: options.scenario,
|
|
terminal: () => reactiveTerminalFor(options.terminal()),
|
|
renderer: options.renderer,
|
|
checkpoints: options.checkpoints,
|
|
interactions: options.capture.interactions,
|
|
journal: options.capture.journal,
|
|
startedAtMs: options.started,
|
|
now: Date.now,
|
|
timeBucketMs: options.scenario.normalization.time_bucket_ms,
|
|
},
|
|
actions,
|
|
signal,
|
|
),
|
|
},
|
|
onDecision: (decision, input) => {
|
|
const kind = controlKind(input);
|
|
if (kind === undefined || decision.kind !== "finished") return;
|
|
controls.push({
|
|
sequence: controls.length,
|
|
kind,
|
|
after_capture_order:
|
|
decision.snapshot.observations.at(-1)?.capture_order ?? -1,
|
|
});
|
|
},
|
|
});
|
|
return {
|
|
coordinator,
|
|
controls,
|
|
unsubscribe: subscribeProcessReactiveObservations({
|
|
journal: options.capture.journal,
|
|
frames: options.capture.frames,
|
|
interactions: options.capture.interactions,
|
|
checkpointAt: (index) => options.checkpoints.at(index),
|
|
processSampleAt: (index) => {
|
|
const rootPid = options.terminal()?.pid;
|
|
return rootPid === undefined
|
|
? undefined
|
|
: normalizeProcessSamples(
|
|
options.capture.processSamples,
|
|
options.scenario,
|
|
rootPid,
|
|
)[index];
|
|
},
|
|
protocolEventAt: (index) =>
|
|
normalizeProtocolEvents(
|
|
options.capture.protocolEvents().slice(index, index + 1),
|
|
options.scenario,
|
|
)[0],
|
|
shimEventAt: (index) => {
|
|
const event = options.shimReplay.events[index];
|
|
const rootPid = options.terminal()?.pid;
|
|
return event === undefined || rootPid === undefined
|
|
? undefined
|
|
: normalizeProcessShimEvent(
|
|
event,
|
|
options.scenario,
|
|
options.capture.temporaryRoot,
|
|
rootPid,
|
|
);
|
|
},
|
|
sink: coordinator,
|
|
}),
|
|
};
|
|
};
|
|
|
|
/** Project internal reducer state into the stable Process Capture shape. */
|
|
export const projectProcessReactiveRun = (
|
|
harness: ProcessReactiveHarness | undefined,
|
|
): ProcessCapture["reactive_run"] => {
|
|
if (harness === undefined) return null;
|
|
const snapshot = harness.coordinator.snapshot;
|
|
const completion =
|
|
snapshot.status === "running"
|
|
? { status: snapshot.status, outcome: null }
|
|
: { status: snapshot.status, outcome: snapshot.outcome };
|
|
return {
|
|
...completion,
|
|
active_state: snapshot.active_state,
|
|
transitions: snapshot.transitions,
|
|
controls: harness.controls,
|
|
};
|
|
};
|