import WebSocket from 'ws'; import { BaseWebSocketConnection } from '../base-websocket'; import type { EnvVarsProvider } from '../env-vars-filter'; import { TerminalSessionManager } from './terminal-session-manager'; /** * Information about a single tmux session. */ export interface TmuxSessionInfo { /** Session name (e.g. "ais-abc123") */ name: string; /** Number of windows in the session */ windows: number; /** Whether the session is currently attached */ attached: boolean; /** Session creation time as Unix timestamp (seconds since epoch) */ created: number; /** Last activity time as Unix timestamp (seconds since epoch) */ activity: number; } /** * Messages sent from API server to agent */ export interface TerminalServerMessage { type: 'open' | 'stdin' | 'resize' | 'close' | 'auth_success' | 'error' | 'tmux_list_sessions' | 'tmux_kill_session' | 'split_pane'; sessionId?: string; data?: string; cols?: number; rows?: number; cwd?: string; message?: string; /** * Additional environment variables to inject into the PTY session. * Merged on top of the provider-based envVarsOverride. * GIT_SSH_KEY_CONTENT_BASE64: base64-encoded PEM private key to set up * GIT_SSH_COMMAND for the session (processed by TerminalSession). */ envVarsOverride?: Record; /** * Resume-only open (3-repo protocol contract). When true the agent must * NEVER spawn a new PTY: it either reattaches to the existing live PTY * (meta exactly matching) or replies with `resume_failed`. */ resume?: boolean; /** * Resume-validation meta recorded at PTY creation and verified on resume. */ meta?: { tenantCode: string; projectCode: string; userId: string; }; /** Request ID for tmux management messages */ requestId?: string; /** Session name for tmux_kill_session */ name?: string; /** * Owner (user) hash injected by the API for tmux management messages. * Computed API-side as sha256(auth.userId).hex.slice(0, 12) — never sent by * the web client. Used to filter tmux_list_sessions to the requester's own * `ais-{userHash}-*` sessions. When absent the agent returns no sessions * (fail-safe; no fallback to listing everything). */ userHash?: string; /** * Attach target tmux session name (forwarded from web via API). * When set, the agent attaches to this existing tmux session instead of * creating a new one named ais-{sessionId}. */ tmuxSessionName?: string; /** * Pane split direction for split_pane messages. * 'horizontal' (default) splits left/right; 'vertical' splits top/bottom. */ direction?: 'horizontal' | 'vertical'; /** * Target tmux session name for split_pane messages. */ sessionName?: string; } /** * Messages sent from agent to API server */ export interface TerminalAgentMessage { type: 'ready' | 'stdout' | 'exit' | 'error' | 'replay' | 'resume_failed' | 'tmux_sessions' | 'tmux_session_killed' | 'tmux_pane_split'; sessionId?: string; data?: string; code?: number | null; error?: string; pid?: number; cols?: number; rows?: number; /** resume_failed reason: 'not_found' | 'meta_mismatch' | 'dead' */ reason?: string; /** Request ID for tmux management responses */ requestId?: string; /** List of tmux sessions (for tmux_sessions response) */ sessions?: TmuxSessionInfo[]; /** Session name (for tmux_session_killed response) */ name?: string; /** Whether the kill operation succeeded (for tmux_session_killed response) */ success?: boolean; /** Session name (for tmux_pane_split response) */ sessionName?: string; } export type { EnvVarsProvider } from '../env-vars-filter'; export declare class TerminalWebSocket extends BaseWebSocketConnection { private readonly token; private readonly agentId; private readonly projectDir?; private readonly envVarsProvider?; private readonly onAuthRejected?; private readonly manager; private readonly wsUrl; constructor(apiUrl: string, token: string, agentId: string, projectDir?: string | undefined, envVarsProvider?: EnvVarsProvider | undefined, onAuthRejected?: (() => void) | undefined); getSessionManager(): TerminalSessionManager; protected createWebSocket(): WebSocket; protected onOpen(_ws: WebSocket, resolve: (value: void) => void): void; protected onParsedMessage(msg: TerminalServerMessage): void; /** * Transient WebSocket drop (ALB idle drop / heartbeat false-positive * terminate / network blip). The base class fires this from the ws 'close' * event, so this is where real transient disconnects land — NOT onDisconnect() * (which is only invoked from the explicit disconnect() method). * * Keep every PTY alive within the grace window so a reconnect with the same * sessionId can resume the user's live shell. If no reconnect arrives within * SESSION_GRACE_TIMEOUT_MS the PTY is killed by the grace timer. A heartbeat * false-positive terminate also fires 'close', so misdetected drops likewise * preserve the PTY for resume. */ protected onWebSocketClose(): void; /** * Server-side permanent authentication rejection (invalid token, or Agent ID * token-binding mismatch). The base class calls this instead of * onWebSocketClose() in that case, since reconnecting to resume is not * possible — the connection will never be re-established with the same * credentials. Kill every PTY immediately rather than arming the grace * window, matching the explicit-shutdown behavior in disconnect(). */ protected onPermanentClose(): void; /** * Explicit, user/agent-initiated shutdown. Unlike a transient drop, this is a * genuine teardown so every PTY is killed immediately rather than being kept * alive for the grace window. * * The base disconnect() calls onDisconnect() (left as the no-op default here, * so it does NOT arm grace), then closes the socket. We follow up with * closeAll() to kill every PTY. Because onDisconnect() does not schedule grace * timers, there is nothing for closeAll() to undo — the two paths no longer * fight (transient = grace via onWebSocketClose; explicit = closeAll here). */ disconnect(): void; private handleOpen; /** * Resume-only open (`resume: true`). Reattaches to the existing live PTY * when the presented meta exactly matches; otherwise replies `resume_failed` * with the validation reason. A failed resume NEVER spawns a new PTY. * * Message order on success is fixed by spec (and tests): * ready → replay (skipped when the scrollback buffer is empty) → stdout… * This whole method is synchronous, so no PTY data event can interleave * between the relay re-registration and the ready/replay sends; any new * output is delivered as `stdout` strictly after `replay`. */ private handleResumeOpen; /** * Wire the session's PTY output/exit to the WebSocket. TerminalSession holds * a SINGLE callback per event (setter semantics), so calling this again on * resume replaces — never stacks — the relay (no duplicate stdout frames). */ private attachSessionRelay; private handleStdin; private handleResize; private handleClose; private handleTmuxListSessions; private handleTmuxKillSession; /** * Split a pane in an existing tmux session. * * Security: validates sessionName against an allowlist of safe characters * (alphanumeric, hyphen, underscore, colon, period) to prevent command * injection, as a second layer of defense alongside API-side validation. */ private handleSplitPane; private send; } //# sourceMappingURL=terminal-websocket.d.ts.map