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:
parent
05776b7eb6
commit
b7742b0247
221
docs/audit/fired-xo-audit.md
Normal file
221
docs/audit/fired-xo-audit.md
Normal 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.
|
||||
Loading…
Reference in New Issue
Block a user