aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
151 lines • 6.92 kB
TypeScript
/**
* WebSocket PTY Bridge
*
* Bridges browser WebSocket connections to PTY sessions. Three modes:
*
* Phase 1 (local exec): spawns commands using node-pty.
* Phase 2a (sandbox WS): bridges to agentic-sandbox management WebSocket.
* Connects to the sandbox's management WS server, subscribes to an agent,
* starts an interactive shell, and multicasts PTY output to all browser clients.
* Leverages the sandbox's tokio broadcast channel for zero-copy multicast.
* Phase 2b (sandbox REST, deprecated): polls /api/v1/tasks log endpoint.
*
* Browser ↔ serve protocol:
* Client → Server: { type: 'data', payload: string } — stdin to PTY
* Client → Server: { type: 'resize', cols: number, rows: number }
* Client → Server: { type: 'close' } — request graceful shutdown
* Server → Client: { type: 'data', payload: string } — stdout/stderr from PTY
* Server → Client: { type: 'exit', code: number } — PTY process exited
* Server → Client: { type: 'error', message: string } — error notification
*
* Sandbox management WS protocol (agentic-sandbox):
* → { type: 'subscribe', agent_id }
* → { type: 'start_shell', agent_id, cols, rows }
* → { type: 'list_sessions', agent_id } — resolves session_name (#901)
* → { type: 'send_input', agent_id, command_id, data }
* → { type: 'pty_resize', agent_id, command_id, cols, rows }
* → { type: 'kill_session', agent_id, session_name } — must use session_name, not command_id
* ← { type: 'shell_started', agent_id, command_id } — idempotent: same cmd_id on reconnect (#903)
* ← { type: 'session_list', agent_id, sessions[] } — provides session_name for kill (#901)
* ← { type: 'output', agent_id, command_id, stream, data, ts, seq? }
* ← { type: 'session_killed' | 'session_detached', agent_id, exit_code? }
*
* Replay-on-attach (#1144). After `shell_started`, AIWG sends:
* → { type: 'join_session', agent_id, command_id, replay_from }
* and the sandbox emits buffered output frames carrying `seq`. AIWG prepends
* `\x1bc` (full terminal reset) ahead of the first replay frame so xterm.js
* agrees with tmux on the starting cursor/screen state, eliminating the
* first-frame alignment glitch. On unexpected disconnect, the bridge resumes
* from the last observed seq instead of replaying from zero.
*
* Capability negotiation (operator review on #1144, 2026-05-07): the original
* implementation gated this strictly on the sandbox advertising `join_session`
* + `replay_buffer` in a server-hello banner (agentic-sandbox#190). That
* banner doesn't ship today, so the gate kept replay disabled even on
* sandboxes that fully support it. The bridge now uses an opportunistic
* probe: if the banner explicitly signals support, replay is enabled; if the
* banner is absent, the bridge sends `join_session` anyway and treats an
* `error` response with an "unknown" / "not supported" / "session not found"
* message as the negative signal. Either path is logged so an operator can
* tell from `aiwg serve` output which mode the bridge is running in.
*
* @issue #712
* @issue #1144 — replay session history when AIWG attaches to running session
* @see #657 — agentic-sandbox PTY transport
* @see #711 — HTTP server scaffold
*/
export interface WsMessage {
type: 'data' | 'resize' | 'close' | 'exit' | 'error';
payload?: string;
message?: string;
cols?: number;
rows?: number;
code?: number;
}
export interface PtySession {
id: string;
/** Connected WebSocket clients (wsId → ws) */
clients: Map<string, WebSocketLike>;
/** Recent output buffer for reconnect replay (max OUTPUT_BUFFER_MAX chars) */
outputBuffer: string;
/** Underlying IPty instance (node-pty) */
pty: PtyLike | null;
/** Timestamp of last client disconnect (for cleanup) */
lastDisconnect: number;
/** Whether the PTY process has exited */
exited: boolean;
}
/** Minimal WebSocket interface for dependency injection / testing */
export interface WebSocketLike {
send(data: string): void;
close(code?: number, reason?: string): void;
readonly readyState: number;
}
/** Minimal IPty interface (subset of node-pty IPty) */
export interface PtyLike {
write(data: string): void;
resize(cols: number, rows: number): void;
kill(signal?: string): void;
onData(callback: (data: string) => void): void;
onExit(callback: (event: {
exitCode: number;
}) => void): void;
}
export declare class PtySessionRegistry {
private sessions;
private cleanupTimer;
constructor();
get(id: string): PtySession | undefined;
create(id: string): PtySession;
delete(id: string): void;
addClient(sessionId: string, clientId: string, ws: WebSocketLike): void;
removeClient(sessionId: string, clientId: string): void;
appendOutput(sessionId: string, data: string): void;
broadcast(sessionId: string, msg: WsMessage): void;
private evictExpired;
shutdown(): void;
}
export declare const registry: PtySessionRegistry;
/**
* Spawn a PTY process for the given session.
*
* Priority order:
* 1. Explicit wsEndpoint opt → sandbox management WebSocket bridge
* 2. Auto-detect: first connected sandbox in registry → sandbox WS bridge
* 3. AIWG_SANDBOX_ENDPOINT env var → legacy REST polling (deprecated)
* 4. Fallback → local node-pty
*
* @issue #657 — agentic-sandbox PTY transport
*/
export declare function spawnPty(session: PtySession, command: string, args: string[], opts?: {
cols?: number;
rows?: number;
cwd?: string;
sandboxEndpoint?: string;
agentId?: string;
/** agentic-sandbox management WebSocket URL (e.g. ws://localhost:8121) */
wsEndpoint?: string;
}): Promise<void>;
/**
* Handle a new WebSocket connection for a PTY session.
*
* @param sessionId - PTY session ID from the URL path
* @param ws - WebSocket-like interface
* @param command - Command to spawn (default: 'aiwg')
* @param args - Command arguments (default: ['mc', 'watch'])
* @param cwd - Working directory
* @param wsEndpoint - Optional: sandbox management WS URL for explicit agent targeting
* @param agentId - Optional: sandbox agent ID to target (requires wsEndpoint)
*/
export declare function handlePtyConnection(sessionId: string, ws: WebSocketLike, command?: string, cmdArgs?: string[], cwd?: string, wsEndpoint?: string, agentId?: string): Promise<void>;
/**
* Create a Hono-compatible WebSocket event object for the PTY route.
*
* Used with `upgradeWebSocket` from `@hono/node-server/ws`:
*
* ```ts
* app.get('/ws/pty/:sessionId', upgradeWebSocket((c) => createPtyWsHandler(c)));
* ```
*/
export declare function createPtyWsHandler(c: any): any;
//# sourceMappingURL=pty-bridge.d.ts.map