# Wraith Desktop — Phase 2: SSH + SFTP Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Connect to remote hosts via SSH with a real terminal (xterm.js), SFTP sidebar with file operations and CWD following, multi-tab sessions, and CodeMirror 6 file editor. **Architecture:** Go SSH service wraps `x/crypto/ssh` for connections, PTY requests, and shell I/O. SFTP service wraps `pkg/sftp` riding the same SSH connection. Data flows: xterm.js ↔ Wails bindings ↔ Go SSH pipes. CWD tracking via OSC 7 shell injection. **Tech Stack:** `golang.org/x/crypto/ssh`, `github.com/pkg/sftp`, xterm.js 5.x + WebGL addon + fit addon + search addon, CodeMirror 6 **Spec:** `docs/superpowers/specs/2026-03-17-wraith-desktop-design.md` (Sections 6, 10, 11) --- ## File Structure (New/Modified) ``` internal/ ssh/ service.go # SSH dial, PTY, shell, I/O goroutines service_test.go # Connection config tests (unit, no real SSH) hostkey.go # Host key verification + storage hostkey_test.go cwd.go # OSC 7 parser for CWD tracking cwd_test.go sftp/ service.go # SFTP operations (list, upload, download, etc.) service_test.go credentials/ service.go # Credential CRUD (encrypted passwords + SSH keys) service_test.go app/ app.go # Add SSH/SFTP/Credential services frontend/ src/ components/ terminal/ TerminalView.vue # xterm.js instance wrapper sftp/ FileTree.vue # Remote filesystem tree TransferProgress.vue # Upload/download progress editor/ EditorWindow.vue # CodeMirror 6 (placeholder for multi-window) composables/ useTerminal.ts # xterm.js lifecycle + Wails binding bridge useSftp.ts # SFTP operations via Wails bindings stores/ session.store.ts # Update with real session management assets/ css/ terminal.css # xterm.js overrides package.json # Add xterm.js, codemirror deps ``` --- ## Task 1: SSH Service — Connect, PTY, Shell I/O **Files:** - Create: `internal/ssh/service.go` - Create: `internal/ssh/service_test.go` The SSH service manages connections and exposes methods to Wails: ```go // SSHService methods (exposed to frontend via Wails bindings): // Connect(connectionID int64) (string, error) → returns sessionID // Write(sessionID string, data string) error → write to stdin // Resize(sessionID string, cols, rows int) error → window change // Disconnect(sessionID string) error → close session // // Events emitted to frontend via Wails events: // "ssh:data:{sessionID}" → terminal output (stdout) // "ssh:connected:{sessionID}" → connection established // "ssh:disconnected:{sessionID}" → connection closed // "ssh:error:{sessionID}" → error message ``` Key implementation details: - Each SSH session runs two goroutines: one reading stdout→Wails events, one for keepalive - Sessions stored in a `map[string]*SSHSession` with mutex - `SSHSession` holds: `*ssh.Client`, `*ssh.Session`, stdin `io.WriteCloser`, connection metadata - PTY requested as `xterm-256color` with initial size from frontend - Auth method determined by credential type (password, SSH key, keyboard-interactive) - Host key verification delegates to `hostkey.go` Tests (unit only — no real SSH server): - TestSSHServiceCreation - TestBuildAuthMethods (password → ssh.Password, key → ssh.PublicKeys) - TestSessionTracking (create, get, remove) - [ ] **Step 1:** Write tests - [ ] **Step 2:** Implement service - [ ] **Step 3:** Run tests, verify pass - [ ] **Step 4:** Commit: `feat: SSH service — connect, PTY, shell I/O with goroutine pipes` --- ## Task 2: Host Key Verification **Files:** - Create: `internal/ssh/hostkey.go` - Create: `internal/ssh/hostkey_test.go` Host key verification stores/checks fingerprints in the `host_keys` SQLite table: - New host → emit `ssh:hostkey-verify` event to frontend with fingerprint, wait for accept/reject - Known host, matching fingerprint → proceed silently - Known host, CHANGED fingerprint → emit warning event, block connection For Phase 2, implement the storage and verification logic. The frontend prompt (accept/reject dialog) will be wired in the frontend task. Tests: - TestStoreHostKey - TestVerifyKnownHost (match → ok) - TestVerifyChangedHost (mismatch → error) - TestVerifyNewHost (not found → returns "new") - [ ] **Step 1:** Write tests - [ ] **Step 2:** Implement hostkey.go - [ ] **Step 3:** Run tests, verify pass - [ ] **Step 4:** Commit: `feat: SSH host key verification — store, verify, detect changes` --- ## Task 3: CWD Tracker (OSC 7 Parser) **Files:** - Create: `internal/ssh/cwd.go` - Create: `internal/ssh/cwd_test.go` Parses OSC 7 escape sequences from terminal output to track the remote working directory: ``` Input: "some output\033]7;file://hostname/home/user\033\\more output" Output: stripped="some output more output", cwd="/home/user" ``` The CWD tracker: 1. Scans byte stream for `\033]7;` prefix 2. Extracts URL between prefix and `\033\\` (or `\007`) terminator 3. Parses `file://hostname/path` to extract just the path 4. Strips the OSC 7 sequence from the output before forwarding to xterm.js 5. Returns the new CWD path when detected Shell injection command (injected after PTY is established): ```bash # bash PROMPT_COMMAND='printf "\033]7;file://%s%s\033\\" "$(hostname)" "$PWD"' # zsh precmd() { printf "\033]7;file://%s%s\033\\" "$(hostname)" "$PWD" } # fish function fish_prompt; printf "\033]7;file://%s%s\033\\" (hostname) "$PWD"; end ``` Tests: - TestParseOSC7Basic - TestParseOSC7WithBEL (terminated by \007 instead of ST) - TestParseOSC7NoMatch (no OSC 7 in output) - TestParseOSC7MultipleInStream - TestStripOSC7FromOutput - [ ] **Step 1:** Write tests - [ ] **Step 2:** Implement cwd.go - [ ] **Step 3:** Run tests, verify pass - [ ] **Step 4:** Commit: `feat: OSC 7 CWD tracker — parse and strip directory change sequences` --- ## Task 4: SFTP Service **Files:** - Create: `internal/sftp/service.go` - Create: `internal/sftp/service_test.go` SFTP service wraps `pkg/sftp` and exposes file operations to the frontend: ```go // SFTPService methods (exposed via Wails bindings): // OpenSFTP(sessionID string) error → start SFTP on existing SSH connection // List(sessionID string, path string) ([]FileEntry, error) → directory listing // ReadFile(sessionID string, path string) (string, error) → read file content (max 5MB) // WriteFile(sessionID string, path string, content string) error → write file // Upload(sessionID string, remotePath string, localPath string) error // Download(sessionID string, remotePath string) (string, error) → returns local temp path // Mkdir(sessionID string, path string) error // Delete(sessionID string, path string) error // Rename(sessionID string, oldPath, newPath string) error // Stat(sessionID string, path string) (*FileEntry, error) ``` `FileEntry` type: ```go type FileEntry struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` IsDir bool `json:"isDir"` Permissions string `json:"permissions"` ModTime string `json:"modTime"` Owner string `json:"owner"` } ``` SFTP client is created from the existing `*ssh.Client` (same connection, separate channel). Stored alongside the SSH session. Tests (unit — mock the sftp.Client interface): - TestFileEntryFromFileInfo - TestListSortsDirectoriesFirst - TestReadFileRejectsLargeFiles (>5MB) - [ ] **Step 1:** Write tests - [ ] **Step 2:** Implement service - [ ] **Step 3:** Add `pkg/sftp` dependency - [ ] **Step 4:** Run tests, verify pass - [ ] **Step 5:** Commit: `feat: SFTP service — list, read, write, upload, download, mkdir, delete` --- ## Task 5: Credential Service (Encrypted SSH Keys + Passwords) **Files:** - Create: `internal/credentials/service.go` - Create: `internal/credentials/service_test.go` CRUD for credentials and SSH keys with vault encryption: ```go // CredentialService methods: // CreatePassword(name, username, password, domain string) (*Credential, error) // CreateSSHKey(name string, privateKey, passphrase []byte) (*SSHKey, error) // GetCredential(id int64) (*Credential, error) // ListCredentials() ([]Credential, error) // DecryptPassword(id int64) (string, error) → decrypt for connection use only // DecryptSSHKey(id int64) ([]byte, string, error) → returns (privateKey, passphrase, error) // DeleteCredential(id int64) error // ImportSSHKeyFile(name, filePath string) (*SSHKey, error) → read .pem file, detect type, store ``` All sensitive data encrypted via VaultService before storage. Decryption only happens at connection time. Tests: - TestCreatePasswordCredential - TestCreateSSHKeyCredential - TestDecryptPassword (round-trip through vault) - TestDecryptSSHKey (round-trip) - TestListCredentialsExcludesEncryptedValues - TestDetectKeyType (RSA, Ed25519, ECDSA) - [ ] **Step 1:** Write tests - [ ] **Step 2:** Implement service - [ ] **Step 3:** Run tests, verify pass - [ ] **Step 4:** Commit: `feat: credential service — encrypted password and SSH key storage` --- ## Task 6: Wire SSH/SFTP/Credentials into App **Files:** - Modify: `internal/app/app.go` - Modify: `main.go` Add SSHService, SFTPService, and CredentialService to WraithApp. Register as Wails services. - [ ] **Step 1:** Update app.go to create and expose new services - [ ] **Step 2:** Update main.go to register them - [ ] **Step 3:** Verify compilation: `go vet ./...` - [ ] **Step 4:** Run all tests: `go test ./... -count=1` - [ ] **Step 5:** Commit: `feat: wire SSH, SFTP, and credential services into Wails app` --- ## Task 7: Frontend — xterm.js Terminal **Files:** - Modify: `frontend/package.json` — add xterm.js + addons - Create: `frontend/src/components/terminal/TerminalView.vue` - Create: `frontend/src/composables/useTerminal.ts` - Create: `frontend/src/assets/css/terminal.css` - Modify: `frontend/src/components/session/SessionContainer.vue` - Modify: `frontend/src/stores/session.store.ts` Install xterm.js dependencies: ``` @xterm/xterm @xterm/addon-fit @xterm/addon-webgl @xterm/addon-search @xterm/addon-web-links ``` `useTerminal` composable: - Creates xterm.js Terminal instance with theme from connection settings - Attaches fit, WebGL, search, web-links addons - Binds `terminal.onData` → Wails `SSHService.Write(sessionId, data)` - Listens for Wails events `ssh:data:{sessionId}` → `terminal.write(data)` - Handles resize via fit addon → Wails `SSHService.Resize(sessionId, cols, rows)` - Cleanup on unmount `TerminalView.vue`: - Receives `sessionId` prop - Mounts xterm.js into a div ref - Applies theme colors from the active theme - Handles focus management `SessionContainer.vue` update: - Replace placeholder with real TerminalView for SSH sessions - Use `v-show` (not `v-if`) to keep terminals alive across tab switches - [ ] **Step 1:** Install xterm.js deps: `cd frontend && npm install @xterm/xterm @xterm/addon-fit @xterm/addon-webgl @xterm/addon-search @xterm/addon-web-links` - [ ] **Step 2:** Create terminal.css (xterm.js container styling) - [ ] **Step 3:** Create useTerminal.ts composable - [ ] **Step 4:** Create TerminalView.vue component - [ ] **Step 5:** Update SessionContainer.vue to render TerminalView - [ ] **Step 6:** Update session.store.ts with real Wails binding calls - [ ] **Step 7:** Build frontend: `npm run build` - [ ] **Step 8:** Commit: `feat: xterm.js terminal with WebGL rendering and Wails binding bridge` --- ## Task 8: Frontend — SFTP Sidebar **Files:** - Create: `frontend/src/components/sftp/FileTree.vue` - Create: `frontend/src/components/sftp/TransferProgress.vue` - Create: `frontend/src/composables/useSftp.ts` - Modify: `frontend/src/layouts/MainLayout.vue` — SFTP sidebar rendering - Modify: `frontend/src/components/sidebar/SidebarToggle.vue` — enable SFTP tab `useSftp` composable: - `listDirectory(sessionId, path)` → calls Wails SFTPService.List - `uploadFile(sessionId, remotePath, file)` → chunked upload with progress - `downloadFile(sessionId, remotePath)` → triggers browser download - `deleteFile(sessionId, path)` → with confirmation - `createDirectory(sessionId, path)` - `renameFile(sessionId, old, new)` - Tracks current path, file list, loading state, transfer progress `FileTree.vue`: - Renders file/directory tree (lazy-loaded on expand) - Path bar at top showing current directory - Toolbar: upload, download, new file, new folder, refresh, delete - File entries show: icon (folder/file), name, size, modified date - Double-click file → open in editor (Task 9) - Drag-and-drop upload zone - "Follow terminal folder" toggle at bottom `TransferProgress.vue`: - Shows active uploads/downloads with progress bars - File name, percentage, speed, ETA - [ ] **Step 1:** Create useSftp.ts composable - [ ] **Step 2:** Create FileTree.vue component - [ ] **Step 3:** Create TransferProgress.vue component - [ ] **Step 4:** Update MainLayout.vue to render SFTP sidebar when toggled - [ ] **Step 5:** Enable SFTP toggle in SidebarToggle.vue - [ ] **Step 6:** Build frontend: `npm run build` - [ ] **Step 7:** Commit: `feat: SFTP sidebar — file tree, upload/download, CWD following` --- ## Task 9: Frontend — Host Key Dialog + Connection Flow **Files:** - Create: `frontend/src/components/common/HostKeyDialog.vue` - Modify: `frontend/src/components/sidebar/ConnectionTree.vue` — double-click to connect - Modify: `frontend/src/stores/session.store.ts` — real connection flow Wire up the full connection flow: 1. User double-clicks connection in sidebar 2. Session store calls Wails `SSHService.Connect(connectionId)` 3. If host key verification needed → show HostKeyDialog 4. On success → create tab, mount TerminalView, open SFTP sidebar 5. On error → show error toast `HostKeyDialog.vue`: - Modal showing: hostname, key type, fingerprint - "New host" vs "CHANGED host key (WARNING)" modes - Accept / Reject buttons - "Always accept for this host" checkbox - [ ] **Step 1:** Create HostKeyDialog.vue - [ ] **Step 2:** Update ConnectionTree.vue with double-click handler - [ ] **Step 3:** Update session.store.ts with connection flow - [ ] **Step 4:** Build frontend: `npm run build` - [ ] **Step 5:** Commit: `feat: connection flow — host key dialog, double-click to connect` --- ## Task 10: Frontend — CodeMirror 6 Editor (Placeholder) **Files:** - Modify: `frontend/package.json` — add CodeMirror deps - Create: `frontend/src/components/editor/EditorWindow.vue` Install CodeMirror 6: ``` codemirror @codemirror/lang-javascript @codemirror/lang-json @codemirror/lang-html @codemirror/lang-css @codemirror/lang-python @codemirror/lang-markdown @codemirror/theme-one-dark ``` `EditorWindow.vue`: - Renders CodeMirror 6 editor with dark theme - Receives file content, path, and sessionId as props - Syntax highlighting based on file extension - Save button → calls Wails SFTPService.WriteFile - Unsaved changes detection - For Phase 2: renders inline (not separate window — multi-window is Phase 4) - [ ] **Step 1:** Install CodeMirror deps - [ ] **Step 2:** Create EditorWindow.vue - [ ] **Step 3:** Wire file click in FileTree.vue to open EditorWindow - [ ] **Step 4:** Build frontend: `npm run build` - [ ] **Step 5:** Commit: `feat: CodeMirror 6 editor — syntax highlighting, dark theme, SFTP save` --- ## Task 11: Workspace Snapshot Persistence **Files:** - Create: `internal/app/workspace.go` - Create: `internal/app/workspace_test.go` Implements workspace snapshot saving/restoring per the spec: ```go // SaveWorkspace() error — serialize current tab layout to settings // LoadWorkspace() (*WorkspaceSnapshot, error) — read last saved layout // Auto-save every 30 seconds via goroutine // Save on clean shutdown ``` WorkspaceSnapshot JSON: ```json { "tabs": [ {"connectionId": 1, "protocol": "ssh", "position": 0}, {"connectionId": 5, "protocol": "rdp", "position": 1} ], "sidebarWidth": 240, "sidebarMode": "connections", "activeTab": 0 } ``` Tests: - TestSaveAndLoadWorkspace - TestEmptyWorkspace - [ ] **Step 1:** Write tests - [ ] **Step 2:** Implement workspace.go - [ ] **Step 3:** Run tests, verify pass - [ ] **Step 4:** Commit: `feat: workspace snapshot persistence — auto-save layout every 30s` --- ## Task 12: Integration Test + Final Verification - [ ] **Step 1:** Run all Go tests: `go test ./... -count=1` - [ ] **Step 2:** Build frontend: `cd frontend && npm run build` - [ ] **Step 3:** Verify Go compiles with embedded frontend: `go vet ./...` - [ ] **Step 4:** Count tests and lines of code - [ ] **Step 5:** Commit any fixes: `chore: Phase 2 complete — SSH + SFTP with terminal and file operations` --- ## Phase 2 Completion Checklist - [ ] SSH service: connect, PTY, shell I/O with goroutine pipes - [ ] Host key verification: store, verify, detect changes - [ ] OSC 7 CWD tracker: parse and strip directory change sequences - [ ] SFTP service: list, read, write, upload, download, mkdir, delete - [ ] Credential service: encrypted password + SSH key storage - [ ] All new services wired into Wails app - [ ] xterm.js terminal with WebGL rendering - [ ] SFTP file tree sidebar with upload/download - [ ] Host key verification dialog - [ ] Double-click connection to connect flow - [ ] CodeMirror 6 inline editor with SFTP save - [ ] Workspace snapshot persistence - [ ] All Go tests passing - [ ] Frontend builds clean