From 30f6126655d68beb76d6aaedd852da6a61299f46 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Thu, 2 Jul 2026 13:23:09 +0300 Subject: [PATCH] =?UTF-8?q?docs(roadmap):=202026-07=20replan=20=E2=80=94?= =?UTF-8?q?=20specs=20078/079,=20telemetry-corrected=20priorities,=20audit?= =?UTF-8?q?=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multi-agent consistency audit (2026-07-02) found roadmap.yaml stale versus merged PRs and carrying several false progress badges from wrong spec links. Corrected CI-filtered telemetry also re-prioritized the personal-edition work. Statuses corrected per merged PRs: - scanner-simplification children: US1 #786 / US2 #792 / US4 #794 marked done; US3 #793 in_review. Epic stays in_progress. Added deep-scan trust-fix task and flagged docs T037-T039 as merge-blocking for #793. - registries-official-protocol marked done (spec 071 shipped 12/12, #572). False badges / wrong provenance fixed (per the file's own convention — link dropped, provenance moved into the note): - sandbox-isolation no longer links spec 054 (unrelated security-gateway spec). - ux-audit no longer links spec 064 (unrelated agent-fleet cockpit spec). - marketplace no longer links spec 070 (that is the registries-search-add spec). - action-log-transparency no longer links spec 024 (shipped backend, not the progress driver for the at-a-glance UX epic). New epics (telemetry- and audit-driven replan): - upgrade-nudge (P0, spec 079): ~60% of active installs run pre-v0.40. - connect-trust (P0, spec 078): 72.4% skip the connect step. - telemetry-identity (P1, in_progress): hashed machine_id + CI-filter hardening. - planning-hygiene (P2): automate the checks this audit did by hand. Windows QA gate: new first child windows-tray-funnel-qa (downloads→actives 12:1 vs macOS 4:1); windows-tray-window now depends on it. ROADMAP.md regenerated; gen-roadmap.py --check passes. Co-Authored-By: Claude Fable 5 --- ROADMAP.md | 57 +++- roadmap.yaml | 143 ++++++++- specs/078-connect-trust-preview/spec.md | 382 ++++++++++++++++++++++++ specs/079-upgrade-nudge/spec.md | 207 +++++++++++++ 4 files changed, 765 insertions(+), 24 deletions(-) create mode 100644 specs/078-connect-trust-preview/spec.md create mode 100644 specs/079-upgrade-nudge/spec.md diff --git a/ROADMAP.md b/ROADMAP.md index 9359dc03..7e053353 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -58,6 +58,7 @@ graph TD end subgraph sg_windows_tray["Windows native tray app"] windows_tray["Windows native tray app
MCP-43"] + windows_tray_funnel_qa["Windows first-run QA pass (downloads→actives 12:1 vs macOS 4:1 — find the funnel break before WebView2 work)"] windows_tray_window["WebView2 native window + profile submenu
MCP-43"] end subgraph sg_ux_audit["Web UI + macOS app UX audit"] @@ -86,6 +87,34 @@ graph TD scanner_simpl_unified_report["US2: single merged report + cross-scanner consensus confidence"] scanner_simpl_deep_optin["US3: opt-in deep scan (off by default), never blocks/degrades baseline; config migration"] scanner_simpl_notifications["US4: collapse scan-notification storm into one debounced settled event (MCP-2207)"] + scanner_simpl_deepscan_fixes["Deep-scan trust fixes: nil-Security gating bug (source fetch runs with deep scan off on default configs), FR-014 verdict inversion (Dangerous deep finding < Warning), surface silently-skipped Docker scanners (non-nil deep_scan descriptor + CLI hint on security enable)"] + end + subgraph sg_upgrade_nudge["Upgrade awareness & guided update"] + upgrade_nudge["Upgrade awareness & guided update"] + upgrade_nudge_surfacing["US1: universal awareness — status output, startup log, dismissible Web UI banner, update_check config block"] + upgrade_nudge_channel["US2: channel-aware guided update command (brew/dmg/deb/rpm/docker/go-install detection, build-time channel marker)"] + upgrade_nudge_quiet["US3: operator control + CI/offline quiet + no prerelease downgrade nudges"] + end + subgraph sg_connect_trust["Connect step trust: preview, visible backup, one-click undo"] + connect_trust["Connect step trust: preview, visible backup, one-click undo"] + connect_trust_preview["US1: preview API + wizard diff UI (exact entry, API-key masking)"] + connect_trust_backup_visibility["US1: surface backup_path in Web UI + retention policy"] + connect_trust_undo["US2: one-click undo/disconnect in wizard"] + connect_trust_tcc_copy["US2: pre-emptive macOS TCC explanation in wizard"] + end + subgraph sg_telemetry_identity["Telemetry identity & data quality (machine_id + CI-filter hardening)"] + telemetry_identity["Telemetry identity & data quality (machine_id + CI-filter hardening)"] + telemetry_machineid_client["Hashed machine_id in heartbeat (schema v6)"] + telemetry_machineid_worker["Worker migration: machine_id column + extraction (repo mcpproxy-telemetry)"] + telemetry_machineid_dash["Dashboard identityExpr prefers machine_id; exclude %-dev versions from human cohort; fix launch_source 79% unknown (repo mcpproxy-dash)"] + telemetry_snapshot_alerting["Alerting on external-downloads snapshot cron (34-day outage went unnoticed)"] + end + subgraph sg_planning_hygiene["Planning/docs truth automation"] + planning_hygiene["Planning/docs truth automation"] + hygiene_roadmap_github_check["gen-roadmap --check-github: cross-check roadmap.yaml statuses vs gh PR state + dangling spec links"] + hygiene_tasks_reconcile["CI rule: PR touching specs/ implementation paths must update tasks.md"] + hygiene_docs_facts["Generate volatile CLAUDE.md/README facts (Go version, built-in tool list, sample config) from code with --check"] + hygiene_quickstart_contract["Run top quickstart.md scenario per spec as contract test in test-api-e2e.sh"] end marketplace["Server marketplace
MCP-37"] siem["Audit SIEM integration
MCP-39"] @@ -107,6 +136,7 @@ graph TD scanner_v2_soft_checks --> scanner_v2_consensus scanner_v2_hard_checks --> scanner_v2_eval_gate scanner_v2_eval_gate --> scanner_v2_docs + windows_tray_funnel_qa --> windows_tray_window ux_audit --> action_log_transparency action_log_glance_view --> action_log_retention_tie_in ux_audit --> analytics_dashboard @@ -117,6 +147,11 @@ graph TD scanner_simpl_baseline --> scanner_simpl_deep_optin scanner_simpl_unified_report --> scanner_simpl_deep_optin scanner_simpl_unified_report --> scanner_simpl_notifications + scanner_simpl_deep_optin --> scanner_simpl_deepscan_fixes + upgrade_nudge_surfacing --> upgrade_nudge_channel + upgrade_nudge_surfacing --> upgrade_nudge_quiet + telemetry_machineid_client --> telemetry_machineid_worker + telemetry_machineid_worker --> telemetry_machineid_dash classDef done fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff; classDef in_progress fill:#1f6feb,stroke:#0b3d91,color:#ffffff; @@ -124,10 +159,10 @@ graph TD classDef blocked fill:#a40e26,stroke:#5c0712,color:#ffffff; classDef todo fill:#6e7781,stroke:#3d4248,color:#ffffff; classDef parked fill:#30363d,stroke:#161b22,color:#9da7b3,stroke-dasharray:4 3; - class profiles_v2,profiles_v2_indexes,profiles_v2_set_profile,profiles_v2_profile_pin,profiles_v2_tray_switcher,sandbox_isolation,sandbox_spike,sandbox_mode_config,sandbox_launcher,sandbox_scanner_parity,sandbox_snap_docker_it,ts_code_exec_ga,ts_code_exec_cookbook,scanner_v2,scanner_v2_foundation,scanner_v2_hard_checks,scanner_v2_soft_checks,scanner_v2_consensus,scanner_v2_eval_gate,scanner_v2_docs done; - class scanner_simplification in_progress; - class windows_tray,windows_tray_window in_review; - class ux_audit,ux_audit_webui_sweep,ux_audit_macos_sweep,action_log_transparency,action_log_glance_view,action_log_retention_tie_in,analytics_dashboard,analytics_token_drain_graphs,analytics_default_landing,registries_search_add,registries_search_ux,registries_official_protocol,scanner_simpl_baseline,scanner_simpl_unified_report,scanner_simpl_deep_optin,scanner_simpl_notifications todo; + class profiles_v2,profiles_v2_indexes,profiles_v2_set_profile,profiles_v2_profile_pin,profiles_v2_tray_switcher,sandbox_isolation,sandbox_spike,sandbox_mode_config,sandbox_launcher,sandbox_scanner_parity,sandbox_snap_docker_it,ts_code_exec_ga,ts_code_exec_cookbook,scanner_v2,scanner_v2_foundation,scanner_v2_hard_checks,scanner_v2_soft_checks,scanner_v2_consensus,scanner_v2_eval_gate,scanner_v2_docs,registries_official_protocol,scanner_simpl_baseline,scanner_simpl_unified_report,scanner_simpl_notifications done; + class scanner_simplification,telemetry_identity in_progress; + class windows_tray,windows_tray_window,scanner_simpl_deep_optin,telemetry_machineid_client in_review; + class windows_tray_funnel_qa,ux_audit,ux_audit_webui_sweep,ux_audit_macos_sweep,action_log_transparency,action_log_glance_view,action_log_retention_tie_in,analytics_dashboard,analytics_token_drain_graphs,analytics_default_landing,registries_search_add,registries_search_ux,scanner_simpl_deepscan_fixes,upgrade_nudge,upgrade_nudge_surfacing,upgrade_nudge_channel,upgrade_nudge_quiet,connect_trust,connect_trust_preview,connect_trust_backup_visibility,connect_trust_undo,connect_trust_tcc_copy,telemetry_machineid_worker,telemetry_machineid_dash,telemetry_snapshot_alerting,planning_hygiene,hygiene_roadmap_github_check,hygiene_tasks_reconcile,hygiene_docs_facts,hygiene_quickstart_contract todo; class marketplace,siem,paid_tier,sdk_v1_migration,sso parked; ``` @@ -136,18 +171,22 @@ graph TD | Epic | Status | Assignee | Priority | Progress | Spec | PR | | --- | --- | --- | --- | --- | --- | --- | | Scanner simplification (deterministic default, opt-in deep scan) | In progress | unassigned | P1 | 0/42 (0%) | [077-scanner-simplification](./specs/077-scanner-simplification/) | | +| Telemetry identity & data quality (machine_id + CI-filter hardening) | In progress | unassigned | P1 | — | | | | Windows native tray app `MCP-43` | In review | BackendEngineer | P2 | 25/60 (42%) | [002-windows-installer](./specs/002-windows-installer/) | | -| Web UI + macOS app UX audit | Todo | unassigned | P0 | — | [064-glass-cockpit](./specs/064-glass-cockpit/) | | -| Action log / transparency — info at a glance | Todo | unassigned | P0 | 63/66 (95%) | [024-expand-activity-log](./specs/024-expand-activity-log/) | | +| Web UI + macOS app UX audit | Todo | unassigned | P0 | — | | | +| Action log / transparency — info at a glance | Todo | unassigned | P0 | — | | | +| Upgrade awareness & guided update | Todo | unassigned | P0 | — | [079-upgrade-nudge](./specs/079-upgrade-nudge/) | | +| Connect step trust: preview, visible backup, one-click undo | Todo | unassigned | P0 | — | [078-connect-trust-preview](./specs/078-connect-trust-preview/) | | | Analytics dashboard as default page | Todo | unassigned | P1 | 16/26 (62%) | [069-observability-usage-graphs](./specs/069-observability-usage-graphs/) | | | Registries — easier search + add-server | Todo | unassigned | P1 | 3/24 (12%) | [070-registry-easy-upstream-add](./specs/070-registry-easy-upstream-add/) | | -| Server marketplace `MCP-37` | Todo (parked) | | P3 | 3/24 (12%) | [070-registry-easy-upstream-add](./specs/070-registry-easy-upstream-add/) | | +| Planning/docs truth automation | Todo | unassigned | P2 | — | | | +| Server marketplace `MCP-37` | Todo (parked) | | P3 | — | | | | Audit SIEM integration `MCP-39` | Todo (parked) | | P3 | — | | | | Paid-tier MVP (billing / seats / license) `MCP-40` | Todo (parked) | | P3 | — | | | | SDK v1 migration | Todo (parked) | | P3 | — | | | | SSO (server edition) | Todo (parked) | | P3 | — | | | | Profiles v2 (per-profile tool views) `MCP-33` | Done | BackendEngineer | P1 | — | | | -| Non-Docker sandbox isolation (Landlock) `MCP-34` | Done | BackendEngineer | P1 | — | [054-mcp-security-gateway](./specs/054-mcp-security-gateway/) | | +| Non-Docker sandbox isolation (Landlock) `MCP-34` | Done | BackendEngineer | P1 | — | | | | Spec 076 deterministic offline tool-scanner `MCP-3574` | Done | BackendEngineer | P1 | 22/24 (92%) | [076-deterministic-tool-scanner](./specs/076-deterministic-tool-scanner/) | | | TypeScript code-execution GA + cookbook `MCP-38` | Done | BackendEngineer | P2 | 19/19 (100%) | [033-typescript-code-execution](./specs/033-typescript-code-execution/) | | @@ -233,3 +272,5 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` | [075-macos-tcc-connect](./specs/075-macos-tcc-connect/) | `in-flight` | 11/30 (37%) | | [076-deterministic-tool-scanner](./specs/076-deterministic-tool-scanner/) | `in-flight` | 22/24 (92%) | | [077-scanner-simplification](./specs/077-scanner-simplification/) | `drafted` | 0/42 (0%) | +| [078-connect-trust-preview](./specs/078-connect-trust-preview/) | — | — | +| [079-upgrade-nudge](./specs/079-upgrade-nudge/) | — | — | diff --git a/roadmap.yaml b/roadmap.yaml index a5550ba9..0c2ce560 100644 --- a/roadmap.yaml +++ b/roadmap.yaml @@ -90,9 +90,8 @@ epics: assignee: BackendEngineer priority: P1 mcp: MCP-34 - spec: specs/054-mcp-security-gateway depends_on: [] - note: "Landlock LSM + setrlimit native sandbox for stdio upstreams; no userns (Ubuntu 24.04 safe). Code in internal/sandbox/." + note: "Landlock LSM + setrlimit native sandbox for stdio upstreams; no userns (Ubuntu 24.04 safe). Originated from roadmap item #11 (no dedicated spec — 054 is the unrelated security-gateway spec). Code in internal/sandbox/; PRs #754/#759/#768/#781/#782." tasks: - id: sandbox-spike title: Landlock sandbox spike (MCP-34.1) @@ -198,13 +197,18 @@ epics: mcp: MCP-43 spec: specs/002-windows-installer depends_on: [] - note: "Option C: WebView2 window reusing shipped Web UI. Most exit criteria already ship; gaps = native window, toasts, profile submenu, Win11 smoke." + note: "Option C: WebView2 window reusing shipped Web UI. Most exit criteria already ship; gaps = native window, toasts, profile submenu, Win11 smoke. Telemetry: Windows = ~23% of GitHub downloads but only ~4% of active installs (downloads→actives ~12:1 vs macOS ~4:1) — gate WebView2 work on finding the funnel break first." tasks: + - id: windows-tray-funnel-qa + title: "Windows first-run QA pass (downloads→actives 12:1 vs macOS 4:1 — find the funnel break before WebView2 work)" + status: todo + priority: P2 + depends_on: [] - id: windows-tray-window title: WebView2 native window + profile submenu status: in_review mcp: MCP-43 - depends_on: [] + depends_on: [windows-tray-funnel-qa] # ── BACKLOG: personal-edition polish (NEW priorities) ───────────────────── - id: ux-audit @@ -212,9 +216,8 @@ epics: status: todo assignee: unassigned priority: P0 - spec: specs/064-glass-cockpit depends_on: [] - note: "End-to-end UX pass across Web UI and the macOS tray app; the umbrella for the polish push." + note: "End-to-end UX pass across Web UI and the macOS tray app; the umbrella for the polish push. (No spec yet — 064 is the unrelated agent-fleet glass-cockpit spec.)" tasks: - id: ux-audit-webui-sweep title: Web UI heuristic + Playwright UX sweep @@ -231,9 +234,8 @@ epics: status: todo assignee: unassigned priority: P0 - spec: specs/024-expand-activity-log depends_on: [ux-audit] - note: "Surface the most important activity/security/connection signals at a glance; reduce digging. Builds on activity-log backend + retention." + note: "Surface the most important activity/security/connection signals at a glance; reduce digging. Builds on the shipped activity-log backend + retention (spec 024, 95% shipped — this epic is the at-a-glance UX on top, not the backend, so 024 is not the progress driver)." tasks: - id: action-log-glance-view title: At-a-glance action log view (top signals, health) @@ -282,8 +284,9 @@ epics: depends_on: [] - id: registries-official-protocol title: Official registry protocol integration - status: todo + status: done spec: specs/071-official-registry-protocol + note: "Spec 071 shipped 12/12 (official MCP registry v0.1 protocol adopted, #572)." depends_on: [] - id: scanner-simplification @@ -293,24 +296,133 @@ epics: priority: P1 spec: specs/077-scanner-simplification depends_on: [scanner-v2] - note: "Make the Spec 076 detect engine the always-on offline default; demote Docker scanners + source extraction to opt-in deep scan that never blocks/degrades the baseline; single unified report. Spec drafted (branch 077-scanner-simplification); plan next. First of the 5 personal-edition polish verticals." + note: "Make the Spec 076 detect engine the always-on offline default; demote Docker scanners + source extraction to opt-in deep scan that never blocks/degrades the baseline; single unified report. US1/US2/US4 merged; US3 (deep-scan opt-in) in review as #793. Docs tasks T037-T039 treated as merge-blocking for PR #793 (docs still describe deleted legacy tpaRules / six-checks / auto_scan_quarantined). First of the 5 personal-edition polish verticals." tasks: - id: scanner-simpl-baseline title: "US1: deterministic offline baseline default + curated hard phrase_injection check (delete duplicate legacy rules)" - status: todo + status: done + pr: "#786" depends_on: [] - id: scanner-simpl-unified-report title: "US2: single merged report + cross-scanner consensus confidence" - status: todo + status: done + pr: "#792" depends_on: [scanner-simpl-baseline] - id: scanner-simpl-deep-optin title: "US3: opt-in deep scan (off by default), never blocks/degrades baseline; config migration" - status: todo + status: in_review + pr: "#793" depends_on: [scanner-simpl-baseline, scanner-simpl-unified-report] - id: scanner-simpl-notifications title: "US4: collapse scan-notification storm into one debounced settled event (MCP-2207)" - status: todo + status: done + pr: "#794" depends_on: [scanner-simpl-unified-report] + - id: scanner-simpl-deepscan-fixes + title: "Deep-scan trust fixes: nil-Security gating bug (source fetch runs with deep scan off on default configs), FR-014 verdict inversion (Dangerous deep finding < Warning), surface silently-skipped Docker scanners (non-nil deep_scan descriptor + CLI hint on security enable)" + status: todo + priority: P1 + depends_on: [scanner-simpl-deep-optin] + + # ── TELEMETRY-DRIVEN REPLAN (2026-07-02) ────────────────────────────────── + - id: upgrade-nudge + title: Upgrade awareness & guided update + status: todo + assignee: unassigned + priority: P0 + spec: specs/079-upgrade-nudge + depends_on: [] + note: "Corrected CI-filtered telemetry (2026-07-02): ~60% of last-14d active installs run pre-v0.40; latest stable v0.46.0 only 18.7%. Turn the existing internal/updatecheck background poll into a universal, non-intrusive, channel-aware upgrade nudge across every surface. Never blocks/modals; silent offline/CI." + tasks: + - id: upgrade-nudge-surfacing + title: "US1: universal awareness — status output, startup log, dismissible Web UI banner, update_check config block" + status: todo + depends_on: [] + - id: upgrade-nudge-channel + title: "US2: channel-aware guided update command (brew/dmg/deb/rpm/docker/go-install detection, build-time channel marker)" + status: todo + depends_on: [upgrade-nudge-surfacing] + - id: upgrade-nudge-quiet + title: "US3: operator control + CI/offline quiet + no prerelease downgrade nudges" + status: todo + depends_on: [upgrade-nudge-surfacing] + + - id: connect-trust + title: "Connect step trust: preview, visible backup, one-click undo" + status: todo + assignee: unassigned + priority: P0 + spec: specs/078-connect-trust-preview + depends_on: [] + note: "72.4% of wizard-engaged users skip the connect step; completers retain ~50% at two weeks vs 6% for non-engaged. Backups already exist (internal/connect/backup.go) but are invisible in the Web UI. Close the trust gap: preview the exact config diff, surface the backup, offer one-click undo, explain the macOS TCC prompt." + tasks: + - id: connect-trust-preview + title: "US1: preview API + wizard diff UI (exact entry, API-key masking)" + status: todo + depends_on: [] + - id: connect-trust-backup-visibility + title: "US1: surface backup_path in Web UI + retention policy" + status: todo + depends_on: [] + - id: connect-trust-undo + title: "US2: one-click undo/disconnect in wizard" + status: todo + depends_on: [] + - id: connect-trust-tcc-copy + title: "US2: pre-emptive macOS TCC explanation in wizard" + status: todo + depends_on: [] + + - id: telemetry-identity + title: "Telemetry identity & data quality (machine_id + CI-filter hardening)" + status: in_progress + assignee: unassigned + priority: P1 + depends_on: [] + note: "Heartbeat lacks a stable identity, so active-install counts are inflated by CI/dev churn and launch_source is 79% unknown. Add a hashed machine_id (schema v6) and harden the worker/dashboard cohort filters. Spans repos mcpproxy-go (client), mcpproxy-telemetry (worker), mcpproxy-dash (dashboard)." + tasks: + - id: telemetry-machineid-client + title: "Hashed machine_id in heartbeat (schema v6)" + status: in_review + pr: "https://github.com/smart-mcp-proxy/mcpproxy-go/pull/796" + depends_on: [] + - id: telemetry-machineid-worker + title: "Worker migration: machine_id column + extraction (repo mcpproxy-telemetry)" + status: todo + depends_on: [telemetry-machineid-client] + - id: telemetry-machineid-dash + title: "Dashboard identityExpr prefers machine_id; exclude %-dev versions from human cohort; fix launch_source 79% unknown (repo mcpproxy-dash)" + status: todo + depends_on: [telemetry-machineid-worker] + - id: telemetry-snapshot-alerting + title: "Alerting on external-downloads snapshot cron (34-day outage went unnoticed)" + status: todo + depends_on: [] + + - id: planning-hygiene + title: Planning/docs truth automation + status: todo + assignee: unassigned + priority: P2 + depends_on: [] + note: "Automate the consistency checks this very audit had to do by hand: roadmap vs GitHub PR state, tasks.md updates on implementation PRs, volatile CLAUDE.md/README facts, and quickstart contract tests." + tasks: + - id: hygiene-roadmap-github-check + title: "gen-roadmap --check-github: cross-check roadmap.yaml statuses vs gh PR state + dangling spec links" + status: todo + depends_on: [] + - id: hygiene-tasks-reconcile + title: "CI rule: PR touching specs/ implementation paths must update tasks.md" + status: todo + depends_on: [] + - id: hygiene-docs-facts + title: "Generate volatile CLAUDE.md/README facts (Go version, built-in tool list, sample config) from code with --check" + status: todo + depends_on: [] + - id: hygiene-quickstart-contract + title: "Run top quickstart.md scenario per spec as contract test in test-api-e2e.sh" + status: todo + depends_on: [] # ── PARKED epics (intentionally on hold) ────────────────────────────────── - id: marketplace @@ -319,9 +431,8 @@ epics: parked: true priority: P3 mcp: MCP-37 - spec: specs/070-registry-easy-upstream-add depends_on: [] - note: "PARKED. ~60% already ships (browse/search/one-click add). Remaining = tray entries, metadata, telemetry." + note: "PARKED. ~60% already ships (browse/search/one-click add). No spec yet; gaps tracked as MCP-3246..3250 (tray entries, metadata, telemetry). (070 is the registries-search-add spec, not a marketplace spec.)" - id: siem title: Audit SIEM integration diff --git a/specs/078-connect-trust-preview/spec.md b/specs/078-connect-trust-preview/spec.md new file mode 100644 index 00000000..05ffa526 --- /dev/null +++ b/specs/078-connect-trust-preview/spec.md @@ -0,0 +1,382 @@ +# Feature Specification: Connect step trust — preview, backup visibility, and one-click undo + +**Feature Branch**: `078-connect-trust-preview` +**Created**: 2026-07-02 +**Status**: Draft +**Input**: User description: "Make the onboarding Connect step trustworthy — show the exact change that will be written to each AI client's config before writing it, always create and visibly surface a timestamped backup, offer a one-click undo that reverts the change, and explain in plain language (including the macOS App-Data prompt) what mcpproxy is about to touch — so users stop skipping the highest-drop step in the onboarding funnel." + +## Overview + +The onboarding wizard's "Clients" step — where mcpproxy registers itself as an MCP +server inside the user's AI client config files (`~/.claude.json`, Claude Desktop +config, Cursor/VS Code settings, Codex `config.toml`, Gemini, OpenCode) — is the +biggest drop in the setup funnel. Telemetry (CI-filtered + deduped, 2026-07-02): +of wizard-engaged users, **72.4% skip** the connect step and only **13.8% +complete** it, yet completing it correlates with ~50% two-week retention versus +6% for non-engaged users. The overall funnel loses 28% between first-run (1,237) +and client-connected (854). + +The working hypothesis is a **trust gap**: users are asked to let mcpproxy modify +config files that belong to other apps, but the step does not show *what* will be +written, does not show a diff, and does not make the automatic backup visible. +On macOS the write path can also trigger a "wants to access data from other apps" +privacy prompt with no forewarning inside the wizard. + +This feature closes the trust gap at the exact place the user hesitates: preview +the change before writing, surface the backup path after writing, offer a visible +one-click undo, and use plain-language copy (including a pre-emptive macOS +permission explanation) so the user understands precisely what mcpproxy touches +and that nothing else in the file changes. + +### Context (current behavior, verified) + +- **Backups already exist but are invisible in the UI.** Every connect and + disconnect writes a timestamped backup before modifying the file + (`internal/connect/backup.go:18`, named `.bak.`, + same directory, same file mode), returned as `ConnectResult.BackupPath` + (`internal/connect/connect.go:31`, JSON `backup_path`). The **CLI** prints it + (`cmd/mcpproxy/connect_cmd.go:255-256`), but the **Web UI does not**: both + `ConnectModal.vue` and `OnboardingWizard.vue` render only + `ConnectResult.Message` (the generic "MCPProxy registered in X as + mcpproxy") and never show `backup_path`. There is **no retention/cleanup** of + backups anywhere — they accumulate one-per-operation indefinitely and this is + undocumented. +- **There is no preview/diff.** No API returns the exact JSON/TOML that *will* + be written before the user confirms. The server entry is built server-side + (`buildServerEntry` at `internal/connect/clients.go:185`, per-client shape: + `{type:"http",url}` for claude-code/vscode, `{command:"npx",args:[...]}` bridge + for Claude Desktop, `{url,type:"sse"}` for Cursor, etc.) and written + immediately on `POST /api/v1/connect/{client}`. The user never sees the entry, + the endpoint URL (which may embed `?apikey=`), or which key is added. +- **macOS TCC handling is reactive, not pre-emptive (Spec 075).** + `GET /api/v1/connect` is stat-only and never prompts; per-client reads / + connect / disconnect resolve `access_state` and a denial surfaces as `403` + + remediation and an in-band `access_state="denied"` banner in `ConnectModal.vue` + (denied-banner with copy-`tccutil` + re-check). But this only appears **after** + the prompt has fired and been denied. The only pre-emptive hint anywhere is a + tooltip on the ConnectModal "Check access" button ("may prompt on macOS", + `ConnectModal.vue:88`). The **wizard** connect rows (`ClientRow` in + `OnboardingWizard.vue:1114`) have **no** access-state handling and **no** TCC + forewarning at all. +- **Disconnect is surgical but not surfaced as undo, and does not restore.** + `Disconnect` deletes only the mcpproxy entry (`delete(serversMap, serverName)`, + `internal/connect/connect.go:475,628`) and re-writes; it takes its own backup + first but does **not** restore the pre-connect file. If Connect used + `force=true` to overwrite a pre-existing entry, disconnect cannot bring the + overwritten entry back. In the wizard, a connected client shows only a static + "Connected" badge with **no** disconnect/undo control (`OnboardingWizard.vue` + ClientRow); disconnect exists only in the separate `ConnectModal.vue`. +- The onboarding funnel steps and skip/complete telemetry are recorded by + `onboarding.markConnectCompleted()` / `markConnectSkipped()` + (`OnboardingWizard.vue:1035,1088`), the metrics this feature's success criteria + are measured against. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - See the exact change before it is written (Priority: P1) + +A user on the wizard's Clients step is asked to connect an AI client. Before any +file is modified, they want to see precisely what mcpproxy will add: which file, +which key, and the exact entry (server name, endpoint URL, how the API key is +handled) — and confirm that nothing else in the file changes. + +**Why this priority**: The absence of a visible change is the core of the trust +gap and the most direct lever on the 72.4% skip rate. A user who can see exactly +what will happen is far more likely to proceed than one asked to authorize a +blind edit to their AI client's config. + +**Independent Test**: Request a connect preview for a client and confirm the +response returns the exact entry that a subsequent connect would write (same key, +same shape as `buildServerEntry`), scoped to that one file, without modifying the +file or creating a backup. + +**Acceptance Scenarios**: + +1. **Given** an installed, accessible client, **When** the user opens the connect + step for it, **Then** a preview shows the target config path, the key that will + be added, and the exact entry contents to be written, and the file is not + modified. +2. **Given** the client config already contains other MCP servers, **When** the + preview is shown, **Then** it makes clear that only the mcpproxy entry is + added/updated and all existing entries are left untouched. +3. **Given** the endpoint URL embeds an API key, **When** the preview is shown, + **Then** the API-key handling is represented honestly (e.g. shown or clearly + masked) so the user knows a credential is being written. +4. **Given** the user confirms after previewing, **When** connect runs, **Then** + the file written matches the previewed entry exactly. + +--- + +### User Story 2 - Always-created backup is visible and understood (Priority: P1) + +After connecting, the user wants to see that a backup was made and exactly where +it is, so they know the operation is reversible outside the app too. + +**Why this priority**: The backup already exists but is invisible in the UI, +wasting the single strongest "you can undo this" signal. Surfacing it is low-cost +and directly reinforces trust at the moment of the edit. + +**Independent Test**: Perform a connect via the Web UI and confirm the surfaced +result includes the backup path returned by the API; perform it against a +non-existent config (bridge client) and confirm the "no prior file to back up" +case is represented correctly. + +**Acceptance Scenarios**: + +1. **Given** a successful connect that modified an existing config, **When** the + result is shown in the Web UI (wizard and standalone modal), **Then** the + timestamped backup path is displayed to the user. +2. **Given** a connect that created a new config file (no prior file existed), + **When** the result is shown, **Then** the UI states that there was no prior + file to back up (rather than showing an empty/blank backup path). +3. **Given** repeated connect/disconnect operations, **When** the user reviews + the documentation, **Then** the backup naming, location, accumulation, and + retention behavior are documented. + +--- + +### User Story 3 - One-click undo that reverts the change (Priority: P2) + +After connecting, the user wants a single, visible action in the same place that +reverts what mcpproxy did — ideally restoring the file to its pre-connect state — +with the change shown again before it is undone. + +**Why this priority**: A visible, trustworthy undo makes the connect decision +low-stakes ("I can take this back in one click"), which lowers the barrier to +trying it. Today the wizard offers no undo control at all and disconnect does not +restore the pre-connect file. + +**Independent Test**: Connect a client, then invoke undo from the same surface; +confirm the mcpproxy entry is removed (or the pre-connect file restored) and the +client returns to a not-connected state, with a preview of what will be reverted +shown beforehand. + +**Acceptance Scenarios**: + +1. **Given** a client mcpproxy just connected, **When** the user triggers undo + from the same connect surface (wizard row and standalone modal), **Then** the + mcpproxy entry is removed and the client is reported not-connected. +2. **Given** connect overwrote a pre-existing entry of the same name, **When** + undo runs, **Then** the pre-connect state is restored from the backup rather + than leaving the overwritten entry lost. +3. **Given** the user requests undo, **When** the revert is about to run, **Then** + the change to be reverted is shown (the same preview surface) before it is + applied. +4. **Given** other MCP servers exist in the config, **When** undo runs, **Then** + only mcpproxy's contribution is reverted and all other entries remain intact. + +--- + +### User Story 4 - Plain-language trust copy, including the macOS prompt (Priority: P2) + +A user reading the connect step wants a plain-language explanation of what will +happen — which file is touched, what is added, that nothing else changes, where +the backup goes — and, on macOS, an explanation of *why* the OS permission prompt +is about to appear, shown **before** it fires. + +**Why this priority**: Even with a preview, users need the "why should I trust +this" narrative in words, and macOS users need the privacy prompt de-mystified +before it appears — an unexplained "wants to access data from other apps" dialog +is itself a reason to abandon the step. + +**Independent Test**: Load the wizard connect step and confirm the trust copy is +present and names the file, the addition, the no-other-changes guarantee, and the +backup location; on macOS, confirm the App-Data prompt is explained before the +first read/write that could trigger it. + +**Acceptance Scenarios**: + +1. **Given** the wizard connect step, **When** it is shown, **Then** it states in + plain language which file will be modified, what entry will be added, that + nothing else in the file changes, and that a backup is created. +2. **Given** a macOS user about to connect (or check access on) a client whose + config lives under another app's protected data, **When** the action is + offered, **Then** the wizard explains why macOS may show a privacy prompt and + what to choose, **before** the prompt fires. +3. **Given** a macOS user who has already been denied (Spec 075 `denied` state), + **When** the step is shown, **Then** the existing remediation surface is + preserved (this feature does not regress the Spec 075 denial banner). + +--- + +### Edge Cases + +- The client config already contains an entry named `mcpproxy` that the user + created: the preview must show it as an **update/overwrite**, and undo must be + able to restore the pre-connect entry (not silently discard it). +- A bridge client (e.g. Claude Desktop) with no existing config file: the preview + shows the entry that would be created, and the "backup" result honestly states + there was no prior file to back up. +- The config file is malformed/unparseable (Spec 075 `malformed`): the preview + cannot be rendered from parsed content and must degrade to a clear message + rather than a misleading empty diff. +- macOS App-Data denial (Spec 075 `denied`) while attempting a preview or undo: + the same actionable remediation as connect/disconnect is surfaced; the preview + is not shown as "no changes". +- TOML clients (Codex) vs JSON clients: the preview must render the correct + format for the client being connected. +- The API key embedded in the endpoint URL must never be silently exposed in a + way that surprises the user, nor hidden so thoroughly that they don't realize a + credential is written — the preview represents it deliberately. +- Backups accumulate with no retention: repeated connect/disconnect must not + break, and the accumulation behavior must be documented (and optionally bounded) + rather than left as a silent surprise. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The system MUST provide a way to preview the exact change a connect + would make to a given client — the target config path, the key that will be + added or updated, and the exact entry contents — **without** modifying the file + or creating a backup. +- **FR-002**: The preview MUST be derived from the same entry construction used by + the actual write (`buildServerEntry`) so that what is previewed equals what is + written for the same client and configuration. +- **FR-003**: The preview MUST make explicit that only the mcpproxy entry is + added/updated and that all other content in the config file is left unchanged, + distinguishing a "create" from an "overwrite of an existing same-named entry". +- **FR-004**: The preview MUST represent API-key handling honestly (shown or + clearly indicated/masked) so the user is aware a credential is written into the + client config, without accidentally leaking it in logs or telemetry. +- **FR-005**: Connect MUST continue to create a timestamped backup before + modifying an existing config (preserving current behavior) and the operation + result MUST carry the backup path. +- **FR-006**: The Web UI (both the onboarding wizard connect step and the + standalone Connect modal) MUST display the backup path to the user after a + successful connect, and MUST clearly represent the "no prior file to back up" + case rather than showing a blank path. +- **FR-007**: The system MUST offer a one-click undo of a connect from the same + surface where the connect was performed (wizard row and standalone modal), + reverting mcpproxy's contribution and returning the client to a not-connected + state. +- **FR-008**: Undo MUST, when connect overwrote a pre-existing same-named entry, + restore the pre-connect state from the backup rather than leaving the prior + entry lost; in the common case (mcpproxy entry newly added) it MUST remove only + the mcpproxy entry and leave all other entries intact. +- **FR-009**: Undo MUST show the change to be reverted (the same preview surface) + before applying it. +- **FR-010**: The wizard connect step MUST present plain-language trust copy + naming the file to be modified, the entry to be added, the no-other-changes + guarantee, and the backup location. +- **FR-011**: On macOS, the wizard MUST explain why an App-Data privacy prompt may + appear **before** the first read/write that could trigger it, in addition to the + existing post-denial remediation (Spec 075) which MUST be preserved. +- **FR-012**: The preview, backup-path, and undo behavior MUST degrade gracefully + for the Spec 075 access states (`absent`, `malformed`, `denied`): a preview MUST + NOT be presented as "no changes" when the config could not be read, and a denial + MUST surface the existing remediation. +- **FR-013**: The preview and undo MUST support both JSON clients and the TOML + client (Codex), rendering the correct format per client. +- **FR-014**: Backup retention/accumulation behavior MUST be documented; if a + retention bound is introduced it MUST be conservative and MUST NOT delete a + backup that a pending undo could still need. +- **FR-015**: The existing Connect REST/CLI surface MUST remain backward + compatible; new capabilities MAY add endpoints/fields/flags but MUST NOT remove + or repurpose existing ones (preserving Spec 075 additive compatibility). +- **FR-016**: The onboarding connect-step telemetry (complete/skip transitions) + MUST continue to be recorded so the funnel effect of this feature is measurable. +- **FR-017**: All new behavior MUST be covered by automated tests, including + preview-equals-write equivalence, backup-path surfacing, and undo restore paths + (new-add and overwrite cases). + +### Key Entities *(include if feature involves data)* + +- **Connect preview**: For one client — the target config path, the config format + (JSON/TOML), the key to be added/updated, the exact entry to be written, and an + action classification (create vs overwrite-existing). +- **Connect result**: Existing outcome of a connect/disconnect — success, action, + config path, and the **backup path** (now surfaced in the UI). +- **Backup record**: The timestamped copy created before a write — its path, + origin config, and timestamp; the basis for restore-based undo and the subject + of documented retention. +- **Undo request/result**: A revert of a prior connect — whether it removed the + mcpproxy entry or restored from backup, and the resulting client state. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: The wizard `connect_step` completion rate among wizard-engaged users + increases from the 13.8% baseline (2026-07-02) after this feature ships. +- **SC-002**: The wizard `connect_step` skip rate among wizard-engaged users + decreases from the 72.4% baseline (2026-07-02). +- **SC-003**: The overall first-run → client-connected funnel loss decreases from + the 28% baseline (1,237 → 854). +- **SC-004**: For every supported client, the previewed entry is byte-for-byte + equal to the entry a subsequent connect writes (same key, same shape), verified + by test across JSON and TOML clients. +- **SC-005**: 100% of successful Web UI connects that modified an existing config + display the backup path to the user; the "no prior file" case is shown + distinctly (never a blank path), verified by test. +- **SC-006**: Undo returns the client to not-connected in 100% of tested cases, + and restores the pre-connect state when connect overwrote a pre-existing + same-named entry, verified by test. +- **SC-007**: On macOS, the App-Data prompt explanation is presented before the + first prompt-triggering action in the wizard, and the Spec 075 post-denial + remediation continues to pass its existing tests (no regression). +- **SC-008**: The existing Connect REST/CLI contract and Spec 075 tests continue + to pass with no breaking changes. + +## Assumptions + +- The exact entry that will be written can be produced deterministically from the + current listen address, API key, and client id ahead of the write, because the + live write already builds it deterministically via `buildServerEntry`. +- Reading a config to render an "overwrite vs create" preview is an explicit, + user-initiated action and is therefore an acceptable place for the Spec 075 + on-demand read (and thus a macOS prompt), consistent with the existing + per-client status/connect/disconnect reads. +- Surgical entry removal is the correct default for undo in the common case (a + newly added mcpproxy entry); backup-based restore is reserved for the overwrite + case where surgical removal would lose the user's prior entry. +- Telemetry for the connect step is already captured (complete/skip) and is the + authoritative measure for the funnel success criteria. +- The API key written into client configs is a real credential and must be handled + as sensitive in previews, logs, and telemetry. + +## Out of Scope + +- Adding new client adapters or extending the set of supported clients. +- Changing the connect protocol, endpoint URL scheme, or the per-client entry + shapes produced by `buildServerEntry`. +- Changes to app signing, notarization, or entitlements, or automating the + granting of any macOS privacy permission (Spec 075 boundary preserved). +- Server-edition multi-user connect flows. +- A general-purpose config-file version history beyond the connect/undo backups. + +## Commit Message Conventions *(mandatory)* + +### Issue References +- ✅ **Use**: `Related #NNN` - Links the commit to the issue without auto-closing +- ❌ **Do NOT use**: `Fixes #`, `Closes #`, `Resolves #` + +### Co-Authorship +- ❌ **Do NOT include** `Co-Authored-By: Claude` or "Generated with Claude Code" trailers. + +### Example Commit Message +``` +feat(connect): preview change, surface backup path, one-click undo + +Related #NNN + +The onboarding connect step now shows the exact entry that will be written +to each client config before writing, displays the timestamped backup path +after connect, and offers a one-click undo that removes mcpproxy's entry +(or restores from backup when an existing entry was overwritten). Plain- +language trust copy explains what is touched and, on macOS, why the App-Data +prompt appears — closing the trust gap behind the funnel's biggest drop. + +## Changes +- Connect preview (path + key + exact entry; create vs overwrite) — no write +- Web UI surfaces backup_path after connect (wizard + modal) +- One-click undo: surgical remove, backup restore on overwrite +- Pre-emptive macOS App-Data explanation; Spec 075 remediation preserved +- Backup retention documented + +## Testing +- preview-equals-write equivalence (JSON + TOML clients) +- backup-path surfacing incl. no-prior-file case +- undo restore paths (new-add and overwrite) +- Spec 075 + existing Connect contract tests pass +``` diff --git a/specs/079-upgrade-nudge/spec.md b/specs/079-upgrade-nudge/spec.md new file mode 100644 index 00000000..20711357 --- /dev/null +++ b/specs/079-upgrade-nudge/spec.md @@ -0,0 +1,207 @@ +# Feature Specification: Upgrade Awareness & Guided Update + +**Feature Branch**: `chore/roadmap-polish-specs` +**Created**: 2026-07-02 +**Status**: Draft +**Input**: User description: "Most active installs run far-behind versions and never learn a newer one exists. Turn the existing background update check into a universal, non-intrusive upgrade nudge that reaches every surface (CLI status/doctor, a log line, a dismissible Web UI banner, both trays), tells each user how far behind they are, and — where the install channel can be identified — shows the exact one-line command to update. Never block, never modal, respect a config opt-out and the existing stable/prerelease channel semantics, and stay silent in offline/CI/ephemeral contexts." + +## Overview + +Corrected telemetry (2026-07-02) shows that of the active installs in the last 14 days, roughly **60% run a pre-v0.40 build** (released ~March), while the latest stable (v0.46.0, released 2026-06-25) has under 19%, and a sticky older cohort nearly ties it. Three in five active users therefore receive none of the shipped improvements — scanner v2, profiles, security fixes — and any breaking config change silently strands them. + +MCPProxy already has most of the *plumbing* for update awareness, but it is split across **two divergent check paths** that were built independently: + +- A newer, centralized background checker `internal/updatecheck/` (`checker.go`, `github.go`, `types.go`) polls GitHub `releases/latest` every 4 hours (`DefaultCheckInterval`, `checker.go`), compares versions with `golang.org/x/mod/semver`, and exposes the result on `GET /api/v1/info` as an `update` object (`available`, `latest_version`, `release_url`, `checked_at`, `is_prerelease`, `check_error`; `types.go` `VersionInfo`/`InfoResponseUpdate`, `internal/contracts/types.go` `UpdateInfo`). It is wired through `internal/runtime/runtime.go` and `internal/server/server.go` (`GetVersionInfo()`/`RefreshVersionInfo()`). `mcpproxy doctor` renders it (`cmd/mcpproxy/doctor_cmd.go` ~L290-300), and the Web UI store `frontend/src/stores/system.ts` exposes `updateAvailable` + a manual `checkForUpdates()` toast, surfaced as a sidebar indicator in `frontend/src/components/SidebarNav.vue` (~L60, L488). +- An older, independent self-updater inside the Go tray, `internal/tray/tray.go` `checkForUpdates()` (L1039), makes its **own** separate `releases/latest` call (L1103), honors `MCPPROXY_UPDATE_NOTIFY_ONLY` (L1053), detects Homebrew to suppress self-update (`isHomebrewInstallation()`, L1176), and downloads/replaces the binary. The Swift tray adds a third path, `native/macos/MCPProxy/MCPProxy/Services/UpdateService.swift`, which is a **Sparkle stub**: it uses `SPUStandardUpdaterController` when the Sparkle SPM dependency is linked and otherwise falls back to its own GitHub API check (L5-6, L18, L54-57, L130-134). + +What is missing is **reach, consistency, and a single source of truth**. The nudge does not appear where far-behind users actually look: `mcpproxy status` (the common command) omits it while `doctor` shows it; there is no startup log line; the Web UI has an easy-to-miss sidebar dot rather than a dismissible banner; and there is no config-file control (only environment variables — `MCPPROXY_DISABLE_AUTO_UPDATE`, `MCPPROXY_UPDATE_NOTIFY_ONLY`, `MCPPROXY_ALLOW_PRERELEASE_UPDATES`). The two Go check paths can disagree (the tray's own check vs `internal/updatecheck`), and there is no notion of an install channel model. Critically, for the large headless / server / Docker / Linux-tarball population — the cohort most likely to be far behind — there is **no actionable "here is how to update" guidance** at all, because the binary never identifies its install channel beyond the Homebrew check used only to *suppress* self-update. + +This feature makes upgrade awareness **universal, consistent, non-intrusive, and actionable**, building on the existing `updatecheck` foundation rather than replacing it: it surfaces the same result on every surface with a clear "N releases / M weeks behind" framing, adds a dismissible per-version Web UI banner and a startup log line, adds config-file control with documented opt-out and channel selection, and — where the install channel can be identified — shows the exact one-line update command for that channel. It never blocks, never opens a modal, and stays quiet offline and in CI/ephemeral contexts. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Universal, non-intrusive upgrade awareness (Priority: P1) + +A user running a months-old build interacts with MCPProxy the way they normally do — running `mcpproxy status`, glancing at the startup log, opening the Web UI, or clicking the tray. From wherever they already look, they learn, without any interruption, that a newer stable version exists and how far behind they are ("v0.46.0 available — you run v0.38.1, 8 releases / ~14 weeks behind"). Nothing blocks, nothing pops a modal, and the message can be dismissed and does not nag repeatedly for the same version. + +**Why this priority**: This is the core of the feature and the MVP. The single biggest lever on the far-behind population is making the existence of a newer release *impossible to miss* on the surfaces they already use, without being annoying enough to be disabled. The backend check already exists; the value is consistent, universal, calm surfacing. + +**Independent Test**: With the checker returning a newer stable than the running build, confirm the "vX available, you run vY, N behind" message appears consistently and non-intrusively in `mcpproxy status`, `mcpproxy doctor`, a single startup log line, the Web UI (as a dismissible banner), and both trays — and that dismissing the Web UI banner suppresses it for that version but not for the next one. + +**Acceptance Scenarios**: + +1. **Given** a newer stable release than the running version, **When** the user runs `mcpproxy status`, **Then** the output includes the current version, the latest version, and a human-readable "N releases / M weeks behind" delta. +2. **Given** a newer stable release, **When** the core starts, **Then** exactly one informational log line announces the available update (version, delta, release URL); when already current, no such line is emitted. +3. **Given** a newer stable release, **When** the user opens the Web UI, **Then** a dismissible, non-modal banner shows the available version, the delta, and a link to the release notes. +4. **Given** the user dismisses the Web UI banner for version vX, **When** they reload the Web UI and vX is still the latest, **Then** the banner stays dismissed; **When** a newer vY later becomes latest, **Then** the banner reappears for vY. +5. **Given** a newer stable release, **When** the user opens either tray, **Then** the update availability is indicated consistently with the other surfaces (same version and framing), and no surface blocks interaction while a check is pending or fails. +6. **Given** the running version equals the latest stable, **When** any surface renders, **Then** no upgrade nudge appears (and status/doctor may state "up to date"). + +--- + +### User Story 2 - The right update command for my install channel (Priority: P2) + +A user who has been told an update exists wants to know *exactly how to get it*. A Homebrew user is shown `brew upgrade …`; a Docker user is shown the `docker pull …` tag to bump; a Linux `.deb`/`.rpm` user is shown the apt/dnf command; a `go install` user is shown the module path. Where the channel cannot be reliably identified (e.g. a bare tarball extraction), they get a safe, generic instruction pointing at the releases page and download docs rather than a command that might be wrong for their setup. + +**Why this priority**: Awareness without an action is a dead end for the headless/server/Docker/Linux population that cannot self-update from a tray. Turning "you are behind" into a copy-pasteable next step is what actually moves those users forward. It builds directly on US1 and is only useful once awareness lands. + +**Independent Test**: For each identifiable channel (brew, dmg/desktop, deb/rpm, docker, go-install) confirm the surfaced guidance is the correct one-line action for that channel and deep-links the release notes; for an unidentifiable channel confirm the generic releases-page instruction is shown and no channel-specific command is emitted. + +**Acceptance Scenarios**: + +1. **Given** an install identified as a given channel, **When** the guided-update guidance is shown, **Then** it presents the one-line action appropriate to that channel (e.g. package-manager upgrade command, Docker pull, or `go install …@latest`). +2. **Given** an install whose channel cannot be reliably determined, **When** guidance is shown, **Then** it presents a generic "download the latest release" instruction linking the releases page and download docs, and does not present a channel-specific command that could be incorrect. +3. **Given** any guided-update guidance, **When** it is shown, **Then** it deep-links the release notes for the latest version. +4. **Given** a package-manager-owned install (brew/deb/rpm/docker), **When** guidance is shown, **Then** it never instructs the user to run an in-app self-update that would conflict with the package manager. + +--- + +### User Story 3 - Operator control and environment-appropriate quiet (Priority: P3) + +An operator running MCPProxy in an air-gapped datacenter, a CI pipeline, or an ephemeral container wants the update behavior under their control from the config file — able to disable checks entirely, or opt into the prerelease channel — and expects the tool to stay silent when it obviously should (no network, non-interactive CI, ephemeral runs) rather than emitting noise or failing. + +**Why this priority**: Control and good defaults are what keep the nudge from becoming something operators disable out of annoyance, but they matter only once the awareness and guidance (US1/US2) exist. The existing environment-variable switches remain, so this is additive hardening rather than a prerequisite. + +**Independent Test**: Toggle the config knobs (disabled, prerelease channel) and confirm behavior changes accordingly and hot-reloads; run the check offline, in a simulated CI/non-interactive context, and as a prerelease build, and confirm the tool degrades to quiet correctness (no crash, no misleading "downgrade" nudge, no UI nag) in each case. + +**Acceptance Scenarios**: + +1. **Given** `update_check.enabled=false` in config, **When** the core runs, **Then** no update check is performed and no nudge appears on any surface; **When** the value is changed and the config hot-reloads, **Then** the new setting takes effect without a restart. +2. **Given** `update_check.channel` set to the prerelease channel, **When** a newer prerelease exists, **Then** it is offered consistently with the documented prerelease semantics; **Given** the default stable channel, **Then** prereleases are never offered as upgrades. +3. **Given** no network access, **When** the check runs, **Then** it fails silently with the error captured for diagnostics and never blocks startup or any surface, and no nudge is shown. +4. **Given** a running prerelease build newer than the latest stable, **When** awareness is computed, **Then** the user is NOT nudged to "upgrade" to an older stable version (no downgrade nudge). +5. **Given** a non-interactive / CI / ephemeral context, **When** surfaces render, **Then** UI nudges (banner, tray, repeated log nagging) are suppressed while machine-readable fields (status/doctor/`/api/v1/info`) still report the facts. +6. **Given** the existing environment-variable switches, **When** they are set, **Then** they continue to work and interoperate with the config knobs with a documented precedence. + +--- + +### Edge Cases + +- **Air-gapped install**: the GitHub check fails on every attempt → the failure is recorded (`check_error`), never surfaced as an alarming state, and the tool behaves exactly as an up-to-date install for gating purposes (no blocking, no repeated error toasts). +- **Downgrade / pinned older**: the latest stable is *older* than the running build (e.g. a prerelease, or a locally built newer version) → no "upgrade" nudge is shown; the delta is never negative. +- **Prerelease user on stable channel**: a user running an `-rc`/`-next` build must not be told to "upgrade" to the matching or older stable; prerelease-vs-stable comparison must not treat a stable tag as newer than the prerelease it precedes. +- **GitHub API rate limiting / transient 5xx**: unauthenticated GitHub API calls are rate-limited → checks are rate-limited (at most a daily check) and back off on failure; a rate-limit or transient error is treated as "unknown", never as "up to date" and never as a hard error. +- **CI / ephemeral / non-interactive**: environment kind is not interactive → UI nudges are suppressed; the tool must not emit a per-run nag that would spam CI logs. +- **Version string is `development`/unversioned**: a locally built binary with no release version → no nudge (cannot meaningfully compare), and status/doctor say so rather than showing a bogus delta. +- **Channel mis-identification risk**: install-channel heuristics are ambiguous (e.g. a tarball copied into `/usr/local/bin`) → the system must prefer a generic instruction over emitting a possibly-wrong channel command. +- **Dismissal across versions**: a banner dismissed for vX must not hide the nudge for a later vY; dismissal is per-version, not permanent. +- **Docker `:latest` users**: users pinned to a moving `:latest` tag may already be current at runtime even if the running image build string looks old → guidance for Docker points at pulling the tag rather than implying they are stranded when they are not. +- **Reaching the existing far-behind cohort**: builds that predate this feature cannot show its nudge; the forward-looking metric (share of actives on latest-or-previous stable) is what this feature moves, and package-manager/Docker-tag/docs channels are the only paths that reach pre-feature installs (acknowledged, see Out of Scope). + +## Requirements *(mandatory)* + +### Functional Requirements + +**Awareness (build on the existing checker)** + +- **FR-001**: The system MUST reuse the `internal/updatecheck` background checker and its `GET /api/v1/info` `update` result as the single source of truth for update state; this feature MUST NOT introduce a second, divergent check pipeline. +- **FR-001a**: The Go tray's independent update check (`internal/tray/tray.go` `checkForUpdates()`, which today makes its own separate `releases/latest` call) MUST converge on the `internal/updatecheck` result so the tray cannot report a different availability/version than the other surfaces; the tray's self-update *action* mechanics may remain, but the *check* MUST be the shared one. Eliminating this divergence is in scope; expanding self-update mechanics is not (see Out of Scope). +- **FR-002**: The system MUST surface upgrade awareness consistently on all of: `mcpproxy status`, `mcpproxy doctor`, a startup log line, the Web UI, and both trays — showing the current version, the latest version, and a human-readable "N releases / M weeks behind" delta. +- **FR-003**: `mcpproxy status` MUST include the update availability and delta (it currently shows only the running version). +- **FR-004**: The core MUST emit exactly one informational log line per process start when (and only when) an update is available, including the latest version and release URL; it MUST NOT repeatedly log the same availability on a timer. +- **FR-005**: The Web UI MUST present a dismissible, non-modal banner when an update is available; dismissal MUST be persisted per-version so it suppresses the current version but reappears for a newer one. +- **FR-006**: No surface MAY block interaction, open a modal, or degrade functionality because a check is pending, failed, or reports an available update. +- **FR-007**: The check MUST run without requiring telemetry consent and MUST hit only an anonymous, static/GitHub endpoint (no user identifier sent); update checking MUST be independent of the telemetry opt-in state. + +**Guided update (channel-aware)** + +- **FR-008**: The system MUST attempt to identify the install channel (at least: Homebrew, macOS desktop/DMG, Linux `.deb`/`.rpm`, Docker, `go install`, and generic tarball) using reliable signals, preferring a build-time channel marker where available and falling back to path/environment heuristics. +- **FR-009**: When the channel is identified, the system MUST present the correct one-line update action for that channel; when it cannot be reliably identified, it MUST present a generic "download the latest release" instruction and MUST NOT emit a channel-specific command that could be wrong. +- **FR-010**: Guided-update guidance MUST deep-link the release notes for the latest version. +- **FR-011**: For package-manager-owned installs (brew/deb/rpm/docker), the system MUST NOT offer an in-app self-update that would conflict with the package manager (preserving today's Homebrew self-update suppression). + +**Configuration & channel semantics** + +- **FR-012**: The system MUST provide config-file control under an `update_check` group with at least `enabled` (default true) and `channel` (default stable; prerelease opt-in), hot-reloaded like other config. +- **FR-013**: The `channel` setting MUST respect the existing stable-vs-prerelease release semantics: on the stable channel, prereleases (`-rc`/`-next`) MUST never be offered as upgrades; the prerelease channel offers them consistently with the documented prerelease rules. +- **FR-014**: The existing environment-variable switches (`MCPPROXY_DISABLE_AUTO_UPDATE`, `MCPPROXY_ALLOW_PRERELEASE_UPDATES`, and the notify-only switch) MUST continue to function, with a documented precedence relative to the new config keys. +- **FR-015**: The opt-out MUST be documented, and when disabled the system MUST perform no network check and show no nudge on any surface. + +**Correctness & quiet defaults** + +- **FR-016**: The system MUST NOT show an "upgrade" nudge when the latest available version is not strictly newer than the running version (no downgrade nudge, no negative delta), including for prerelease builds newer than the latest stable. +- **FR-017**: The system MUST NOT show a nudge for an unversioned/`development` build and MUST clearly report that state rather than a fabricated delta. +- **FR-018**: The update check MUST be rate-limited (at most a daily check) and MUST back off on failure, treating rate-limit/transient errors as "unknown" rather than "up to date". +- **FR-019**: In non-interactive / CI / ephemeral contexts, UI nudges (Web UI banner, tray indicator, repeated logging) MUST be suppressed while machine-readable fields (status/doctor/`/api/v1/info`) still report the facts. +- **FR-020**: A failed check (offline, rate-limited, error) MUST never block startup or any surface and MUST never surface as an alarming/error state to the end user; the error is retained for diagnostics only. + +**Compatibility** + +- **FR-021**: The `GET /api/v1/info` `update` contract MUST remain backward compatible; the payload MAY add fields (e.g. delta, channel, guided command) but MUST NOT remove or repurpose existing ones (`available`, `latest_version`, `release_url`, `checked_at`, `is_prerelease`, `check_error`). +- **FR-022**: All new behavior MUST be covered by automated tests, including tests that inject a mocked release result (newer stable, equal, older/prerelease, error/offline) without hitting the network, and a test that a dismissed banner version reappears for a newer version. + +### Key Entities *(include if feature involves data)* + +- **Update state**: The result of a check — current version, latest version, availability, release URL, checked-at, is-prerelease, and check-error — extended with a human-readable release/time delta. (Extends the existing `VersionInfo` / `/api/v1/info` `update` object; not a new pipeline.) +- **Install channel**: The identified distribution channel of the running binary (homebrew, desktop/dmg, deb, rpm, docker, go-install, tarball, or unknown) plus the corresponding one-line update action or generic fallback instruction. +- **Update-check config**: The `update_check` group — `enabled` and `channel` — plus documented precedence with the existing environment-variable switches. +- **Banner dismissal**: A per-version record (client-side) that a user dismissed the Web UI nudge for a specific latest version. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: The share of last-14-day active installs running the latest-or-previous stable release (currently ~23%) increases past 50% within two release cycles after this ships. +- **SC-002**: With a newer stable available, 100% of the surfaces — `mcpproxy status`, `mcpproxy doctor`, startup log, Web UI banner, and both trays — show the same available version and consistent "behind" framing, verified by test. +- **SC-003**: A Web UI banner dismissed for version vX stays dismissed for vX and reappears for a later vY, verified by test. +- **SC-004**: For each identifiable install channel, the guided command shown is the correct one-line action for that channel (0 wrong-channel commands), and unidentifiable channels always fall back to the generic instruction, verified by test. +- **SC-005**: With update checking disabled via config, zero network checks are made and zero nudges appear on any surface, verified by test. +- **SC-006**: In offline, rate-limited, downgrade, prerelease, and `development`-build cases, no misleading or alarming nudge is shown and no surface blocks, verified by test for each case. +- **SC-007**: In a non-interactive/CI/ephemeral context, no UI nudge or per-run log nag is emitted while machine-readable fields still report update state, verified by test. +- **SC-008**: The `GET /api/v1/info` `update` contract retains all existing fields and its current consumers (doctor, Web UI store) continue to pass their tests. + +## Assumptions + +- The existing `internal/updatecheck` background checker, its semver comparison, and the `/api/v1/info` `update` contract are sound and are the intended foundation; this feature extends surfacing and configuration rather than re-implementing the check. +- Hitting the anonymous GitHub `releases/latest` API (and `releases` for the prerelease channel) is an acceptable, privacy-preserving check that requires no user identity and no telemetry consent; a static redirect endpoint is an acceptable alternative if GitHub rate limits prove problematic. +- A build-time channel marker (injected at release/packaging time, analogous to how the version is stamped) is the most reliable channel signal; path/environment heuristics (Homebrew Cellar prefix, deb/rpm-owned paths, `/.dockerenv`, missing release version for `go install`) are an acceptable best-effort fallback where the marker is absent. +- "N releases behind" can be derived from the tag sequence between the running and latest versions, and "M weeks behind" from release publish dates; where the exact release count is unavailable, a version-delta and date-delta approximation is acceptable. +- The far-behind cohort that predates this feature cannot be reached by an in-app nudge; the metric this feature moves is forward-looking (share behind at each future release), and package-manager auto-upgrade, Docker tag moves, and docs are the mechanisms that reach pre-feature installs. +- Environment kind (interactive vs CI/ephemeral) is already detectable from existing environment/launch signals used elsewhere in the codebase. + +## Out of Scope + +- Expanding binary self-update to new channels or building one-click self-update beyond what the desktop trays already do; self-update remains a desktop-tray concern and is explicitly not added for package-manager-owned or headless installs. (The convergence of the tray's *check* onto `internal/updatecheck` is in scope per FR-001a; the tray's download/replace/restart *mechanics* are not.) +- Linking or completing the Swift **Sparkle** integration (`UpdateService.swift` is a stub that falls back to a GitHub check when Sparkle is not linked); wiring up Sparkle's full appcast-based update UX is a separate initiative. This feature only requires the Swift tray's *awareness* to be consistent with the shared check. +- Any server-side "your version is end-of-life" broadcast or forced-update mechanism; awareness stays local and advisory. +- Automatically running package-manager upgrade commands on the user's behalf (guidance is shown, never executed). +- Rescuing the existing pre-feature far-behind cohort through the in-app nudge (impossible by construction); only forward-looking behavior is in scope. +- Redesigning the release/packaging pipeline beyond adding a build-time channel marker. +- Changing the tray self-update download/replace/restart mechanics that already ship. + +## Commit Message Conventions *(mandatory)* + +When committing changes for this feature, follow these guidelines: + +### Issue References +- ✅ **Use**: `Related #[issue-number]` — links the commit to the issue without auto-closing. +- ❌ **Do NOT use**: `Fixes #`, `Closes #`, `Resolves #` — these auto-close issues on merge. + +### Co-Authorship +- ❌ **Do NOT include**: `Co-Authored-By: Claude ` +- ❌ **Do NOT include**: "🤖 Generated with [Claude Code](https://claude.com/claude-code)" + +### Example Commit Message +``` +feat(update): universal upgrade nudge + channel-aware guided update + +Related #[issue-number] + +Surface the existing background update check consistently across +mcpproxy status/doctor, a startup log line, a dismissible per-version +Web UI banner, and both trays, with a "N releases / M weeks behind" +delta. Add channel-aware guided update commands and an update_check +config group. Never blocks; quiet offline and in CI/ephemeral contexts. + +## Changes +- status: show update availability + delta (parity with doctor) +- Web UI: dismissible per-version update banner +- startup log line when an update is available +- install-channel detection + per-channel one-line update guidance +- update_check.{enabled,channel} config with env-var precedence + +## Testing +- Mocked release results (newer/equal/older/prerelease/error) +- Per-version banner dismissal, channel guidance, disabled opt-out, + CI/ephemeral suppression +```