/** * interactive.ts, PTY-backed prompt-answer path for the exec tool. * * PROBLEM. All exec spawn paths pipe stdout/stderr and leave stdin unwired, so * a child that stops to ask a question (an `ssh` host-key confirmation, a * `gh auth login` flow, a `sudo` password ask) hangs until the timeout and the * exchange is lost. Many of those prompts are written to and read from the * controlling terminal (`/dev/tty`), so even piping stdin would not reach them *, the child needs a real PTY. * * APPROACH. Prompt-prone commands (and any command with `interactive: true`) * run under a PTY allocated by the host's `script(1)` binary (util-linux on * Linux, the BSD variant on macOS). The PTY wrapper is nested INSIDE the * sandbox argv, `[...sandboxArgv, script, ...]`, so when the per-command * bwrap boundary is active it stays the outermost layer and holds unchanged * under the PTY. When output goes quiet on a prompt-shaped tail, the pending * prompt text is surfaced through the injected `requestPromptAnswer` seam, * wired at the composition root to the SAME approval broker as a permission * ask, so every surface's existing approval/attention machinery renders it. * The typed answer is written to the PTY and the run continues; the full * exchange (prompt, echoed answer, subsequent output) lands in the tool * result transcript. * * DETECTION LIMITS (honest). There is no in-band signal that a child is * blocked reading its terminal, the only observable signals are the output * stream and time. Detection is therefore a heuristic: an unterminated final * line that looks like a question (ends with `:` or `?`, or carries a * `[y/N]` / `(yes/no)` style choice) followed by a quiet window with the * process still alive. This misses prompts that do not match the shapes below * (a bare `> ` REPL prompt, localized text, full-screen TUIs) and cannot see * a no-echo password read that printed nothing. A prompt that is never * answered, seam unwired, surface ignored it, or the human walked away, * ends in the normal timeout, with the detected prompt text reported on the * result (`pending_prompt`) so the failure is diagnosable instead of a silent * hang. PTY output merges stderr into stdout by nature; interactive results * carry the merged transcript in `stdout` and note `pty: true`. */ import type { ExecCommandInput, ExecCommandResult } from './schema.js'; /** The honest, host-probed availability of the PTY backend. */ export interface PtyAvailability { readonly available: boolean; readonly backend: 'script' | 'none'; /** Resolved `script` path when available. */ readonly scriptPath?: string | undefined; /** util-linux (`script -qefc cmd /dev/null`) vs BSD (`script -q /dev/null sh -c cmd`) argv shape. */ readonly flavor?: 'util-linux' | 'bsd' | undefined; /** Stated reason, a diagnosis when unavailable, a one-line summary when available. */ readonly reason: string; } /** Raw host-probe inputs so {@link detectPtyAvailability} stays pure and unit-testable. */ export interface PtyHostProbe { /** `process.platform`. */ readonly platform: string; /** Resolved `script` path, or null when not on PATH. */ readonly scriptPath: string | null; } /** Decide PTY availability from a host probe. Pure. */ export declare function detectPtyAvailability(probe: PtyHostProbe): PtyAvailability; /** Probe the real host for a `script` binary. Impure; non-PTY platforms short-circuit. */ export declare function probePtyHost(): PtyHostProbe; /** * Construct the PTY wrapper argv that REPLACES `['/bin/sh','-c',cmd]`. The * caller prepends the sandbox argv unchanged, so the boundary (when active) * wraps the PTY allocation itself: `[...sandboxArgv, ...buildPtyArgv(...)]`. */ export declare function buildPtyArgv(availability: PtyAvailability, command: string): string[]; /** * Extract the pending prompt from an output transcript tail, or null when the * tail does not look like a prompt. A prompt is an unterminated (no trailing * newline) final line of plausible length matching a known question shape. */ export declare function findPendingPrompt(transcript: string): string | null; /** Whether any segment of the command has a prompt-prone base command. */ export declare function isPromptProneCommand(command: string): boolean; /** A pending prompt surfaced through the approval/attention machinery. */ export interface ExecPromptAsk { readonly command: string; /** The detected prompt line (the unterminated output tail). */ readonly prompt: string; /** Bounded recent transcript for context (last ~2000 chars). */ readonly recentOutput: string; readonly workingDirectory?: string | undefined; } /** The surface's answer. `answered: false` means the ask was declined. */ export interface ExecPromptAnswer { readonly answered: boolean; /** The text to feed the waiting child (a trailing newline is appended). */ readonly text?: string | undefined; } /** * The resolved interactive context the exec runtime threads per call. Null on * a createExecTool with no interactive wiring, then every command runs the * unchanged pipe-based path. */ export interface ExecInteractionRuntime { readonly availability: PtyAvailability; /** * Broker a pending-prompt answer through the approval broker. Wired at the * composition root (see runtime/permissions/exec-prompt-wiring.ts). When * absent, prompts are still detected and reported on the result, but cannot * be answered. */ readonly requestPromptAnswer?: ((ask: ExecPromptAsk) => Promise) | undefined; /** Quiet window before a prompt-shaped tail counts as pending. Default 1200ms. */ readonly quietWindowMs?: number | undefined; } /** * Whether this command should take the PTY path: explicit `interactive: true`, * or a prompt-prone base command, in both cases only when the host actually * has a PTY backend (never faked; unavailable → the unchanged pipe path). */ export declare function shouldRunInteractive(interaction: ExecInteractionRuntime | null, cmdInput: ExecCommandInput, cmdStr: string): boolean; interface InteractiveRunInput { readonly cmdStr: string; readonly cwd: string | undefined; readonly env: Record; readonly timeoutMs: number; readonly startTime: number; /** Sandbox argv prefix, prepended UNCHANGED so the boundary wraps the PTY. */ readonly sandboxArgv: readonly string[]; readonly interaction: ExecInteractionRuntime; readonly signal?: AbortSignal | undefined; } /** * Run a command under a PTY with the prompt-answer loop. The transcript * (stdout+stderr merged by the PTY) accumulates in `stdout`; each detected * prompt is brokered through `requestPromptAnswer` and the answer is written * back to the child's terminal. See the module doc for detection limits. */ export declare function runInteractiveCommand(input: InteractiveRunInput): Promise; export {}; //# sourceMappingURL=interactive.d.ts.map