# Phase 2: WebSocket Gateway — Implementation Plan ## Goal Add a WebSocket gateway to the Flynn daemon that allows real-time communication from web clients (and eventually any WS client). The gateway wraps existing daemon components (session manager, agent, tool registry) — no refactoring of existing code. ## Approach Incremental, additive. Existing Telegram bot and TUI continue working unchanged. The gateway is a new module that starts alongside them. ## Architecture ``` src/gateway/ ├── protocol.ts # JSON-RPC message types + event types ├── server.ts # WebSocket server (ws library), connection lifecycle ├── router.ts # Method routing → handler dispatch ├── auth.ts # Token auth + Tailscale identity headers ├── session-bridge.ts # Maps WS client connections to agent sessions └── handlers/ ├── agent.ts # agent.send (streaming), agent.cancel, agent.status ├── sessions.ts # sessions.list, sessions.history, sessions.create ├── tools.ts # tools.list, tools.invoke └── system.ts # system.health, system.info ``` ## Protocol JSON-RPC-like over WebSocket (not full JSON-RPC 2.0 — simpler): ```typescript // Client → Server interface GatewayRequest { id: number; // Client-assigned request ID method: string; // e.g. "agent.send" params: object; // Method-specific parameters } // Server → Client (success) interface GatewayResponse { id: number; // Matches request ID result: object; // Method-specific result } // Server → Client (error) interface GatewayError { id: number; // Matches request ID error: { code: number; // Error code (negative = protocol, positive = app) message: string; // Human-readable description }; } // Server → Client (streaming event, multiple per request) interface GatewayEvent { id: number; // Matches originating request ID event: string; // Event type name data: object; // Event-specific payload } ``` ### Event Types (for agent.send streaming) | Event | Data | When | |-------|------|------| | `content` | `{ text: string }` | Text chunk from model | | `tool_start` | `{ tool: string, args: object }` | Tool execution beginning | | `tool_end` | `{ tool: string, result: { success, output, error? } }` | Tool execution complete | | `done` | `{ content: string, usage: { inputTokens, outputTokens } }` | Final response | | `error` | `{ code: number, message: string }` | Error during processing | ### Error Codes | Code | Meaning | |------|---------| | -1 | Parse error (invalid JSON) | | -2 | Invalid request (missing id/method) | | -3 | Method not found | | -4 | Authentication required | | -5 | Authentication failed | | 1 | Session not found | | 2 | Tool not found | | 3 | Agent busy (already processing) | | 4 | Request cancelled | ## Methods ### agent.send Send a message to the agent and receive streaming response. ``` Params: { message: string, sessionId?: string } Events: content, tool_start, tool_end, done, error Response: (none — final state sent as "done" event) ``` ### agent.cancel Cancel the currently running agent request. ``` Params: { sessionId?: string } Response: { cancelled: boolean } ``` ### sessions.list List all active sessions. ``` Params: {} Response: { sessions: [{ id: string, messageCount: number }] } ``` ### sessions.history Get message history for a session. ``` Params: { sessionId: string, limit?: number, offset?: number } Response: { messages: Message[], total: number } ``` ### sessions.create Create a new session. ``` Params: { sessionId?: string } Response: { sessionId: string } ``` ### tools.list List all registered tools. ``` Params: {} Response: { tools: [{ name, description, inputSchema }] } ``` ### tools.invoke Directly invoke a tool (bypasses agent). ``` Params: { tool: string, args: object } Response: { success: boolean, output: string, error?: string } ``` ### system.health Health check. ``` Params: {} Response: { status: "ok", uptime: number, version: string, sessions: number, tools: number } ``` ## Session Bridge Each WebSocket connection is associated with a session: 1. Client connects → assigned default session ID `ws:{connectionId}` 2. Client can specify `sessionId` in `agent.send` to use a named session 3. Sessions are created on demand via SessionManager 4. Multiple WS clients can share a session (e.g. multiple browser tabs) 5. Disconnection does NOT destroy the session (persistence via SQLite) ## Auth (Phase 2b) Two auth modes (checked in order): 1. **Token auth**: `Authorization: Bearer ` header on WS upgrade, or `{ method: "auth", params: { token: "..." } }` as first message 2. **Tailscale identity**: `Tailscale-User-Login` header set by Tailscale Funnel/proxy Config addition: ```yaml server: port: 18800 auth: token: "optional-static-token" # If set, required for all WS connections tailscale_identity: true # Trust Tailscale-User-Login header ``` No auth initially (Phase 2a) — gateway only listens on localhost. ## Implementation Order 1. `protocol.ts` — Types only, no runtime code 2. `router.ts` — Method dispatch (pure function, easy to test) 3. `session-bridge.ts` — Client-to-session mapping 4. `handlers/system.ts` — Simplest handler, proves the pattern 5. `handlers/sessions.ts` — Session listing/history 6. `handlers/tools.ts` — Tool listing/invocation 7. `handlers/agent.ts` — The main handler (streaming, tool events) 8. `server.ts` — WebSocket server, ties everything together 9. Wire into `daemon/index.ts` 10. Tests throughout ## Dependencies Need `ws` package: ```bash pnpm add ws pnpm add -D @types/ws ``` ## Test Strategy - Unit tests for protocol message validation - Unit tests for router dispatch - Unit tests for each handler (mock session manager, agent, tool registry) - Integration test: WS client → server → handler → response - Test streaming events for agent.send --- *Plan Version: 1.0* *Created: 2026-02-05* *Parent: docs/plans/2026-02-05-openclaw-parity-design.md Phase 2*