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
+26 -2
View File
@@ -7,6 +7,7 @@ import { createTelegramBot } from '../frontends/telegram/index.js';
import { SessionStore, SessionManager } from '../session/index.js';
import { HookEngine } from '../hooks/index.js';
import { ToolRegistry, ToolExecutor, allBuiltinTools } from '../tools/index.js';
import { GatewayServer } from '../gateway/index.js';
import { resolve } from 'path';
import { homedir } from 'os';
import { mkdirSync, readFileSync, existsSync } from 'fs';
@@ -22,6 +23,7 @@ export interface DaemonContext {
modelRouter: ModelRouter;
toolRegistry: ToolRegistry;
toolExecutor: ToolExecutor;
gateway: GatewayServer;
}
function loadSystemPrompt(): string {
@@ -133,6 +135,9 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
// Initialize model router
const modelRouter = createModelRouter(config);
// Load system prompt once for reuse
const systemPrompt = loadSystemPrompt();
// Get Telegram session
const telegramUserId = String(config.telegram.allowed_chat_ids[0]);
const session = sessionManager.getSession('telegram', telegramUserId);
@@ -140,7 +145,7 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
// Initialize native agent with session and tools
const agent = new NativeAgent({
modelClient: modelRouter,
systemPrompt: loadSystemPrompt(),
systemPrompt,
session,
toolRegistry,
toolExecutor,
@@ -153,6 +158,17 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
hookEngine,
});
// Initialize gateway WebSocket server
const gateway = new GatewayServer({
port: config.server.port,
host: config.server.localhost ? '127.0.0.1' : '0.0.0.0',
sessionManager,
modelClient: modelRouter,
systemPrompt,
toolRegistry,
toolExecutor,
});
// Register signal handlers
const signalHandler = () => {
lifecycle.shutdown().then(() => process.exit(0));
@@ -179,9 +195,17 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
},
});
// Start gateway
lifecycle.onShutdown(async () => {
await gateway.stop();
console.log('Gateway server stopped');
});
await gateway.start();
console.log('Flynn daemon started');
return { config, lifecycle, bot, agent, sessionStore, sessionManager, hookEngine, modelRouter, toolRegistry, toolExecutor };
return { config, lifecycle, bot, agent, sessionStore, sessionManager, hookEngine, modelRouter, toolRegistry, toolExecutor, gateway };
}
export { Lifecycle } from './lifecycle.js';