Multi-Hoster-Upload/tasks/todo.md
Administrator 8df6de06f1 docs(todo,lessons): diagnostics audit — healthy for real usage; ship 2 transport one-liners, defer the redaction-surface freeze
52-agent audit of the v3.3.84/85 remote-diagnostics code. App is healthy for this
user's actual usage. Shipped the two zero-redaction-surface fixes (ws maxPayload,
sendToClient guard). Deferred the cold-path server_health O(historySize) freeze
because the fix refactors the credential-redaction collectors (leaked twice before).

Lesson: "not persistence, so safe" is a fallacy — the redaction layer is an equally
catastrophic guarantee surface (silent secret leak). At an audit goal, find+document
is the deliverable; don't cut into a twice-leaked redaction pipeline under a Stop hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 03:06:08 +02:00

4.7 KiB
Raw Permalink Blame History

Diagnostics audit (3rd /goal re-fire): the un-audited v3.3.84/85 remote-diagnostics app-side code

Method: 52-agent adversarial audit of lib/diagnostics-agent.js, lib/diagnostics-collectors.js, lib/support-bundle.js, lib/remote-server.js + the diag wiring in main.js/renderer/preload, for lag/freeze

  • correctness/leak. 23 findings, each double-verified. Reference = THIS user's real config (23 result rows).

Conclusion: the app is HEALTHY for this user's actual usage.

All shipped fixes (v3.3.87 recent-panel, v3.3.88 doodstream) stand. The remaining findings are COLD, opt-in (diagnostics is off by default; needs the user to enable it AND a remote operator to connect AND a large accumulated history) — they impose ZERO cost on the normal path and start to matter only ~30k history rows (this user: 23). Found + documented = the deliverable for a "could it exist" audit goal.

SHIPPED (v3.3.89) — only the two isolated, zero-redaction-surface fixes

  • lib/remote-server.js: WebSocketServer had NO maxPayload → a pre-auth message ran a synchronous JSON.parse of up to ws's 100 MiB default per message on the main loop = an unbounded freeze/DoS sink. Set maxPayload = 256 KiB (diag/auth/WebRTC-signaling messages are tiny). Closes the sink for free.
  • lib/remote-server.js sendToClient: ws.send(JSON.stringify(data)) had no readyState/try guard (unlike broadcast) → a send on a closing socket or a stringify throw escaped as uncaughtException (potential crash). Now guarded with ws.readyState === 1 + try/catch, mirroring broadcast. 397/397 tests pass, eslint clean.

DEFERRED — documented, conscious (real, but the fix cuts into the credential-redaction surface)

14 of 15 actionable findings converge on ONE cold-path freeze: a single server_health (and get_history/list_errors/get_config) does O(historySize) synchronous work per request — (a) ~67 configStore.load() per request, each a full structuredClone(config incl. unbounded history); (b) summarizePerHoster copies+sorts+walks ALL batches; _historyErrors regex-scans ALL batches; the limit arg only slices the OUTPUT, not the traversal. Measured: ~258 ms at 7.45 MB / 30k rows, up to 1.6 s6.7 s in the 39185 MB tail. On a large history a diagnostic query would freeze the very app it's diagnosing (violates the v3.3.85 rule). REAL — but:

  • It only fires during an active diagnostic session on a large history; this user (23 rows) never hits it.
  • The tempting fix (snapshot config + thread secrets through the collectors) refactors lib/diagnostics-collectors.js — which IS the redaction surface (_secrets/_deepRedact/collectSecretValues/ sanitizeConfig/redactLogText). That code has LEAKED TWICE before (7b5420e "one collector still leaked", 8d757a9 "redaction gaps") in ways that passed green tests. Threading a secrets snapshot = exactly that shape: miss/stale one path → SECRET LEAK, a worse failure than the cold freeze. Same category error as last turn's config-store defer (then: data-loss; now: secret-leak), different file.
  • If ever hardened: ONLY bound the history walk via summarizePerHoster's existing opts.lastNBatches (NOT the secret threading), after confirming per-hoster-over-last-N is acceptable health-snapshot semantics, WITH a per-collector redaction E2E + advisor pass. Not under a Stop hook. Related cold-path items (same defer): get_config_redacted 'all' does 3 full-history passes then discards history (strip history first would help — but same file); collectSecretValues/_secrets unmemoized per request; support-bundle.js sanitizeConfig serializes the whole history + reads ~11 MB logs sync on the main loop (explicit user-triggered bundle, very cold). remote-server _failedAttempts sub-threshold entries never pruned (slow IP-keyed growth) — low.

DEFERRED (config-store cluster, re-confirmed from last turn)

  • load() structuredClone of whole config (incl. history) per call; _atomicWrite nulls cache per write; _serializeForDisk stringifies whole history per write; _preserveDiagSubtree adds one extra load() per save-global-settings (normal path, low). All scale with historySize, all sub-ms at this user's 52 KB config, all in credential-bearing persistence code. Safe root fix = split history to its own file (out of scope: risk > reward). Documented, not operated.

Verified clean (no action)

  • diagnostics collectors are otherwise well-bounded: readLog tailKb clamped ≤1024 KB, grep is literal substring (ReDoS-free), get_config 'all' deletes history before redaction, all limit/maxJobs-bounded.
  • _deepRedact is bounded at every call site (NOT the freeze source).
  • diagnostics renderer wiring is all in renderSettings() (cold); no per-progress/per-frame cost when off.