Skip to content

IPC Protocol Dictionary

1. Channel Topology

flowchart TB
  R[Renderer] --> P[preload.ts]
  P -->|ipcRenderer.send/invoke| M[main/index.ts ipcMain]
  M -->|HTTP + internal token| B[backend routes]

2. Channel Dictionary

ChannelIPC TypeParamsReturn SchemaMain Handler Behavior
app:close-windowsendnonenoneRequests focused/main window close; the main window close guard checks backend SSH/SFTP activity before allowing destruction
app:close-confirmation-requestMain-to-renderer event{ requestId: string }noneRequests the shared renderer close-warning dialog; preload validates the opaque ID and buffers one early request until the React listener mounts
app:close-confirmation-responsesend{ requestId: string, confirmed: boolean }noneResolves only the pending close decision owned by the sending webContents and matching opaque request ID
i18n:get-localeinvokenonePromise<string>Returns current resolved locale
i18n:set-localeinvokelocale: stringPromise<string>Resolves/persists in-memory locale and updates title
app:get-runtime-user-nameinvokenonePromise<string>Returns OS username fallback chain
app:get-version-infoinvokenonePromise<{ appName: string; version: string; buildVersion: string; buildTime: string; commit: string; electron: string; chromium: string; node: string; v8: string; os: string }>Returns About metadata including app version/build plus runtime technical information
app:get-pending-launch-working-directoryinvokenonePromise<string | null>Returns current pending context-launch working directory parsed from CLI
app:get-downloads-pathinvokenonePromise<string>Returns the OS downloads directory for local save defaults
app:create-sftp-temporary-fileinvokefileName: stringPromise<string>Creates a unique local destination under the Cosmosh SFTP temp root for backend download/open flows
app:create-sftp-downloads-fileinvokefileName: stringPromise<string>Authorizes one exact single-use destination under the OS Downloads directory for the requesting renderer
app:select-sftp-upload-filesinvokenonePromise<{ canceled: boolean; files: Array<{ name: string; localPath: string; size: number; modifiedAt: string }> }>Opens a native multi-file picker and copies selected regular files into isolated directories under the controlled SFTP temp root
app:stage-sftp-dropped-upload-filesinvokeentries: Array<{ name: string; localPath?: string }>Promise<{ canceled: false; files: Array<{ name: string; localPath: string; size: number; modifiedAt: string }>; rejectedEntries?: Array<{ name: string; reason: 'directory-unsupported' | 'not-file' | 'path-unavailable' | 'unreadable' }> }>Stages preload-resolved local files dropped onto SFTP directory targets and reports unsupported folders/non-files without exposing raw path staging to renderer
app:cleanup-sftp-temporary-filesinvokelocalPaths: string[]Promise<boolean>Best-effort removes validated staged upload files and their now-empty isolated temp directories
app:open-sftp-temporary-fileinvokelocalPath: stringPromise<boolean>Opens an existing file under the Cosmosh SFTP temp root with the OS default application
app:read-sftp-temporary-image-previewinvokelocalPath: stringPromise<string>Validates an existing image file under the Cosmosh SFTP temp root and returns a bounded data URL for renderer image preview
app:start-sftp-temporary-file-watchinvokelocalPath: stringPromise<string>Starts a debounced watcher for one existing file under the Cosmosh SFTP temp root and returns a watch id
app:stop-sftp-temporary-file-watchinvokewatchId: stringPromise<boolean>Stops a previously created SFTP temp-file watcher
app:show-sftp-open-with-dialoginvokelocalPath: stringPromise<boolean>Windows only: validates a temp file path and opens the system Open With picker through the shell openas verb
app:list-sftp-open-with-applicationsinvokelocalPath: stringPromise<Array<{ id: string; name: string; path: string; bundleIdentifier?: string; iconDataUrl?: string }>>macOS only: validates a temp file path and returns NSWorkspace applications that can open it
app:open-sftp-file-with-applicationinvokelocalPath: string, applicationPath: stringPromise<boolean>macOS only: validates the temp file and selected app against the available application list, then opens the file with that app
app:sftp-temporary-file-changedevent (main -> renderer){ watchId: string; localPath: string; size: number; modifiedAt: string }nonePushes one debounced change event for a watched SFTP temp file owned by the renderer webContents
app:get-database-security-infoinvokenonePromise<{ runtimeMode: 'development' | 'production'; resolverMode: 'development-fixed-key' | 'safe-storage' | 'master-password-fallback'; safeStorageAvailable: boolean; databasePath: string; securityConfigPath: string; hasEncryptedDbMasterKey: boolean; hasMasterPasswordHash: boolean; hasMasterPasswordSalt: boolean; hasMasterPasswordEnv: boolean; fallbackReady: boolean }>Returns non-sensitive database encryption bootstrap diagnostics for Settings → Advanced
app:resolve-system-proxyinvoke{ host: string; port: number }Promise<{ proxyRules: string }>Validates one SSH server destination and resolves Chromium system/PAC proxy rules for https://host:port/
app:launch-working-directoryevent (main -> renderer)cwd: stringnonePushes context-launch working directory when a second instance is invoked
app:menu-actionevent (main -> renderer)action: 'open-about' | 'open-settings' | 'new-tab' | 'close-current-tab' | 'close-right-tabs' | 'show-tab-switcher'noneDispatches validated app-menu commands from the macOS system menu to renderer tab/state handlers
app:open-devtoolsinvokenonePromise<boolean>Opens devtools for the current main window when available
app:toggle-devtoolsinvokenonePromise<boolean>Toggles detached DevTools for the current main window (open when closed, close when open)
app:reload-webviewinvokenonePromise<boolean>Reloads the active renderer webContents and bypasses cache for deterministic debug refresh
app:restart-backend-runtimeinvokenonePromise<boolean>Restarts backend runtime in-place during development without full app restart
app:show-in-file-managerinvoketargetPath?: stringPromise<boolean>Opens file/folder in OS file manager
app:open-external-urlinvoketargetUrl: stringPromise<boolean>Opens trusted HTTP(S) URL with system default browser
app:set-windows-system-menu-symbol-colorinvokesymbolColor: stringPromise<boolean>Applies token-driven Windows title bar system-menu symbol color to current main window overlay
app:show-save-file-dialoginvokedefaultPath?: stringPromise<{ canceled: boolean; filePath?: string }>Opens a native save dialog and authorizes the selected path for one SFTP download by the requesting renderer
app:import-private-keyinvokenonePromise<{ canceled: boolean; fileName?: string; content?: string }>Opens native file picker and returns the selected file name plus UTF-8 private key content
app:get-process-performance-statsinvokenonePromise<{ sampledAt: number; cpuPercent: number | null; mainProcessMemory: { rssBytes: number; heapTotalBytes: number; heapUsedBytes: number; externalBytes: number; arrayBuffersBytes: number }; rendererProcessMemory: { residentSetBytes: number; privateBytes: number; sharedBytes: number } | null; backendProcess: { pid: number; cpuPercent: number | null; memoryRssBytes: number | null } | null }>Samples main process CPU + memory, resolves renderer process memory from active window, and includes backend child-process CPU/RSS memory for debug monitoring overlay
app:export-main-heap-snapshotinvokenonePromise<{ ok: boolean; filePath?: string; message?: string }>Writes a V8 heap snapshot for the main process into app user-data debug snapshot directory
debug:backend-request-trace-listinvokenonePromise<BackendRequestTrace[]>Returns the retained sanitized backend request mirror list and subscribes the renderer webContents to future trace events; empty when request tracing is disabled
debug:backend-request-trace-clearinvokenonePromise<boolean>Clears the retained development request mirror ring buffer
debug:backend-request-trace-eventevent (main -> renderer)BackendRequestTracenonePushes one completed sanitized backend proxy request mirror to subscribed renderer webContents
backend:test-pinginvokenonePromise<ApiTestPingResponse | ApiErrorResponse>Calls backend health test endpoint
backend:settings-getinvokenonePromise<ApiSettingsGetResponse | ApiErrorResponse>GET persisted application settings
backend:settings-updateinvokepayload: ApiSettingsUpdateRequestPromise<ApiSettingsUpdateResponse | ApiErrorResponse>PUT application settings snapshot
backend:audit-list-eventsinvokequery?: ApiAuditEventListQueryPromise<ApiAuditEventListResponse | ApiErrorResponse>GET audit event list with filter + pagination
backend:audit-get-event-by-idinvokeeventId: stringPromise<ApiAuditEventDetailResponse | ApiErrorResponse>GET single audit event detail
backend:ssh-list-serversinvokenonePromise<ApiSshListServersResponse | ApiErrorResponse>GET SSH server list
backend:ssh-create-serverinvokepayload: ApiSshCreateServerRequestPromise<ApiSshCreateServerResponse | ApiErrorResponse>POST create SSH server
backend:ssh-update-serverinvokeserverId: string, payload: ApiSshUpdateServerRequestPromise<ApiSshUpdateServerResponse | ApiErrorResponse>PUT update SSH server
backend:ssh-get-server-credentialsinvokeserverId: stringPromise<ApiSshGetServerCredentialsResponse | ApiErrorResponse>GET decrypted credentials
backend:ssh-list-foldersinvokenonePromise<ApiSshListFoldersResponse | ApiErrorResponse>GET folder list
backend:ssh-create-folderinvokepayload: ApiSshCreateFolderRequestPromise<ApiSshCreateFolderResponse | ApiErrorResponse>POST create folder
backend:ssh-update-folderinvokefolderId: string, payload: ApiSshUpdateFolderRequestPromise<ApiSshUpdateFolderResponse | ApiErrorResponse>PUT update folder
backend:ssh-list-tagsinvokenonePromise<ApiSshListTagsResponse | ApiErrorResponse>GET tag list
backend:ssh-create-taginvokepayload: ApiSshCreateTagRequestPromise<ApiSshCreateTagResponse | ApiErrorResponse>POST create tag
backend:ssh-list-keychainsinvokenonePromise<ApiSshListKeychainsResponse | ApiErrorResponse>GET keychain list
backend:ssh-create-keychaininvokepayload: ApiSshCreateKeychainRequestPromise<ApiSshCreateKeychainResponse | ApiErrorResponse>POST create keychain
backend:ssh-update-keychaininvokekeychainId: string, payload: ApiSshUpdateKeychainRequestPromise<ApiSshUpdateKeychainResponse | ApiErrorResponse>PUT update keychain
backend:ssh-get-keychain-credentialsinvokekeychainId: stringPromise<ApiSshGetKeychainCredentialsResponse | ApiErrorResponse>GET decrypted keychain credentials
backend:ssh-create-sessioninvokepayload: ApiSshCreateSessionRequestPromise<ApiSshCreateSessionResponse | ApiSshCreateSessionHostVerificationRequiredResponse | ApiErrorResponse>POST create SSH shell session
backend:ssh-trust-fingerprintinvokepayload: ApiSshTrustFingerprintRequestPromise<ApiSshTrustFingerprintResponse | ApiErrorResponse>POST trust host fingerprint
backend:ssh-close-sessioninvokesessionId: stringPromise<{ success: boolean }>DELETE SSH session
backend:ssh-delete-serverinvokeserverId: stringPromise<{ success: boolean }>DELETE SSH server
backend:ssh-delete-folderinvokefolderId: stringPromise<{ success: boolean }>DELETE SSH folder
backend:ssh-delete-keychaininvokekeychainId: stringPromise<{ success: boolean }>DELETE SSH keychain
backend:port-forward-list-rulesinvokenonePromise<ApiPortForwardListRulesResponse | ApiErrorResponse>GET persisted SSH port-forwarding rules and merge in-memory runtime status
backend:port-forward-create-ruleinvokepayload: ApiPortForwardCreateRuleRequestPromise<ApiPortForwardCreateRuleResponse | ApiErrorResponse>POST create a stopped port-forwarding rule
backend:port-forward-update-ruleinvokeruleId: string, payload: ApiPortForwardUpdateRuleRequestPromise<ApiPortForwardUpdateRuleResponse | ApiErrorResponse>PUT update a stopped port-forwarding rule
backend:port-forward-start-ruleinvokeruleId: string, payload: ApiPortForwardStartRuleRequestPromise<ApiPortForwardStartRuleResponse | ApiErrorResponse>POST start one rule with optional transient system proxy rules; may return shared SSH_HOST_UNTRUSTED payload for fingerprint trust retry
backend:port-forward-stop-ruleinvokeruleId: stringPromise<ApiPortForwardStopRuleResponse | ApiErrorResponse>POST stop one active rule; stopped rules are handled idempotently by backend
backend:port-forward-delete-ruleinvokeruleId: stringPromise<{ success: boolean }>DELETE one stopped port-forwarding rule
backend:sftp-create-sessioninvokepayload: ApiSftpCreateSessionRequestPromise<ApiSftpCreateSessionResponse | ApiSftpCreateSessionHostVerificationRequiredResponse | ApiErrorResponse>POST create SFTP file-system session
backend:sftp-list-directoryinvokesessionId: string, query?: ApiSftpListDirectoryQueryPromise<ApiSftpListDirectoryResponse | ApiErrorResponse>GET one SFTP directory listing
backend:sftp-get-entry-detailsinvokesessionId: string, payload: ApiSftpEntryDetailsRequestPromise<ApiSftpEntryDetailsResponse | ApiErrorResponse>POST fetch non-recursive metadata for selected SFTP entries
backend:sftp-read-fileinvokesessionId: string, query: ApiSftpReadFileQueryPromise<ApiSftpReadFileResponse | ApiErrorResponse>GET bounded UTF-8 file preview from one SFTP session
backend:sftp-write-fileinvokesessionId: string, payload: ApiSftpWriteFileRequestPromise<ApiSftpWriteFileResponse | ApiErrorResponse>POST save editable UTF-8 SFTP preview content back to one regular remote file after remote size/mtime conflict checks; remote conflicts return SFTP_UPLOAD_CONFLICT
backend:sftp-download-fileinvokesessionId: string, payload: ApiSftpDownloadFileRequestPromise<ApiSftpDownloadFileResponse | ApiErrorResponse>POST stream one regular remote SFTP file only into an exact owner-bound path authorized by app utility IPC
backend:sftp-upload-fileinvokesessionId: string, payload: ApiSftpUploadFileRequestPromise<ApiSftpUploadFileResponse | ApiErrorResponse>POST stream one controlled local temp file to a new remote path, or replace an existing regular file after snapshot/explicit overwrite confirmation; conflicts return SFTP_UPLOAD_CONFLICT
backend:sftp-get-transfer-progressinvoketransferId: stringPromise<ApiSftpTransferProgressResponse | ApiErrorResponse>GET byte progress, rolling speed, status, and optional failure reason for one active or recently completed SFTP transfer
backend:sftp-create-directoryinvokesessionId: string, payload: ApiSftpCreateDirectoryRequestPromise<ApiSftpCreateDirectoryResponse | ApiErrorResponse>POST create remote SFTP directory
backend:sftp-create-fileinvokesessionId: string, payload: ApiSftpCreateFileRequestPromise<ApiSftpCreateFileResponse | ApiErrorResponse>POST create empty remote SFTP file
backend:sftp-rename-entryinvokesessionId: string, payload: ApiSftpRenameRequestPromise<ApiSftpRenameResponse | ApiErrorResponse>POST rename or move remote SFTP entry
backend:sftp-copy-entryinvokesessionId: string, payload: ApiSftpCopyRequestPromise<ApiSftpCopyResponse | ApiErrorResponse>POST copy remote SFTP file or directory tree
backend:sftp-delete-entryinvokesessionId: string, payload: ApiSftpDeleteRequestPromise<ApiSftpDeleteResponse | ApiErrorResponse>POST delete remote SFTP file, symlink, or directory tree
backend:sftp-batch-operationinvokesessionId: string, payload: ApiSftpBatchOperationRequestPromise<ApiSftpBatchOperationResponse | ApiErrorResponse>POST ordered batch copy, move, link, or delete across SFTP entries
backend:sftp-start-taskinvokesessionId: string, payload: ApiSftpStartTaskRequestPromise<ApiSftpStartTaskResponse | ApiErrorResponse>POST one asynchronous SFTP task; download admission consumes the exact owner/path/transferId authorization before forwarding
backend:sftp-list-tasksinvokesessionId: stringPromise<ApiSftpListTasksResponse | ApiErrorResponse>GET retained task snapshots and release terminal download authorization leases owned by the calling renderer
backend:sftp-get-taskinvokesessionId: string, taskId: stringPromise<ApiSftpGetTaskResponse | ApiErrorResponse>GET one retained task snapshot using its accepted session id; terminal download observation releases the owner-bound authorization lease
backend:sftp-get-archive-capabilitiesinvokesessionId: stringPromise<ApiSftpArchiveCapabilitiesResponse | ApiErrorResponse>GET the fixed remote archive-tool capability matrix cached for one SFTP session
backend:sftp-start-archive-operationinvokesessionId: string, payload: ApiSftpArchiveOperationRequestPromise<ApiSftpArchiveOperationAcceptedResponse | ApiErrorResponse>POST a structured compression or extraction request; Main never accepts or constructs command text
backend:sftp-get-archive-operationinvokesessionId: string, operationId: stringPromise<ApiSftpArchiveOperationStatusResponse | ApiErrorResponse>GET retained archive state, named phase (including post-extraction verifying), conflict summary, cancellation state, and stable terminal result
backend:sftp-resolve-archive-conflictinvokesessionId: string, operationId: string, payload: ApiSftpArchiveConflictResolutionRequestPromise<ApiSftpArchiveConflictResolutionResponse | ApiErrorResponse>POST one task-wide overwrite, keep-both, or cancel decision for staged extraction conflicts
backend:sftp-cancel-archive-operationinvokesessionId: string, operationId: stringPromise<ApiSftpArchiveCancelResponse | ApiErrorResponse>DELETE requests bounded cancellation; terminal state is observed through archive-operation polling
backend:sftp-close-sessioninvokesessionId: stringPromise<{ success: boolean }>DELETE SFTP session
backend:local-terminal-list-profilesinvokenonePromise<ApiLocalTerminalListProfilesResponse | ApiErrorResponse>GET local terminal profile list
backend:local-terminal-create-sessioninvokepayload: ApiLocalTerminalCreateSessionRequestPromise<ApiLocalTerminalCreateSessionResponse | ApiErrorResponse>POST local terminal session (Main may inject one-shot cwd from launch context)
backend:local-terminal-close-sessioninvokesessionId: stringPromise<{ success: boolean }>DELETE local terminal session

