vnc sidecar: per-user desktop sessions via sidecar architecture

replaces the single hardcoded systemd VNC service with a dynamic
sidecar that manages per-user VNC sessions on demand. any authenticated
user can now access their own desktop, not just Super Admin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 10:49:53 +00:00
co-authored by Claude Opus 4.6
parent a0d9ea63f9
commit daf5580c39
12 changed files with 467 additions and 190 deletions
+31 -41
View File
@@ -2,17 +2,18 @@ import type { ServerWebSocket } from 'bun';
import type {
SidecarCommand,
SidecarEvent,
SidecarState,
ClaudeState,
ClaudeSpawnParams,
ClaudeSpawnStreamingParams,
ClaudeCodeResult,
PiSpawnParams,
PtyCommand,
PtyEvent,
VncStartParams,
VncSessionInfo,
} from './sidecar/protocol';
import type { SidecarRegistration } from './sidecar/registration-protocol';
import type { PiEvent } from './api/pi/types';
import type { Job, EnqueueParams } from './queue/types';
// ── Types ──
@@ -36,7 +37,7 @@ type EventHandler = (event: SidecarEvent | PtyEvent) => void;
const sidecars = new Map<string, RegisteredSidecar>();
const pending = new Map<string, PendingRequest>();
const eventHandlers = new Map<string, Set<EventHandler>>();
let cachedState: SidecarState | null = null;
let cachedState: ClaudeState | null = null;
let idCounter = 0;
function nextId(): string {
@@ -81,7 +82,7 @@ export function unregisterSidecar(id: string): void {
pending.delete(reqId);
}
// Clear cached state if the process sidecar disconnects
// Clear cached state if the claude sidecar disconnects
if (sc.capabilities.includes('proxy')) {
cachedState = null;
}
@@ -110,12 +111,6 @@ function findSidecarByCapability(cap: string): RegisteredSidecar | undefined {
return undefined;
}
function requireSidecar(cap: string): RegisteredSidecar {
const sc = findSidecarByCapability(cap);
if (!sc) throw new Error(`No sidecar with capability "${cap}" is connected`);
return sc;
}
// ── Event dispatch ──
function dispatchEvent(msg: SidecarEvent | PtyEvent) {
@@ -171,17 +166,17 @@ function sendFire(cap: string, cmd: SidecarCommand | PtyCommand): void {
}
}
// ── Public API (same signatures as sidecar-client.ts) ──
// ── Public API ──
export function isConnected(): boolean {
return findSidecarByCapability('proxy') !== undefined;
}
export function getCachedState(): SidecarState | null {
export function getCachedState(): ClaudeState | null {
return cachedState;
}
export async function syncState(): Promise<SidecarState> {
export async function syncState(): Promise<ClaudeState> {
const res = await sendCommand('proxy', { type: 'state:sync', id: nextId() });
if (res.type === 'state:sync') {
cachedState = res.state;
@@ -272,34 +267,6 @@ export function onPiEvent(handler: (sessionId: string, event: PiEvent) => void):
});
}
// ── Queue ──
export async function enqueueJob(params: EnqueueParams): Promise<Job> {
const res = await sendCommand('queue', { type: 'queue:enqueue', id: nextId(), params });
if (res.type === 'queue:enqueued') return res.job;
if (res.type === 'queue:error') throw new Error(res.error);
throw new Error('Unexpected response');
}
export async function cancelJob(jobId: string): Promise<Job | null> {
const res = await sendCommand('queue', { type: 'queue:cancel', id: nextId(), jobId });
if (res.type === 'queue:cancelled') return res.job;
if (res.type === 'queue:error') throw new Error(res.error);
throw new Error('Unexpected response');
}
export async function listJobs(): Promise<Job[]> {
const res = await sendCommand('queue', { type: 'queue:list', id: nextId() });
if (res.type === 'queue:list') return res.jobs;
throw new Error('Unexpected response');
}
export async function getJob(jobId: string): Promise<Job | null> {
const res = await sendCommand('queue', { type: 'queue:get', id: nextId(), jobId });
if (res.type === 'queue:get') return res.job;
throw new Error('Unexpected response');
}
// ── Terminal (PTY sidecar) ──
export function sendPtyCommand(cmd: PtyCommand): void {
@@ -313,3 +280,26 @@ export async function sendPtyCommandAsync(cmd: PtyCommand, timeoutMs = DEFAULT_T
export function isTerminalConnected(): boolean {
return findSidecarByCapability('terminal') !== undefined;
}
// ── VNC ──
export async function startVnc(params: VncStartParams): Promise<{ port: number; display: number }> {
const res = await sendCommand('vnc', { type: 'vnc:start', id: nextId(), params });
if (res.type === 'vnc:started') return { port: res.port, display: res.display };
if (res.type === 'vnc:error') throw new Error(res.error);
throw new Error('Unexpected response');
}
export function stopVnc(email: string): void {
sendFire('vnc', { type: 'vnc:stop', id: nextId(), email });
}
export async function getVncStatus(email: string): Promise<VncSessionInfo | null> {
const res = await sendCommand('vnc', { type: 'vnc:status', id: nextId(), email });
if (res.type === 'vnc:status') return res.session;
throw new Error('Unexpected response');
}
export function isVncConnected(): boolean {
return findSidecarByCapability('vnc') !== undefined;
}