/** Node stdio binding and type-state handshake for the MSP connection. */ import { spawn } from "node:child_process"; import type { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from "node:child_process"; import type { InitializeParams, InitializeResult } from "../msp.js"; import { Connection } from "./connection.js"; import type { ConnectionOptions, DuplexTransport, NotificationHandler, ProtocolErrorHandler, ServerRequestHandler } from "./connection.js"; import type { FingerprintWarning } from "../fingerprint.js"; /** * What `spawnMspConnection` needs to launch a host: the command to run, plus * optional arguments, working directory, environment, connection tuning, a * stderr tap, and the shutdown drain budget. */ export interface SpawnMspConnectionOptions { readonly command: string; readonly args?: readonly string[]; readonly cwd?: string; readonly env?: NodeJS.ProcessEnv; readonly connection?: ConnectionOptions; /** Raw stderr chunks, drained from process birth and never parsed. */ readonly onStderr?: (chunk: string) => void; /** See {@link MuseServeChildOptions.shutdownTimeoutMs}. */ readonly shutdownTimeoutMs?: number; } /** * How the host process ended: its exit code, or the signal that ended it. * Exactly one of the two is non-null. */ export interface ProcessExit { readonly code: number | null; readonly signal: NodeJS.Signals | null; } /** * The SDK's reading of a host exit. Branch on `kind`: a clean shutdown, a * crash, an unhandled error, or one of the documented error exits. Every arm * but `cleanShutdown` carries its exit evidence (code or signal plus a * bounded stderr tail), and, where the meaning is known, whether retrying * can help. */ export type ExitClassification = { readonly kind: "cleanShutdown"; } | { readonly kind: "unhandledError"; readonly exitCode: number; readonly stderrTail: readonly string[]; } | { readonly kind: "usageError" | "configError" | "leaseUnavailable" | "sdkSurfaceUnavailable"; readonly exitCode: number; readonly stderrTail: readonly string[]; readonly retry: "never" | "fix-config" | "after-lease-release"; } | { readonly kind: "crash"; readonly exitCode: number | null; readonly exitSignal: string | null; readonly stderrTail: readonly string[]; }; /** * Options for launching an SDK-owned host process: the binary to run, plus * optional arguments, working directory, environment, a stderr tap, and the * shutdown drain budget. */ export interface MuseServeChildOptions { readonly museBin: string; readonly args?: readonly string[]; readonly cwd?: string; readonly env?: NodeJS.ProcessEnv; /** Raw evidence callback for consumers that also keep a full diagnostic log. */ readonly onStderr?: (chunk: string) => void; /** * How long `close()` lets the host drain after stdin EOF before the SDK * ends the process it owns. Defaults to SS2.1.2's 30 s host drain timeout; * set it to match a host configured with a different one. `0` skips the * drain window (SIGTERM at once) — it is NOT an instant kill: the fixed * SIGTERM→SIGKILL grace still runs. Must be an integer in * `0..2147483647`; anything else throws `RangeError` at spawn. */ readonly shutdownTimeoutMs?: number; /** * Deterministic-test seam; production always uses `child_process.spawn`. * A stub here observes the exact options the call site passes, so a * `windowsHide` override at the call site fails the pin (FR-45141-1). */ readonly spawnFn?: typeof spawn; } declare const ownedTransport: unique symbol; declare const adoptFlushSource: unique symbol; /** * Module friend seam: did `initialize` get a usable answer before the * handshake failed? Exported from this MODULE only (never the package * barrel) so the facade can word a host lost AFTER answering truthfully * (#49912) without a new public member on `MspHandshake`. */ export declare const initializeAnswered: unique symbol; /** * Shutdown deadline contract: `expired` resolves and never rejects; after * `clear()` it may remain pending forever. */ interface ShutdownDeadline { readonly expired: Promise; clear(): void; } type ShutdownDeadlineFactory = (budgetMs: number) => ShutdownDeadline; /** * Is this `stdin.end()` error the benign close race — the child exited * between the liveness check and the end() call, so its stdin is already * gone? That IS the closed end-state close() wants (SS2.11: the caller * still awaits `exited` and gates on the exit code); anything else is a * real failure. `ECANCELED` is the same owned-teardown race one stage later: * the SIGTERM->SIGKILL backstop destroys stdin while a wedged write is still * queued, and libuv cancels that write instead of erroring it. Exported only * for its deterministic unit test. */ export declare function isBenignCloseRace(error: unknown): boolean; /** * Internal transport over an already-spawned child's stdio. Exported from * this MODULE only (never the package barrel) so the deterministic * close()-race tests can construct it around a stub child — the live arm * awaits `exited` first and cannot reach the race branch (PR #15641 review). */ export declare class ChildStdioTransport implements DuplexTransport { #private; readonly child: ChildProcessWithoutNullStreams; readonly incoming: AsyncIterable; readonly exited: Promise; constructor(child: ChildProcessWithoutNullStreams, onStderr?: (chunk: string) => void, shutdownTimeoutMs?: number, ownsProcessGroup?: boolean, /** Deterministic-test seam; production always uses `makeShutdownDeadline`. */ deadlineFactory?: ShutdownDeadlineFactory); write(chunk: string): Promise; /** * The single owner of shutdown (#15943). EOF first, then bounded: SS2.1.2 * gives the host a drain window and says the remainder past it has crash * semantics, so past the window the SDK ends the process it owns rather * than waiting on an EOF that may never come. Memoized, so concurrent and * repeated calls share one attempt and one set of timers. */ /** Module-private friend seam used by the handshake that owns both sides. */ [adoptFlushSource](source: () => Promise): void; close(flushed?: Promise): Promise; } /** * The exact options `MuseServeChild.spawn` hands to `child_process.spawn`, * factored out so the spawn-options pin can assert on them without spawning. * Exported from this MODULE only (never the package barrel) — the same * module-only-test-seam precedent as `isBenignCloseRace`. */ export declare function buildSpawnOptions(options: { readonly cwd?: string; readonly env?: NodeJS.ProcessEnv; readonly detached: boolean; }): SpawnOptionsWithoutStdio; /** One owned MSP host process with SS2.11 diagnostics and total exit mapping. */ export declare class MuseServeChild { #private; readonly exit: Promise; private constructor(); static spawn(options: MuseServeChildOptions): MuseServeChild; get stderrTail(): readonly string[]; /** Module-private friend seam used by the handshake machine for this child. */ [ownedTransport](): ChildStdioTransport; close(): Promise; } /** The only public state before `initialized`: callers cannot send traffic. */ export declare class MspHandshake { #private; readonly child: MuseServeChild; constructor(options: SpawnMspConnectionOptions); get exited(): Promise; onServerRequest(handler: ServerRequestHandler): void; onNotification(handler: NotificationHandler): void; onProtocolError(handler: ProtocolErrorHandler): void; close(): Promise; /** See {@link initializeAnswered}. */ [initializeAnswered](): boolean; initialize(params: InitializeParams): Promise; } /** A fully initialized connection plus its owned process boundary. */ export declare class SpawnedMspConnection { #private; readonly connection: Connection; readonly child: MuseServeChild; readonly initializeResult: InitializeResult; readonly fingerprintWarning: FingerprintWarning | undefined; readonly exited: Promise; constructor(connection: Connection, transport: ChildStdioTransport, child: MuseServeChild, initializeResult: InitializeResult, fingerprintWarning: FingerprintWarning | undefined); close(): Promise; } /** Spawn one owned host and begin its SS1.4 handshake state machine. */ export declare function spawnMspConnection(options: SpawnMspConnectionOptions): MspHandshake; export {};