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:
2026-03-06 08:48:19 +00:00
co-authored by Claude Opus 4.6
parent 6cfa40bad1
commit a0d9ea63f9
17 changed files with 878 additions and 712 deletions
+49 -8
View File
@@ -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 () => {