Skip to content
1 change: 0 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({
npmPackageName: "@anthropic-ai/claude-code",
homebrewFormula: "claude-code",
nativeUpdate: {
executable: "claude",
args: ["update"],
lockKey: "claude-native",
isCommandPath: isClaudeNativeCommandPath,
Expand Down
147 changes: 147 additions & 0 deletions apps/server/src/provider/Drivers/CodexDriver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { describe, expect, it } from "@effect/vitest";

import {
deriveCodexStandaloneUpdateEnvironment,
isCodexStandaloneCommandPath,
resolveCodexProviderMaintenanceCapabilities,
} from "./CodexDriver.ts";

describe("isCodexStandaloneCommandPath", () => {
it("matches current-symlink standalone layouts", () => {
expect(
isCodexStandaloneCommandPath("/home/u/.codex/packages/standalone/current/bin/codex"),
).toBe(true);
expect(isCodexStandaloneCommandPath("/home/u/.codex/packages/standalone/current/codex")).toBe(
true,
);
});

it("matches the versioned release binary the current symlink resolves to", () => {
expect(
isCodexStandaloneCommandPath(
"/home/u/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl/codex",
),
).toBe(true);
expect(
isCodexStandaloneCommandPath(
"C:\\Users\\u\\.codex\\packages\\standalone\\releases\\0.111.0-x86_64-pc-windows-msvc\\codex.EXE",
),
).toBe(true);
});

it("rejects paths outside a standalone install", () => {
expect(isCodexStandaloneCommandPath("/home/u/.local/bin/codex")).toBe(false);
expect(isCodexStandaloneCommandPath("/usr/lib/node_modules/@openai/codex/bin/codex.js")).toBe(
false,
);
expect(
isCodexStandaloneCommandPath("/home/u/monorepo/packages/standalone/node_modules/.bin/codex"),
).toBe(false);
expect(
isCodexStandaloneCommandPath(
"/home/u/.codex/packages/standalone/releases/0.111.0/notes/codex.txt",
),
).toBe(false);
});
});

describe("deriveCodexStandaloneUpdateEnvironment", () => {
it("pins CODEX_HOME to the install root of the matched binary", () => {
expect(
deriveCodexStandaloneUpdateEnvironment(
"/home/julius/codex-home/packages/standalone/current/codex",
),
).toEqual({ CODEX_HOME: "/home/julius/codex-home" });
expect(
deriveCodexStandaloneUpdateEnvironment(
"/home/u/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl/codex",
),
).toEqual({ CODEX_HOME: "/home/u/.codex" });
expect(
deriveCodexStandaloneUpdateEnvironment(
"C:\\Users\\u\\.codex\\packages\\standalone\\releases\\1.0.0\\codex.exe",
),
).toEqual({ CODEX_HOME: "C:\\Users\\u\\.codex" });
});

it("returns null when no install root precedes the standalone segment", () => {
expect(deriveCodexStandaloneUpdateEnvironment("/packages/standalone/current/codex")).toBeNull();
});
});

describe("resolveCodexProviderMaintenanceCapabilities", () => {
it.each([
{
label: "bare command",
binaryPath: "codex",
resolvedCommandPath: "/home/u/.local/bin/codex",
expectedExecutable: "/home/u/.local/bin/codex",
},
{
label: "current alias",
binaryPath: "/home/u/.codex/packages/standalone/current/codex",
resolvedCommandPath: "/home/u/.codex/packages/standalone/current/codex",
expectedExecutable: "/home/u/.codex/packages/standalone/current/codex",
},
{
label: "visible alias",
binaryPath: "/usr/local/bin/codex",
resolvedCommandPath: "/usr/local/bin/codex",
expectedExecutable: "/usr/local/bin/codex",
},
])("allows native updates for a $label with a versioned release realpath", (testCase) => {
expect(
resolveCodexProviderMaintenanceCapabilities({
binaryPath: testCase.binaryPath,
platform: "linux",
resolvedCommandPath: testCase.resolvedCommandPath,
realCommandPath:
"/home/u/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl/codex",
}),
).toEqual({
provider: "codex",
packageName: "@openai/codex",
update: {
command: `CODEX_HOME=/home/u/.codex ${testCase.expectedExecutable} update`,
executable: testCase.expectedExecutable,
args: ["update"],
lockKey: "codex-native",
env: { CODEX_HOME: "/home/u/.codex" },
},
});
});

it.each([
"/home/u/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl/codex",
"/home/u/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl/bin/codex",
])("disables one-click updates for an explicitly configured release path: %s", (binaryPath) => {
expect(
resolveCodexProviderMaintenanceCapabilities({
binaryPath,
resolvedCommandPath: binaryPath,
realCommandPath: binaryPath,
}),
).toEqual({
provider: "codex",
packageName: "@openai/codex",
update: null,
});
});

it("disables one-click updates when a bare command resolves directly to a release", () => {
const releasePath =
"/home/u/.codex/packages/standalone/releases/0.111.0-x86_64-unknown-linux-musl/codex";

expect(
resolveCodexProviderMaintenanceCapabilities({
binaryPath: "codex",
resolvedCommandPath: releasePath,
realCommandPath: releasePath,
}),
).toEqual({
provider: "codex",
packageName: "@openai/codex",
update: null,
});
});
});
77 changes: 74 additions & 3 deletions apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
makeManualOnlyProviderMaintenanceCapabilities,
makePackageManagedProviderMaintenanceResolver,
normalizeCommandPath,
type ProviderMaintenanceCapabilityResolutionOptions,
type ProviderMaintenanceCapabilitiesResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
Expand All @@ -61,13 +65,80 @@ const decodeCodexSettings = Schema.decodeSync(CodexSettings);

const DRIVER_KIND = ProviderDriverKind.make("codex");
const SNAPSHOT_REFRESH_INTERVAL = Duration.minutes(5);
const UPDATE = makePackageManagedProviderMaintenanceResolver({

// The standalone installer's `current` entry is a symlink into `releases/`,
// so the realpath of an on-PATH codex is the versioned binary — which sits
// directly in the release dir, without a `bin/` segment.
const CODEX_STANDALONE_RELEASE_BINARY_PATTERN =
/\/packages\/standalone\/releases\/[^/]+\/(?:bin\/)?codex(?:\.exe)?$/;
const CODEX_STANDALONE_ROOT_SEGMENT_PATTERN = /\/packages\/standalone\//i;
const CODEX_NPM_PACKAGE_NAME = "@openai/codex";

export function isCodexStandaloneCommandPath(commandPath: string): boolean {
const normalized = normalizeCommandPath(commandPath);
return (
normalized.includes("/packages/standalone/") &&
(normalized.endsWith("/bin/codex") ||
normalized.endsWith("/bin/codex.exe") ||
normalized.endsWith("/packages/standalone/current/codex") ||
normalized.endsWith("/packages/standalone/current/codex.exe") ||
CODEX_STANDALONE_RELEASE_BINARY_PATTERN.test(normalized))
);
}

export function deriveCodexStandaloneUpdateEnvironment(
commandPath: string,
): Readonly<Record<string, string>> | null {
// `codex update` locates the standalone install via $CODEX_HOME, and the
// maintenance runner spawns with the server's own environment — pin
// CODEX_HOME to the install root the matched binary actually lives in.
const match = CODEX_STANDALONE_ROOT_SEGMENT_PATTERN.exec(commandPath.replaceAll("\\", "/"));
if (!match || match.index === 0) {
return null;
}
return { CODEX_HOME: commandPath.slice(0, match.index) };
}

const PACKAGE_MANAGED_UPDATE = makePackageManagedProviderMaintenanceResolver({
provider: DRIVER_KIND,
npmPackageName: "@openai/codex",
npmPackageName: CODEX_NPM_PACKAGE_NAME,
homebrewFormula: "codex",
nativeUpdate: null,
nativeUpdate: {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
args: ["update"],
lockKey: "codex-native",
isCommandPath: isCodexStandaloneCommandPath,
deriveEnv: deriveCodexStandaloneUpdateEnvironment,
},
});

export function resolveCodexProviderMaintenanceCapabilities(
options?: ProviderMaintenanceCapabilityResolutionOptions,
) {
const binaryPath = options?.binaryPath?.trim();
const resolvedCommandPath = options?.resolvedCommandPath?.trim() ?? binaryPath;

// A command that resolves directly to a release path stays pinned after
// `codex update`: the installer adds a sibling release and repoints
// `current`, but it does not replace that path. Keep matching release
// *realpaths* below so movable aliases still get native updates; only the
// path the provider will continue invoking makes this manual-only.
if (
resolvedCommandPath &&
CODEX_STANDALONE_RELEASE_BINARY_PATTERN.test(normalizeCommandPath(resolvedCommandPath))
) {
return makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: CODEX_NPM_PACKAGE_NAME,
});
}

return PACKAGE_MANAGED_UPDATE.resolve(options);
}

const UPDATE = {
resolve: resolveCodexProviderMaintenanceCapabilities,
} satisfies ProviderMaintenanceCapabilitiesResolver;

/**
* Services the driver needs to materialize an instance. Surfaced as the
* driver's `R` so the registry layer aggregates these across every
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/provider/Drivers/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({
npmPackageName: "opencode-ai",
homebrewFormula: "anomalyco/tap/opencode",
nativeUpdate: {
executable: "opencode",
args: ["upgrade"],
lockKey: "opencode-native",
isCommandPath: isOpenCodeNativeCommandPath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
displayName: "Codex (work)",
enabled: false,
config: makeCodexConfig({
binaryPath: "/opt/codex-work/bin/codex",
binaryPath: "/home/julius/codex-home/packages/standalone/current/codex",
homePath: "/home/julius/.codex",
customModels: ["work-preview"],
}),
Expand Down Expand Up @@ -180,6 +180,17 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
expect(workSnapshot.driver).toBe(codexDriverKind);
expect(workSnapshot.enabled).toBe(false);
expect(workSnapshot.continuation?.groupKey).toBe("codex:home:/home/julius/.codex");
expect(work!.snapshot.maintenanceCapabilities).toMatchObject({
packageName: "@openai/codex",
update: {
command:
"CODEX_HOME=/home/julius/codex-home /home/julius/codex-home/packages/standalone/current/codex update",
executable: "/home/julius/codex-home/packages/standalone/current/codex",
args: ["update"],
lockKey: "codex-native",
env: { CODEX_HOME: "/home/julius/codex-home" },
},
});

// Nothing goes to the unavailable bucket — both drivers are registered.
const unavailable = yield* registry.listUnavailable;
Expand Down
Loading
Loading