3. Schema Sources

  • API payload types come from @cosmosh/api-contract, generated from packages/api-contract/openapi/cosmosh.openapi.yaml.
  • Backend, Main IPC proxy, and renderer HTTP callers must use API_PATHS and related generated contract exports from @cosmosh/api-contract instead of hard-coded route strings.
  • Archive IPC accepts only generated structured paths, enums, and conflict decisions. Remote command text, flags, tool output, and temporary paths never cross the preload boundary.
  • IPC-only payloads that are not generated from OpenAPI, including AppMenuAction, SftpOpenWithApplication, SftpTemporaryFileWatchChange, and BackendRequestTrace, are defined in packages/api-contract/src/ipc.ts and consumed by main, preload, and renderer type declarations.
  • Terminal WebSocket payloads and Remote Enhancements protocol constants are defined in packages/api-contract/src/terminal-protocol.ts; backend and renderer import those discriminated unions directly.
  • BackendRequestTrace is development diagnostics only. It is populated by the main-process backend proxy in unpackaged development runs; production packages do not collect traces or load the DevTools extension.

3.1 SSH Visual Metadata Fields

The following SSH entity payloads now include visual metadata for persistent icon/color customization:

  • ApiSshCreateServerRequest / ApiSshUpdateServerRequest: optional iconKey, optional colorKey.
  • ApiSshCreateFolderRequest / ApiSshUpdateFolderRequest: optional iconKey, optional colorKey.
  • ApiSshListServersResponse: each server item includes iconKey and colorKey.
  • ApiSshListFoldersResponse: each folder item includes iconKey and colorKey.

