diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index fe9ec4c4fc81..2ee436f3dcbf 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -1,3 +1,5 @@ +import * as NodeOS from "node:os"; + import { historyResponseItems } from "../ContextHandoffBudget.ts"; import type { ProviderAdapterV2HistoricalContext } from "../ProviderAdapter.ts"; import { @@ -38,6 +40,7 @@ import * as Predicate from "effect/Predicate"; import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; @@ -45,6 +48,7 @@ import * as Stream from "effect/Stream"; import { TestClock } from "effect/testing"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; +import { ServerConfig } from "../../config.ts"; import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; import { @@ -77,6 +81,7 @@ import { CodexAppServerClientFactory, codexAppServerClientFactoryFromSettingsLayer, type CodexAppServerClientFactoryShape, + createCodexAdapterV2, makeCodexAdapterV2, makeCodexAppServerProtocolLogger, makeCodexAppServerSpawnCommand, @@ -746,6 +751,49 @@ describe("CodexAdapterV2 process spawning", () => { ]); }).pipe(Effect.provideService(HostProcessPlatform, "linux")), ); + + it.effect("expands ~ in the configured binary path before spawning", () => + Effect.gen(function* () { + const spawnedCommands: Array = []; + const spawner = ChildProcessSpawner.make((command) => { + if (ChildProcess.isStandardCommand(command)) spawnedCommands.push(command.command); + return Effect.fail( + PlatformError.systemError({ _tag: "NotFound", module: "ChildProcess", method: "spawn" }), + ); + }); + const path = yield* Path.Path; + const adapter = yield* createCodexAdapterV2({ + instanceId: CODEX_DEFAULT_INSTANCE_ID, + displayName: undefined, + environment: [], + enabled: true, + config: { ...DEFAULT_CODEX_SETTINGS, binaryPath: "~/bin/codex" }, + }).pipe( + Effect.provide( + Layer.mergeAll( + codexAppServerClientFactoryFromSettingsLayer, + ServerConfig.layerTest(process.cwd(), { prefix: "t3-codex-binary-home-" }), + ), + ), + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(ProviderEventLoggers, NoOpProviderEventLoggers), + ); + + yield* adapter + .openSession({ + threadId: ThreadId.make("thread-binary-home"), + providerSessionId: ProviderSessionId.make("provider-session-binary-home"), + modelSelection: CODEX_TEST_MODEL_SELECTION, + runtimePolicy: CODEX_TEST_RUNTIME_POLICY, + }) + .pipe(Effect.scoped, Effect.exit); + + assert.deepEqual(spawnedCommands, [path.join(NodeOS.homedir(), "bin", "codex")]); + }).pipe( + Effect.provide(Layer.merge(idAllocatorLayer, NodeServices.layer)), + Effect.provideService(HostProcessPlatform, "linux"), + ), + ); }); describe("CodexAdapterV2 dynamic tool projection", () => { diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index 5af8a14fbf65..46cb637eb932 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -79,6 +79,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts"; import { ServerConfig } from "../../config.ts"; +import { expandHomePath } from "../../pathExpansion.ts"; import { buildCodexDeveloperInstructions } from "../../provider/CodexDeveloperInstructions.ts"; import { describeMcpElicitation, @@ -1411,6 +1412,7 @@ export const createCodexAdapterV2 = ( const settings = { ...config, enabled, + binaryPath: expandHomePath(config.binaryPath), homePath: homeLayout.effectiveHomePath ?? "", } satisfies CodexSettings;