wraith/docs/superpowers/specs/2026-03-17-wraith-desktop-design.md
Vantz Stockwell 8a096d7f7b
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client
Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego)
183 tests, 76 source files, 9,910 lines of code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:19:29 -04:00

51 KiB
Raw Blame History

Wraith Desktop — Design Spec

Date: 2026-03-17 Purpose: Native Windows desktop replacement for MobaXTerm — SSH + SFTP + RDP in a single binary Stack: Go + Wails v3 (Vue 3 frontend, WebView2) + SQLite + FreeRDP3 (purego) Target: Personal tool for daily MSP/sysadmin work — Windows only Name: Wraith — exists everywhere, all at once.


1. What This Is

A Windows desktop application that replaces MobaXTerm. Multi-tabbed SSH terminal with SFTP sidebar (MobaXTerm's killer feature), RDP via FreeRDP3 dynamic linking, connection manager with hierarchical groups, and an encrypted vault for SSH keys and passwords. Ships as wraith.exe + freerdp3.dll. No Docker, no database server, no sidecar processes.

What this is NOT: A web app, a SaaS platform, a team tool. It's a personal remote access workstation built as a native desktop binary.

Prior art: This is a ground-up rebuild of Wraith, which was previously a self-hosted web application (Nuxt 3 + NestJS + guacd + PostgreSQL). The web version proved the feature set; this version delivers it as a proper desktop tool.


2. Technology Stack

Backend (Go)

Component Technology Purpose
Framework Wails v3 (alpha) Desktop app shell, multi-window, Go↔JS bindings
SSH golang.org/x/crypto/ssh SSH client connections, PTY, auth
SFTP github.com/pkg/sftp Remote filesystem operations over SSH
RDP FreeRDP3 via purego / syscall.NewLazyDLL RDP protocol, bitmap rendering
Database SQLite via modernc.org/sqlite (pure Go) Connections, credentials, settings
Encryption crypto/aes + crypto/cipher (GCM) Vault encryption at rest
Key derivation golang.org/x/crypto/argon2 Master password → encryption key

Frontend (Vue 3 in WebView2)

Component Technology Purpose
Framework Vue 3 (Composition API) UI framework
Terminal xterm.js 5.x + WebGL addon SSH terminal emulator
File editor CodeMirror 6 Remote file editing (separate window)
CSS Tailwind CSS Utility-first styling
Components Naive UI Tree, tabs, modals, dialogs, inputs
State Pinia Reactive stores for sessions, connections, app state
Build Vite Frontend build tooling

Distribution

Artifact Notes
wraith.exe Single Go binary, ~8-10MB
freerdp3.dll FreeRDP3 dynamic library, shipped alongside
Data %APPDATA%\Wraith\wraith.db (SQLite)
Installer NSIS via Wails build

3. Architecture

┌─────────────────────────────────────────────────────────────────────┐
│  Wails v3 Application (wraith.exe)                                  │
│                                                                     │
│  ┌─ Go Backend ──────────────────────────────────────────────────┐  │
│  │                                                               │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌───────────────────┐   │  │
│  │  │  SSH Service  │  │ SFTP Service │  │   RDP Service     │   │  │
│  │  │  x/crypto/ssh │  │  pkg/sftp    │  │ purego→freerdp3   │   │  │
│  │  └──────┬───────┘  └──────┬───────┘  └────────┬──────────┘   │  │
│  │         │                 │                    │              │  │
│  │  ┌──────▼─────────────────▼────────────────────▼──────────┐   │  │
│  │  │              Session Manager                            │   │  │
│  │  │  • Tracks all active SSH/RDP sessions                   │   │  │
│  │  │  • Routes I/O between frontend and protocol backends    │   │  │
│  │  │  • Supports tab detach/reattach (session ≠ window)      │   │  │
│  │  └────────────────────────┬───────────────────────────────┘   │  │
│  │                           │                                   │  │
│  │  ┌────────────────────────▼───────────────────────────────┐   │  │
│  │  │              Vault Service                              │   │  │
│  │  │  • Master password → Argon2id → AES-256-GCM key         │   │  │
│  │  │  • SQLite storage (%APPDATA%\Wraith\wraith.db)          │   │  │
│  │  │  • Encrypts: SSH keys, passwords, RDP credentials       │   │  │
│  │  └────────────────────────────────────────────────────────┘   │  │
│  │                                                               │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐    │  │
│  │  │  Connection   │  │   Import     │  │  Host Key        │    │  │
│  │  │  Manager      │  │  .mobaconf   │  │  Store           │    │  │
│  │  └──────────────┘  └──────────────┘  └──────────────────┘    │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                           ▲                                         │
│                    Wails v3 Bindings (type-safe Go↔JS)              │
│                           ▼                                         │
│  ┌─ Vue 3 Frontend (WebView2) ───────────────────────────────────┐  │
│  │                                                               │  │
│  │  ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────────┐   │  │
│  │  │ xterm.js │ │ SFTP Tree │ │ RDP      │ │ CodeMirror 6  │   │  │
│  │  │ +WebGL   │ │ Sidebar   │ │ Canvas   │ │ (sep window)  │   │  │
│  │  └──────────┘ └───────────┘ └──────────┘ └───────────────┘   │  │
│  │                                                               │  │
│  │  ┌───────────────────────────────────────────────────────┐    │  │
│  │  │  Tab Bar (detachable) + Connection Sidebar            │    │  │
│  │  │  Command Palette (Ctrl+K) | Dark theme                │    │  │
│  │  │  Tailwind CSS + Naive UI                               │    │  │
│  │  └───────────────────────────────────────────────────────┘    │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
         │                    │                    │
    SSH (port 22)        SFTP (over SSH)     RDP (port 3389)
         │                    │                    │
         ▼                    ▼                    ▼
   Linux/macOS hosts    Remote filesystems   Windows hosts

Key Architectural Decisions

Sessions ≠ Windows. SSH and RDP sessions live as objects in the Go Session Manager. The frontend is a view. Detaching a tab spawns a new Wails window pointing at the same backend session. Re-attaching destroys the window and re-renders the session in the original tab. The session itself never drops.

Wails v3 multi-window risk mitigation: This is the project's biggest technical risk. The detach/reattach model depends on Wails v3's alpha application.NewWebviewWindow() API. Three fallback plans, validated in priority order during Phase 1:

  • Plan A (target): Wails v3 NewWebviewWindow() — true native multi-window. Spike this in Phase 1 with a minimal two-window prototype before committing.
  • Plan B: Single Wails window with internal "floating panel" detach — session renders in a draggable, resizable overlay within the main window. Not true OS windows, but close enough. No external dependency.
  • Plan C: Wails v3 server mode — detached sessions open in the default browser at localhost:{port}/session/{id}. Functional but breaks the native feel.

If Plan A fails, we fall to Plan B (which is entirely within our control). Plan C is the emergency fallback. This must be validated in Phase 1, not discovered in Phase 4.

Single binary + DLL. No Docker, no sidecar processes. SQLite is embedded (pure Go driver). FreeRDP3 is the only external dependency, loaded dynamically via purego.

SFTP rides SSH. SFTP opens a separate SSH channel on the same x/crypto/ssh connection as the terminal. No separate TCP connection is needed. pkg/sftp.NewClient() takes an *ssh.Client (not the shell *ssh.Session) and opens its own subsystem channel internally. The terminal shell session and SFTP operate as independent channels multiplexed over the same connection.

RDP via pixel buffer. FreeRDP3 is loaded via purego (dynamic linking, no CGO). FreeRDP writes decoded bitmap frames into a shared Go pixel buffer. The Go backend serves frame data to the frontend via a local HTTP endpoint (localhost:{random_port}/frame) that returns raw RGBA data. The frontend renders frames on a <canvas> element using requestAnimationFrame. Performance target: 1080p @ 30fps using Bitmap Update callbacks. The local HTTP approach is the default; if benchmarking reveals issues, Wails binding with base64-encoded frames is the fallback.


4. Data Model (SQLite)

-- Connection groups (hierarchical folders)
CREATE TABLE groups (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  name        TEXT NOT NULL,
  parent_id   INTEGER REFERENCES groups(id) ON DELETE SET NULL,
  sort_order  INTEGER DEFAULT 0,
  icon        TEXT,
  created_at  DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Saved connections
CREATE TABLE connections (
  id               INTEGER PRIMARY KEY AUTOINCREMENT,
  name             TEXT NOT NULL,
  hostname         TEXT NOT NULL,
  port             INTEGER NOT NULL DEFAULT 22,
  protocol         TEXT NOT NULL CHECK(protocol IN ('ssh','rdp')),
  group_id         INTEGER REFERENCES groups(id) ON DELETE SET NULL,
  credential_id    INTEGER REFERENCES credentials(id) ON DELETE SET NULL,
  color            TEXT,
  tags             TEXT DEFAULT '[]',       -- JSON array: ["Prod","Linux","Client-RSM"]
  notes            TEXT,
  options          TEXT DEFAULT '{}',       -- JSON: protocol-specific settings
  sort_order       INTEGER DEFAULT 0,
  last_connected   DATETIME,
  created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Credentials (password or SSH key reference)
CREATE TABLE credentials (
  id               INTEGER PRIMARY KEY AUTOINCREMENT,
  name             TEXT NOT NULL,
  username         TEXT,
  domain           TEXT,
  type             TEXT NOT NULL CHECK(type IN ('password','ssh_key')),
  encrypted_value  TEXT,
  ssh_key_id       INTEGER REFERENCES ssh_keys(id) ON DELETE SET NULL,
  created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- SSH private keys (encrypted at rest)
CREATE TABLE ssh_keys (
  id                     INTEGER PRIMARY KEY AUTOINCREMENT,
  name                   TEXT NOT NULL,
  key_type               TEXT,
  fingerprint            TEXT,
  public_key             TEXT,
  encrypted_private_key  TEXT NOT NULL,
  passphrase_encrypted   TEXT,
  created_at             DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Terminal themes (16-color ANSI + fg/bg/cursor)
CREATE TABLE themes (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  name            TEXT NOT NULL UNIQUE,
  foreground      TEXT NOT NULL,
  background      TEXT NOT NULL,
  cursor          TEXT NOT NULL,
  black           TEXT NOT NULL,
  red             TEXT NOT NULL,
  green           TEXT NOT NULL,
  yellow          TEXT NOT NULL,
  blue            TEXT NOT NULL,
  magenta         TEXT NOT NULL,
  cyan            TEXT NOT NULL,
  white           TEXT NOT NULL,
  bright_black    TEXT NOT NULL,
  bright_red      TEXT NOT NULL,
  bright_green    TEXT NOT NULL,
  bright_yellow   TEXT NOT NULL,
  bright_blue     TEXT NOT NULL,
  bright_magenta  TEXT NOT NULL,
  bright_cyan     TEXT NOT NULL,
  bright_white    TEXT NOT NULL,
  selection_bg    TEXT,
  selection_fg    TEXT,
  is_builtin      BOOLEAN DEFAULT 0
);

-- Connection history (for recent connections + frequency sorting)
CREATE TABLE connection_history (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  connection_id   INTEGER NOT NULL REFERENCES connections(id) ON DELETE CASCADE,
  protocol        TEXT NOT NULL,
  connected_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
  disconnected_at DATETIME,
  duration_secs   INTEGER
);

-- Known SSH host keys
CREATE TABLE host_keys (
  hostname    TEXT NOT NULL,
  port        INTEGER NOT NULL,
  key_type    TEXT NOT NULL,
  fingerprint TEXT NOT NULL,
  raw_key     TEXT,
  first_seen  DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (hostname, port, key_type)
);

-- App settings (key-value)
CREATE TABLE settings (
  key   TEXT PRIMARY KEY,
  value TEXT NOT NULL
);

connections.options — JSON blob for protocol-specific settings. SSH: keepalive interval, preferred auth method, shell integration toggle. RDP: color depth, security mode (NLA/TLS/RDP), console session, audio redirection, display resolution. Keeps the schema clean and extensible as we discover edge cases without adding nullable columns.

connections.tags — JSON array searchable via SQLite's json_each(). Enables filtering across groups (type "Prod" in search, see only production hosts regardless of which group they're in).

Connections → Credentials is many-to-one. Multiple hosts can share the same credential.

SQLite WAL mode: Enable Write-Ahead Logging (PRAGMA journal_mode=WAL) on database open in db/sqlite.go. WAL mode allows concurrent reads during writes, preventing "database is locked" errors when the frontend queries connections while the backend is writing session history or updating last_connected timestamps. Also set PRAGMA busy_timeout=5000 as a safety net.

Host keys are keyed by (hostname, port, key_type). Supports multiple key types per host. Separated from connections so host key verification works independently of saved connections (e.g., quick connect).


5. UI Layout

Visual Identity

Dark theme inspired by the Wraith brand: deep dark backgrounds (#0d1117), blue accent (#58a6ff), green for SSH indicators (#3fb950), blue for RDP indicators (#1f6feb). The aesthetic is "operator command center" — atmospheric, moody, professional. Reference: docs/karens-wraith-layout.png for the target mood.

Logo: images/wraith-logo.png — ghost with "$" symbol, used in the title bar and app icon.

The "alive" feel: Tabs use a 0.5s CSS transition on background-color and border-color when switching between active and backgrounded states. The active tab's background subtly brightens; backgrounded tabs dim. This creates a fluid, "breathing" quality as you switch between sessions — the Wraith is present without being loud. Same 0.5s transition applies to sidebar item hover states and toolbar button interactions. No animations on the terminal itself — that would be distracting.

Main Window Layout

┌─────────────────────────────────────────────────────────────┐
│  [👻 WRAITH v1.0]  File  View  Tools  Settings  Help       │ ← Title/Menu bar
├─────────────────────────────────────────────────────────────┤
│  [⚡ Quick connect...]  [+SSH] [+RDP]    4 sessions  🔒 ⚙  │ ← Toolbar
├────────────┬────────────────────────────────────────────────┤
│            │  [Asgard ●] [Docker ●] [Predator ●] [VM01 ●]+ │ ← Tab bar
│  SIDEBAR   │────────────────────────────────────────────────│
│            │                                                │
│  Toggles:  │           Terminal / RDP Canvas                 │
│  📂 Conn   │                                                │
│  📁 SFTP   │         (xterm.js or <canvas>)                 │
│            │                                                │
│  Search    │           Primary workspace area               │
│  Tags      │           Takes dominant space                  │
│  Groups    │                                                │
│  Tree      │                                                │
│            │                                                │
├────────────┴────────────────────────────────────────────────┤
│  SSH · root@asgard:22 ⚠️   ↑1.2K ↓3.4K   Dark+  UTF-8 120×40  │ ← Status bar
└─────────────────────────────────────────────────────────────┘

Sidebar Behavior

The left sidebar is a single panel that toggles context between Connections and SFTP (same as MobaXTerm, not two panels):

  • Connections view: Search bar, tag filter pills, recent connections, hierarchical group tree with connection entries. Green dots for SSH, blue dots for RDP. "Connected" indicator on active sessions. Right-click context menu for edit, delete, duplicate, move to group.
  • SFTP view: Activates when an SSH session connects. Path bar showing current remote directory. Toolbar with upload, download, new file, new folder, refresh, delete. File tree with name, size, modified date. "Follow terminal folder" toggle at bottom.

The sidebar is resizable. Minimum width ~200px, collapsible to icon-only rail.

Tab Bar

  • Color-coded dots: green = SSH, blue = RDP
  • Protocol icon on each tab
  • Environment badges: optional colored pills (PROD, ROOT, DEV) derived from connection tags
  • Root session warning: tabs connected as root get a subtle warm accent
  • Close button (×) on each tab
  • Pop-out icon (↗) on hover for tab detach
  • Overflow: chevron dropdown for hidden tabs when 10+ are open (not multi-line rows)
  • Drag to reorder tabs
  • Drag out of tab bar to detach into new window
  • "+" button to open new session

Tab Detach/Reattach

  • Detach: Drag tab out of bar OR click ↗ icon → spawns new Wails window with that session still alive. Original tab shows "Session detached — [Reattach]" placeholder.
  • Reattach: Click "Reattach" button in placeholder OR close the detached window → session snaps back into the tab bar.
  • Works for both SSH and RDP sessions.
  • The session lives in the Go backend, not the window. Detaching is just moving the view.

Command Palette (Ctrl+K)

Modal overlay with fuzzy search across:

  • Connection names, hostnames, group names, tags
  • Actions: "New SSH", "New RDP", "Open Vault", "Settings", "Import MobaXTerm"
  • Active sessions: "Switch to Asgard", "Disconnect Docker"
  • Keyboard-first — arrow keys to navigate, Enter to select, Esc to close

Status Bar

  • Left: Protocol, user@host:port, privilege warning (⚠️ when root), transfer speed
  • Right: Active theme name, encoding (UTF-8), terminal dimensions (cols×rows)
  • Active session count in toolbar area

Terminal Theming

Built-in themes: Dracula, Nord, Monokai, Solarized Dark, One Dark, Gruvbox, plus a "MobaXTerm Classic" theme matching the colors from the user's .mobaconf export.

Custom theme creation via settings. Full 16-color ANSI palette + foreground/background/cursor maps directly to xterm.js ITheme objects.

Per-connection theme override via connections.options JSON field.

Keyboard Shortcuts

Shortcut Action
Ctrl+K Command palette
Ctrl+T New SSH session (from quick connect)
Ctrl+W Close current tab
Ctrl+Tab Next tab
Ctrl+Shift+Tab Previous tab
Ctrl+1-9 Switch to tab N
Ctrl+B Toggle sidebar
Ctrl+Shift+D Detach current tab
F11 Fullscreen
Ctrl+Shift+C Copy (terminal)
Ctrl+Shift+V Paste (terminal)
Ctrl+F Search in terminal scrollback

CodeMirror 6 Editor (Separate Window)

  • Opens as a new Wails window when clicking a text file in the SFTP sidebar
  • File size guard: files over 5MB refused for inline editing (offered download instead)
  • Syntax highlighting based on file extension
  • Save button writes content back to remote via SFTP
  • Unsaved changes warning on close
  • Window title: filename — host — Wraith Editor

6. SSH + SFTP Flow

SSH Connection

User double-clicks "Asgard" in connection sidebar
  → Go: ConnectionManager.Connect(connectionId)
  → Go: VaultService.DecryptCredential(credentialId) → auth method
  → Go: SSHService.Dial(hostname, port, authConfig)
       → x/crypto/ssh.Dial() with host key callback
       → If new host key: emit event to frontend, user accepts/rejects
       → If changed host key: BLOCK connection, warn user (no silent accept)
       → If accepted: store in host_keys table
  → Go: SessionManager.Create(sshClient, connectionId) → sessionId
  → Go: SSHService.RequestPTY(session, "xterm-256color", cols, rows)
  → Go: SSHService.Shell(session) → stdin/stdout pipes
  → Frontend: xterm.js instance created, bound to sessionId
  → Wails bindings: bidirectional data flow
       → xterm.js onData → Go SSHService.Write(sessionId, bytes)
       → Go SSHService.Read(sessionId) → Wails event → xterm.js write

SSH Authentication

Supports three auth methods:

  1. SSH Key: Decrypt private key from vault. If key has passphrase, decrypt that too. Pass to ssh.PublicKeys() signer.
  2. Password: Decrypt password from vault. Pass to ssh.Password().
  3. Keyboard-Interactive: For servers with 2FA/MFA prompts. ssh.KeyboardInteractive() callback relays challenge prompts to the frontend, user responds in a dialog. Common in MSP environments with PAM-based MFA.

Auth methods are tried in order: key → password → keyboard-interactive. The credential type determines which are attempted first, but keyboard-interactive is always available as a fallback for servers that require it.

Terminal Resize

Frontend: xterm.js fit addon detects container resize
  → Wails binding: SSHService.Resize(sessionId, cols, rows)
  → Go: session.WindowChange(rows, cols)

SFTP Sidebar

SSH connection established:
  → Go: SFTPService.Open(sshClient) → pkg/sftp.NewClient(sshClient)
  → Go: SFTPService.List(sessionId, homeDir) → directory listing
  → Frontend: sidebar switches to SFTP view, renders file tree

SFTP uses the same SSH connection as the terminal (SFTP subsystem). No separate connection needed.

File operations: All SFTP commands route through Go via Wails bindings, targeting the correct pkg/sftp client by sessionId.

Operation Go function Notes
List directory sftp.ReadDir(path) Lazy-loaded on tree expand
Upload sftp.Create(path) + chunked write Drag-and-drop from Windows Explorer
Download sftp.Open(path) + read Browser-style save dialog
Delete sftp.Remove(path) / sftp.RemoveAll(path) Confirmation prompt
Rename/Move sftp.Rename(old, new)
Mkdir sftp.Mkdir(path)
Chmod sftp.Chmod(path, mode)
Read file sftp.Open(path) → content Opens in CodeMirror window
Write file sftp.Create(path) ← content Save from CodeMirror

CWD Following

SSH session starts:
  → Go: injects shell hook after PTY is established:
       PROMPT_COMMAND='printf "\033]7;file://%s%s\033\\" "$(hostname)" "$PWD"'
       (or precmd for zsh)
  → Go: SSHService reads stdout, scans for OSC 7 escape sequences
  → Go: strips OSC 7 before forwarding to xterm.js (user never sees it)
  → Go: emits CWD change event with new path
  → Frontend: if "Follow terminal folder" is enabled, calls SFTPService.List(newPath)
  → Frontend: SFTP tree navigates to new directory

"Follow terminal folder" is a per-session toggle (checkbox at bottom of SFTP sidebar), enabled by default.

Shell detection: The OSC 7 injection assumes a bash-like shell (PROMPT_COMMAND) or zsh (precmd). For fish, the equivalent is function fish_prompt; printf "\033]7;file://%s%s\033\\" (hostname) "$PWD"; end. If shell detection fails (unknown shell, restricted shell, non-interactive session), CWD following is silently disabled — the SFTP sidebar stays at the initial home directory and requires manual navigation.

Upload Flow

User drags file from Windows Explorer onto SFTP sidebar:
  → Frontend: reads file via File API, sends chunks to Go
  → Go: SFTPService.Upload(sessionId, remotePath, fileData)
  → Go: sftp.Create(remotePath) → write chunks → close
  → Progress events emitted back to frontend
  → SFTP tree refreshes on completion

7. RDP Flow

Architecture

FreeRDP3 is loaded via purego (or syscall.NewLazyDLL) at runtime. No CGO, no C compiler needed. The Go binary loads freerdp3.dll from the application directory.

Connection

User double-clicks "CLT-VMHOST01" in connection sidebar:
  → Go: ConnectionManager.Connect(connectionId)
  → Go: VaultService.DecryptCredential(credentialId) → username, password, domain
  → Go: RDPService.Connect(host, port, username, password, domain, options)
       → purego: freerdp_new() → configure settings → freerdp_connect()
       → Register BitmapUpdate callback
  → Go: allocate pixel buffer (width × height × 4 bytes RGBA)
  → FreeRDP: decoded bitmap frames written into pixel buffer
  → Go: frame data served to frontend
  → Frontend: <canvas> renders frames via requestAnimationFrame (30fps target)

Frame Delivery

FreeRDP writes decoded frame data into a shared Go pixel buffer. The frontend retrieves frame data via one of:

  • Local HTTP endpoint: localhost:{random_port}/frame returns raw RGBA or PNG
  • Blob URL: Go encodes frame, passes via Wails binding as base64
  • Optimal approach TBD during implementation — benchmark both

Performance target: 1080p @ 30fps. Focus on Bitmap Update callbacks. No H.264 pipeline needed — raw bitmap updates with basic RLE compression is sufficient for remote management work.

Input Handling

Frontend: mouse/keyboard events captured on <canvas> element
  → Wails binding → Go: RDPService.SendMouseEvent(sessionId, x, y, flags)
  → Wails binding → Go: RDPService.SendKeyEvent(sessionId, keycode, pressed)
  → Go: translate JS virtual keycodes to RDP scancodes via lookup table
  → Go: purego calls freerdp_input_send_mouse_event / freerdp_input_send_keyboard_event

Scancode mapping: JavaScript KeyboardEvent.code values (e.g., "KeyA", "ShiftLeft") must be translated to RDP hardware scancodes that FreeRDP expects. A static lookup table in internal/rdp/input.go maps JS key codes → RDP scancodes. This is a known complexity in web-based RDP — the table must handle extended keys (e.g., right Alt, numpad) and platform-specific quirks. Reference: FreeRDP's scancode.h for the canonical scancode list.

System key pass-through: The Windows key and Alt+Tab require special handling. By default, these keys are captured by the local OS. A per-connection toggle in connections.options ("grabKeyboard": true) controls whether system keys are forwarded to the remote host or stay local. When enabled, the RDP canvas captures all keyboard input including Win key, Alt+Tab, Ctrl+Alt+Del (via a toolbar button). Power users toggling between remote and local need this to be fast and obvious — surface it as an icon in the RDP toolbar.

Clipboard Sync

Remote → Local:
  → Go: FreeRDP clipboard channel callback fires
  → Go: emits clipboard event to frontend
  → Frontend: writes to system clipboard via Wails API

Local → Remote:
  → Frontend: detects clipboard change (or user pastes)
  → Wails binding → Go: RDPService.SendClipboard(sessionId, data)
  → Go: writes to FreeRDP clipboard channel

RDP Connection Options

Stored in connections.options JSON field:

{
  "colorDepth": 32,
  "security": "nla",
  "consoleSession": false,
  "audioRedirect": false,
  "width": 1920,
  "height": 1080,
  "scaleFactor": 100
}

HiDPI / display scaling: On Windows with display scaling (e.g., 150% on a 4K monitor), the RDP session resolution must account for the scale factor. scaleFactor in connection options controls whether to send the physical pixel resolution or the scaled logical resolution to FreeRDP. Default behavior: detect the current Windows DPI setting and scale the RDP resolution accordingly. Override via the scaleFactor option (100 = no scaling, 150 = 150%).


8. Vault + Encryption

Master Password Flow

App launch:
  → Master password prompt (modal, cannot be bypassed)
  → If first launch:
       → Generate random 32-byte salt
       → Store salt in settings table (key: "vault_salt")
       → Derive key: Argon2id(password, salt, t=3, m=65536, p=4, keyLen=32)
       → Encrypt a known test value ("wraith-vault-check") with derived key
       → Store encrypted test value in settings (key: "vault_check")
  → If returning:
       → Read salt and encrypted test value from settings
       → Derive key with same parameters
       → Attempt to decrypt test value
       → If decryption succeeds → vault unlocked
       → If fails → wrong password, prompt again
  → Derived key held in memory only, never written to disk
  → Key zeroed from memory on app close

Encryption Functions

Encrypt(plaintext string) → string:
  → Generate random 12-byte IV (crypto/rand)
  → AES-256-GCM Seal(): returns ciphertext with authTag appended (Go's native format)
  → Return "v1:{iv_hex}:{sealed_hex}"
  → (sealed = ciphertext || authTag, as produced by cipher.AEAD.Seal())

Decrypt(blob string) → string:
  → Parse version prefix, IV (12B), sealed data
  → AES-256-GCM Open(): decrypts and verifies authTag (Go's native format)
  → Return plaintext

The v1: version prefix enables future key rotation without re-encrypting all stored values.

What Gets Encrypted

Data Encrypted Reason
SSH private keys Yes Sensitive key material
SSH key passphrases Yes Passphrase is a secret
Password credentials Yes Passwords are secrets
RDP passwords Yes Via credential reference
Hostnames, ports, usernames No Not secrets, needed for display
Public keys, fingerprints No Public by definition
Group names, tags, notes No Not secrets
Settings, themes No User preferences

Argon2id Parameters

Parameter Value Rationale
Time cost (t) 3 OWASP recommended minimum
Memory cost (m) 65536 (64MB) Resists GPU attacks
Parallelism (p) 4 Matches typical core count
Key length 32 bytes (256-bit) AES-256 key size
Salt 32 bytes, random Unique per installation

Future: Windows DPAPI Integration (Post-MVP)

The current vault is secure and portable (works on any Windows machine, backup the .db file and go). Post-MVP, an optional DPAPI layer could wrap the derived AES key with Windows Data Protection API, tying the vault to the current Windows user account. This would enable:

  • Transparent unlock when logged into Windows (no master password prompt)
  • Hardware-backed key protection on machines with TPM
  • Enterprise trust (DPAPI is a known quantity for IT departments)

Implementation: the Argon2id-derived key gets wrapped with CryptProtectData() and stored. On unlock, DPAPI unwraps the key. Master password remains the fallback for portability (moving the database to another machine). This is designed-for but not built in MVP — the v1: encryption prefix enables adding a v2: scheme without re-encrypting existing data.


9. MobaXTerm Importer

Config Format

MobaXTerm exports configuration as .mobaconf files — INI format with %-delimited session strings.

[Bookmarks_1]
SubRep=AAA Vantz's Stuff          # Group name
ImgNum=41                          # Icon index
*Asgard=#109#0%192.168.1.4%22%vstockwell%...  # SSH session
CLT-VMHOST01=#91#4%100.64.1.204%3389%...      # RDP session

[SSH_Hostkeys]
ssh-ed25519@22:192.168.1.4=0x29ac...          # Known host keys

[Colors]
ForegroundColour=236,236,236                   # Terminal colors
BackgroundColour=36,36,36

[Passwords]
vstockwell@192.168.1.214=_@9jajOXK...         # Encrypted (can't import)

Session String Parsing

Protocol Type code Fields (%-delimited)
SSH #109# host, port, username, ..., SSH key path, ..., colors
RDP #91# host, port, username, ..., color depth, security

Import Flow

1. User: File → Import → Select .mobaconf file
2. Go: parse INI sections
3. Go: extract groups from [Bookmarks_N] SubRep values
4. Go: parse session strings → connections
5. Go: parse [SSH_Hostkeys] → host_keys table
6. Go: parse [Colors] + [Font] → create "MobaXTerm Import" theme
7. Frontend: show preview dialog:
     "Found: 18 connections, 1 group, 4 host keys, 1 color theme"
8. User confirms import
9. Go: create groups, connections, host keys, theme in SQLite
10. Frontend: report results:
      "Imported! 5 connections reference SSH keys — re-import key files.
       3 connections had stored passwords — re-enter in Wraith vault."

What Gets Imported

Data Imported Notes
Connection names Yes
Groups (folder hierarchy) Yes From SubRep values
Hostnames, ports Yes
Usernames Yes
Protocol (SSH/RDP) Yes From type code #109# / #91#
SSH key file paths As notes User must re-import actual key files
Host keys Yes To host_keys table
Terminal colors Yes As a new theme
Font preferences Yes To settings
Encrypted passwords No MobaXTerm-encrypted, can't decrypt

10. Frontend Structure

frontend/
  src/
    App.vue                       # Root: master password → main layout
    layouts/
      MainLayout.vue              # Sidebar + tab container + status bar
      UnlockLayout.vue            # Master password prompt
    components/
      sidebar/
        ConnectionTree.vue        # Group tree with connection entries
        SftpBrowser.vue           # SFTP file tree + toolbar
        SidebarToggle.vue         # Connections ↔ SFTP toggle
      session/
        SessionContainer.vue      # Holds all active sessions (v-show, not v-if)
        TabBar.vue                # Draggable, detachable tab bar
        TabBadge.vue              # PROD/ROOT/DEV environment pills
      terminal/
        TerminalView.vue          # xterm.js instance wrapper
        ThemePicker.vue           # Terminal color scheme selector
      rdp/
        RdpView.vue               # Canvas-based RDP renderer
        RdpToolbar.vue            # Clipboard, fullscreen controls
      sftp/
        FileTree.vue              # Remote filesystem tree (lazy-loaded)
        TransferProgress.vue      # Upload/download progress indicator
      vault/
        VaultManager.vue          # SSH keys + credentials management
        KeyImportDialog.vue       # SSH key import modal
        CredentialForm.vue        # Password/key credential form
      common/
        CommandPalette.vue        # Ctrl+K fuzzy search overlay
        QuickConnect.vue          # Quick connect input
        StatusBar.vue             # Bottom status bar
    composables/
      useSession.ts               # Session lifecycle + tab management
      useTerminal.ts              # xterm.js + Wails binding bridge
      useSftp.ts                  # SFTP operations via Wails bindings
      useRdp.ts                   # RDP canvas rendering + input capture
      useVault.ts                 # Key/credential CRUD
      useConnections.ts           # Connection CRUD + search + tags
      useTheme.ts                 # Terminal theme management
      useCommandPalette.ts        # Command palette search + actions
    stores/
      session.store.ts            # Active sessions, tab order, detach state
      connection.store.ts         # Connections, groups, search state
      app.store.ts                # Global state: unlocked, settings, active theme

Session architecture: Active sessions render as persistent components inside SessionContainer.vue. Switching tabs toggles v-show visibility (not v-if destruction), so xterm.js and RDP canvas instances stay alive across tab switches. This is critical — destroying and recreating terminal instances would lose scrollback and session state.


11. Go Backend Structure

internal/
  app/
    app.go                        # Wails app setup, window management
    menu.go                       # Application menu definitions
  session/
    manager.go                    # Session lifecycle, tab detach/reattach
    session.go                    # Session struct (SSH or RDP, backend state)
  ssh/
    service.go                    # SSH dial, PTY, shell, I/O pipes
    hostkey.go                    # Host key verification + storage
    cwd.go                        # OSC 7 parsing for CWD tracking
  sftp/
    service.go                    # SFTP operations (list, upload, download, etc.)
  rdp/
    service.go                    # RDP session management
    freerdp.go                    # purego bindings to freerdp3.dll
    pixelbuffer.go                # Shared frame buffer management
    input.go                      # Mouse/keyboard event translation
  vault/
    service.go                    # Encrypt/decrypt, master password, key derivation
    vault_test.go                 # Encryption round-trip tests
  connections/
    service.go                    # Connection CRUD, group management
    search.go                     # Full-text search + tag filtering
  importer/
    mobaconf.go                   # MobaXTerm .mobaconf parser
    mobaconf_test.go              # Parser tests with real config samples
  db/
    sqlite.go                     # SQLite connection, migrations
    migrations/                   # SQL migration files
  settings/
    service.go                    # Key-value settings CRUD
  theme/
    service.go                    # Theme CRUD, built-in theme definitions
    builtins.go                   # Dracula, Nord, Monokai, etc.
  plugin/
    interfaces.go                 # Plugin interfaces (ProtocolHandler, Importer, etc.)
    registry.go                   # Plugin registration and lifecycle

Plugin Interface

Wraith exposes Go interfaces that community developers can implement to extend functionality:

// ProtocolHandler — add support for new protocols (VNC, Telnet, etc.)
type ProtocolHandler interface {
    Name() string
    Connect(config ConnectionConfig) (Session, error)
    Disconnect(sessionId string) error
}

// Importer — add support for importing from other tools
type Importer interface {
    Name() string
    FileExtensions() []string
    Parse(data []byte) (*ImportResult, error)
}

Plugins are compiled into the binary (not runtime-loaded). Community developers fork the repo, implement the interface, register it in plugin/registry.go, and build. This keeps distribution simple (single binary) while enabling extensibility.


12. MVP Scope

In MVP (launch-blocking)

Feature Priority Phase Notes
Wails v3 scaffold + SQLite + vault P0 1 Foundation — nothing works without this
Connection manager sidebar P0 1 Groups, tree, search, tags
SSH terminal (xterm.js) P0 2 Multi-tab, 8+ concurrent sessions
SFTP sidebar P0 2 Auto-open, CWD following, file ops
Credential vault UI P0 2 SSH key import, credential management
Host key verification P0 2 Accept/reject new, block changed
RDP in tabs P0 3 FreeRDP3/purego, embedded canvas
MobaXTerm importer P1 4 Parse .mobaconf, first-run detection
Terminal theming P1 4 6+ built-in themes, custom themes
Tab detach/reattach P1 4 Drag out, pop-out icon, reattach button
CodeMirror 6 editor P1 4 Separate window, syntax highlighting
Command palette (Ctrl+K) P1 4 Fuzzy search connections + actions
Session context awareness P1 4 Root warning, user@host in status bar
Tab badges P1 4 Protocol icon, environment tags
Quick connect P1 4 user@host:port in toolbar
Plugin interface P1 1 Define interfaces, implement in later phases
README.md P1 1 Developer docs, architecture, contribution guide

Post-MVP

Feature Notes
Split panes Horizontal/vertical splits within a tab
Session recording/playback asciinema-compatible
Jump host / bastion proxy ProxyJump chain support
Port forwarding manager Local, remote, dynamic SSH tunnels
Saved snippets/macros Quick-execute command library
Tab grouping/stacking Browser-style tab groups
Live latency monitoring Ping/packet loss in status bar
Dual-pane SFTP Server-to-server file operations
Auto-detect environment Parse hostname for prod/dev/staging classification
Subtle glow effects "Wraith" personality — energy on active sessions
Dynamic plugin loading Drop-in plugins without recompilation (longer-term)
Windows DPAPI vault Optional OS-backed encryption layer for transparent unlock
Claude Code plugin First official plugin — see below

Post-MVP Plugin: Claude Code Integration

The first plugin built on the Wraith plugin interface. Embeds Claude Code directly into Wraith as a sidebar panel or tab, with full access to the active session's context.

Authentication: User authenticates with their Anthropic API key or Claude account (stored encrypted in the vault alongside SSH keys and passwords). Key is decrypted on demand, never persisted in plaintext.

Core capabilities:

  • Terminal integration: Claude Code runs in a dedicated Wraith tab (xterm.js instance). It can see the active SSH session's terminal output and type commands into it — same as a human operator switching tabs.
  • SFTP-aware file access: Claude Code can read and write files on the remote host via the active SFTP session. "Read /etc/nginx/nginx.conf" pulls the file through SFTP, Claude analyzes/modifies it, and writes it back. No need for Claude to SSH separately — it rides the existing Wraith session.
  • CodeMirror handoff: Claude can open files in the CodeMirror editor window, make changes, and save back to the remote host. The user sees the edits happening in real-time.
  • Context awareness: Claude sees which host you're connected to, the current working directory (via CWD tracking), and recent terminal output. "Fix the nginx config on this server" just works because Claude already knows where "this" is.

UX flow:

  1. User opens Claude Code panel (sidebar tab or dedicated session tab)
  2. Types a prompt: "Check why nginx is returning 502 on this server"
  3. Claude reads recent terminal output, pulls nginx config via SFTP, analyzes logs
  4. Claude proposes a fix, user approves, Claude writes the file via SFTP
  5. Claude types nginx -t && systemctl reload nginx into the terminal

Plugin interface usage: This plugin implements ProtocolHandler (for the Claude Code tab) and extends the SFTP/terminal services to allow programmatic read/write. It proves the plugin architecture works and becomes the reference implementation for community plugin developers.


13. Build Phases

Error Handling + Logging Strategy

Structured logging: Use log/slog (Go 1.21+ standard library) with JSON output. Log levels: DEBUG, INFO, WARN, ERROR. Log to %APPDATA%\Wraith\wraith.log with daily rotation (keep 7 days).

Connection drops: When an SSH/RDP connection drops unexpectedly:

  1. Session Manager marks session as disconnected
  2. Frontend tab shows "Connection lost — [Reconnect] [Close]"
  3. Auto-reconnect is opt-in (configurable per connection via options JSON)
  4. If auto-reconnect is enabled, retry 3 times with exponential backoff (1s, 2s, 4s)

Error surfacing: Errors from Go backend are emitted as Wails events with a severity level. Frontend shows:

  • Transient errors (network timeout) → toast notification, auto-dismiss
  • Actionable errors (auth failure) → modal with explanation and action button
  • Fatal errors (vault corruption) → full-screen error with instructions

Sensitive data in logs: Never log passwords, private keys, or decrypted credentials. Log only: connection IDs, hostnames, session IDs, error types.

Crash Recovery + Workspace Restore

When the app crashes, the system reboots, or Wails dies, SSH/RDP sessions are gone — there's no way to recover a dropped TCP connection. But the workspace layout can be restored.

Workspace snapshots: The Session Manager periodically writes a workspace snapshot to SQLite (every 30 seconds and on clean shutdown):

{
  "tabs": [
    {"connectionId": 1, "protocol": "ssh", "position": 0, "detached": false},
    {"connectionId": 5, "protocol": "rdp", "position": 1, "detached": false},
    {"connectionId": 3, "protocol": "ssh", "position": 2, "detached": true, "windowBounds": {...}}
  ],
  "sidebarWidth": 240,
  "sidebarMode": "connections",
  "activeTab": 0
}

On restart after crash:

  1. Detect unclean shutdown (snapshot exists but no clean_shutdown flag)
  2. Show: "Wraith closed unexpectedly. Restore previous workspace? [Restore] [Start Fresh]"
  3. If Restore: recreate tab layout, attempt to reconnect each session
  4. Tabs that fail to reconnect show "Connection lost — [Retry] [Close]"

Users care about continuity more than perfection. Even if every session dies, restoring the layout and offering one-click reconnect is a massive UX win.

Resource Management

With 20+ SSH sessions and multiple RDP sessions, resource awareness is critical:

Memory budget: Each SSH session costs ~2-5MB (PTY buffer + SFTP client). Each RDP session costs ~8-12MB (pixel buffer at 1080p). Target: stable at 20 SSH + 3 RDP (~100-120MB total backend memory).

Session limits:

  • Default max: 32 concurrent sessions (SSH + RDP combined)
  • Configurable via settings
  • When limit reached: "Maximum sessions reached. Close a session to open a new one."

Inactive session handling:

  • Sessions idle for 30+ minutes get a subtle "idle" indicator on the tab (dimmed text)
  • SSH keepalive (ServerAliveInterval equivalent) prevents server-side timeouts — configurable per connection via options.keepAliveInterval (default: 60 seconds)
  • No automatic session suspension — users control their sessions explicitly
  • SFTP idle connections are closed after 10 minutes of inactivity and silently reopened on next file operation

Monitoring: Expose a "Sessions" panel in Settings showing per-session memory usage, connection duration, and idle time. Simple table, not a dashboard.


14. Licensing + Open Source

License: MIT. All dependencies must be MIT, Apache 2.0, BSD, or ISC compatible. No GPL/AGPL dependencies.

Dependency license audit is part of Phase 1. Key libraries and their licenses:

  • golang.org/x/crypto — BSD-3-Clause ✓
  • github.com/pkg/sftp — BSD-2-Clause ✓
  • github.com/ebitengine/purego — Apache 2.0 ✓
  • modernc.org/sqlite — BSD-3-Clause ✓
  • FreeRDP3 — Apache 2.0 ✓ (dynamically linked, no license contamination)
  • xterm.js — MIT ✓
  • Vue 3 — MIT ✓
  • Naive UI — MIT ✓
  • Tailwind CSS — MIT ✓
  • CodeMirror 6 — MIT ✓

Plugin architecture: The Go backend exposes a plugin interface so community developers can extend Wraith with custom protocol handlers, importers, or sidebar panels. Plugins are Go packages that implement defined interfaces and are compiled into the binary (no runtime plugin loading — keeps the binary simple and portable).

README.md: Comprehensive developer-facing documentation covering: architecture overview, build instructions, project structure walkthrough, plugin development guide, contribution guidelines, and the design philosophy. Written as part of Phase 1.


15. First-Run Experience

On first launch:

  1. Master password creation dialog (set + confirm)
  2. Detect if .mobaconf files exist in common locations (%APPDATA%\MobaXterm\, user's Documents folder)
  3. If found: prompt "We found a MobaXTerm configuration. Import your sessions?" with file path shown
  4. If not found: offer "Import from MobaXTerm" button + "Start fresh"
  5. After import (or skip): land on the empty connection manager with a "Create your first connection" prompt

16. Build Phases

Phase Deliverables
1: Foundation Wails v3 scaffold (including multi-window spike — validate Plan A/B/C), SQLite schema + migrations (WAL mode), vault service (master password, Argon2id, AES-256-GCM), connection CRUD, group tree, Vue 3 shell with sidebar + tab container, dark theme, Naive UI integration, plugin interface definitions, README.md, license audit, RDP frame transport spike (benchmark HTTP vs base64 with a test canvas — don't wait until Phase 3)
2: SSH + SFTP SSH service (x/crypto/ssh), PTY + shell, xterm.js terminal rendering, multi-tab sessions, SFTP sidebar (pkg/sftp), file tree, upload/download, CWD following (OSC 7), CodeMirror 6 editor in separate window, workspace snapshot persistence
3: RDP FreeRDP3 purego bindings, pixel buffer, canvas rendering (using proven transport from Phase 1 spike), mouse/keyboard input mapping (including scancode table + system key pass-through), clipboard sync, connection options
4: Polish Command palette, tab detach/reattach, terminal theming (built-in + custom), MobaXTerm importer (with first-run detection), tab badges, session context awareness, quick connect, host key management UI, settings page, crash recovery / workspace restore, resource management panel, NSIS installer