fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(server): only run provider updates through the installer that owns the binary - #9325

Open
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership
Open

fix(server): only run provider updates through the installer that owns the binary#9325
juliusmarminge wants to merge 6 commits into
mainfrom
fix/provider-maintenance-ownership

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

T3 Code could pick an update command from one installation while the active executable belonged to another:

Fix

Ownership is derived from the resolved executable's real path, and a package manager is only invoked against an install it has evidence of owning:

EvidenceCommandLatest
Native installer path<resolved binary> updatenpm registry
…/Cellar/<name>/ or …/Caskroom/<name>/brew upgrade [--cask] <name>brew info --json=v2
<prefix>/lib/node_modules/<pkg>/npm install -g --prefix <prefix> <pkg>@latestnpm registry
pnpm / bun / vp global pathsas beforenpm registry
anything elsemanual-only, version gap still shownnpm registry

The npm prefix is pinned because the npm on PATH can belong to a different Node than the one that owns the provider. Native installs share npm's version train, so the registry stays authoritative for them and native users keep their launch toast.

Resolution is cached per instance for an hour. The maintenance runner re-reads it fresh immediately before executing, refuses with "Provider installation changed" if the lock key moved since the advisory, and reads fresh again afterwards so a Homebrew upgrade's new latest is reflected. Settings tracks in-flight updates per instance instead of per driver.

Docs: docs/internals/providers.md gains a "Provider updates" section; docs/user/install.md gains "Keeping Provider CLIs Current".

Relationship to #6436

Supersedes #6436 by @ettoc00, which identified the same root cause and whose npm --prefix pinning, Homebrew formula/cask detection, and per-instance settings state are carried over here. The rest of that PR (Scoop, WinGet, Grok native, identity hashing, wrapper-script parsing, the "Check for updates" affordance) is left out: the native-install branch there dropped latestVersion to null, which removed update notifications for the default Claude/Codex/OpenCode install paths, and the Vite+ detector shelled out to vp root -g, which does not exist. Scoop/WinGet support is worth its own issue with a Windows validator.

Closes#5629
Closes#6245
Closes#7730