colorKey is constrained to the predefined palette enum in the API contract.

SSH security policy fields in current contract:

  • ApiSshCreateServerRequest / ApiSshUpdateServerRequest: host/transport and renderer policy fields include strictHostKey, enableSshCompression, remoteEnhancementsEnabled, disableCharacterWidthCompatibilityMode, terminalClipboardAccess, and proxy policy.
  • ApiSshListServersResponse: every server item requires persisted strictHostKey, enableSshCompression, remoteEnhancementsEnabled, disableCharacterWidthCompatibilityMode, terminalClipboardAccess, and proxyMode; consumers must not fail open with local defaults when a response omits policy.
  • ApiSshCreateSessionRequest: optional strictHostKey, enableSshCompression, and remoteEnhancementsEnabled values bind one attempt to its resolved snapshot. The Remote Enhancements request field is disable-only: effective access is the current global setting, persisted server field, and request !== false.
  • Character width compatibility is not sent to SSH session creation or terminal WS messages; renderer applies it when creating xterm instances.

3.2 SSH Port Forwarding Contract

Port forwarding payloads are generated from the OpenAPI source and consumed by backend, main, preload, and renderer wrappers:

  • ApiPortForwardListRulesResponse
  • ApiPortForwardCreateRuleRequest / ApiPortForwardCreateRuleResponse
  • ApiPortForwardUpdateRuleRequest / ApiPortForwardUpdateRuleResponse
  • ApiPortForwardStartRuleResponse
  • ApiPortForwardStopRuleResponse

