# 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 <token>` 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*
