docs: fired XO audit — spec vs reality gap analysis

Full codebase audit against the 983-line design spec. Documents what
works, what's implemented but unwired, what's missing, bugs found and
fixed, unused dependencies, and recommended priority fix order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-17 13:26:03 -04:00
parent 05776b7eb6
commit b7742b0247

View File

@ -0,0 +1,221 @@
# Wraith Desktop — Fired XO Audit
> **Date:** 2026-03-17
> **Auditor:** VHQ XO (Claude Opus 4.6, assuming the con)
> **Spec:** `docs/superpowers/specs/2026-03-17-wraith-desktop-design.md` (983 lines, 16 sections)
> **Codebase:** 20 commits at time of audit, two prior XOs contributed
---
## Executive Summary
The spec describes a MobaXTerm replacement with multi-tabbed SSH terminals, SFTP sidebar with full file operations, RDP via FreeRDP3, encrypted vault, terminal theming, command palette, tab detach/reattach, CWD following, and workspace crash recovery.
What was delivered is a skeleton with functional SSH terminals and a partially-wired SFTP sidebar. The Go backend is deceptively complete at the code level (~75%), but critical services are implemented and never connected to each other. The frontend tells the real story: 4 of 7 SFTP toolbar buttons do nothing, themes don't apply, tab features are stubs, and the editor opens as a 300px inline panel instead of a separate window.
**User-facing functionality: ~40% of spec. Code-level completeness: ~65%.**
---
## Bugs Fixed During This Audit
### BUG-001: UTF-8 Terminal Rendering (v0.7.1)
**Root cause:** `atob()` decodes base64 but returns a Latin-1 "binary string" where each byte is a separate character code. Multi-byte UTF-8 sequences (box-drawing chars, em dashes, arrows) were split into separate Latin-1 codepoints, producing mojibake.
**Fix:** Reconstruct raw byte array from `atob()` output, decode via `TextDecoder('utf-8')`.
**File:** `frontend/src/composables/useTerminal.ts` line 117
### BUG-002: Split UTF-8 Across Chunk Boundaries (v0.7.2)
**Root cause:** The v0.7.1 fix created a `new TextDecoder()` on every Wails event. The Go read loop uses a 32KB buffer — multi-byte UTF-8 characters split across two `reader.Read()` calls produced two separate events. Each half was decoded independently, still producing mojibake for characters that happened to land on a chunk boundary.
**Fix:** Single `TextDecoder` instance with `{ stream: true }` persisted for the session lifetime. Buffers incomplete multi-byte sequences between events.
**File:** `frontend/src/composables/useTerminal.ts`
### BUG-003: Typewriter Lag (v0.7.2)
**Root cause:** Every Go `reader.Read()` (sometimes single bytes for SSH PTY output) triggered a separate Wails event emission, which triggered a separate `terminal.write()`. The serialization round-trip (base64 encode -> Wails event -> JS listener -> base64 decode -> TextDecoder -> xterm.js write) for every few bytes produced visible line-by-line lag.
**Fix:** `requestAnimationFrame` write batching. Incoming data accumulates in a string buffer and flushes to xterm.js once per animation frame (~16ms). All chunks within a frame render in a single write.
**File:** `frontend/src/composables/useTerminal.ts`
### BUG-004: PTY Baud Rate (v0.7.2)
**Root cause:** `TTY_OP_ISPEED` and `TTY_OP_OSPEED` set to 14400 (1995 modem speed). Some remote PTYs throttle output to match the declared baud rate.
**Fix:** Bumped to 115200.
**File:** `internal/ssh/service.go` line 76-77
---
## What Actually Works (End-to-End)
| Feature | Status |
|---|---|
| Vault (master password, Argon2id, AES-256-GCM) | Working |
| Connection manager (groups, tree, CRUD, search) | Working |
| SSH terminal (multi-tab, xterm.js, resize) | Working (fixed in v0.7.1/v0.7.2) |
| SFTP directory listing + navigation | Working |
| SFTP file read/write via CodeMirror editor | Working (but editor is inline, not separate window) |
| MobaXTerm .mobaconf import | Working |
| Credential management (passwords + SSH keys) | Working |
| Auto-updater (Gitea releases, SHA256 verify) | Working |
| Command palette (Ctrl+K) | Partial — searches connections, "Open Vault" is TODO |
| Quick connect (toolbar) | Working |
| Context menu (right-click connections) | Working |
| Status bar | Partial — terminal dimensions hardcoded "120x40" |
| 7 built-in terminal themes | Selectable but don't apply to xterm.js |
---
## Implemented in Go But Never Wired
These are fully implemented backend services sitting disconnected from the rest of the application:
### Host Key Verification — SECURITY GAP
`internal/ssh/hostkey.go``HostKeyStore` with `Verify()`, `Store()`, `Delete()`, `GetFingerprint()`. All real logic.
**Problem:** SSH service hardcodes `ssh.InsecureIgnoreHostKey()` at `service.go:58`. The `HostKeyStore` is never instantiated or called. Every connection silently accepts any server key. MITM attacks would succeed without warning.
**Frontend:** `HostKeyDialog.vue` exists with accept/reject UI and MITM warning text. Also never wired.
### CWD Tracking (SFTP "Follow Terminal Folder")
`internal/ssh/cwd.go` — Full OSC 7 escape sequence parser. `ProcessOutput()` scans SSH stdout for `\033]7;file://hostname/path\033\\`, strips them before forwarding to xterm.js, and emits CWD change events. Shell hook generator for bash/zsh/fish.
**Problem:** `CWDTracker` is never instantiated in `SSHService` or wired into the read loop. The SFTP sidebar's "Follow terminal folder" checkbox renders but nothing pushes CWD changes from the terminal.
### Session Manager
`internal/session/manager.go``Create()`, `Detach()`, `Reattach()`, `SetState()`, max 32 sessions enforcement. All real logic.
**Problem:** Instantiated in `app.go` but `Create()` is never called when SSH/RDP sessions open. The SSH service has its own internal session map. These two tracking systems are parallel and unconnected. Tab detach/reattach (which depends on the Session Manager) cannot work.
### Workspace Restore (Crash Recovery)
`internal/app/workspace.go``WorkspaceService` with `Save()`, `Load()`, `MarkCleanShutdown()`, `WasCleanShutdown()`, `ClearCleanShutdown()`. All real logic via settings persistence.
**Problem:** `WorkspaceService` is never instantiated in `app.go`. Dead code. Crash recovery does not exist.
### Plugin Registry
`internal/plugin/registry.go``RegisterProtocol()`, `RegisterImporter()`, `GetProtocol()`, `GetImporter()`. All real logic.
**Problem:** Instantiated in `app.go`, nothing is ever registered. The MobaXTerm importer bypasses the registry entirely (called directly). Empty scaffolding.
---
## Missing Features (Spec vs. Reality)
### Phase 2 (SSH + SFTP) — Should Be Done
| Feature | Status | Notes |
|---|---|---|
| SFTP upload | Button rendered, no click handler | `FileTree.vue` toolbar |
| SFTP download | Button rendered, no click handler | `FileTree.vue` toolbar |
| SFTP delete | Button rendered, no click handler | `FileTree.vue` toolbar |
| SFTP new folder | Button rendered, no click handler | `FileTree.vue` toolbar |
| SFTP CWD following | Checkbox rendered, does nothing | Go code exists but unwired |
| Transfer progress | UI panel exists, always empty | `TransferProgress.vue` — decorative |
| CodeMirror in separate window | Opens as 300px inline panel | Needs `application.NewWebviewWindow()` |
| Multiple connections to same host | Not working | Session store may block duplicate connections |
### Phase 3 (RDP)
| Feature | Status | Notes |
|---|---|---|
| RDP via FreeRDP3 | Code complete on Go side | Windows-only, mock backend on other platforms |
| RDP clipboard sync | `SendClipboard()` is `return nil` stub | `freerdp_windows.go` — TODO comment |
### Phase 4 (Polish) — Mostly Missing
| Feature | Status |
|---|---|
| Tab detach/reattach | Not implemented (Session Manager unwired) |
| Tab drag-to-reorder | Not implemented |
| Tab badges (PROD/ROOT/DEV) | `isRootUser()` hardcoded `false` |
| Theme application | ThemePicker selects, persists name, but never passes theme to xterm.js |
| Per-connection theme | Not implemented |
| Custom theme creation | Not implemented |
| Vault management UI | No standalone UI; keys/passwords created inline in ConnectionEditDialog |
| Host key management UI | `HostKeyDialog.vue` exists, not wired |
| Keyboard shortcuts | Only Ctrl+K works. No Ctrl+T/W/Tab/1-9/B/Shift+D/F11/F |
| Connection history | `connection_history` table not in migrations |
| First-run .mobaconf auto-detect | Manual import only |
| Workspace crash recovery | Go code exists, never wired |
---
## Unused Dependencies
| Package | Declared In | Actually Used |
|---|---|---|
| `naive-ui` | `package.json` | Zero components imported anywhere — entire UI is hand-rolled Tailwind |
| `@xterm/addon-webgl` | `package.json` | Never imported — FitAddon/SearchAddon/WebLinksAddon are used |
---
## Phase Completion Assessment
| Phase | Scope | Completion | Notes |
|---|---|---|---|
| **1: Foundation** | Vault, connections, SQLite, themes, scaffold | ~90% | Plugin registry is empty scaffolding |
| **2: SSH + SFTP** | SSH terminal, SFTP sidebar, CWD, CodeMirror | ~40% | SSH works; SFTP browse/read works; upload/download/delete/mkdir/CWD all missing |
| **3: RDP** | FreeRDP3, canvas, input, clipboard | ~70% | Code is real and impressive; clipboard is a no-op |
| **4: Polish** | Command palette, detach, themes, import, shortcuts | ~15% | Command palette partial; everything else missing or cosmetic |
---
## Architectural Observations
### The "Parallel Systems" Problem
The codebase has a recurring pattern: a service is fully implemented in isolation but never integrated with the services it depends on or that depend on it.
- `session.Manager` and `ssh.SSHService` both track sessions independently
- `HostKeyStore` and `SSHService.Connect()` both exist but aren't connected
- `CWDTracker` and the SFTP sidebar both exist but aren't connected
- `WorkspaceService` exists but nothing calls it
- `plugin.Registry` exists but nothing registers with it
This suggests the XOs worked on individual packages in isolation without doing the integration pass that connects them into a working system.
### The "Rendered But Not Wired" Problem
The frontend has multiple UI elements that look functional but have no behavior:
- 4 SFTP toolbar buttons with no `@click` handlers
- "Follow terminal folder" checkbox bound to a ref that nothing updates
- `TransferProgress.vue` that never receives transfer data
- `HostKeyDialog.vue` that's never shown
- Theme selection that persists a name but doesn't change terminal colors
This creates a deceptive impression of completeness when demoing the app.
### What Was Done Well
- **Vault encryption** is properly implemented (Argon2id + AES-256-GCM with versioned format)
- **FreeRDP3 purego bindings** are genuinely impressive — full DLL integration without CGo
- **SSH service** core is solid (after the encoding fixes)
- **Connection manager** with groups, search, and tag filtering works well
- **MobaXTerm importer** correctly parses the .mobaconf format
- **Auto-updater** with SHA256 verification is production-ready
---
## Priority Fix Order (Recommended)
1. **SFTP toolbar buttons** — wire upload/download/delete/mkdir. The Go `SFTPService` already has all these methods. Just needs `@click` handlers in `FileTree.vue`.
2. **Host key verification** — wire `HostKeyStore` into `SSHService.Connect()`. Security gap.
3. **CWD following** — wire `CWDTracker` into the SSH read loop. This is MobaXTerm's killer differentiator.
4. **Theme application** — pass selected theme to `terminal.options.theme`. One line of code.
5. **Session Manager integration** — call `sessionMgr.Create()` on connect. Prerequisite for tab detach.
6. **Editor in separate window** — requires Go-side `application.NewWebviewWindow()` + dedicated route.
7. **Keyboard shortcuts** — straightforward `handleKeydown` additions in `MainLayout.vue`.
8. **Workspace restore** — wire `WorkspaceService` into app lifecycle.