SPB Git

Repository Public

wire.ts 4.8 KB 109 lines typescript

1/**2 * WebSocket wire formats.3 *4 * Terminal stream  (browser <-> server <-> runtime):  /api/v1/sessions/:id/stream?cols=&rows=5 *   - BINARY frames: raw PTY bytes (server->client output, client->server input). Never JSON.6 *   - TEXT frames: JSON control messages below.7 * Event stream (server -> browser): /api/v1/events — TEXT JSON `ServerEvent` only.8 */9import { z } from 'zod';10import { AgentTask, Notification, TerminalSession } from './dto.js';1112export const TermClientMsg = z.discriminatedUnion('t', [13  z.object({ t: z.literal('resize'), cols: z.number().int().min(20).max(500), rows: z.number().int().min(5).max(200) }),14  z.object({ t: z.literal('ping'), n: z.number() }),15]);16export type TermClientMsg = z.infer<typeof TermClientMsg>;17export const TermServerMsg = z.discriminatedUnion('t', [18  z.object({ t: z.literal('ready'), sessionId: z.string(), cols: z.number(), rows: z.number() }),19  z.object({ t: z.literal('pong'), n: z.number() }),20  z.object({ t: z.literal('exit'), code: z.number().nullable() }), // the session process ended21  z.object({ t: z.literal('error'), code: z.string(), message: z.string() }),22]);23export type TermServerMsg = z.infer<typeof TermServerMsg>;2425/** WS close codes used by the terminal stream. */26export const WS_CLOSE = {27  UNAUTHORIZED: 4401,28  FORBIDDEN: 4403,29  NOT_FOUND: 4404,30  SESSION_ENDED: 4410,31  BACKPRESSURE: 4429,32  RUNTIME_UNAVAILABLE: 4503,33} as const;3435export const ServerEvent = z.discriminatedUnion('type', [36  z.object({ type: z.literal('hello'), serverTime: z.string() }),37  z.object({ type: z.literal('session.updated'), session: TerminalSession }),38  z.object({ type: z.literal('session.removed'), id: z.string() }),39  z.object({ type: z.literal('task.updated'), task: AgentTask }),40  z.object({ type: z.literal('notification'), notification: Notification }),41  z.object({ type: z.literal('auth.expired') }),42]);43export type ServerEvent = z.infer<typeof ServerEvent>;4445// ---------------- runtime IPC (server <-> runtime over a 0600 unix socket) ----------------46// HTTP+JSON and WebSocket on the same unix socket. Every request carries47// `Authorization: Bearer <CR_RUNTIME_TOKEN>` (random, generated at deploy, shared via env).48// All `root` values are ABSOLUTE canonical directories chosen by the server from the49// configured workspace roots / project table; `rel` values are untrusted relative paths50// that the runtime must resolve + realpath + verify stay inside `root` (symlink-safe).5152export const RtSessionSpec = z.object({53  id: z.string().uuid(),54  kind: z.enum(['claude', 'shell']),55  cwd: z.string(), // absolute, already validated by server; runtime re-validates against roots56  args: z.array(z.string()).default([]), // extra CLI args for claude (e.g. ["--resume", id])57  env: z.record(z.string(), z.string()).default({}),58  cols: z.number().int(),59  rows: z.number().int(),60});61export type RtSessionSpec = z.infer<typeof RtSessionSpec>;62export const RtSessionInfo = z.object({63  id: z.string(),64  alive: z.boolean(), // tmux session exists and pane process not dead65  pid: z.number().nullable(),66  attached: z.number(), // attached tmux clients67  lastActivityMs: z.number().nullable(), // tmux window_activity68  exitCode: z.number().nullable(), // when pane is dead69  cols: z.number().nullable(),70  rows: z.number().nullable(),71});72export type RtSessionInfo = z.infer<typeof RtSessionInfo>;7374export const RtTaskSpec = z.object({75  id: z.string().uuid(),76  command: z.string(),77  cwd: z.string(), // absolute local dir (ignored for remote hosts unless remoteCwd given)78  host: z.string(), // "local" or allow-listed alias79  timeoutSec: z.number().nullable(),80  env: z.record(z.string(), z.string()).default({}),81});82export type RtTaskSpec = z.infer<typeof RtTaskSpec>;83export const RtTaskInfo = z.object({84  id: z.string(),85  state: z.enum(['RUNNING', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'TIMED_OUT', 'LOST']),86  exitCode: z.number().nullable(),87  startedAtMs: z.number().nullable(),88  endedAtMs: z.number().nullable(),89  logBytes: z.number(),90  cpuSec: z.number().nullable(),91  maxRssMb: z.number().nullable(),92});93export type RtTaskInfo = z.infer<typeof RtTaskInfo>;9495export const RtEvent = z.discriminatedUnion('type', [96  z.object({ type: z.literal('session.exit'), id: z.string(), exitCode: z.number().nullable() }),97  z.object({ type: z.literal('session.activity'), id: z.string(), atMs: z.number(), attached: z.number() }),98  /** From Claude Code hooks (Notification / Stop / UserPromptSubmit / SessionStart). */99  z.object({100    type: z.literal('claude.hook'),101    id: z.string(), // CR session id (from CR_SESSION_ID env inside the tmux session)102    event: z.string(),103    conversationId: z.string().nullable(),104    message: z.string().nullable(),105  }),106  z.object({ type: z.literal('task.updated'), task: RtTaskInfo }),107]);108export type RtEvent = z.infer<typeof RtEvent>;109
Files main
Menu

Theme