Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 33 additions & 35 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ graph LR
**Independent epics** (15) — no cross-epic prerequisites; each stands alone:

- 🔵 **Upgrade awareness & guided update** — In progress · P0
- 🔵 **Connect step trust: preview, visible backup, one-click undo** — In progress · P0
- 🔵 **Release qualification gate (auto-QA matrix blocks the tag)** — In progress · P0
- 🟡 **Windows native tray app** — In review · P2
- 🔴 **MCP protocol upgrade to 2026-07-28 revision** — Blocked · P3
Expand All @@ -75,6 +74,7 @@ graph LR
- ⚫ **SSO (server edition)** — Todo · P3 · parked
- ⚪ **Security gateway Tracks C/D (per-arg least-privilege + signature provenance)** — Todo · P3
- ⚪ **Discovery-quality eval harness (Spec 065 second half)** — Todo · P3
- 🟢 **Connect step trust: preview, visible backup, one-click undo** — Done · P0
- 🟢 **Registries — easier search + add-server** — Done · P1
- 🟢 **Tray↔core decoupling: socket/REST API only, no config-file reads** — Done · P2

Expand All @@ -101,47 +101,17 @@ graph LR
upgrade_nudge_surfacing --> upgrade_nudge_quiet

classDef done fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff;
classDef todo fill:#6e7781,stroke:#3d4248,color:#ffffff;
classDef in_review fill:#9a6700,stroke:#5c3d00,color:#ffffff;
class upgrade_nudge_status_log,upgrade_nudge_surfacing,upgrade_nudge_channel done;
class upgrade_nudge_quiet todo;
class upgrade_nudge_quiet in_review;
```

| Task | Status | Refs |
| --- | --- | --- |
| US1 slice: update availability in mcpproxy status + deduped startup log | 🟢 Done | #798 |
| US1 remainder: dismissible Web UI banner + update_check config block | 🟢 Done | #805 |
| US2: channel-aware guided update command (brew/dmg/deb/rpm/docker/go-install detection, build-time channel marker) | 🟢 Done | #818 |
| US3: operator control + CI/offline quiet + no prerelease downgrade nudges | ⚪ Todo | — |

</details>

<details>
<summary>🔵 Connect step trust: preview, visible backup, one-click undo — In progress · P0</summary>

> Legacy wizard telemetry APPEARED to show 72.4% of engaged users skipping the connect step - debunked 2026-07-06: an instrumentation artifact, genuine never-connected skip = 0% (the wizard stamped skipped on users who connected via ConnectModal/CLI/manual config); real cliff is one-and-done installs ~48% (day-1 return 31%, identity-deduped 2026-07-10), see specs/080. Completers retain ~50% at two weeks vs 6% for non-engaged (correlation with engagement, not causation by the connect step). 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.

Spec: [078-connect-trust-preview](./specs/078-connect-trust-preview/)

```mermaid
graph LR
connect_trust_preview["US1: preview API + wizard diff UI (exact entr…"]
connect_trust_backup_visibility["US1: surface backup_path in Web UI + retentio…"]
connect_trust_undo["US2: one-click undo/disconnect in wizard"]
connect_trust_tcc_copy["US2: pre-emptive macOS TCC explanation in wiz…"]


classDef done fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff;
classDef in_review fill:#9a6700,stroke:#5c3d00,color:#ffffff;
class connect_trust_preview,connect_trust_backup_visibility,connect_trust_undo done;
class connect_trust_tcc_copy in_review;
```

| Task | Status | Refs |
| --- | --- | --- |
| US1: preview API + wizard diff UI (exact entry, API-key masking) | 🟢 Done | #802 |
| US1: surface backup_path in Web UI + retention policy | 🟢 Done | #799 |
| US2: one-click undo/disconnect in wizard | 🟢 Done | #804 |
| US2: pre-emptive macOS TCC explanation in wizard | 🟡 In review | #910 |
| US3: operator control + CI/offline quiet + no prerelease downgrade nudges | 🟡 In review | #911 |

</details>

Expand Down Expand Up @@ -475,6 +445,34 @@ Spec: [065-evaluation-foundation](./specs/065-evaluation-foundation/)

</details>

<details>
<summary>🟢 Connect step trust: preview, visible backup, one-click undo — Done · P0</summary>

> Legacy wizard telemetry APPEARED to show 72.4% of engaged users skipping the connect step - debunked 2026-07-06: an instrumentation artifact, genuine never-connected skip = 0% (the wizard stamped skipped on users who connected via ConnectModal/CLI/manual config); real cliff is one-and-done installs ~48% (day-1 return 31%, identity-deduped 2026-07-10), see specs/080. Completers retain ~50% at two weeks vs 6% for non-engaged (correlation with engagement, not causation by the connect step). 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.

Spec: [078-connect-trust-preview](./specs/078-connect-trust-preview/)

```mermaid
graph LR
connect_trust_preview["US1: preview API + wizard diff UI (exact entr…"]
connect_trust_backup_visibility["US1: surface backup_path in Web UI + retentio…"]
connect_trust_undo["US2: one-click undo/disconnect in wizard"]
connect_trust_tcc_copy["US2: pre-emptive macOS TCC explanation in wiz…"]