Rule type is local, remote, or dynamic.

Type-specific fields:

  • Local: localBindHost, localBindPort, targetHost, targetPort
  • Remote: remoteBindHost, remoteBindPort, targetHost, targetPort
  • Dynamic: localBindHost, localBindPort

Runtime status is returned as runtime.status and is not persisted. Start can return SSH_HOST_UNTRUSTED; renderer must trust the fingerprint through backend:ssh-trust-fingerprint before retrying.

3.3 SFTP Batch Operation Contract

SFTP batch payloads are generated from OpenAPI and used unchanged by renderer, main IPC proxy, and backend routes.

  • ApiSftpBatchOperationRequest.operation is copy, move, link, or delete.
  • targetDirectoryPath is required for copy, move, and link; it is ignored for delete.
  • link creates an absolute symbolic link in the target directory that points to the source remote absolute path. The target name uses the source basename and the same conflict suffix policy as copy.
  • The response shape stays ApiSftpBatchOperationResponse: ordered per-entry results, completed/failed/skipped counts, fail-fast execution, and no rollback of already completed entries.

3.4 Terminal WebSocket Contract (Renderer ↔ Backend)

Although terminal stream messages are not Electron IPC channels, they are part of the same cross-process contract surface. terminal-protocol.ts is the source of truth, and the current remote helper protocol version is 2.

  • Client to server (/ws/ssh/{sessionId} and /ws/local-terminal/{sessionId}):
    • input, resize, ping, close, history-delete
    • completion-request with requestId, linePrefix, cursorIndex, optional workingDirectoryHint, optional limit, optional fuzzyMatch, optional source filters (includeHistory, includeBuiltInCommands, includePathSuggestions, includePasswordSuggestions), and trigger (typing or manual)
  • Server to client:
    • ready, output, telemetry, history, pong, error, exit
    • completion-response with requestId, replacePrefixLength, and ranked completion items
    • bootstrap-status for side-channel Remote Bootstrap install/probe status
    • remote-enhancement-runtime-status with backend-owned state (pending, active, or disabled), optional helperVersion, protocolVersion, capabilities, code, and message
    • remote-shell-event for runtime shell state emitted by the installed helper over OSC 777; every event requires helperVersion, integer protocolVersion, capabilities, shell, event, and timestamp

