sidecar self-registration: sidecars connect to API server instead of vice versa
Flips the connection model so sidecars register themselves with the API server via WebSocket at /api/sidecar/register, enabling dynamic discovery, location independence, and automatic reconnection from either side. - Add registration protocol types and PTY command/event types - Create sidecar-registry.ts (replaces sidecar-client.ts) as passive registry - Create sidecar connector (connect.ts) with exponential backoff reconnect - Convert process sidecar from WS server to WS client - Convert PTY sidecar from WS server to multiplexed WS client - Simplify terminal bridge to thin adapter using registry - Add PTY sidecar as PM2-managed process - Update all consumer imports Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+49
-8
@@ -4,7 +4,7 @@ import { serve } from 'bun';
|
||||
import { honoServer } from './servers/hono';
|
||||
import { verify } from './servers/jwt';
|
||||
import { isTokenBlacklisted } from 'officerdb';
|
||||
import { terminalWebsocket, initTerminalSidecars } from './servers/api/terminal/websocket';
|
||||
import { terminalWebsocket } from './servers/api/terminal/websocket';
|
||||
import { piWebsocket } from './servers/api/pi/websocket';
|
||||
import { cliampWebsocket } from './servers/api/cliamp/websocket';
|
||||
import { cliampAudioWebsocket } from './servers/api/cliamp/audio-ws';
|
||||
@@ -12,7 +12,8 @@ import { desktopWebsocket } from './servers/api/desktop/websocket';
|
||||
import { findEntryByProxyId, touchEntry } from './servers/api/dev-server/router';
|
||||
import officerWeb from './apps/officer-web/index.html';
|
||||
import { startBrowserRelay } from './servers/api/browser/relay';
|
||||
import { initSidecarClient } from './servers/sidecar-client';
|
||||
import { registerSidecar, unregisterSidecar, handleSidecarMessage } from './servers/sidecar-registry';
|
||||
import type { SidecarRegistration } from './servers/sidecar/registration-protocol';
|
||||
import { toShellUsername } from './servers/data-path';
|
||||
|
||||
const { PORT = '5000' } = process.env;
|
||||
@@ -22,7 +23,7 @@ type WSData = {
|
||||
email: string;
|
||||
username: string;
|
||||
role: string;
|
||||
provider: 'terminal' | 'pi' | 'dev-server' | 'cliamp' | 'cliamp-audio' | 'desktop';
|
||||
provider: 'terminal' | 'pi' | 'dev-server' | 'cliamp' | 'cliamp-audio' | 'desktop' | 'sidecar';
|
||||
sandboxed: boolean;
|
||||
sessionId?: string;
|
||||
cwd?: string;
|
||||
@@ -36,12 +37,50 @@ type WSData = {
|
||||
wsToken?: string;
|
||||
};
|
||||
|
||||
// Sidecar registration WebSocket handler
|
||||
const sidecarConnections = new Map<ServerWebSocket<WSData>, string>(); // ws → sidecar ID
|
||||
|
||||
const sidecarWebsocket = {
|
||||
open(_ws: ServerWebSocket<WSData>) {
|
||||
// Wait for registration message
|
||||
},
|
||||
message(ws: ServerWebSocket<WSData>, raw: string | Buffer) {
|
||||
try {
|
||||
const data = typeof raw === 'string' ? raw : raw.toString();
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
if (msg.type === 'register') {
|
||||
const id = registerSidecar(ws, msg as SidecarRegistration);
|
||||
sidecarConnections.set(ws, id);
|
||||
ws.send(JSON.stringify({ type: 'registered', id }));
|
||||
return;
|
||||
}
|
||||
|
||||
const id = sidecarConnections.get(ws);
|
||||
if (id) {
|
||||
handleSidecarMessage(id, msg);
|
||||
}
|
||||
} catch {
|
||||
// skip malformed messages
|
||||
}
|
||||
},
|
||||
close(ws: ServerWebSocket<WSData>) {
|
||||
const id = sidecarConnections.get(ws);
|
||||
if (id) {
|
||||
unregisterSidecar(id);
|
||||
sidecarConnections.delete(ws);
|
||||
}
|
||||
},
|
||||
drain() {},
|
||||
};
|
||||
|
||||
const handlers: Record<string, any> = {
|
||||
terminal: terminalWebsocket,
|
||||
pi: piWebsocket,
|
||||
cliamp: cliampWebsocket,
|
||||
'cliamp-audio': cliampAudioWebsocket,
|
||||
desktop: desktopWebsocket,
|
||||
sidecar: sidecarWebsocket,
|
||||
};
|
||||
|
||||
// Dev-server WebSocket proxy: bridges client WS ↔ upstream dev server WS (for HMR etc.)
|
||||
@@ -187,6 +226,12 @@ const server = serve({
|
||||
if (req.headers.get('upgrade') === 'websocket') return upgradeDevServerWs(req, server);
|
||||
return honoServer.fetch(req, server);
|
||||
},
|
||||
'/api/sidecar/register': (req: Request, server: any) => {
|
||||
const ok = server.upgrade(req, {
|
||||
data: { provider: 'sidecar', userId: 0, email: '', username: '', role: '', sandboxed: false },
|
||||
});
|
||||
if (!ok) return new Response('Upgrade failed', { status: 500 });
|
||||
},
|
||||
'/api/terminal/ws': (req, server) => upgradeWs(req, server, 'terminal'),
|
||||
'/api/pi/chat/ws': (req, server) => upgradeWs(req, server, 'pi'),
|
||||
'/api/cliamp/ws': (req, server) => upgradeWs(req, server, 'cliamp'),
|
||||
@@ -229,11 +274,7 @@ try {
|
||||
console.error('[browser-relay] failed to start:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
|
||||
// Connect to process sidecar (owns proxy, Claude Code, Pi, queue)
|
||||
initSidecarClient();
|
||||
|
||||
|
||||
void initTerminalSidecars();
|
||||
// Sidecars connect to us via /api/sidecar/register — no init needed
|
||||
|
||||
// Ensure PulseAudio is running with virtual sink for cliamp audio streaming
|
||||
(async () => {
|
||||
|
||||
Reference in New Issue
Block a user