classDef done fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff;
class connect_trust_preview,connect_trust_backup_visibility,connect_trust_undo,connect_trust_tcc_copy done;
```

| Task | Status | Refs |
| --- | --- | --- |
| US1: preview API + wizard diff UI (exact entry, API-key masking) | 🟢 Done | #802 |
| US1: surface backup_path in Web UI + retention policy | 🟢 Done | #799 |
| US2: one-click undo/disconnect in wizard | 🟢 Done | #804 |
| US2: pre-emptive macOS TCC explanation in wizard | 🟢 Done | #910 |

</details>

<details>
<summary>🟢 Non-Docker sandbox isolation (Landlock) — Done · P1 · MCP-34</summary>

Expand Down Expand Up @@ -632,7 +630,6 @@ graph LR
| Epic | Status | Priority | Progress | Spec | PR |
| --- | --- | --- | --- | --- | --- |
| Upgrade awareness & guided update | In progress | P0 | — | [079-upgrade-nudge](./specs/079-upgrade-nudge/) | |
| Connect step trust: preview, visible backup, one-click undo | In progress | P0 | — | [078-connect-trust-preview](./specs/078-connect-trust-preview/) | |
| Release qualification gate (auto-QA matrix blocks the tag) | In progress | P0 | — | [081-release-qa-gate](./specs/081-release-qa-gate/) | |
| Analytics dashboard as default page | In progress | P1 | 25/26 (96%) | [069-observability-usage-graphs](./specs/069-observability-usage-graphs/) | |
| Telemetry identity & data quality (machine_id + CI-filter hardening) | In progress | P1 | — | | |
Expand All @@ -650,6 +647,7 @@ graph LR
| Paid-tier MVP (billing / seats / license) `MCP-40` | Todo (parked) | P3 | — | | |
| SDK v1 migration | Todo (parked) | P3 | — | | |
| SSO (server edition) | Todo (parked) | P3 | — | | |
| Connect step trust: preview, visible backup, one-click undo | Done | P0 | — | [078-connect-trust-preview](./specs/078-connect-trust-preview/) | |
| Non-Docker sandbox isolation (Landlock) `MCP-34` | Done | P1 | — | | |
| Spec 076 deterministic offline tool-scanner `MCP-3574` | Done | P1 | 22/24 (92%) | [076-deterministic-tool-scanner](./specs/076-deterministic-tool-scanner/) | |
| Registries — easier search + add-server | Done | P1 | 21/24 (88%) | [070-registry-easy-upstream-add](./specs/070-registry-easy-upstream-add/) | |
Expand Down
3 changes: 3 additions & 0 deletions cmd/generate-types/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ export interface UpdateInfo {
// One-line update command for the channel; only present when an update is
// available and the channel has a safe command (FR-009).
update_command?: string;
// Spec 079 US3 (FR-019): the core runs in a CI / non-interactive context —
// UI surfaces must stay quiet while the facts remain machine-readable.
nudges_suppressed?: boolean;
}

export interface InfoResponse {
Expand Down
14 changes: 14 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,20 @@ The env vars only widen in one direction (disable checks / enable
prereleases); they cannot force-enable checking that config disabled — with
`update_check.enabled: false`, checks stay off regardless of environment.

**Check cadence and quiet environments** (Spec 079 US3):

- The background check runs **at most daily** and **backs off on failure**
(each consecutive failed check doubles the wait, capped at 8× the
interval) — offline or rate-limited environments are treated as
"unknown", never retried aggressively and never surfaced as an error.
A manual `/api/v1/info?refresh=true` bypasses the backoff.
- With `CI=true` (or `CI=1`, the same convention the telemetry filter
uses) the process is treated as **non-interactive**: the startup
"Update available" log line is demoted to debug and the `update` payload
carries `nudges_suppressed: true`, which hides the Web UI banner. The
machine-readable facts (`mcpproxy status`, `doctor`, `/api/v1/info`)
are unaffected.

See [Version Updates](features/version-updates.md) for where updates are
surfaced.

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/SidebarNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
v{{ displayVersion }}
</span>
<span
v-if="systemStore.updateAvailable"
v-if="systemStore.updateAvailable && !systemStore.updateNudgesSuppressed"
class="badge badge-xs badge-primary shrink-0"
:title="latestVersionTitle"
>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/UpdateBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ async function copyCommand() {
const visible = computed(
() =>
systemStore.updateAvailable &&
// Spec 079 US3 (FR-019): the core stamps nudges_suppressed in CI /
// non-interactive contexts — status/doctor/info keep the facts, the
// banner stays quiet.
!systemStore.updateNudgesSuppressed &&
!!latestVersion.value &&
latestVersion.value !== dismissedVersion.value
)
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/stores/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export const useSystemStore = defineStore('system', () => {
// Version information
const version = computed(() => info.value?.version ?? '')
const updateAvailable = computed(() => info.value?.update?.available ?? false)
// Spec 079 US3 (FR-019): the core stamps nudges_suppressed in CI /
// non-interactive contexts — every UI nudge surface (banner, sidebar
// badge) must check this; machine-readable facts stay available.
const updateNudgesSuppressed = computed(
() => info.value?.update?.nudges_suppressed ?? false
)
const latestVersion = computed(() => info.value?.update?.latest_version ?? '')
// Spec 079 US2: detected install channel + channel-aware one-line update
// command (empty when the channel has no safe command, FR-009).
Expand Down Expand Up @@ -428,7 +434,13 @@ export const useSystemStore = defineStore('system', () => {
updateCheckedAt.value = response.data.update?.checked_at ?? new Date().toISOString()
const checkErr = response.data.update?.check_error
if (checkErr) {
addToast({ type: 'error', title: 'Update check failed', message: checkErr })
// Spec 079 FR-020: a failed check (offline, rate-limited) is
// "unknown", never an alarming error state — inform, don't alarm.
addToast({
type: 'info',
title: 'Could not check for updates',
message: 'The update service was unreachable (offline or rate-limited). Try again later.',
})
return { ok: false, error: checkErr }
}
if (response.data.update?.available) {
Expand Down Expand Up @@ -488,6 +500,7 @@ export const useSystemStore = defineStore('system', () => {
currentThemeConfig,
version,
updateAvailable,
updateNudgesSuppressed,
latestVersion,
installChannel,
updateCommand,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/types/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,9 @@ export interface UpdateInfo {
// One-line update command for the channel; only present when an update is
// available and the channel has a safe command (FR-009).
update_command?: string;
// Spec 079 US3 (FR-019): the core runs in a CI / non-interactive context —
// UI surfaces must stay quiet while the facts remain machine-readable.
nudges_suppressed?: boolean;
}

export interface InfoResponse {
Expand Down
13 changes: 13 additions & 0 deletions frontend/tests/unit/update-banner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function mountBanner(update?: {
available: boolean
latest_version?: string
release_url?: string
nudges_suppressed?: boolean
}) {
const pinia = createPinia()
setActivePinia(pinia)
Expand Down Expand Up @@ -61,6 +62,18 @@ describe('UpdateBanner (Spec 079 FR-005)', () => {
expect(wrapper.find('[data-test="update-banner"]').exists()).toBe(false)
})

// Spec 079 US3 (FR-019): in CI / non-interactive contexts the core stamps
// nudges_suppressed on the update payload — the facts stay machine-readable
// but UI surfaces must not nag.
it('does not render when the core says nudges are suppressed (CI / non-interactive)', () => {
const { wrapper } = mountBanner({
available: true,
latest_version: 'v1.3.0',
nudges_suppressed: true,
})
expect(wrapper.find('[data-test="update-banner"]').exists()).toBe(false)
})

it('dismiss hides the banner and persists the dismissed version', async () => {
const { wrapper } = mountBanner({ available: true, latest_version: 'v1.3.0' })
await wrapper.find('[data-test="update-banner-dismiss"]').trigger('click')
Expand Down
17 changes: 9 additions & 8 deletions internal/contracts/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1127,14 +1127,15 @@ type HealthStatus struct {

// UpdateInfo represents version update check information
type UpdateInfo struct {
Available bool `json:"available"` // Whether an update is available
LatestVersion string `json:"latest_version,omitempty"` // Latest version available (e.g., "v1.2.3")
ReleaseURL string `json:"release_url,omitempty"` // URL to the release page
CheckedAt *time.Time `json:"checked_at,omitempty"` // When the update check was performed
IsPrerelease bool `json:"is_prerelease,omitempty"` // Whether the latest version is a prerelease
CheckError string `json:"check_error,omitempty"` // Error message if update check failed
InstallChannel string `json:"install_channel,omitempty"` // Detected install channel (homebrew, dmg, deb, rpm, docker, go-install, windows-installer, tarball, unknown) — Spec 079 FR-008
UpdateCommand string `json:"update_command,omitempty"` // One-line update command for the channel; only set when an update is available and the channel has one — Spec 079 FR-009
Available bool `json:"available"` // Whether an update is available
LatestVersion string `json:"latest_version,omitempty"` // Latest version available (e.g., "v1.2.3")
ReleaseURL string `json:"release_url,omitempty"` // URL to the release page
CheckedAt *time.Time `json:"checked_at,omitempty"` // When the update check was performed
IsPrerelease bool `json:"is_prerelease,omitempty"` // Whether the latest version is a prerelease
CheckError string `json:"check_error,omitempty"` // Error message if update check failed
InstallChannel string `json:"install_channel,omitempty"` // Detected install channel (homebrew, dmg, deb, rpm, docker, go-install, windows-installer, tarball, unknown) — Spec 079 FR-008
UpdateCommand string `json:"update_command,omitempty"` // One-line update command for the channel; only set when an update is available and the channel has one — Spec 079 FR-009
NudgesSuppressed bool `json:"nudges_suppressed,omitempty"` // UI surfaces must stay quiet (CI / non-interactive context); machine-readable fields still report the facts — Spec 079 FR-019
}

// InfoEndpoints represents the available API endpoints
Expand Down
13 changes: 11 additions & 2 deletions internal/tray/tray.go
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,9 @@ type coreUpdateInfo struct {
LatestVersion string `json:"latest_version"`
ReleaseURL string `json:"release_url"`
IsPrerelease bool `json:"is_prerelease"`
// NudgesSuppressed: the core runs in a CI / non-interactive context and
// UI nudges must stay quiet (Spec 079 FR-019).
NudgesSuppressed bool `json:"nudges_suppressed"`
}

// fetchCoreUpdateInfo queries the core's /api/v1/info endpoint. It returns the
Expand Down Expand Up @@ -1958,15 +1961,21 @@ func (a *App) checkUpdateFromAPI() {
a.latestReleaseURL = update.ReleaseURL
a.updateCheckMu.Unlock()

// Update menu visibility
if update.Available {
// Update menu visibility. When the core reports nudges_suppressed
// (CI / non-interactive context, Spec 079 FR-019) the tray keeps the
// facts in its state but shows no nudge and logs at debug only.
if update.Available && !update.NudgesSuppressed {
if !wasAvailable {
a.logger.Info("Update available",
zap.String("current", a.version),
zap.String("latest", update.LatestVersion))
}
a.showUpdateMenuItem(update.LatestVersion, update.IsPrerelease)
} else {
if update.Available && update.NudgesSuppressed {
a.logger.Debug("Update available but nudges suppressed (CI/non-interactive)",
zap.String("latest", update.LatestVersion))
}
a.hideUpdateMenuItem()
}
}
Expand Down
Loading
Loading