Verification

  • apps/server, apps/web typecheck; vp lint on touched files
  • 224 server tests across the touched files, 53 web tests in the touched files
  • New coverage: npm prefix pinning via real symlinks in a temp dir, POSIX/Windows/nested prefix parsing, Homebrew cask through a fake spawner asserting exactly one brew info call, keg/cask/plain-/usr/local/bin ownership (fix(server): avoid treating Ubuntu binaries as Homebrew-managed #8832 case), cache-until-fresh, runner re-resolve ordering, lock-key-changed abort with no spawn
  • Not exercised against a real Homebrew keg on macOS; parsing and command shape are unit-tested

Written by Claude Fable 5 in Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how update commands are chosen and executed (including spawning brew during resolution); wrong ownership logic could block valid updates or run the wrong installer, but unproven paths fail closed to manual-only.

Overview
Provider one-click updates now infer who installed the CLI from the resolved executable and its real path, instead of assuming npm/Homebrew defaults. Unproven installs stay manual-only (canUpdate: false) while still showing version gaps; proven paths get the matching command (native update, brew upgrade [--cask] <name> with brew info for “latest”, npm with pinned --prefix, pnpm/bun/vp globals). Codex gains standalone-installer updates; Cursor binds updates to the resolved cursor-agent path.

API shape: snapshots expose resolveMaintenance() (hourly cache, { fresh: true } to re-derive) instead of a static maintenanceCapabilities. The registry resolves maintenance from the instance registry (not stale reconciliation state) and supports optional fresh. The update runner re-resolves before spawn and aborts if the lock key changed (“Provider installation changed”), then re-resolves after success for post-upgrade advisories.

UI: provider settings track in-flight updates per instance and show inline “Update now” when canUpdate is true for that instance.

Docs add provider-update internals and user-facing “Keeping Provider CLIs Current.”

Reviewed by Cursor Bugbot for commit 98c0e32. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Run provider updates only through the installer that owns the binary path

  • Replaces the static maintenanceCapabilities value on ServerProviderShape with a cached resolveMaintenance effect that performs path-based resolution.
  • Adds filesystem and child-process probes in providerMaintenance.ts to verify the real executable path belongs to a supported installer (npm, Homebrew, pnpm, Bun, or native).
  • providerMaintenanceRunner.updateProvider now performs a fresh capability resolution before command execution and aborts the update if the lock key differs from the cached capability.
  • Risk: Missing, unresolved, or mismatched installations now return manual-only capabilities, disabling one-click updates for paths that cannot be mapped to a supported installer.

Macroscope summarized 98c0e32.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Sep 3, 2026
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CodexDriver.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts Outdated
…s the binary
T3 Code could pick an update command from one installation while the active
executable belonged to another: a standalone Codex install got `npm install
-g` (#5629), a `claude-code@latest` keg got `brew upgrade claude-code`
(#6245), and Homebrew casks were compared against npm's latest so `brew
upgrade` could never clear the advisory (#7730).
Ownership is now derived from the resolved executable's real path. Native
installs run their own `update` subcommand on the resolved binary; Homebrew
kegs and casks get the exact formula or cask name from the path, and their
latest version from `brew info`; npm installs are pinned to the global prefix
that owns the package. Anything unproven stays manual-only but still shows the
version gap. Resolution is cached per instance and re-read before and after
executing an update so the command matches the install at click time.
Settings tracks in-flight updates per instance instead of per driver so two
instances of one driver can be updated independently.
Closes#5629Closes#6245Closes#7730
Co-Authored-By: Claude Code <noreply@anthropic.com>
@juliusmarminge
juliusmarmingeforce-pushed the fix/provider-maintenance-ownership branch from bab148d to 7009338CompareSeptember 3, 2026 03:08
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.5 KiB13.5 KiB+2 B (+0.0%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB−1 B (−0.0%)7.3 KiB
CodexLive turn WebSocket wire6.6 KiB6.6 KiB+3 B (+0.0%)7.8 KiB
CodexLive turn WebSocket decoded57.0 KiB57.0 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages10100 (0.0%)21
ClaudeTotal thread wire13.5 KiB13.3 KiB−176 B (−1.3%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB−3 B (−0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.6 KiB6.4 KiB−173 B (−2.6%)7.8 KiB
ClaudeLive turn WebSocket decoded57.8 KiB56.4 KiB−1.4 KiB (−2.5%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 66419a1 · PR result: 98c0e32 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment threadapps/web/src/components/settings/ProviderSettingsPanel.tsx
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/Layers/ProviderRegistry.ts Outdated
Comment threadapps/server/src/provider/providerMaintenance.ts
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
@macroscopeapp

macroscopeappBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XL PR substantially rewires provider update ownership detection, advisory resolution, command execution, post-update verification, and per-instance settings behavior across server and web code. The changes affect which installers can run on a customer machine and add cross-platform filesystem and Homebrew process probing, creating a runtime scope that requires human review.

You can add or adjust custom eligibility rules. Learn more.

juliusmarmingeand others added 2 commits September 2, 2026 20:23
Review follow-ups on the ownership resolver:
- npm: only `<prefix>/lib/node_modules/<pkg>/` counts as a global install on
POSIX; a project-local `node_modules` no longer yields `npm install -g
--prefix <project>`. Windows keeps the shim-plus-manifest proof.
- Homebrew: the keg prefix must match `brew --prefix` of the resolved `brew`,
so a `Cellar` directory elsewhere is not offered `brew upgrade`. stderr is
drained during probes.
- A configured binary path that does not exist resolves to no context instead
of a native update command it cannot run.
- Codex standalone detection matches any CODEX_HOME, not just `~/.codex`.
- Cursor resolves through the same cache so `fresh` re-reads the executable.
- The registry reads instances from ProviderInstanceRegistry rather than the
trailing live-subscription map.
- Settings only offers inline updates for `behind_latest` advisories.
- `catchTag` → `catchTags` per repo convention.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit b92c85b. Configure here.

Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Comment threadapps/server/src/provider/Drivers/CursorDriver.ts
Cursor's resolver returned a `cursor-agent` update for a configured binary
that could not be resolved, under the same lock key, so the runner would run
whatever `cursor-agent` PATH found. No resolved executable now means
manual-only. The displayed command stays `cursor-agent update` like the other
providers.
Windows npm ownership also accepts the extensionless sh script npm writes
beside `<cmd>.cmd`; the manifest-beside-shim proof is gated to win32 because a
POSIX project checkout has the same shape.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment threadapps/server/src/provider/providerMaintenance.ts Outdated
Co-Authored-By: Claude Code <noreply@anthropic.com>
@ettoc00ChatGPT Codex Connector

ettoc00 commented Sep 3, 2026

Copy link
Copy Markdown

Quick Windows validation update: Scoop and WinGet both completed real provider updates successfully across Claude and OpenCode through the actual resolver + maintenance runner, including a same-provider Scoop/WinGet coexistence case where only the explicitly selected installation was updated.

I also found one generic post-update edge case on Windows: if the updater exits successfully but the provider disappears or its installation can no longer be ownership-verified afterward, the runner can still report succeeded simply because it is no longer behind_latest.

Separately, I found a few test-only Windows portability issues in the existing fixtures, e.g. POSIX/macOS symlink fixtures hitting EPERM, a Darwin fixture being evaluated with Windows path semantics, and a /tmp expectation that isn’t host-normalized.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

2 participants

@juliusmarminge@ettoc00