wraith/docs/audit/fired-xo-audit.md
Vantz Stockwell b7742b0247 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>
2026-03-17 13:26:03 -04:00

11 KiB

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.goHostKeyStore 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.goCreate(), 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.goWorkspaceService 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.goRegisterProtocol(), 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

  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.