feat(gateway): add WebSocket gateway with JSON-RPC protocol and auth

Phase 2 of the Flynn roadmap. Adds a WebSocket gateway server that
starts alongside the Telegram bot, providing real-time API access to
the agent, sessions, and tools.

Protocol: JSON-RPC-like (request/response/event) over WebSocket.
8 methods: agent.send, agent.cancel, sessions.list, sessions.history,
sessions.create, tools.list, tools.invoke, system.health.

Auth: Bearer token + Tailscale identity header support.
Session bridge: per-connection agent instances with shared model router.

New files: src/gateway/ (protocol, router, server, auth, session-bridge,
handlers for agent/sessions/tools/system).
57 new tests (181 total), typecheck clean.
This commit is contained in:
William Valentin
2026-02-05 19:11:25 -08:00
parent ad7fc241f1
commit f30a8bc318
21 changed files with 1878 additions and 2 deletions
+37
View File
@@ -0,0 +1,37 @@
import type { GatewayRequest, OutboundMessage } from '../protocol.js';
import { makeResponse, makeError, ErrorCode } from '../protocol.js';
import type { ToolRegistry } from '../../tools/registry.js';
import type { ToolExecutor } from '../../tools/executor.js';
export interface ToolHandlerDeps {
toolRegistry: ToolRegistry;
toolExecutor: ToolExecutor;
}
export function createToolHandlers(deps: ToolHandlerDeps) {
return {
'tools.list': async (request: GatewayRequest): Promise<OutboundMessage> => {
const tools = deps.toolRegistry.list().map(t => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
}));
return makeResponse(request.id, { tools });
},
'tools.invoke': async (request: GatewayRequest): Promise<OutboundMessage> => {
const params = request.params as { tool?: string; args?: Record<string, unknown> } | undefined;
if (!params?.tool) {
return makeError(request.id, ErrorCode.InvalidRequest, 'tool name is required');
}
const tool = deps.toolRegistry.get(params.tool);
if (!tool) {
return makeError(request.id, ErrorCode.ToolNotFound, `Tool not found: ${params.tool}`);
}
const result = await deps.toolExecutor.execute(params.tool, params.args ?? {});
return makeResponse(request.id, result);
},
};
}