/** * Run a governed child process under hard limits. * * G8 (review findings A-R1/B-I5, A-R3). `delegate` used to spawn `pi` with no output cap, no timeout, * an abort listener attached too late to observe an already-aborted signal, and a non-zero exit reported * to the model as an ordinary result. Each of those is a way for a child to outlive or overwhelm the * orchestrator that is supposed to be governing it — which is the whole premise of this package. * * It lives here, out of `extensions/grants.ts`, so it can be tested against real processes without pi. */ export { ENV_CHILD_IDLE_TIMEOUT, ENV_CHILD_TIMEOUT } from "./env-names.ts"; /** * The longest prefix of `text` that fits in `budget` BYTES, never splitting a character. * * Both halves matter. Truncating by `String.prototype.slice` counts UTF-16 code units against a byte budget, * which overruns by the encoding width of whatever the child printed. Truncating the *buffer* instead would * respect the budget and split a multi-byte character or a surrogate pair, putting a lone `\ud83d` in the * result. So the walk is by code POINT (`for…of` iterates code points, keeping surrogate pairs whole) and the * budget is checked in bytes before each one is admitted. * * Exported for the test that feeds it CJK and emoji: an ASCII-only test cannot fail on any of this, which is * exactly why the original defect survived a test named for it. */ export declare function takeBytes(text: string, budget: number): string; /** * Operator override for the child wall-clock limit, in seconds. * * Deliberately NOT in `GRANT_ENV_KEYS`: those are stripped from a child's environment and re-supplied * only by the spawn plan, which is right for capability state and wrong for an operator preference. This * one should simply inherit, so a bound set at the root applies all the way down. */ /** Read the override, falling back to the default on absent *or* malformed input (G7's rule). */ export declare function timeoutFromEnv(raw: string | undefined): number; /** The inactivity bound, same rule: absent, malformed and zero all select the default rather than disabling it. */ export declare function idleTimeoutFromEnv(raw: string | undefined): number; export interface ChildRunRequest { command: string; args: string[]; env: NodeJS.ProcessEnv; cwd: string; signal?: AbortSignal; /** Hard cap on captured output. Beyond it the child is killed and the result flagged. */ maxOutputBytes?: number; /** Wall-clock ceiling. On expiry: SIGTERM, then SIGKILL after `killGraceMs`. A runaway bound, not the working bound. */ timeoutMs?: number; killGraceMs?: number; /** * Inactivity bound (ADR-0038 note, PR 3e): the child is stopped when this long passes with no activity. Activity * is any stdout/stderr byte, or a change in what `activityProbe` returns. Undefined means no inactivity bound. */ idleTimeoutMs?: number; /** * A cheap marker of the child's progress that is not on its stdout, polled every `activityProbeIntervalMs`: the * size and mtime of its pi session file, which pi appends to on every message and tool result. A change resets * the inactivity clock; `undefined` (file not there yet) counts as no change. Never awaited on the control path. */ activityProbe?: () => Promise | string | number | undefined; activityProbeIntervalMs?: number; /** Absolute epoch-millisecond hard cap: SIGKILL fires here even when the SIGTERM grace has not elapsed. */ hardDeadlineAt?: number; /** * Called with each chunk as it arrives, for the parent's progress display (ADR-0032). * * **Display only, and the distinction is load-bearing.** The child's answer is still `text`, assembled here * and returned; a caller must never treat what it saw streamed as the result. A partial stream that could be * mistaken for a complete answer is R-03's defect — a missing result indistinguishable from an empty one — * with a new cause. * * Bounded by the same `maxOutputBytes` as `text`, so the cap governs the transcript and not merely memory. * * Exceptions are swallowed: a renderer is not a governance control, and one that throws must not kill a * governed child mid-task. */ onOutput?: (chunk: string) => void; /** Optional raw observation; isolated from control, never awaited, separate from display text. */ onObservation?: (stream: "stdout" | "stderr", bytes: Uint8Array) => void; /** Passive EOF observation. Never a replacement child/settlement handle. Exceptions are isolated. */ onStreamEnd?: (stream: "stdout" | "stderr") => void; /** Security hook called immediately after spawn, before output handling (for lease attachment). */ onSpawn?: (pid: number) => void; } export interface ChildRunResult { /** Exit code, or `null` when nothing was spawned or the child was killed by a signal. */ code: number | null; text: string; truncated: boolean; timedOut: boolean; /** Set with `timedOut` when it was the inactivity bound, not the wall-clock ceiling, that stopped the child. */ idle?: true; aborted: boolean; /** Signal that ended the process, separate from the nullable numeric exit code. */ signal?: NodeJS.Signals | null; /** Set when the process could not be started at all. */ spawnError?: string; } /** 1 MiB. A delegation returns a summary; anything larger is a runaway, not an answer. */ export declare const DEFAULT_MAX_OUTPUT_BYTES: number; /** * Six hours (PR 3e, 2026-09-22). No longer the working bound: `DEFAULT_IDLE_TIMEOUT_MS` is what stops a hung child. * This is the runaway ceiling for a child that keeps producing events forever, and it exists so that no typo in the * inactivity variable can make a child unbounded. The history: ten minutes, twenty (ADR-0038), sixty (its note), each * time because a working child was cut off by a wall clock that cannot tell working from hung. */ export declare const DEFAULT_TIMEOUT_MS: number; /** Fifteen minutes with no stdout byte and no session-file change. A hung child is a silent one; a build is not. */ export declare const DEFAULT_IDLE_TIMEOUT_MS: number; export declare const DEFAULT_ACTIVITY_PROBE_INTERVAL_MS = 2000; /** Grace between SIGTERM and SIGKILL. A child that ignores SIGTERM must not make the timeout advisory. */ export declare const DEFAULT_KILL_GRACE_MS = 5000; export declare function runChild(request: ChildRunRequest): Promise; //# sourceMappingURL=run-child.d.ts.map