/** * CodexDriver — first concrete `ProviderDriver` in the new per-instance model. * * A driver is a plain value (not a Context.Service) whose `create()` returns * one `ProviderInstance` bundling: * - `snapshot` — the live `ServerProviderShape` for this instance; * - `adapter` — the Codex session/turn/approval runtime; * - `textGeneration` — commit/PR/branch/title generation via `codex exec`. * * Each call to `create()` captures the `codexConfig` argument in closures * owned by the returned instance. Two instances created with different * `homePath`s (e.g. `codex_personal` + `codex_work`) therefore run with * fully independent Codex app-server processes and `CODEX_HOME` * environments — no shared mutable state. * * Resource lifecycle: `create()` runs in a scope handed in by the registry. * Closing that scope releases the adapter's child processes, the managed * snapshot's refresh fibre, and the text-generation binaries' transient * scratch files. The registry uses this to tear down an instance when its * `providerInstances` entry disappears or its config changes. * * @module provider/Drivers/CodexDriver */ import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { HttpClient } from "effect/unstable/http"; import { ChildProcessSpawner } from "effect/unstable/process"; import { makeCodexTextGeneration } from "../../textGeneration/CodexTextGeneration.ts"; import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makeCodexAdapter } from "../Layers/CodexAdapter.ts"; import { checkCodexProviderStatus, makePendingCodexProvider, probeCodexSkillsForCwd, } from "../Layers/CodexProvider.ts"; import { resolveCodexLaunchArgs } from "../Layers/codexLaunchArgs.ts"; import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; import * as ModelManifest from "../ModelManifest.ts"; import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts"; import { withInstanceIdentity } from "./instanceIdentity.ts"; import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; import { enrichProviderSnapshotWithVersionAdvisory, makePackageManagedProviderMaintenanceResolver, resolveProviderMaintenanceCapabilitiesEffect, } from "../providerMaintenance.ts"; import { haveProviderSnapshotSettingsChanged, makeProviderSnapshotSettingsSource, type ProviderSnapshotSettings, } from "../providerUpdateSettings.ts"; import { codexContinuationIdentity, materializeCodexShadowHome, resolveCodexHomeLayout, } from "./CodexHomeLayout.ts"; const decodeCodexSettings = Schema.decodeSync(CodexSettings); const DRIVER_KIND = ProviderDriverKind.make("codex"); const UPDATE = makePackageManagedProviderMaintenanceResolver({ provider: DRIVER_KIND, npmPackageName: "@openai/codex", homebrewFormula: "codex", nativeUpdate: null, }); /** * Services the driver needs to materialize an instance. Surfaced as the * driver's `R` so the registry layer aggregates these across every * registered driver and the runtime satisfies them once. */ export type CodexDriverEnv = | BackgroundPolicy.BackgroundPolicy | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | ModelManifest.ModelManifest | Path.Path | ProviderEventLoggers | ServerConfig | ServerSettingsService; export const CodexDriver: ProviderDriver = { driverKind: DRIVER_KIND, metadata: { displayName: "Codex", supportsMultipleInstances: true, }, configSchema: CodexSettings, defaultConfig: (): CodexSettings => decodeCodexSettings({}), create: ({ instanceId, displayName, accentColor, environment, enabled, config }) => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const httpClient = yield* HttpClient.HttpClient; const serverSettings = yield* ServerSettingsService; const eventLoggers = yield* ProviderEventLoggers; const modelManifest = yield* ModelManifest.ModelManifest; const processEnv = mergeProviderInstanceEnvironment(environment); const homeLayout = yield* resolveCodexHomeLayout(config); const continuationIdentity = codexContinuationIdentity(homeLayout); const stampIdentity = withInstanceIdentity({ instanceId, driverKind: DRIVER_KIND, displayName, accentColor, continuationGroupKey: continuationIdentity.continuationKey, }); yield* materializeCodexShadowHome(homeLayout).pipe( Effect.mapError( (cause) => new ProviderDriverError({ driver: DRIVER_KIND, instanceId, detail: cause.message, cause, }), ), ); const effectiveConfig = { ...config, enabled, homePath: homeLayout.effectiveHomePath ?? "", } satisfies CodexSettings; const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, { binaryPath: effectiveConfig.binaryPath, env: processEnv, }); // `makeCodexAdapter` and `makeCodexTextGeneration` have `never` error // channels at construction time — their failure modes are all on the // per-operation closures they return. No `mapError` wrapper is needed // here; the registry only has to worry about snapshot-build and // spawner-availability failures surfaced from `checkCodexProviderStatus` // below. const adapter = yield* makeCodexAdapter(effectiveConfig, { instanceId, environment: processEnv, ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}), }); const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv); // Build a managed snapshot whose settings never change — mutations come // in as instance rebuilds from the registry rather than in-place // updates. Pre-provide `ChildProcessSpawner` so the check fits // `makeManagedServerProvider.checkProvider`'s `R = never`. // Kick the TTL-gated manifest refresh in the background and classify // with the in-memory manifest, so a slow or hung fetch never delays the // provider check. A refresh that lands mid-probe applies on the next one. const checkProvider = modelManifest.refreshInBackground.pipe( Effect.andThen( Effect.zipWith( checkCodexProviderStatus(effectiveConfig, undefined, processEnv), modelManifest.current, (draft, manifest) => stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)), { concurrent: true }, ), ), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), ); const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings); const snapshot = yield* makeManagedServerProvider>({ maintenanceCapabilities, getSettings: snapshotSettings.getSettings, streamSettings: snapshotSettings.streamSettings, haveSettingsChanged: haveProviderSnapshotSettingsChanged, initialSnapshot: (settings) => Effect.zipWith( makePendingCodexProvider(settings.provider), modelManifest.current, (draft, manifest) => stampIdentity(ModelManifest.applyModelManifest(draft, manifest, DRIVER_KIND)), ), checkProvider, enrichSnapshot: ({ settings, snapshot, publishSnapshot }) => enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, { enableProviderUpdateChecks: settings.enableProviderUpdateChecks, }).pipe( Effect.provideService(HttpClient.HttpClient, httpClient), Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)), ), }).pipe( Effect.mapError( (cause) => new ProviderDriverError({ driver: DRIVER_KIND, instanceId, detail: `Failed to build Codex snapshot: ${cause.message ?? String(cause)}`, cause, }), ), ); const snapshotForCwd = (cwd: string) => !effectiveConfig.enabled ? snapshot.getSnapshot : Effect.all([ snapshot.getSnapshot, probeCodexSkillsForCwd({ binaryPath: effectiveConfig.binaryPath, homePath: effectiveConfig.homePath, launchArgs: resolveCodexLaunchArgs(effectiveConfig.launchArgs, processEnv), cwd, environment: processEnv, }).pipe( Effect.scoped, Effect.timeout("20 seconds"), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), ), ]).pipe( Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills })), Effect.mapError( (cause) => new ProviderDriverError({ driver: DRIVER_KIND, instanceId, detail: `Failed to probe Codex skills for '${cwd}'`, cause, }), ), ); return { instanceId, driverKind: DRIVER_KIND, continuationIdentity, displayName, accentColor, enabled, snapshot, snapshotForCwd, adapter, textGeneration, } satisfies ProviderInstance; }), };