step 3/4: sidecar routing keys become handles — NOT YET VERIFIED LIVE
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.
This commit is contained in:
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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<string, unknown>, reply as ReplyFn);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user