Files
platform/src/servers/sidecar/protocol.ts
T

170 lines
7.0 KiB
TypeScript

import type { MessageCost, TurnMessage } from '../api/chat/types';
// ── Envelope ──
export type SidecarMessage = SidecarCommand | SidecarEvent;
// ── Commands (API server → sidecar) ──
export type SidecarCommand =
| { type: 'ping'; id: string }
| { type: 'state:sync'; id: string }
// Proxy
| { type: 'proxy:secret'; id: string }
// Claude Code
| { type: 'claude:spawn'; id: string; params: ClaudeSpawnParams }
| { type: 'claude:spawn-streaming'; id: string; params: ClaudeSpawnStreamingParams }
| { type: 'claude:kill'; id: string; sessionKey: string }
| { type: 'claude:interrupt'; id: string; sessionKey: string }
| { type: 'claude:clear-session'; id: string; sessionKey: string }
// OpenCode — drive a turn via `opencode run … --format json` (tools re-anchored to cwd via --dir)
| { type: 'opencode:run-streaming'; id: string; params: OpenCodeRunParams }
| { type: 'opencode:kill'; id: string; sessionKey: string }
// VNC
| { type: 'vnc:start'; id: string; params: VncStartParams }
// Provision the VNC password without starting a server — the UI needs it before it can connect
| { type: 'vnc:ensure-password'; id: string; email: string }
| { type: 'vnc:stop'; id: string; email: string }
| { type: 'vnc:status'; id: string; email: string };
// ── Responses/Events (sidecar → API server) ──
export type SidecarEvent =
| { type: 'pong'; id: string }
| { type: 'state:sync'; id: string; state: ClaudeState }
| { type: 'proxy:secret'; id: string; secret: string }
// Claude Code
| { type: 'claude:spawned'; id: string; sessionKey: string }
// A finished, browser-facing turn message. The agent has already committed it to chat_session_events
// and `seq` is its cursor id there; officer relays it verbatim. No `seq` means it is not durable —
// an `assistant:delta` (superseded by the text that follows) or a message whose write failed.
| { type: 'claude:message'; sessionKey: string; msg: TurnMessage; seq?: number }
| { type: 'claude:result'; id: string; result: ClaudeCodeResult }
| { type: 'claude:error'; id: string; error: string }
| { type: 'claude:killed'; id: string }
| { type: 'claude:interrupted'; id: string }
| { type: 'claude:session-cleared'; id: string }
// VNC
| { type: 'vnc:started'; id: string; port: number; display: number }
| { type: 'vnc:password'; id: string; password: string }
| { type: 'vnc:stopped'; id: string }
| { type: 'vnc:status'; id: string; session: VncSessionInfo | null }
| { type: 'vnc:error'; id: string; error: string }
// Email
| { type: 'email:new'; userEmail: string }
// OpenCode — the sidecar reports where its `opencode serve` is listening (random port) on connect
| { type: 'opencode:server'; port: number }
// OpenCode turn streaming (analog of claude:*): spawned ack, per-message stream, session id report
| { type: 'opencode:spawned'; id: string; sessionKey: string }
// Same contract as `claude:message`: a finished turn message the sidecar has already committed to
// chat_session_events, plus the cursor id it landed on. Officer relays it and folds it into its
// in-memory transcript; it does not translate or persist.
| { type: 'opencode:message'; sessionKey: string; msg: TurnMessage; seq?: number }
| { type: 'opencode:session'; sessionKey: string; sessionId: string }
| { type: 'opencode:error'; id: string; error: string }
// Music — the sidecar reports where its audio-streaming HTTP server is listening (random port) on connect
| { type: 'music:server'; port: number }
// Vault — the sidecar reports where its Vaultwarden reverse-proxy HTTP/WS server is listening on connect
| { type: 'vault:server'; port: number }
// slskd — the sidecar reports where its slskd reverse-proxy HTTP server is listening (random port) on connect
| { type: 'slskd:server'; port: number }
// Generic
| { type: 'error'; id?: string; error: string };
// ── Claude sidecar state ──
export type ClaudeState = {
proxySecret: string;
claudeSessions: Record<string, string>; // sessionKey → Claude Code session_id
};
// ── Param types ──
export type ClaudeSpawnParams = {
userId: number;
email: string;
username: string;
prompt: string;
sessionKey: string;
model?: string;
cwd?: string;
};
export type ClaudeSpawnStreamingParams = {
userId: number;
email: string;
username: string;
prompt: string;
sessionKey: string;
cwd?: string;
model?: string;
resumeSessionId?: string; // resume this Claude session uuid (from the /chat session list)
// Whether turn output should be committed to chat_session_events (default true). A chat session wants
// it — that is what survives an officer restart. A pipeline step does not: its sessionKey is a throwaway
// uuid no browser will ever replay, and the job's own event log is its record.
durable?: boolean;
};
export type ClaudeCodeResult = {
text: string;
sessionId: string;
model: string;
cost: MessageCost;
};
// ── OpenCode turn params ──
export type OpenCodeRunParams = {
sessionKey: string;
prompt: string;
cwd?: string; // passed to `opencode run --dir` — hard-re-anchors tools to this directory
model?: string; // `providerID/modelID` (e.g. opencode/claude-haiku-4-5); passed to --model verbatim
resumeSessionId?: string; // OpenCode `ses_…` id to continue (`--session`)
durable?: boolean; // commit turn output to chat_session_events (default true) — see ClaudeSpawnStreamingParams
};
// ── VNC types ──
export type VncStartParams = {
email: string;
username: string;
resolution?: string;
};
export type VncSessionInfo = {
email: string;
display: number;
port: number;
pid: number;
alive: boolean;
};
// ── PTY types ──
// What officer knows about a terminal, and nothing more. The shell, its arguments, the home directory and
// whether the shell is sandboxed are the sidecar's own decisions — they used to travel in here, which is
// how officer ended up reading the owner's SHELL and HOME and hardcoding `host: true`.
export type PtyInitConfig = {
sessionId: string;
/** The folder the panel was opened on. `~`, `~/x` and absolute paths only; resolved by the sidecar. */
cwd?: string;
cols?: number;
rows?: number;
};
// PTY commands (API → PTY sidecar)
export type PtyCommand =
| { type: 'pty:init'; id: string; sessionId: string; config: PtyInitConfig }
| { type: 'pty:input'; id: string; sessionId: string; data: string }
| { type: 'pty:resize'; id: string; sessionId: string; cols: number; rows: number }
| { type: 'pty:close'; id: string; sessionId: string };
// PTY events (PTY sidecar → API)
export type PtyEvent =
| { type: 'pty:ready'; id: string; sessionId: string }
| { type: 'pty:output'; sessionId: string; data: string }
// Scrollback sent on re-attach, which the client may already be showing in part — distinct from
// `pty:output` so it can rebuild the screen rather than append a second copy of it.
| { type: 'pty:replay'; sessionId: string; data: string }
| { type: 'pty:exit'; sessionId: string; exitCode: number; signal?: number };