From 5afa2d832e5aa0702f179e30a60003a5ad0f8ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 15 Aug 2026 16:23:31 +0000 Subject: [PATCH] =?UTF-8?q?step=203/4:=20sidecar=20routing=20keys=20become?= =?UTF-8?q?=20`handles`=20=E2=80=94=20NOT=20YET=20VERIFIED=20LIVE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Committed before restarting, deliberately: this changes the registration wire, and the restart that proves it also bounces officer-claude-code, which is what the session doing the work runs on. An uncommitted 25-file protocol change is worse to inherit than one marked unverified. `capabilities: ['music']` → `handles: ['music']` on the registration message, across 20 sidecars, both plugins, the connector, the registry and the protocol type. findSidecarByCapability → findSidecarHandling, waitForCapability → waitForHandler. The name: a sidecar already has `handleCommand`, so the list is literally what it handles. `provider` was rejected — 390 existing uses and it already means websocket door. `serves` was rejected — collides with HTTP serving, which sidecars also do. THIS IS A BREAKING WIRE CHANGE with no compatibility shim. A platform expecting `handles` reads `undefined` from a sidecar sending `capabilities`, registers it with an empty list, and every sendCommand finds nobody — chat, terminal and music all fail with "No sidecar handling X is connected". So the whole estate has to restart together; there is no rolling upgrade. If it goes wrong: `git revert HEAD` and `pm2 restart all` again. The registry is in-memory and nothing about this touches the database, so a revert is complete. Found a fifth meaning of the word on the way, correctly named and untouched: the pty sidecar's terminfo capability queries (XTGETTCAP escape sequences). That is now five — permissions, the item store, routing keys, Lightning wallet features, and terminfo. tsgo clean, 797 tests, 787 pass, same 7. Not exercised against a live sidecar. --- plugins/music/sidecar/index.ts | 2 +- plugins/offscale/sidecar/index.ts | 2 +- src/server.tsx | 10 ++-- src/servers/sidecar-registry.test.ts | 4 +- src/servers/sidecar-registry.ts | 60 ++++++++++---------- src/servers/sidecar/caldav/index.ts | 2 +- src/servers/sidecar/claude/index.ts | 2 +- src/servers/sidecar/claude/user-instance.ts | 4 +- src/servers/sidecar/connect.ts | 4 +- src/servers/sidecar/email/index.ts | 2 +- src/servers/sidecar/gitea/index.ts | 2 +- src/servers/sidecar/invoiceshelf/index.ts | 2 +- src/servers/sidecar/jellyfin/index.ts | 2 +- src/servers/sidecar/memos/index.ts | 2 +- src/servers/sidecar/notify/index.ts | 2 +- src/servers/sidecar/opencode/index.ts | 2 +- src/servers/sidecar/photos/index.ts | 2 +- src/servers/sidecar/pty/index.mjs | 2 +- src/servers/sidecar/registration-protocol.ts | 12 +++- src/servers/sidecar/slskd/index.ts | 2 +- src/servers/sidecar/transmission/index.ts | 2 +- src/servers/sidecar/vault/index.ts | 2 +- src/servers/sidecar/vnc/index.ts | 2 +- src/servers/sidecar/wallet/index.ts | 2 +- 24 files changed, 70 insertions(+), 60 deletions(-) diff --git a/plugins/music/sidecar/index.ts b/plugins/music/sidecar/index.ts index 7c879fbb..27063e60 100644 --- a/plugins/music/sidecar/index.ts +++ b/plugins/music/sidecar/index.ts @@ -474,7 +474,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'music', - capabilities: ['music'], + handles: ['music'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/plugins/offscale/sidecar/index.ts b/plugins/offscale/sidecar/index.ts index 1581ebd9..ebb5ff53 100644 --- a/plugins/offscale/sidecar/index.ts +++ b/plugins/offscale/sidecar/index.ts @@ -121,7 +121,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'headscale', - capabilities: ['headscale'], + handles: ['headscale'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/server.tsx b/src/server.tsx index 600e6926..13dd1aa4 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -146,7 +146,7 @@ const handlers: Record = { sidecar: sidecarWebsocket, }; -// Both doors into the platform, checked against the capability registry before either opens. +// Both doors into the platform, checked against the permission registry before either opens. // // This throws rather than warns, and it throws HERE — before serve() — so a surface nobody has gated // cannot be reached even once. The HTTP half comes from hono.ts's own mount table and the socket half @@ -283,14 +283,14 @@ async function upgradeWs( if (await isTokenBlacklisted(user.jti)) return new Response('Unauthorized', { status: 401 }); } - // The capability backstop, applied to sockets. Everything above this line AUTHENTICATES — it proves + // The permission backstop, applied to sockets. Everything above this line AUTHENTICATES — it proves // who is calling and never asks what they may reach. That is why a Member with a valid token could // open a terminal here in the same minute it was 403'd on GET /api/tasks. // // This resolves against the same registry as the HTTP door rather than a parallel list, which is the // whole point: the two doors cannot disagree about what a role holds, because there is only one // declaration to read. `terminal`, `chat`, `task-runner`, `pipeline` and `desktop` are refused here - // by being `execution` capabilities, not by being absent from an array someone has to maintain. + // by being `execution` permissions, not by being absent from an array someone has to maintain. if (!(await isWsProviderAllowed(user.id, provider))) { return new Response('Forbidden', { status: 403 }); } @@ -316,14 +316,14 @@ async function upgradeWs( // The chat socket's owner-only refusal was removed on 2026-08-12, with `api/chat/chat.ts`'s in the same // commit — they were always one guard in two places. A member's turn now runs as their own Linux account - // with their own credential and their own transcripts; the capability check above is what gates it. + // with their own credential and their own transcripts; the permission check above is what gates it. if (provider === 'terminal') { const resolved = await resolveHomeDir(user.id); if (!resolved.ok) return new Response('Forbidden', { status: 403 }); if (!resolved.isOwner) { const dbUser = await getUserById(user.id); - // A confined capability is only granted to an account with an OS user, so this should not happen — + // A confined permission is only granted to an account with an OS user, so this should not happen — // and if it ever does, refusing beats opening the owner's shell. if (!dbUser?.osUser) return new Response('Forbidden', { status: 403 }); url.searchParams.set('osUser', dbUser.osUser); diff --git a/src/servers/sidecar-registry.test.ts b/src/servers/sidecar-registry.test.ts index c429dee8..edfbd869 100644 --- a/src/servers/sidecar-registry.test.ts +++ b/src/servers/sidecar-registry.test.ts @@ -17,9 +17,9 @@ function silentSocket() { const registered: string[] = []; /** Register a fake sidecar and remember it, so a failed assertion can't leak it into the next test. */ -function register(name: string, capabilities: string[]) { +function register(name: string, handles: string[]) { const socket = silentSocket(); - const id = registerSidecar(socket.ws, { type: 'register', name, capabilities }); + const id = registerSidecar(socket.ws, { type: 'register', name, handles }); registered.push(id); return { ...socket, id }; } diff --git a/src/servers/sidecar-registry.ts b/src/servers/sidecar-registry.ts index 628f10aa..49304d2a 100644 --- a/src/servers/sidecar-registry.ts +++ b/src/servers/sidecar-registry.ts @@ -19,7 +19,7 @@ import type { TurnMessage } from './api/chat/types'; type RegisteredSidecar = { id: string; name: string; - capabilities: string[]; + handles: string[]; ws: ServerWebSocket; }; @@ -61,18 +61,18 @@ export function registerSidecar(ws: ServerWebSocket, registration: SidecarR sidecars.set(id, { id, name: registration.name, - capabilities: registration.capabilities, + handles: registration.handles, ws, }); console.log( - `[registry] registered sidecar "${registration.name}" (id=${id}, capabilities=[${registration.capabilities.join(', ')}])`, + `[registry] registered sidecar "${registration.name}" (id=${id}, handles=[${registration.handles.join(', ')}])`, ); // A registration socket lives and dies with its process, so an agent showing up here is an agent that // has just started — and whatever it was running before is gone. Announced rather than inferred from // the *disconnect*, which is the wrong signal entirely: every `pm2 restart officer` drops these sockets // while the sidecars, and their turns, carry on perfectly well. - if (registration.capabilities.includes('claude')) { + if (registration.handles.includes('claude')) { for (const handler of claudeRestartHandlers) { try { handler(); @@ -110,7 +110,7 @@ export function unregisterSidecar(id: string): void { } // Clear cached state if the claude sidecar disconnects - if (sc.capabilities.includes('proxy')) { + if (sc.handles.includes('proxy')) { cachedState = null; } } @@ -131,9 +131,9 @@ export function handleSidecarMessage(id: string, msg: SidecarEvent): void { // ── Lookup ── -function findSidecarByCapability(cap: string): RegisteredSidecar | undefined { +function findSidecarHandling(kind: string): RegisteredSidecar | undefined { for (const sc of sidecars.values()) { - if (sc.capabilities.includes(cap)) return sc; + if (sc.handles.includes(kind)) return sc; } return undefined; } @@ -168,11 +168,11 @@ export function on(eventType: string, handler: EventHandler): () => void { const DEFAULT_TIMEOUT_MS = 30_000; const LONG_TIMEOUT_MS = 6 * 60 * 1000; -function sendCommand(cap: string, cmd: SidecarCommand, timeoutMs = DEFAULT_TIMEOUT_MS): Promise { +function sendCommand(kind: string, cmd: SidecarCommand, timeoutMs = DEFAULT_TIMEOUT_MS): Promise { return new Promise((resolve, reject) => { - const sc = findSidecarByCapability(cap); + const sc = findSidecarHandling(kind); if (!sc) { - reject(new Error(`No sidecar with capability "${cap}" is connected`)); + reject(new Error(`No sidecar handling "${kind}" is connected`)); return; } @@ -186,8 +186,8 @@ function sendCommand(cap: string, cmd: SidecarCommand, timeoutMs = DEFAULT_TIMEO }); } -function sendFire(cap: string, cmd: SidecarCommand): void { - const sc = findSidecarByCapability(cap); +function sendFire(kind: string, cmd: SidecarCommand): void { + const sc = findSidecarHandling(kind); if (sc) { sc.ws.send(JSON.stringify(cmd)); } @@ -220,26 +220,26 @@ function sendFireToSidecar(sc: RegisteredSidecar, cmd: SidecarCommand): void { // arrive a beat before the sidecar has finished dialling in. Wait briefly rather than failing the // request. (This replaces ~77 lines of spawn-and-poll: `ensureClaudeSidecar`, // `spawnAndWaitForRegistration`, and the per-email `claudeProcs`/`claudeSpawnWaiters` maps.) -const CAPABILITY_WAIT_MS = 15_000; -const CAPABILITY_POLL_MS = 100; +const HANDLER_WAIT_MS = 15_000; +const HANDLER_POLL_MS = 100; -async function waitForCapability(cap: string, timeoutMs = CAPABILITY_WAIT_MS): Promise { - const existing = findSidecarByCapability(cap); +async function waitForHandler(kind: string, timeoutMs = HANDLER_WAIT_MS): Promise { + const existing = findSidecarHandling(kind); if (existing) return existing; const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { - await Bun.sleep(CAPABILITY_POLL_MS); - const sc = findSidecarByCapability(cap); + await Bun.sleep(HANDLER_POLL_MS); + const sc = findSidecarHandling(kind); if (sc) return sc; } - throw new Error(`No sidecar with capability "${cap}" registered within ${timeoutMs}ms`); + throw new Error(`No sidecar handling "${kind}" registered within ${timeoutMs}ms`); } // ── Public API ── export function isConnected(): boolean { - return findSidecarByCapability('proxy') !== undefined; + return findSidecarHandling('proxy') !== undefined; } export function getCachedState(): ClaudeState | null { @@ -266,14 +266,14 @@ export function getProxySecretSync(): string { return cachedState?.proxySecret ?? ''; } -// ── Claude Code (the `officer-agent` sidecar, capability 'claude') ── +// ── Claude Code (the `officer-agent` sidecar, handling 'claude') ── -// Single-user platform, so there is exactly one agent sidecar and it is found by capability like every +// Single-user platform, so there is exactly one agent sidecar and it is found by what it handles, like every // other one. The `email` on the params is still passed through to the sidecar — it needs it to resolve // paths — but officer no longer uses it to *locate* anything. export async function spawnClaude(params: ClaudeSpawnParams): Promise { - const sc = await waitForCapability('claude'); + const sc = await waitForHandler('claude'); const res = await sendCommandToSidecar(sc, { type: 'claude:spawn', id: nextId(), params }, LONG_TIMEOUT_MS); if (res.type === 'claude:result') return res.result; if (res.type === 'claude:error') throw new Error(res.error); @@ -281,7 +281,7 @@ export async function spawnClaude(params: ClaudeSpawnParams): Promise { - const sc = await waitForCapability('claude'); + const sc = await waitForHandler('claude'); const res = await sendCommandToSidecar(sc, { type: 'claude:spawn-streaming', id: nextId(), params }); if (res.type === 'claude:spawned') return; if (res.type === 'claude:error') throw new Error(res.error); @@ -324,7 +324,7 @@ export function interruptClaude(sessionKey: string, userId: number): void { * sessions, and an enumeration that reports things that may not exist is worse than a short one. */ export async function listLiveClaudeSessions(userId: number): Promise { - const sc = findSidecarByCapability('claude'); + const sc = findSidecarHandling('claude'); if (!sc) return []; try { const res = await sendCommandToSidecar(sc, { type: 'claude:list', id: nextId(), userId }); @@ -342,7 +342,7 @@ export async function listLiveClaudeSessions(userId: number): Promise { - const sc = findSidecarByCapability('opencode'); + const sc = findSidecarHandling('opencode'); if (!sc) return []; try { const res = await sendCommandToSidecar(sc, { type: 'opencode:list', id: nextId() }); @@ -353,7 +353,7 @@ export async function listLiveOpenCodeSessions(): Promise } export async function isClaudeGenerating(sessionKey: string, userId: number): Promise { - const sc = findSidecarByCapability('claude'); + const sc = findSidecarHandling('claude'); if (!sc) return false; try { const res = await sendCommandToSidecar(sc, { type: 'claude:is-generating', id: nextId(), sessionKey, userId }); @@ -374,7 +374,7 @@ export async function isClaudeGenerating(sessionKey: string, userId: number): Pr * back to today's behaviour of leaving the socket unattached rather than binding it to a guess. */ export async function findClaudeSessionKey(claudeSessionId: string, userId: number): Promise { - const sc = findSidecarByCapability('claude'); + const sc = findSidecarHandling('claude'); if (!sc) return null; try { const res = await sendCommandToSidecar(sc, { type: 'claude:find-session', id: nextId(), claudeSessionId, userId }); @@ -399,7 +399,7 @@ export function onClaudeMessage(handler: (sessionKey: string, msg: TurnMessage, }); } -// ── OpenCode Code (single sidecar, capability 'opencode') ── +// ── OpenCode Code (single sidecar, handling 'opencode') ── export async function spawnOpenCodeStreaming(params: OpenCodeRunParams): Promise { const res = await sendCommand('opencode', { type: 'opencode:run-streaming', id: nextId(), params }); @@ -459,5 +459,5 @@ export function stopVnc(email: string): void { } export function isVncConnected(): boolean { - return findSidecarByCapability('vnc') !== undefined; + return findSidecarHandling('vnc') !== undefined; } diff --git a/src/servers/sidecar/caldav/index.ts b/src/servers/sidecar/caldav/index.ts index 0635aba8..a0eaa3b6 100644 --- a/src/servers/sidecar/caldav/index.ts +++ b/src/servers/sidecar/caldav/index.ts @@ -215,7 +215,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'caldav', - capabilities: ['caldav'], + handles: ['caldav'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/claude/index.ts b/src/servers/sidecar/claude/index.ts index 1c3fc684..cda52508 100644 --- a/src/servers/sidecar/claude/index.ts +++ b/src/servers/sidecar/claude/index.ts @@ -60,7 +60,7 @@ async function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'proxy', - capabilities: ['proxy'], + handles: ['proxy'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/claude/user-instance.ts b/src/servers/sidecar/claude/user-instance.ts index c820a3b5..b61f09e2 100644 --- a/src/servers/sidecar/claude/user-instance.ts +++ b/src/servers/sidecar/claude/user-instance.ts @@ -334,13 +334,13 @@ async function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { // ── Connect to API server ── -// A stable identity, not a per-email one. Officer looks this sidecar up by the 'claude' capability, so +// A stable identity, not a per-email one. Officer looks this sidecar up by the 'claude' handler, so // it no longer needs to know which user is running to find it — that was the last thing tying the // registry's claude verbs to an email argument. const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'agent', - capabilities: ['claude'], + handles: ['claude'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/connect.ts b/src/servers/sidecar/connect.ts index da49c237..ce540e52 100644 --- a/src/servers/sidecar/connect.ts +++ b/src/servers/sidecar/connect.ts @@ -7,7 +7,7 @@ type AnyEvent = SidecarEvent; type SidecarConnectorConfig = { apiUrl: string; // ws://127.0.0.1:9000/api/sidecar/register name: string; - capabilities: string[]; + handles: string[]; onCommand: (cmd: AnyCommand, reply: (msg: AnyEvent) => void) => void; onConnected?: () => void; onDisconnected?: () => void; @@ -47,7 +47,7 @@ export function createSidecarConnector(config: SidecarConnectorConfig): SidecarC const registration: SidecarRegistration = { type: 'register', name: config.name, - capabilities: config.capabilities, + handles: config.handles, }; ws!.send(JSON.stringify(registration)); }; diff --git a/src/servers/sidecar/email/index.ts b/src/servers/sidecar/email/index.ts index 501438d0..b88aaf3c 100644 --- a/src/servers/sidecar/email/index.ts +++ b/src/servers/sidecar/email/index.ts @@ -40,7 +40,7 @@ const serverPort = startEmailServer(); const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'email', - capabilities: ['email'], + handles: ['email'], onCommand(cmd, reply) { handleCommand(cmd as Record, reply as ReplyFn); }, diff --git a/src/servers/sidecar/gitea/index.ts b/src/servers/sidecar/gitea/index.ts index 09c45c27..7279ad80 100644 --- a/src/servers/sidecar/gitea/index.ts +++ b/src/servers/sidecar/gitea/index.ts @@ -265,7 +265,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'gitea', - capabilities: ['gitea'], + handles: ['gitea'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/invoiceshelf/index.ts b/src/servers/sidecar/invoiceshelf/index.ts index 2a760828..97b1c4a7 100644 --- a/src/servers/sidecar/invoiceshelf/index.ts +++ b/src/servers/sidecar/invoiceshelf/index.ts @@ -152,7 +152,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'invoiceshelf', - capabilities: ['invoiceshelf'], + handles: ['invoiceshelf'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/jellyfin/index.ts b/src/servers/sidecar/jellyfin/index.ts index 8bf42026..eee6925b 100644 --- a/src/servers/sidecar/jellyfin/index.ts +++ b/src/servers/sidecar/jellyfin/index.ts @@ -149,7 +149,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'jellyfin', - capabilities: ['jellyfin'], + handles: ['jellyfin'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/memos/index.ts b/src/servers/sidecar/memos/index.ts index 650fc81f..698ea98d 100644 --- a/src/servers/sidecar/memos/index.ts +++ b/src/servers/sidecar/memos/index.ts @@ -163,7 +163,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'memos', - capabilities: ['memos'], + handles: ['memos'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/notify/index.ts b/src/servers/sidecar/notify/index.ts index 07626c9f..a62845dd 100644 --- a/src/servers/sidecar/notify/index.ts +++ b/src/servers/sidecar/notify/index.ts @@ -95,7 +95,7 @@ console.log( const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'notify', - capabilities: ['notify'], + handles: ['notify'], onCommand(cmd, reply) { const c = cmd as SidecarCommand; if (c.type === 'ping') return reply({ type: 'pong', id: c.id }); diff --git a/src/servers/sidecar/opencode/index.ts b/src/servers/sidecar/opencode/index.ts index c2fabd74..6019ab75 100644 --- a/src/servers/sidecar/opencode/index.ts +++ b/src/servers/sidecar/opencode/index.ts @@ -194,7 +194,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'opencode', - capabilities: ['opencode'], + handles: ['opencode'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/photos/index.ts b/src/servers/sidecar/photos/index.ts index 1a14857b..2f57bf96 100644 --- a/src/servers/sidecar/photos/index.ts +++ b/src/servers/sidecar/photos/index.ts @@ -158,7 +158,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'photos', - capabilities: ['photos'], + handles: ['photos'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/pty/index.mjs b/src/servers/sidecar/pty/index.mjs index c641cd39..5313d04f 100644 --- a/src/servers/sidecar/pty/index.mjs +++ b/src/servers/sidecar/pty/index.mjs @@ -62,7 +62,7 @@ function connect() { ws.on('open', () => { reconnectAttempt = 0; console.log('[pty-sidecar] connected, sending registration...'); - sendJson({ type: 'register', name: 'pty', capabilities: ['terminal'] }); + sendJson({ type: 'register', name: 'pty', handles: ['terminal'] }); // Re-announce on every reconnect: officer forgets the port when the socket drops, and this process // keeps the same listener across officer restarts. if (serverPort) sendJson({ type: 'pty:server', port: serverPort }); diff --git a/src/servers/sidecar/registration-protocol.ts b/src/servers/sidecar/registration-protocol.ts index d30e4418..4cd91275 100644 --- a/src/servers/sidecar/registration-protocol.ts +++ b/src/servers/sidecar/registration-protocol.ts @@ -4,7 +4,17 @@ export type SidecarRegistration = { type: 'register'; name: string; // 'process' | 'pty' | custom - capabilities: string[]; // ['claude', 'pi', 'queue', 'proxy'] or ['terminal'] + /** + * What this sidecar can be sent. `sendCommand('claude', …)` finds the process that HANDLES that kind, + * which is why the list is not the same thing as the sidecar's name — one process can handle several. + * + * Called `handles` since 2026-08-15. It was `capabilities`, which collided with the two other things + * that word means here: the permission registry (now `permissions/`) and the item store under + * `$OFFICER_ROOT/capabilities/`, which is what capabilities actually are. Nothing about routing a + * command to a process was ever a capability in that sense; it is a list of what the process answers to, + * and `handleCommand` in every sidecar is where it answers. + */ + handles: string[]; }; // API server → sidecar ack diff --git a/src/servers/sidecar/slskd/index.ts b/src/servers/sidecar/slskd/index.ts index 0fa76bb1..41596519 100644 --- a/src/servers/sidecar/slskd/index.ts +++ b/src/servers/sidecar/slskd/index.ts @@ -162,7 +162,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'slskd', - capabilities: ['slskd'], + handles: ['slskd'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/transmission/index.ts b/src/servers/sidecar/transmission/index.ts index 54a4a443..c3b5684c 100644 --- a/src/servers/sidecar/transmission/index.ts +++ b/src/servers/sidecar/transmission/index.ts @@ -131,7 +131,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'transmission', - capabilities: ['transmission'], + handles: ['transmission'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/vault/index.ts b/src/servers/sidecar/vault/index.ts index 17f4f183..e683d714 100644 --- a/src/servers/sidecar/vault/index.ts +++ b/src/servers/sidecar/vault/index.ts @@ -208,7 +208,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'vault', - capabilities: ['vault'], + handles: ['vault'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/vnc/index.ts b/src/servers/sidecar/vnc/index.ts index c2c03469..6e946826 100644 --- a/src/servers/sidecar/vnc/index.ts +++ b/src/servers/sidecar/vnc/index.ts @@ -53,7 +53,7 @@ async function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'vnc', - capabilities: ['vnc'], + handles: ['vnc'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); }, diff --git a/src/servers/sidecar/wallet/index.ts b/src/servers/sidecar/wallet/index.ts index c9f3a7e8..b809befa 100644 --- a/src/servers/sidecar/wallet/index.ts +++ b/src/servers/sidecar/wallet/index.ts @@ -196,7 +196,7 @@ function handleCommand(cmd: SidecarCommand, reply: ReplyFn) { const connection = createSidecarConnector({ apiUrl: `${API_URL}/api/sidecar/register`, name: 'wallet', - capabilities: ['wallet'], + handles: ['wallet'], onCommand(cmd, reply) { handleCommand(cmd as SidecarCommand, reply as ReplyFn); },