Adds the server/app half of a remote-diagnostics system so Claude (via a local
MCP gateway, added separately) can read a server's full state to diagnose
problems: logs, errors, queue/app state, redacted config, system health. All
read-only; security review folded in as hard requirements.
Architecture (design-verified):
- A SECOND, independent RemoteServer instance (diagnosticMode) on its own port
(default 9110, bind 127.0.0.1) with NO capture/input callbacks, so screen
capture and sendInputEvent are structurally unreachable from the diagnostic
path. The existing screen-share server (port 9100) is byte-for-byte unchanged.
- lib/remote-server.js: a `host` bind option, a post-auth `diag-request` branch
that delegates to onDiagnosticRequest and replies with a reqId-correlated
`diag-response`, a `diagnosticMode` guard (capture never spawns), a
timing-safe token compare (length-guarded), and getLastAccess().
- lib/diagnostics-agent.js: handle(op,args) enforces a HARDCODED read-only op
whitelist as the sole authority — no write/exec op, no run_health_check.
- lib/diagnostics-collectors.js: pure, dependency-injected collectors
(server_health one-shot hub, read_log, list_errors, get_queue_state,
get_history, get_config_redacted, get_system_info, list_logs, get_app_events,
get_rotation_state, get_health) reusing support-bundle + stats.
Security (all mandatory, implemented):
- Redaction gates every off-box payload. support-bundle now adds webhookUrl +
diagToken to CRED_KEYS, and exports redactLogText (value-scrub of live secret
strings + pattern-scrub of Discord webhooks / Bearer / api_key= / cookies /
sess ids) + valueScrub + collectSecretValues. read_log takes a logical NAME
(no path traversal); doodstream-debug.log is excluded from the readable set
(it logs live api-key-bearing HTML). grep is length-capped (ReDoS guard).
- diagnostics config subtree (enabled/port/token/label/codeIssuedAt/bindAddress)
defaults OFF, bind 127.0.0.1; deep-merge makes it migration-free. The
server-owned token is preserved against renderer clobber in both
save-global-settings handlers; the diagnostics:* IPC is the only mutator.
- app.requestSingleInstanceLock() (also the approved fix#6) so a relaunch can't
EADDRINUSE-kill the agent and two instances can't clobber the config.
main.js: buildDiagnosticCode (mhu1_ base64url, no host embedded), start/stop +
auto-start-on-launch + stop-on-quit, the four diagnostics:* IPC handlers.
renderer: a "Diagnose-Zugriff" settings subtab (toggle, port, bind-address,
copyable connection code + regenerate, status, read-only security note).
382 tests pass (incl. 11 new: collectors+redaction, agent whitelist, and a live
RemoteServer protocol test proving auth->diag-response correlation, that a
diagnostic client never triggers the capture window, and brute-force lockout).
Lint clean (0 errors). Smoke boots identically to baseline. The standalone MCP
gateway package + operator setup docs land in a follow-up commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The error handler was missing clearTimeout for the auth timeout timer
and didn't clean up authenticated client state (signaling disconnect,
destroying capture window when last client drops).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>