From e228530d05c377297c229f36af3aad2e946df5b4 Mon Sep 17 00:00:00 2001 From: Matheson Steplock Date: Thu, 27 Aug 2026 00:05:58 -0400 Subject: [PATCH] fix(server): use Codex native updater --- .../src/provider/Drivers/CodexDriver.ts | 8 ++ .../src/provider/providerMaintenance.test.ts | 84 +++++++++++++++++ .../src/provider/providerMaintenance.ts | 91 +++++++++++++++---- .../providerMaintenanceRunner.test.ts | 52 +++++++++++ .../src/provider/providerMaintenanceRunner.ts | 10 +- 5 files changed, 225 insertions(+), 20 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 15d7a1ff0216..542b4119f8b5 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -65,6 +65,14 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ npmPackageName: "@openai/codex", homebrewFormula: "codex", nativeUpdate: null, + versionedNativeUpdate: { + // `codex update` first shipped in stable Codex 0.128.0. Older CLIs + // still need the package-manager command selected by maintenance. + minimumVersion: "0.128.0", + executable: "codex", + args: ["update"], + lockKey: "codex-native", + }, }); /** diff --git a/apps/server/src/provider/providerMaintenance.test.ts b/apps/server/src/provider/providerMaintenance.test.ts index 5683da2c1a82..6e26ce50f38b 100644 --- a/apps/server/src/provider/providerMaintenance.test.ts +++ b/apps/server/src/provider/providerMaintenance.test.ts @@ -59,6 +59,18 @@ const scopedPackageToolUpdate = makePackageManagedProviderMaintenanceResolver({ isCommandPath: isNativeTestCommandPath("/.scoped-package-tool/bin/scoped-package-tool"), }, }); +const versionedPackageToolUpdate = makePackageManagedProviderMaintenanceResolver({ + provider: driver("versionedPackageTool"), + npmPackageName: "@example/versioned-package-tool", + homebrewFormula: "versioned-package-tool", + nativeUpdate: null, + versionedNativeUpdate: { + minimumVersion: "2.0.0", + executable: "versioned-package-tool", + args: ["update"], + lockKey: "versioned-package-tool-native", + }, +}); const staticToolUpdate = makeStaticProviderMaintenanceResolver( makeProviderMaintenanceCapabilities({ provider: driver("staticTool"), @@ -183,6 +195,78 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => { }); }); + it("uses the resolved native updater once the installed version supports it", () => { + const capabilities = versionedPackageToolUpdate.resolve({ + binaryPath: "versioned-package-tool", + resolvedCommandPath: "/opt/versioned/bin/versioned-package-tool", + }); + + expect(capabilities.versionedUpdate?.update).toMatchObject({ + command: "versioned-package-tool update", + executable: "/opt/versioned/bin/versioned-package-tool", + args: ["update"], + lockKey: "versioned-package-tool-native", + }); + expect( + createProviderVersionAdvisory({ + driver: driver("versionedPackageTool"), + currentVersion: "2.0.0", + latestVersion: "2.1.0", + maintenanceCapabilities: capabilities, + }), + ).toMatchObject({ + updateCommand: "versioned-package-tool update", + canUpdate: true, + }); + }); + + it("keeps the package-manager fallback for versions without the native updater", () => { + const capabilities = versionedPackageToolUpdate.resolve({ + binaryPath: "versioned-package-tool", + resolvedCommandPath: "/usr/local/lib/node_modules/@example/versioned-package-tool/bin.js", + }); + + expect( + createProviderVersionAdvisory({ + driver: driver("versionedPackageTool"), + currentVersion: "1.9.9", + latestVersion: "2.1.0", + maintenanceCapabilities: capabilities, + }), + ).toMatchObject({ + updateCommand: + "npm install -g --allow-scripts=@example/versioned-package-tool @example/versioned-package-tool@latest", + canUpdate: true, + }); + }); + + it("only enables standalone updates when the installed version supports the native updater", () => { + const capabilities = versionedPackageToolUpdate.resolve({ + binaryPath: "/opt/versioned/bin/versioned-package-tool", + resolvedCommandPath: "/opt/versioned/bin/versioned-package-tool", + }); + + expect( + createProviderVersionAdvisory({ + driver: driver("versionedPackageTool"), + currentVersion: "1.9.9", + latestVersion: "2.1.0", + maintenanceCapabilities: capabilities, + }), + ).toMatchObject({ updateCommand: null, canUpdate: false }); + expect( + createProviderVersionAdvisory({ + driver: driver("versionedPackageTool"), + currentVersion: "2.0.0", + latestVersion: "2.1.0", + maintenanceCapabilities: capabilities, + }), + ).toMatchObject({ + updateCommand: "/opt/versioned/bin/versioned-package-tool update", + canUpdate: true, + }); + }); + it("keeps update commands owned by provider maintenance capabilities", () => { expect(staticToolUpdate.resolve()).toEqual({ provider: driver("staticTool"), diff --git a/apps/server/src/provider/providerMaintenance.ts b/apps/server/src/provider/providerMaintenance.ts index 14d17cf365c3..bf62b8e63393 100644 --- a/apps/server/src/provider/providerMaintenance.ts +++ b/apps/server/src/provider/providerMaintenance.ts @@ -40,7 +40,10 @@ const readCommandLookupEnv = CommandLookupEnvConfig.pipe(Effect.orElseSucceed(() export interface ProviderMaintenanceCapabilities { readonly provider: ProviderDriverKind; readonly packageName: string | null; + /** Update path for versions that predate a provider-owned updater. */ readonly update: ProviderMaintenanceCommandAction | null; + /** Preferred update path once the installed provider reaches its minimum version. */ + readonly versionedUpdate?: ProviderMaintenanceVersionedCommandAction; } export interface ProviderMaintenanceCommandAction { @@ -50,6 +53,11 @@ export interface ProviderMaintenanceCommandAction { readonly lockKey: string; } +export interface ProviderMaintenanceVersionedCommandAction { + readonly minimumVersion: string; + readonly update: ProviderMaintenanceCommandAction; +} + export interface ProviderMaintenanceCapabilityResolutionOptions { readonly binaryPath?: string | null; readonly env?: NodeJS.ProcessEnv; @@ -73,6 +81,12 @@ export interface PackageManagedProviderMaintenanceDefinition { readonly lockKey: string; readonly isCommandPath: (commandPath: string) => boolean; } | null; + readonly versionedNativeUpdate?: { + readonly minimumVersion: string; + readonly executable: string; + readonly args: ReadonlyArray; + readonly lockKey: string; + }; } export interface ProviderVersionCacheEntry { @@ -278,13 +292,36 @@ export function resolvePackageManagedProviderMaintenance( options?: ProviderMaintenanceCapabilityResolutionOptions, ): ProviderMaintenanceCapabilities { const binaryPath = nonEmptyString(options?.binaryPath); + const resolvedCommandPath = + options?.resolvedCommandPath ?? + (binaryPath && hasPathSeparator(binaryPath) ? binaryPath : null); + const withVersionedUpdate = ( + capabilities: ProviderMaintenanceCapabilities, + ): ProviderMaintenanceCapabilities => { + const versionedNativeUpdate = definition.versionedNativeUpdate; + if (!versionedNativeUpdate) { + return capabilities; + } + + const commandExecutable = binaryPath ?? versionedNativeUpdate.executable; + return { + ...capabilities, + versionedUpdate: { + minimumVersion: versionedNativeUpdate.minimumVersion, + update: { + command: [commandExecutable, ...versionedNativeUpdate.args].join(" "), + executable: resolvedCommandPath ?? commandExecutable, + args: versionedNativeUpdate.args, + lockKey: versionedNativeUpdate.lockKey, + }, + }, + }; + }; + if (!binaryPath) { - return makeNpmGlobalProviderMaintenanceCapabilities(definition); + return withVersionedUpdate(makeNpmGlobalProviderMaintenanceCapabilities(definition)); } - const resolvedCommandPath = - options?.resolvedCommandPath ?? (hasPathSeparator(binaryPath) ? binaryPath : null); - if (resolvedCommandPath) { const commandPaths = [ resolvedCommandPath, @@ -296,36 +333,53 @@ export function resolvePackageManagedProviderMaintenance( nativeUpdate && commandPaths.some((commandPath) => nativeUpdate.isCommandPath(commandPath)) ) { - return ( + return withVersionedUpdate( makeNativeProviderMaintenanceCapabilities(definition) ?? - makeNpmGlobalProviderMaintenanceCapabilities(definition) + makeNpmGlobalProviderMaintenanceCapabilities(definition), ); } if (commandPaths.some(isVitePlusGlobalCommandPath)) { - return makeVitePlusGlobalProviderMaintenanceCapabilities(definition); + return withVersionedUpdate(makeVitePlusGlobalProviderMaintenanceCapabilities(definition)); } if (commandPaths.some(isBunGlobalCommandPath)) { - return makeBunGlobalProviderMaintenanceCapabilities(definition); + return withVersionedUpdate(makeBunGlobalProviderMaintenanceCapabilities(definition)); } if (commandPaths.some(isPnpmGlobalCommandPath)) { - return makePnpmGlobalProviderMaintenanceCapabilities(definition); + return withVersionedUpdate(makePnpmGlobalProviderMaintenanceCapabilities(definition)); } if (commandPaths.some(isNpmGlobalCommandPath)) { - return makeNpmGlobalProviderMaintenanceCapabilities(definition); + return withVersionedUpdate(makeNpmGlobalProviderMaintenanceCapabilities(definition)); } if (commandPaths.some(isHomebrewCommandPath)) { - return makeHomebrewProviderMaintenanceCapabilities(definition); + return withVersionedUpdate(makeHomebrewProviderMaintenanceCapabilities(definition)); } } if (!hasPathSeparator(binaryPath)) { - return makeNpmGlobalProviderMaintenanceCapabilities(definition); + return withVersionedUpdate(makeNpmGlobalProviderMaintenanceCapabilities(definition)); } - return makeManualOnlyProviderMaintenanceCapabilities({ - provider: definition.provider, - packageName: definition.npmPackageName, - }); + return withVersionedUpdate( + makeManualOnlyProviderMaintenanceCapabilities({ + provider: definition.provider, + packageName: definition.npmPackageName, + }), + ); +} + +export function resolveProviderMaintenanceUpdate( + capabilities: ProviderMaintenanceCapabilities, + currentVersion: string | null, +): ProviderMaintenanceCommandAction | null { + const versionedUpdate = capabilities.versionedUpdate; + if ( + versionedUpdate && + currentVersion && + compareSemverVersions(currentVersion, versionedUpdate.minimumVersion) >= 0 + ) { + return versionedUpdate.update; + } + return capabilities.update; } export function makePackageManagedProviderMaintenanceResolver( @@ -413,6 +467,7 @@ export function createProviderVersionAdvisory(input: { }): ServerProviderVersionAdvisory { const capabilities = input.maintenanceCapabilities ?? makeManualProviderMaintenanceCapabilities(input.driver); + const update = resolveProviderMaintenanceUpdate(capabilities, input.currentVersion); const latestVersion = input.latestVersion ?? null; const advisory = deriveVersionAdvisory({ currentVersion: input.currentVersion, @@ -423,8 +478,8 @@ export function createProviderVersionAdvisory(input: { status: advisory.status, currentVersion: input.currentVersion, latestVersion, - updateCommand: capabilities.update?.command ?? null, - canUpdate: capabilities.update !== null, + updateCommand: update?.command ?? null, + canUpdate: update !== null, checkedAt: input.checkedAt ?? null, message: advisory.message, }; diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 641c9b52e56c..b176288bc2f0 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -299,6 +299,58 @@ describe("providerMaintenanceRunner", () => { ); }); + it.effect("uses the version-gated native update command for supported provider versions", () => { + const calls: Array<{ command: string; args: ReadonlyArray }> = []; + return Effect.gen(function* () { + const { registry } = yield* makeRegistry({ + ...baseProvider, + version: "0.128.0", + }); + const fallback = makeProviderMaintenanceCapabilities({ + provider: CODEX_DRIVER, + packageName: "@openai/codex", + updateExecutable: "npm", + updateArgs: ["install", "-g", "@openai/codex@latest"], + updateLockKey: "npm-global", + }); + const updater = yield* makeTestRunner({ + ...registry, + getProviderMaintenanceCapabilitiesForInstance: () => + Effect.succeed({ + ...fallback, + versionedUpdate: { + minimumVersion: "0.128.0", + update: { + command: "codex update", + executable: "/opt/codex/bin/codex", + args: ["update"], + lockKey: "codex-native", + }, + }, + }), + }); + + yield* updater.updateProvider(CODEX_DRIVER); + assert.deepStrictEqual(calls, [ + { + command: "/opt/codex/bin/codex", + args: ["update"], + }, + ]); + }).pipe( + Effect.provide( + Layer.mergeAll( + NonWindowsPlatform, + latestVersionHttpClient("0.128.0"), + mockSpawnerLayer((command, args) => { + calls.push({ command, args }); + return { stdout: "updated" }; + }), + ), + ), + ); + }); + it.effect( "runs update commands through Effect ChildProcess when no test runner is injected", () => { diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index 3c114dd83d87..c2bdeb8cc11f 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -23,7 +23,10 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { ProviderRegistry } from "./Services/ProviderRegistry.ts"; import { makeProviderMaintenanceCommandCoordinator } from "./providerMaintenanceCommandCoordinator.ts"; -import { enrichProviderSnapshotWithVersionAdvisory } from "./providerMaintenance.ts"; +import { + enrichProviderSnapshotWithVersionAdvisory, + resolveProviderMaintenanceUpdate, +} from "./providerMaintenance.ts"; import type { ProviderMaintenanceCapabilities } from "./providerMaintenance.ts"; import { collectUint8StreamText } from "../stream/collectUint8StreamText.ts"; const isServerProviderUpdateError = Schema.is(ServerProviderUpdateError); @@ -297,7 +300,10 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () { instanceId, provider, ); - const update = capabilities.update; + const currentProvider = (yield* providerRegistry.getProviders).find( + (candidate) => candidate.instanceId === instanceId && candidate.driver === provider, + ); + const update = resolveProviderMaintenanceUpdate(capabilities, currentProvider?.version ?? null); if (!update) { return yield* new ServerProviderUpdateError({ provider,