Remote shell event union rules:

  • cwd requires an absolute decoded cwd and the cwd capability.
  • command-start and foreground-command require sanitized command plus commandId; command-end additionally requires integer exitCode and durationMs.
  • line-state requires lineLength, cursorIndex, and promptGeneration, carries no input text, and is currently advertised only by Zsh.
  • Sh/Ash advertise only cwd and prompt-ready. No event is accepted unless its name and required fields agree with the exact pre-shell capability contract.

Completion item contract notes:

  • items[].source includes history, inshellisense, and runtime-computed runtime.
  • items[].kind includes existing command-spec/history categories plus runtime categories (path, secret).
  • Runtime categories are used for path candidates and interactive secret-fill actions while preserving the same completion-response envelope.

Current implementation note:

  • Completion messages are handled in SshSessionService and LocalTerminalSessionService via shared normalization in terminal/shared.ts and shared ranking engine in terminal/completion/engine.ts.
  • remote-enhancement-runtime-status and remote-shell-event are SSH-only and never appear on local-terminal sessions. A successful pre-shell Bootstrap ensure starts the runtime as pending; a matching integration-ready event within 10 seconds changes it to active. Ensure failure, BOOTSTRAP_ENSURE_TIMEOUT, HELPER_HANDSHAKE_TIMEOUT, or any live contract mismatch changes it to disabled. Renderer keeps the latest runtime status separately from bounded event history and must treat it as diagnostics, not as authority to reconstruct backend helper state.
  • remote-shell-event payloads must not carry passwords, secrets, full terminal output, full command lines, line-buffer contents, or large arbitrary data. Dynamic helper cwd/command fields are canonical Base64 inside the OSC JSON envelope, then decoded and validated before forwarding. Backend caps decoded OSC payloads at 8 KiB, strips Cosmosh OSC, and streams non-Cosmosh OSC unchanged.
  • Renderer routes the complete server message union through the source pane's runtime/reducer. Completion responses, password prompts, status, telemetry, errors, exits, debug events, reconnect, and command markers must never fall back to primary/active pane state implicitly.

3.5 Main-Owned Active Connection Contract

The close guard keeps activity authority in authenticated Main-to-Backend HTTP calls and exposes only a narrow renderer confirmation handshake:

  • GET /api/v1/runtime/active-connections returns sshCount, sftpCount, and their totalCount from backend session registries.
  • DELETE /api/v1/runtime/active-connections closes all currently registered SSH/SFTP sessions and returns the counts closed by that call.
  • Both HTTP operations require the internal token in Electron Main mode. They are not exposed by preload.ts, so renderer code cannot bulk-disconnect sessions or supply activity counts to the guard.
  • app:close-window keeps its existing fire-and-forget signature. Guarding is attached to the main BrowserWindow close lifecycle, so title-bar close, last-tab close, macOS close role, and app quit share the same authority.
  • Main sends app:close-confirmation-request only after the authoritative probe requires confirmation. Renderer presents the shared Dialog and returns app:close-confirmation-response; Main accepts the response only from the owning webContents with the matching opaque request ID.

4. Change Rules

When adding/modifying a channel, update in one commit:

  1. packages/main/src/preload.ts
  2. packages/main/src/index.ts
  3. packages/renderer/src/vite-env.d.ts
  4. relevant renderer transport/service wrappers
  5. this file (docs/developer/core/ipc-protocol.md)

5. Channel Addition Template

Use this checklist when introducing a new channel:

  1. Channel name: domain:action-name
  2. IPC type: invoke or send
  3. Params schema: explicit type in bridge and renderer declarations
  4. Return schema: success and error shape
  5. Main behavior: backend proxy or privileged local action
  6. Security notes: token/header handling, permission boundary, exposure limits
  7. Docs sync: update EN + ZH protocol pages in same change set

6. Server Proxy Contract

  • ApiSshCreateServerRequest / ApiSshUpdateServerRequest carry optional proxyMode = default | off | custom and optional proxyUrl.
  • ApiSshListServersResponse returns persisted proxy mode and URL for editing and connection planning.
  • ApiSshCreateSessionRequest, ApiSftpCreateSessionRequest, and ApiPortForwardStartRuleRequest carry optional transient systemProxyRules; this field is never persisted.
  • SystemProxyResolveRequest and SystemProxyResolveResult are IPC-only types in packages/api-contract/src/ipc.ts.
  • Main constructs the resolution URL from validated host/port fields. Renderer cannot submit an arbitrary URL to Session.resolveProxy.