Cosmosh Architecture
1. Runtime Topology
Cosmosh uses an Electron dual-process model with an embedded backend service:
- Main Process (
packages/main/src/index.ts): app lifecycle, BrowserWindow creation, preload wiring, IPC registration, backend process orchestration. - Preload Bridge (
packages/main/src/preload.ts): strict API surface exposed viacontextBridge. - Renderer Process (
packages/renderer/src): React UI, xterm UI, state orchestration. - Backend Process (
packages/backend/src/index.ts): Hono HTTP API + WebSocket session services for SSH/local terminal, plus SFTP browser, download, file-operation sessions, and SSH port-forwarding runtimes.
flowchart LR R[Renderer React App] -->|window.electron.*| P[Preload Bridge] P -->|ipcRenderer.invoke/send| M[Electron Main] M -->|HTTP localhost + internal token| B[Backend Hono API] R -->|WebSocket token URL| WS1[SSH WS Service] R -->|WebSocket token URL| WS2[Local Terminal WS Service] B --> WS1 B --> WS2 B --> DB[(SQLCipher via Prisma adapter)]
2. Main ↔ Renderer Responsibilities
Main Process (packages/main/src/index.ts)
- Starts BrowserWindow and backend warmup in parallel during app bootstrap.
- Keeps a single in-flight backend startup promise to deduplicate concurrent startup triggers.
- Main-process backend proxy requests now ensure backend readiness before forwarding HTTP calls.
- In development startup, main uses an incremental preflight (
packages/main/scripts/dev-preflight.cjs) and skips@cosmosh/api-contract/@cosmosh/i18nrebuilds when outputs are fresh. The same lifecycle probes the SQLCipher native binding under the system Node runtime and rebuilds it only when the current ABI is incompatible. - Development profiles are managed by
pnpm dev:profile(scripts/dev-profile.mjs). When a profile is selected or passed throughCOSMOSH_DEV_PROFILE, main applies it before window/backend startup so ElectronuserData, the SQLite file, and backend-only secret storage all resolve under.cosmosh/dev-profiles/<name>/. packages/main/scripts/dev-main.cjsruns under the workspace system Node, compiles Main, and passes that canonical Node executable to Electron through the development-onlyCOSMOSH_DEV_NODE_EXEC_PATHhandoff.- Main launches the development backend directly with the validated system Node executable and
tsx, avoiding both package-script orphan processes and Electron-versus-Node native ABI conflicts. Packaged Main continues to launch the synchronized backend with Electron'sprocess.execPathplusELECTRON_RUN_AS_NODE=1. - Production packaging does not rely on the app asar to resolve backend packages. Main prebuild copies built backend/api-contract/i18n artifacts plus curated recursive third-party runtime dependencies into
packages/main/resources-runtime/node_modules, then validates every non-workspace@cosmosh/backendproduction dependency resolves there. Any new backend production dependency must be covered bypackages/main/scripts/sync-backend-runtime.cjs, otherwise installer builds fail before launch instead of shipping a missing module. - CI packaging can also write
resources/remote-bootstrap/manifest-url.jsonwhenCOSMOSH_REMOTE_BOOTSTRAP_MANIFEST_URLis provided. Packaged main reads this resource only as a fallback after the environment variable, preserving local override behavior while allowing tagged release installers andmainbuild artifacts to discover their intended bootstrap manifest automatically. Unpackaged development runs fall back once more to the rollingremote-bootstrap-devmanifest URL, so local Remote Enhancements testing does not require per-shell setup. - Owns app-level capabilities: locale persistence (in-memory), window/devtools/file-manager actions.
- Owns the window/app close guard. Main prevents the initial close, queries backend-owned SSH/SFTP registries, delegates confirmation presentation to the renderer, and closes only after no activity is present or the user explicitly confirms interruption.
- Proxies renderer requests to backend endpoints with:
COSMOSH_INTERNAL_TOKENas internal auth header.- locale header for i18n-compatible backend responses.
Development Backend Runtime Boundary
The development launcher owns the system Node executable handoff because it runs before Electron replaces process.execPath. Main accepts only an absolute path that resolves to a canonical regular file, requires the POSIX executable bits where applicable, rejects the Electron host executable itself, and removes the handoff variable before spawning Backend. This value is development orchestration metadata, not part of the Backend environment contract.
Development and packaging intentionally use different native targets. The Main and standalone Backend predev lifecycles, plus Backend predb:init, invoke ensure-sqlcipher-native.cjs --runtime=node --if-needed; packaging invokes the same script without arguments to force an Electron-targeted release rebuild. Both paths open and close an in-memory database under the selected runtime after building; a failed probe aborts before Backend startup or packaged-runtime synchronization. On Windows, all Cosmosh processes using the shared binding must be closed before switching targets because a loaded .node file cannot be replaced.
Backend Process (packages/backend/src/index.ts)
- Registers idempotent graceful-shutdown flow for runtime signals and fatal process events.
- Exposes internal-token-protected runtime connection summary/close operations used only by Main's close guard; renderer does not receive a bulk-disconnect bridge.
- Shutdown order is explicit: stop WS session services, close HTTP listener, then disconnect Prisma/SQLite handles.
- Windows-specific termination (
SIGBREAK) is handled in the same path as POSIX signals to reduce stale DB lock cases. - Local terminal profile discovery now uses short-lived in-memory caching and parallel probing, reducing repeated profile scan latency on Home/Settings first-load paths.
- After the primary SSH transport authenticates and before it opens the interactive PTY,
SshSessionServiceruns the Remote Enhancements ensure flow throughRemoteBootstrapServicewhen global and server-level gates allow it. Remote commands use a lazily opened temporary SSH transport with the same credential, host-trust, compression, and proxy policy; graceful teardown of that transport begins before the primary callsshell(). The primary transport therefore never opens a bootstrapexecchannel, preserving server login messages while still allowing a newly installed profile hook to load in the first interactive shell. Backend owns manifest loading, remote probing, installed-status validation, conditional download orchestration, status forwarding, runtime event gating, and audit logging;packages/remote-bootstrapowns the downloaded user-scoped Go installer plus the generated helper's protocol/capability contract. The manifest URL comes fromCOSMOSH_REMOTE_BOOTSTRAP_MANIFEST_URLfirst, then the packaged CI resource when present, then the development-onlyremote-bootstrap-devdefault for unpackaged runs. Tagged release packages point to a versioned release manifest;mainpackages point to the fixedremote-bootstrap-devprerelease manifest; pushed branches whose name containsremote-bootstrapcan point to branch-scoped temporary prerelease manifests for end-to-end CI testing. A current installed contract skips asset download; an ensure failure starts temporary-transport teardown, disables enhancement data for that session, and does not prevent the already-authenticated primary transport from opening an ordinary shell. SshSessionServicepreserves original PTY ordering whenRemoteShellEventOscParserseparates visible output from helper events. It forwards ordered frames instead of grouping them by type, so echoed input and line movement that precedecommand-startreach renderer xterm before command-marker geometry is captured. While the WebSocket is detached, both frame kinds share one bounded arrival-ordered queue; attach sends current control state first and then flushes retained frames without reordering them.RemoteBootstrapServiceshares concurrent manifest loads and caches only validated successes for five minutes. Session cancellation stops one waiter without aborting the shared request; failed loads remain immediately retryable.- Startup includes idempotent Prisma migration-file execution in
initializeDatabase(...), so first install launch and every subsequent launch both converge local DB structure to the current backend schema contract before serving HTTP routes. - Production constructs Prisma with
PrismaSqlCipherAdapterFactory; the factory loads thebetter-sqlite3-multiple-ciphersnative binding into Prisma's better-sqlite3 adapter and applies the database key before exposing the connection. Schema migrations and business queries therefore share one keyed SQLCipher connection path. - A canonical plaintext SQLite header triggers a one-time copy/rekey/verify/replace migration. Unknown or wrong-key files fail without plaintext fallback, and fixed migration artifacts allow interrupted rename windows to recover on the next startup.
- Simple Prisma
ALTER TABLE ... ADD COLUMNmigrations are reconciled against live SQLite table metadata before execution. If a column already exists but_prisma_migrationslacks the row, startup records the migration as applied instead of re-running duplicate DDL; non-simple migration drift still fails fast. - Schema sync is fail-fast: backend startup stops when required tables still cannot be reconciled after runtime migration execution, preventing partial/undefined API behavior.
- Migration ledger metadata is stored in Prisma-compatible
_prisma_migrationsformat to keep a future path open for nativeprisma migrate deploy/resolveworkflows.
Renderer Process (packages/renderer/src)
- Uses
window.electronbridge only (no direct Node API usage). - Creates SSH/local terminal sessions and SFTP browser/download/file-operation sessions through backend APIs.
- Connects terminal data channels through WebSocket and renders with
xterm.js. - Non-home renderer pages, including SSH and the CodeMirror-backed settings editor, are lazy-loaded to keep heavyweight assets out of the default startup path.
- Renderer bootstrap hydrates settings from local cache first, then refreshes canonical values from backend in background.
- Development StrictMode is opt-in via
VITE_ENABLE_STRICT_MODE=trueto reduce duplicate effect execution during local performance profiling. - SSH page uses tab-scoped connection intent snapshots and pane-scoped runtimes. Every primary/secondary pane owns its xterm, WebSocket/session, transport state, telemetry, completion state, Remote Enhancements state, debug history, and trusted command timeline markers; all inbound messages use one pane-aware reducer. Complete timeline command text is reconstructed from rendered xterm input and remains only in that pane runtime's memory.
- Hidden tabs cannot start new SSH connect side effects. On reactivation, the optional reconnect-on-focus path evaluates every failed pane independently, while the first activation always starts a deferred primary pane. Retrying or reconnecting one pane preserves all sibling pane runtimes.
- Renderer consumes backend
bootstrap-status,remote-enhancement-runtime-status, and trusted protocol-v2remote-shell-eventmessages per pane. Debug visibility is controlled byremoteEnhancementsDebugEnabled, and the overlay always reflects its source/active pane.
3. IPC Lifecycle (Current)
sequenceDiagram
participant UI as Renderer UI
participant PB as Preload Bridge
participant MP as Main Process
participant BE as Backend API
participant WS as WS Session Service
UI->>PB: window.electron.backendSshCreateSession(payload)
PB->>MP: ipcRenderer.invoke('backend:ssh-create-session', payload)
MP->>BE: POST /api/v1/ssh/sessions (+internal token)
BE-->>MP: sessionId + websocketUrl + websocketToken
MP-->>PB: API payload
PB-->>UI: API payload
UI->>WS: WebSocket connect (url + token)
WS-->>UI: { type: 'ready' }
UI->>WS: { type: 'input' | 'resize' | 'ping' }
WS-->>UI: { type: 'output' | 'telemetry' | 'pong' | 'exit' }
UI->>PB: close session
PB->>MP: ipcRenderer.invoke('backend:ssh-close-session', sessionId)
MP->>BE: DELETE /api/v1/ssh/sessions/{sessionId}
3.1 Guarded Window And App Close Lifecycle
sequenceDiagram
participant OS as Window/App Close Intent
participant MP as Main Process
participant BE as Backend Runtime
participant RD as Renderer Dialog
OS->>MP: BrowserWindow close or app before-quit
MP->>MP: preventDefault + coalesce repeated requests
MP->>BE: GET /api/v1/runtime/active-connections
alt no active SSH/SFTP sessions
MP->>MP: continue window close or app shutdown
else active sessions or probe unavailable
MP->>BE: GET /api/v1/settings
alt close confirmation disabled
MP->>BE: DELETE /api/v1/runtime/active-connections
MP->>MP: continue window close or app shutdown
else confirmation enabled or preference unavailable
MP->>RD: request localized warning (opaque requestId)
alt user cancels
RD-->>MP: confirmed=false
else user confirms
RD-->>MP: confirmed=true
MP->>BE: DELETE /api/v1/runtime/active-connections
MP->>MP: continue window close or app shutdown
end
end
end
- An active connection is an SSH or SFTP session still present in the backend service registry. Local terminals and port-forwarding runtimes are intentionally outside this warning scope.
windowCloseConfirmationEnabledis registered under General > Behavior and defaults totrue. Main reads this persisted backend setting only when active sessions exist or the activity probe is unavailable. Disabling it skips the renderer dialog but still closes registered SSH/SFTP sessions before window or application shutdown; a preference read failure preserves the default warning behavior.- Main validates non-negative, internally consistent counts before using them. A failed or malformed probe follows the configured confirmation behavior instead of silently closing or permanently blocking exit.
- Repeated title-bar, last-tab, menu, and shortcut close requests share one in-flight decision. Main binds the renderer response to both an opaque request ID and the owning
webContents; preload validates and buffers the request until the React listener is mounted. A responsive renderer timeout resolves to the safe cancel decision, while an unavailable or destroyed renderer is allowed to exit instead of becoming permanently blocked. - On Windows and Linux, an approved main-window close continues through full application shutdown. On macOS, an approved window-only close disconnects SSH/SFTP sessions before destroying the window while leaving the application runtime available for activation; app quit still runs full shutdown.
- Fatal startup/process exits bypass interactive confirmation but retain the existing backend and SFTP temporary-root cleanup path.
4. Security Model
Electron Surface Hardening
nodeIntegration: falsecontextIsolation: true- Renderer gets only explicit bridge APIs via
contextBridge.exposeInMainWorld. - Renderer Content Security Policy keeps
script-srcrestricted to'self'plus'wasm-unsafe-eval'. The WebAssembly allowance is required by renderer-bundled libraries such as@xterm/addon-imagefor inline image decoding, and does not enable general JavaScripteval. - The sandboxed preload script must not import workspace packages at runtime. It may use shared API contract types at compile time, but runtime validators used inside preload must stay local or be bundled so Electron does not need to resolve project modules before the bridge loads.
- Internal privileged operations stay in Main/Backend process.
- Renderer-requested app windows are denied by default. The current allow-list only permits same-renderer SFTP Properties popups, and those child windows reuse the secure preload with
nodeIntegrationdisabled andcontextIsolationenabled.
Backend Access Boundary
- Backend HTTP explicitly binds to the IPv4 loopback interface (
127.0.0.1) in every runtime mode. The listener must never rely on the Node server default, which can expose standalone development APIs on non-loopback interfaces. - The Vite renderer development server also binds explicitly to
127.0.0.1. Electron's development load URL, renderer popup trust origin, renderer CSP, and Backend CORS must use that exact origin and the sharedCOSMOSH_RENDERER_DEV_PORT;localhostis not an interchangeable development origin. - Electron-main mode additionally guards
/api/v1/*with an internal runtime token (COSMOSH_INTERNAL_TOKEN). Standalone mode remains loopback-only even though it does not require that token. - Main process injects headers and never exposes internal token to renderer.
- Development request mirror: in unpackaged development runs, Main records sanitized mirrors of backend proxy requests into an in-memory ring buffer and exposes them to the custom DevTools panel through debug IPC. This does not change the real request path (
renderer -> preload IPC -> main -> backend), does not issue mirror fetches, and does not add fake rows to the native Network tab. The mirror redacts internal auth headers, secret-like payload keys, and local absolute paths before renderer/DevTools visibility. Production packages do not collect traces or load the extension. If theCosmosh Requestspanel is missing in development, check the main-process terminal for the[debug]extension load/skip log first. - Main also capability-gates local SFTP download destinations. App utility IPC authorizes an exact normalized path for the requesting renderer webContents, and the backend proxy rejects any download path without that owner-bound authorization. Temporary preview/open paths are reusable; Downloads and save-dialog paths are consumed after one request.
- Credential encryption key is derived from
COSMOSH_SECRET_KEY/internal token hash in backend bootstrap. - HTTP i18n is request-scoped: backend middleware resolves locale from
x-cosmosh-locale(fallbackaccept-language), then injects a per-request translator used by all route response messages. - WS runtime i18n is session-scoped: session creation carries resolved locale into SSH/local terminal runtime so WS
error/exitmessages and close reasons are localized consistently. - i18n runtime is resource-injected: consumers register locale JSON payloads during
createI18n(...)setup, so each process bundles only its required scope data.
Session Channel Hardening
- WebSocket path includes sessionId and query token.
- Token mismatch or stale session causes immediate close (
1008). - Session attach timeout is enforced (30 seconds) to avoid orphaned resources.
Release Supply-Chain Boundary
- Ordinary CI and rolling remote-bootstrap channels remain separate from versioned public releases. The rolling
remote-bootstrap-devandremote-bootstrap-branch-*assets are intentionally replaceable; tagged applications use only their exact versioned manifest URL. - GitHub Actions are pinned to full commit SHAs and updated through reviewed Dependabot pull requests. Build jobs are repository read-only and stage short-lived workflow artifacts; only the final release job can create or update a draft.
- Formal release assembly validates the complete platform inventory, writes
SHA256SUMS, creates GitHub provenance attestations, and refuses to modify a release after publication. - Windows signing is currently policy-gated.
auditpermits a visibly marked unsigned draft for pipeline validation, whileenforcerequires valid Authenticode signatures, timestamps, and the configured publisher identity before draft creation. - Draft mutability is intentional. Repository-side immutable releases, a protected
releaseenvironment, and av*tag ruleset complete the boundary before the first public release. See Release Security for the operating contract and remaining setup.
5. Runtime Capabilities
SSH and local terminal sessions use WebSocket data channels for terminal I/O.
SSH sessions ensure the user-scoped Remote Enhancements runtime after primary transport authentication and before PTY creation when Settings
remoteEnhancementsEnabled, the server recordremoteEnhancementsEnabled, and a manifest URL allow it. The first remote command lazily opens a separate bootstrap transport; all probe/install/statusexecchannels stay there, and its teardown begins beforeshell()becomes the primary transport's first session channel. This optional pre-shell path has a shared 15-second budget across settings, manifest I/O, bootstrap transport/proxy connection, and exec work. Expiry cancels active work, destroys the temporary client, and opens an ordinary PTY with codeBOOTSTRAP_ENSURE_TIMEOUT. Disabled gates emitREMOTE_ENHANCEMENTS_DISABLED; missing manifest configuration remains an explicit failed bootstrap status before any bootstrap transport or remote probe is opened. The installed Go binary is queried first; matching version, manifest asset SHA-256, protocol, helper, and profile state skips download, while missing or legacy status triggers reinstall and post-install verification. Tagged release installers,mainbuild artifacts, and opted-in remote-bootstrap branch builds can provide the default manifest URL through the packagedremote-bootstrap/manifest-url.jsonresource, whileCOSMOSH_REMOTE_BOOTSTRAP_MANIFEST_URLremains the explicit override. Unpackaged development runs useremote-bootstrap-devwhen neither override nor packaged resource is present. Ordinary PR and branch builds do not package a default manifest URL. The Go installer writes only remote user XDG/home files and shell profile hooks; seepackages/remote-bootstrap/README.mdfor the module contract.Remote helper data uses a fail-closed
pending→active/disabledstate machine. A successful ensure enterspending; only a matchingintegration-readyevent within 10 seconds activates consumption, and missing the deadline yieldsHELPER_HANDSHAKE_TIMEOUT. Protocol-v2 events must match the pre-shell helper version, protocol version, shell, capability set, and capability-specific required fields. Manifest/install/settings failure, legacy events without contract fields, a missing handshake, or any runtime mismatch leaves ordinary SSH usable while helper-derived state is ignored and trusted renderer cwd/line calibration is cleared.Renderer keeps current backend runtime state independently from a bounded 200-entry diagnostic history for each pane, so long sessions retain authoritative current diagnostics after older events are evicted. Structured command lifecycle events drive backend count/history refresh and pane-local xterm command timelines. Backend command counting/history refresh may retain raw Enter parsing as a degraded path, but the renderer timeline has no local fallback: it is visible only while an authenticated active helper advertises
command-start.SFTP uses request/response IPC + backend HTTP routes for directory browsing, local-file upload, download, create, rename, copy, delete, batch file operations, and asynchronous remote archive jobs.
Remote archive jobs reuse the active SFTP tab's authenticated SSH client but run only backend-generated POSIX command templates.
SftpArchiveServiceprobes a fixed tool list, owns one archive job per session, stages output beside the destination, creates validated missing destination directories, and exposes only structured state through HTTP/IPC. Extraction runs as a directly signallable remote executable, followed by a cancellable staged-tree verification phase that reuses SFTP directory metadata. Renderer input never becomes a command or flag.Port Forwarding uses request/response IPC + backend HTTP routes for persisted rule CRUD and manual start/stop. Runtime state stays in backend memory, so app/backend restart resets all rules to stopped.
SFTP local OS-open flows download regular files into a Cosmosh-controlled temp root through the existing backend download endpoint, then ask main-process app utility IPC to open only validated temp files. Windows uses the shell
openasverb for Open With, resolves the PowerShell primary route and rundll32/shell32 fallback independently from the kernel-ownedGLOBALROOT\SystemRoot\System32namespace instead of inherited environment/PATH/CWD values, and enriches the primary child environment through Windows known-folder APIs. A blocked or unavailable PowerShell route cannot prevent the validated rundll32 fallback from running with a kernel-anchored minimal environment. Packaged macOS runs accept only the compiled NSWorkspace helper underprocess.resourcesPath; repository binary/source fallbacks are development-only and unavailable whenapp.isPackagedis true. Linux omits Open With.SFTP directory upload/download, chmod, generalized byte-transfer cancellation/resume, richer persisted transfer queues, and SSH terminal session reuse remain planned follow-up work. Archive jobs have their own bounded cancellation protocol and do not reuse a terminal shell.
5.1 SSH Port Forwarding Runtime (Implemented)
- Port forwarding rules are persisted in SQLite through
PortForwardRule, with type-specific fields for local, remote, and dynamic SOCKS forwarding. PortForwardSessionServiceowns active SSH clients,net.Serverlisteners, sockets, channels, remote-forward listeners, and shutdown cleanup.- Start opens SSH clients through the shared
packages/backend/src/ssh/connect.tshelper, so keychain credential decryption and strict host-key behavior stay aligned with SSH/SFTP. - Local forwarding listens on the backend host and opens
ssh2.Client.forwardOut(...)per inbound local socket. - Remote forwarding calls
client.forwardIn(...)and connects accepted SSH channels from backend to the configured target host/port. - Dynamic forwarding implements SOCKS5 no-auth TCP CONNECT for IPv4, IPv6, and domain targets; UDP ASSOCIATE, BIND, and SOCKS authentication are not supported.
- Default local bind host is
127.0.0.1; non-localhost bind hosts are allowed only with renderer risk messaging. - Each rule is capped at 64 concurrent connections with a 15-second connection setup timeout.
5.2 Settings Runtime (Implemented)
- Settings are now persisted by backend route
GET/PUT /api/v1/settings. - Storage model is a single-row JSON payload per scope (
scopeAccountId+scopeDeviceId) inAppSettings. - Scope defaults to local device (
deviceId=local-device) while keeping account scope field for future sync. - Renderer bootstrap (
packages/renderer/src/main.tsx) applies persisted language/theme using cached settings at startup, then synchronizes with backend. - Renderer date-time display uses persisted time-zone/date/time format settings through
packages/renderer/src/lib/date-time-format.ts;systempreserves the OS time zone, and the Settings UI lists runtime-supported IANA time zones with their current UTC offsets. - Renderer terminal character width compatibility is stored as
terminalCharacterWidthCompatibilityModeEnabled; SSH server records can opt out per server withdisableCharacterWidthCompatibilityMode, while local terminal sessions only follow the global setting. - Remote Enhancements use
global setting && persisted SshServer field && request override !== false. The request override can only narrow access and cannot re-enable a server disabled in current backend state.remoteEnhancementsDebugEnabledindependently controls pane-specific diagnostics and does not enable the remote runtime. - Non-visual settings (for example SSH runtime limits) are persisted and discoverable, but some are intentionally not bound to runtime behavior yet.
- All setting definitions (types, defaults, constraints, enum sets, JSON schemas, UI metadata, categories) live in a single registry:
packages/api-contract/src/settings-registry.ts. Adding or removing a setting only requires editing this file (plus i18n locale files). - Validation logic in
packages/api-contract/src/settings.tsis now generic and registry-driven for common scalar rules (type check, enum, range, maxLength), with narrow custom validators for settings that need runtime checks or structured JSON normalization such as IANA time-zone support and the SFTP directory-list view. - Settings UI surfaces structured JSON settings as explicit rows, but they do not render inline editors or per-item Settings Editor actions. They provide a single Settings Editor link so full-object editing remains schema-backed and centralized, while default reset remains available through the regular item menu.
- The OpenAPI
SettingsValuesschema is intentionally loose (type: object); strict TypeScript types and constraints live exclusively in the code registry. - Settings API response types (
ApiSettingsGetResponse,ApiSettingsUpdateResponse) are hand-crafted inpackages/api-contract/src/index.tsusing the strictSettingsValuesfrom the registry rather than generated from OpenAPI. - Stored settings payload parsing is forward-compatible: missing/new fields are backfilled per-field from defaults instead of resetting the entire settings object.
- Strict full-schema validation is still enforced for update requests (
PUT /api/v1/settings) to keep persisted payload shape deterministic.
sequenceDiagram
participant UI as Renderer
participant PB as Preload
participant MP as Main IPC
participant BE as Backend Settings Route
participant DB as SQLite(AppSettings)
UI->>PB: window.electron.backendSettingsGet()
PB->>MP: ipcRenderer.invoke('backend:settings-get')
MP->>BE: GET /api/v1/settings
BE->>DB: load AppSettings row by scope
DB-->>BE: payloadJson + revision
BE-->>MP: SettingsGetSuccess
MP-->>UI: settings payload
UI->>UI: apply language + theme
5.3 Local-First Audit Runtime (Implemented)
- Security-core operations are persisted to
AuditEventwith stable correlation fields (requestId,sessionId,entityId,relatedRecordId) for forensic traceability. - Existing
SshLoginAuditremains active for backward-compatible SSH last-used sorting, whileAuditEventis used as the cross-domain audit stream. - Audit writes are best-effort and non-blocking by contract: failures are logged in backend runtime and do not fail parent request/session flows.
- Metadata persistence is sanitized before storage (secret-like keys are redacted) and capped by serialized size limits to prevent payload inflation.
- Retention is local policy-driven (default 180 days) with periodic sweeps in audit service runtime.
- Future sync checkpoint state is pre-modeled by
AuditSyncCursorwithout introducing current mandatory remote dependency.
Current event categories in runtime wiring include:
ssh-sessionssh-host-trustssh-serverssh-keychainport-forwardsettings
6. Core Data-Flow Views
6.1 Session Bootstrap Data Flow
flowchart TD UI[Renderer UI] --> BRIDGE[window.electron bridge] BRIDGE --> MAIN[ipcMain handler] MAIN --> API[Backend route] API --> SERVICE[Session service] SERVICE --> DB[(Prisma / SQLCipher adapter)] SERVICE --> REMOTE[SSH host or local PTY] SERVICE --> TOKEN[WS token + session registry] TOKEN --> UI
6.2 Runtime Stream Data Flow
flowchart LR XT[Active pane xterm.js] --> IN[pane input events] IN --> WS[Pane WebSocket] WS --> SVC[Backend session runtime] SVC --> REM[Remote shell / PTY] REM --> PARSER[OSC 777 streaming parser] PARSER --> OUT[Visible stdout + stderr] PARSER --> GATE[Contract and trust gate] OUT --> WS2[Pane WebSocket messages] GATE --> WS2 WS2 --> REDUCER[Pane runtime and reducer] REDUCER --> XT2[xterm write, completion, markers, diagnostics]
6.3 SFTP Transfer Progress Data Flow
sequenceDiagram
participant UI as Renderer Task Queue
participant PB as Preload Bridge
participant MP as Main IPC
participant BE as Backend SFTP Service
participant FS as Local/Remote Stream
UI->>PB: upload/download(payload + transferId)
PB->>MP: invoke final transfer request
MP->>BE: POST upload/download
BE->>FS: pipeline through byte-counting Transform
loop every 500 ms while request is pending
UI->>PB: get progress(transferId)
PB->>MP: backend:sftp-get-transfer-progress
MP->>BE: GET /api/v1/sftp/transfers/{transferId}
BE-->>UI: bytes + total + rolling speed + status
end
BE-->>UI: final success or stable API error
- File bytes stay on the existing backend stream path; only bounded progress metadata crosses HTTP and IPC.
- Backend samples speed at most every 250 ms and retains terminal records in memory for 60 seconds. Renderer polling stops with the final transfer request.
- Renderer transfer progress is attached to concurrently started backend tasks. The task is polled with the session id captured at acceptance, while byte progress remains keyed by
transferId; neither path provides generalized cancellation, resume, or persisted history.
6.4 SFTP Backend Task Scheduling Data Flow
sequenceDiagram
participant C as Task API Consumer
participant API as Backend SFTP Routes
participant SCH as Session Task Scheduler
participant SVC as SFTP/Archive Runner
participant RH as Remote Host
C->>API: POST /sessions/{sessionId}/tasks(descriptor)
API->>SCH: enqueue with resources, claims, and absolute deadline
SCH-->>C: 202 accepted task snapshot
SCH->>SCH: admit by total/heavy/mutation limits and path claims
SCH->>SVC: run with AbortSignal and remaining deadline
SVC->>RH: bounded remote operation
loop list or detail polling
C->>API: GET task list/detail
API-->>C: retained in-memory snapshot
end
SVC-->>SCH: result or terminal cleanup settlement
SCH->>SCH: release capacity and claims
- Each SFTP session has independent fixed limits:
total=3,heavy=2, andmutation=1. Equal and ancestor/descendant POSIX path claims serialize, while disjoint sibling claims may run concurrently. - Supported public task descriptors are
create-file,create-directory,rename,upload,download, andbatch. Previewwrite-fileretains its synchronous HTTP contract but executes as hidden scheduler work; all legacy SFTP operation routes use that same coordinated service boundary. - The absolute deadline includes queue wait. Deadline expiry publishes
failedimmediately; a running task continues to own capacity and claims until its runner settles, and timed-out mutations publishoutcomeUnknown: true. - Task records are memory-only, remain readable after a recent session close, and are bounded to 512 records per session with a seven-day post-release TTL. Backend stop clears all records. The task API exposes start, list, and detail only: there is no public task cancel, resume, or persistence contract.
- Renderer routes all six supported descriptors through Main/preload into this API. It starts unrelated tasks concurrently and retains a separate serial lane only for synchronous preview writes and stateful archive orchestration.
6.5 SFTP Remote Archive Data Flow
sequenceDiagram
participant UI as Renderer Serial Archive Lane
participant MP as Main/Preload Proxy
participant API as Backend Archive Routes
participant SCH as Session Task Scheduler
participant AS as SftpArchiveService
participant RH as Remote POSIX Host
UI->>MP: structured compress/extract request
MP->>API: POST archive-operations
API->>SCH: acquire exclusive session claim
SCH->>AS: start one session-scoped job
AS->>RH: fixed exec template on the SFTP tab SSH client
loop every 750 ms
UI->>API: GET operation status
API-->>UI: stage/state/conflicts/result only
end
opt destination conflict
UI->>API: overwrite / keep-both / cancel
API->>AS: resume staged commit
end
AS->>RH: commit and clean known temporary paths
AS-->>SCH: terminal cleanup settled
SCH->>SCH: release exclusive claim
- The remote command, tool output, and random staging paths are backend-private. Public contracts carry paths, format, level, destination mode, phase, conflict summaries, and stable errors only.
- Archive capability probing acquires the scheduler's exclusive session claim until the probe settles. Archive startup acquires the same claim and retains it through terminal cleanup. Both use immediate-only admission and report
SFTP_ARCHIVE_BUSYinstead of waiting without a pollable identifier. Renderer archive requests enter their serial lane, so multiple archive operations preserve selection order without globally serializing ordinary backend tasks. - Closing a session first requests archive cancellation and bounded cleanup, then disconnects SSH. Bulk session close waits for sessions in parallel and preserves the existing active-connection count contract.
6.6 Failure Boundary Model
- Renderer boundary: visual state and user interaction; failures should stay recoverable via UI retry.
- Main boundary: capability routing and internal auth injection; failures should never leak privileged tokens.
- Backend boundary: protocol validation, session lifecycle, and resource cleanup ownership.
- Remote boundary: SSH host / local shell instability is treated as external and mapped to stable UI error codes.
7. SSH Keychain Credential Model (2026-03)
- SSH credentials are now persisted in
SshKeychainand linked fromSshServer.keychainId. SshServerkeeps connection identity, host/transport policy (host,port,username,strictHostKey,enableSshCompression), and renderer terminal compatibility flags (disableCharacterWidthCompatibilityMode) but no longer stores encrypted password/private-key fields directly.- SSH transport compression is disabled by default. When enabled on a server record, the backend applies the same compression negotiation policy to SSH shell sessions, SFTP sessions, and port-forwarding clients.
- Keychain organization metadata reuses the same
SshFolderandSshTagdomains used by servers (no separate keychain-only folder/tag tables). - Existing per-server edit UX is preserved by allowing inline credential input in the SSH editor; backend transparently materializes/updates hidden keychains.
- Server updates that keep inline credential mode may omit password/private-key fields; the backend retains the existing encrypted values and only rejects the update when the stored credential material cannot satisfy the selected auth type.
- Shared keychains can be reused by multiple servers; hidden keychains are intended for single-server private use.
- SSH session creation resolves credentials through server → keychain relation before opening
ssh2connections.
7.1 Development Profile Runtime
Development profile mode is a developer-only isolation layer for fresh-install verification. It does not change packaged production storage or database key policy.
The first non-help pnpm dev:profile command automatically imports the legacy implicit default identity into .cosmosh/dev-profiles/default/. The import copies the legacy workspace database, SQLite WAL/SHM sidecars, Electron userData, and backend secret storage on a best-effort basis. Missing or unreadable legacy sources are recorded in the profile manifest instead of aborting the command.
The default profile is a managed recovery snapshot, not a throwaway test profile. It can be selected with pnpm dev:profile use default or rebuilt with pnpm dev:profile import-default --force --use, but regular create default, reset default, and delete default commands are rejected to avoid losing the recovery path.
Use pnpm dev:profile to create, switch, reset, inspect, or delete local test profiles:
pnpm dev:profile create fresh --usecreates.cosmosh/dev-profiles/fresh/and makes it the default development profile.pnpm dev:profile reset freshclears only that profile's runtime data so the next development launch behaves like a new install for the same identity.pnpm dev:profile delete fresh --forceremoves the profile and clears the current pointer if it was active.pnpm dev:profile run fresh --create --reset -- pnpm dev:mainruns one command with an isolated, freshly reset profile. The root scriptpnpm dev:main:freshis the shorthand for this flow.
A profile owns these paths:
.cosmosh/dev-profiles/<name>/user-data: injected into Electron viaapp.setPath('userData', ...)before app storage is touched..cosmosh/dev-profiles/<name>/database/cosmosh.db: injected asCOSMOSH_DB_PATHand used by both main and backend database path resolvers..cosmosh/dev-profiles/<name>/backend-storage: injected asCOSMOSH_BACKEND_STORAGE_PATHfor backend-only secret material such assecret.key..cosmosh/dev-profiles/default/profile.json: import manifest for the managed default profile, including source paths and per-source copy status.
If no development profile is active, direct development launches keep the legacy workspace database path .dev_data/cosmosh.db and default Electron development storage. This preserves existing local data unless a developer explicitly opts into profile isolation.
8. Architecture Decision Rationale
- Keep the backend as a separate runtime process to isolate protocol and credential handling from renderer attack surface.
- Use preload as a minimal bridge to reduce API exposure and preserve strict process contracts.
- Prefer WS data plane for terminal streams to avoid IPC bottlenecks on high-frequency I/O.
- Keep main as orchestrator/proxy instead of business-logic host for easier future server-client decoupling.
9. Boundary Case Playbook
9.1 Backend Not Ready at Startup
sequenceDiagram participant MAIN as Main Process participant BE as Backend Process participant UI as Renderer Window MAIN->>BE: start backend runtime MAIN->>UI: create BrowserWindow in parallel MAIN->>BE: poll /health BE-->>MAIN: not ready MAIN->>MAIN: retry with bounded wait BE-->>MAIN: healthy UI->>MAIN: first backend IPC request MAIN->>BE: await startup promise if needed
Handling principle:
- UI should become visible as early as possible while backend continues warming in parallel.
- First backend-bound IPC request must still observe backend ready-state before forwarding.
- Startup failure paths should be explicit and observable.
9.2 WS Attach Token Mismatch
sequenceDiagram
participant UI as Renderer
participant WS as Backend WS Gateway
UI->>WS: connect /ws/ssh/{sessionId}?token=invalid
WS-->>UI: close code 1008
UI->>UI: transition to failed state
UI->>UI: allow explicit retry flow
Handling principle:
- Token/session mismatch is security-sensitive and must fail closed.
- Recovery should create a fresh session/token path.
9.3 Renderer Reload During Active Session
sequenceDiagram participant UI1 as Renderer Instance A participant WS as Backend Session Runtime participant UI2 as Renderer Instance B UI1->>WS: active attach UI1-->>UI1: renderer reload UI2->>WS: re-attach with new token/session flow WS-->>UI2: ready or reject based on session state
Handling principle:
- Session runtime must guard against stale attach state.
- Renderer should treat reload as a new lifecycle and re-establish state explicitly.
8.4 Production Database Encryption and Recovery
sequenceDiagram
participant BE as Backend Bootstrap
participant DB as Database File
participant MAIN as Electron Main
BE->>DB: Inspect file header
alt Canonical plaintext SQLite
BE->>DB: checkpoint + copy + rekey encrypted copy
BE->>DB: integrity/schema verification + atomic promotion
else Encrypted database
BE->>DB: verify SQLCipher key and integrity
end
BE->>DB: connect Prisma through keyed SQLCipher adapter
DB-->>BE: ready or explicit migration/key error
BE-->>MAIN: continue only with encrypted storage
Handling principle:
- Production has no plaintext Prisma fallback. Only a canonical plaintext header enters the one-time migration; unknown/corrupt files and incorrect keys fail without rotating key material.
- Migration keeps the source authoritative until an encrypted copy passes integrity and schema-count checks. Fixed
.sqlcipher-migrationand.plaintext-backupartifacts support restart recovery across rename interruptions. An encrypted temp that fails verification is preserved with its plaintext backup and causes startup to fail; recovery never rotates the database key by silently restoring and re-encrypting with an unverified key.
8.5 Startup Schema Upgrade Path
sequenceDiagram participant BE as Backend Bootstrap participant DB as SQLCipher via Prisma adapter BE->>DB: initializeDatabase(...) BE->>DB: apply PRAGMA + pending Prisma migration.sql files DB-->>BE: schema aligned (or error) BE->>BE: validate required table set BE-->>BE: continue startup only when validation passes
Handling principle:
- Runtime migration sync is idempotent and executes on every startup.
- Existing user data must remain intact while structural drift is repaired incrementally.
10. Server Proxy Runtime
- Global settings define
serverProxyMode = off | system | customandserverProxyUrl; the default issystem. - Each
SshServerdefinesproxyMode = default | off | customand an optionalproxyUrl.defaultinherits the global policy. - Renderer resolves system/PAC proxy rules through the privileged
app:resolve-system-proxyMain IPC only when the effective mode issystem. - Backend remains the policy authority.
packages/backend/src/ssh/proxy.tsreloads persisted global settings, applies the server override, parses ordered Chromium proxy rules, and creates HTTP, HTTPS CONNECT, SOCKS5, or explicitDIRECTsockets. - The prepared socket is injected through
ssh2ConnectConfig.sock, so SSH shell, SFTP, and port-forwarding connections share one proxy implementation. - Proxy candidates share the configured SSH connection timeout. Proxy failure never silently falls back to direct transport; direct transport is allowed only for
offmode or an explicit systemDIRECTcandidate. - Audit metadata records only proxy mode and protocol. Proxy URLs and embedded credentials are never written to audit metadata.