-
Notifications
You must be signed in to change notification settings - Fork 0
RPC Dispatch, Handlers & Service Bridges
The Server Edition reuses the desktop IPC vocabulary but replaces the Electron transport with a WebSocket JSON-RPC channel. This page covers the three pieces of that substitution: the dispatcher (rpc.ts), the handler table (handlers.ts), and the boot wiring in index.ts that starts the agent bridge and space-sync hooks alongside the HTTP/WS shell. The renderer-side counterpart is documented under Renderer Shim & Server Boundary.
rpc.ts is deliberately tiny and transport-free. Its contract is:
-
RpcRequest = { id, method, args },RpcResponse = { id, ok, result?, error? }. -
createDispatcher(handlers)returnsasync (message: unknown) => Promise<RpcResponse | null>. - A message that is not an object with a string
methodreturnsnull— the dispatcher drops it silently rather than replying. - Unknown methods return
{ ok: false, error: "unhandled: <method>" }, so a half-added capability fails loudly on the caller side. - Handler invocation is wrapped in
try/catch; a thrown error becomes{ ok: false, error: String(error) }. Non-arrayargsare coerced to[]. - Handlers may be sync or async — the result is awaited before the envelope is built.
flowchart TD
A[WebSocket frame] --> B{object with string method?}
B -- no --> X[return null - dropped]
B -- yes --> C{handlers method exists?}
C -- no --> E["ok:false, error: unhandled: method"]
C -- yes --> D["await handler(args or [])"]
D -- resolves --> F["ok:true, result"]
D -- throws --> G["ok:false, error, String(error)"]
Key nodes: the null return at B means malformed frames die at the dispatcher and never reach a handler; the unhandled branch at C is the observable signal that the server table and the renderer shim have drifted out of sync; the catch at D→G guarantees a rejection is always converted into an envelope rather than killing the socket.
buildHandlers(platform: CorePlatform, lifecycle?) returns Record<string, RpcHandler> keyed by IPC constants from ../shared/ipc. The method name in a request is the IPC channel name, which is what lets the browser shim emulate the preload API without a translation layer.
Construction order inside buildHandlers matters:
-
readVersion()parsespackage.jsonforIPC.appVersion(fallback'0.0.0'). -
WorkspaceStore(platform)— the single on-disk source of truth for projects, nodes and links. - A
let linkServiceis declared (not yet constructed) so the PTY broadcast tap below can reach it. -
PtyManager({ userDataPath, broadcast })— the broadcast wrapper forwards every event toplatform.broadcast, and additionally, for channels prefixed${IPC.ptyData}:, callslinkService?.notePtyActivity(nodeId). PTY output is therefore both a renderer stream and a link-engine activity signal. -
chatRuntimeviacreateChatRuntime— provider resolution reads app settings plus anTERMSPRAWL_PROVIDER_KEY_<PROVIDER_ID>environment override; provider kind is inferred asanthropicvsopenaiby regex overbaseUrl/name. Chat events are broadcast onchat:event:<nodeId>. -
linkService = new LinkService({...})— wired toWorkspaceStore(links, project lookup, node lookup, run recording), toPtyManager(capturePane,write), toplatform.broadcastfor chat replies, and to the A2A peer client. It is registered withlifecycle?.onDispose(() => linkService?.dispose()).
Two construction-time helpers protect the boundary:
-
safeResolve(root, rel)resolves a leaf under a root and throwspath outside rooton traversal; it backs the file list/read surface used by the file tree. -
redactSettings(settings)structurally clones settings and replaceschat.keys[].keywith{ providerId, hasKey }, and nullstelegram.token. Secrets never leave the process onapp:settings-get; writes go back throughapp:settings-setwith full values.
resolveRepoRoot(target) is the Git gate: it calls resolveGitScope, accepts only kind === 'local', and then findRepoRoot(cwd). A target that is not a known project resolves to null, and every git handler short-circuits to a failure result instead of touching the filesystem.
| Domain | Channels (representative) | Backing service | Notable behavior |
|---|---|---|---|
| App / boot reads |
app:version, app:settings-get/set, update:*, announcement:get
|
readVersion, loadAppSettings/saveAppSettings, idleUpdateStatus
|
Updates are inert; announcements return null; settings reads are redacted |
| Capability pages |
settings:capabilities-get, skill/plugin/hook setters, settings:usage-get
|
— | Return supported: false payloads, or throw explicit "unavailable in Server Edition" errors |
| Workspace |
workspace:snapshot, workspace:save-nodes
|
WorkspaceStore |
Direct pass-through to the store |
| Projects |
project:add/import/close/archive/reopen/delete/update-settings/rename, github:import
|
WorkspaceStore, ensureFolderProjectRoot, importGitHubRepo
|
project:add dedupes by cwd and validates the folder up front; project:import preserves the original project id; github:import requires TS_CLOUD_API + TS_SPACE_BOOT_TOKEN and clones under <userData>/projects-src
|
| Git | stage/unstage/discard/commit/commit-message/create-branch/checkout/push/pull/publish/worktrees list/add/remove |
git-service, commit-message
|
Every handler funnels through resolveRepoRoot; when it is null they return { code: 1, stderr: 'no project folder' } (or [] / { ok:false }) |
| Chat |
chat:send, chat:stop, chat:approve
|
createChatRuntime, projectChatTools
|
send validates nodeId + messages array before touching the runtime; approve accepts only 'approve' | 'deny'; tools are anchored to the first open local project |
| Links / A2A |
links:list, links:run, links:mark-dirty, links:update, links:send-to-peer
|
LinkService, loadProjectFile, sendText
|
update persists via saveLinks then calls linksChanged(); peer sends resolve the peer from settings with an env-token override and a 20 s timeout |
A note on the file header: it describes a deliberately narrow "v1 scope" (projects + terminals + boot reads, with git/cloud/accounts/hooks called out as unimplemented). The table returned at the bottom of the same file now contains the full git surface, so the header is best read as historical context rather than a current capability list. The authoritative scope limit is the renderer shim, which decides which channels the browser is allowed to call.
Dispatch is unidirectional for requests; broadcasts bypass it entirely.
sequenceDiagram
autonumber
participant R as Renderer (shim.js)
participant W as WS layer (index.ts)
participant D as Dispatcher (rpc.ts)
participant H as Handler table (handlers.ts)
participant S as Core services
R->>W: RpcRequest { id, method, args }
W->>D: dispatch(message)
D->>H: handlers[method](args)
H->>S: PtyManager / Git / chat runtime / LinkService
S-->>H: result
H-->>D: result
D-->>W: RpcResponse { id, ok, result }
W-->>R: RpcResponse frame
Note over S,W: Async pushes skip the request path:<br/>platform.broadcast(channel, payload) fans out to WS clients,<br/>e.g. pty:data:<nodeId>, chat:event:<nodeId>
Key points: the dispatcher never sees broadcasts, so a streaming chat:event:* or pty:data:* message has no request id and is not awaitable. lifecycle.onDispose is the only teardown hook registered here, and it exists because LinkService holds timers/subscriptions that must be released when the boot gate shuts down.
index.ts is the shell that makes the above reachable. It:
- Resolves the port from
PORT, thenargv[2], then3110; servesout/rendererwithsrc/server/shim.jsinjected; serves only existing regular files (isRegularFile) with a MIME table. - Extracts
?token=from upgrade URLs as the browser fallback auth channel (tokenFromUrl), delegating the actual decision tocreateAuthPolicy/authorizeUpgrade/timingSafeCompare. - Enforces bind safety with
assertSafeServerBindand path containment withresolveContainedPath. - Constructs
ServerPlatform, thenbuildHandlers(platform), thencreateDispatcher(handlers)— the only place the handler table is instantiated. - Starts two independent service bridges next to the dispatcher:
startAgentBridge(...)andcreateSpacePusher(...)/restoreFromCloud(...)fromspace-sync-wiring. Both are wired here rather than insidehandlers.ts, which keeps the RPC table free of background-service lifecycle.
-
Malformed frames are dropped (
null), not answered — callers relying on any reply should still guard with a timeout. -
Unknown methods answer
unhandled: <method>; this is the failure mode when a new IPC channel is added toshared/ipcand the shim but not tobuildHandlers. -
Missing secret material never crosses the boundary: provider keys become
hasKeyflags, the Telegram token becomesundefined. -
Path traversal is rejected inside
safeResolve; git targets outside known local projects are rejected insideresolveRepoRoot. -
Space-only features (
github:import) degrade to{ ok: false, error: 'space env missing' }when the boot tokens are absent. -
Desktop-only features (skill/plugin/hook management, capability discovery, usage) either return
supported: falseor throw a named error, keeping the server surface honest about what it cannot do. -
Disposal: anything constructed during boot that owns resources must be registered via
lifecycle.onDispose; currentlyLinkServiceis the only RPC-table-owned subscription.
-
New channel: define the constant in
src/shared/ipc.ts, add a key to the object returned bybuildHandlers, and expose it in the shim. The dispatcher needs no change — the table is the registry. -
New broadcast stream: call
platform.broadcast(channel, payload)from the handler or from a bridged service; prefix-based taps (like the PTY-to-link activity hook) can be layered on top of the same call. -
New background service: follow the
agent-bridge/space-sync-wiringpattern — a separate module with astart*/create*entry, invoked fromindex.tsand given the platform/workspace handles it needs. -
New service bridge: mirror
LinkService's construction — a callback bag of narrow functions wired toWorkspaceStoreandPtyManager, plus disposal registration.
Sources: src/server/rpc.ts, src/server/handlers.ts, src/server/handlers.ts, src/server/handlers.ts, src/server/index.ts, src/server/agent-bridge.ts, src/server/space-sync-wiring.ts
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance