diff --git a/src/backend/codex-stdio.ts b/src/backend/codex-stdio.ts index 5f1a7bb..28255a5 100644 --- a/src/backend/codex-stdio.ts +++ b/src/backend/codex-stdio.ts @@ -1,7 +1,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { createHash } from "node:crypto"; import { accessSync, constants as fsConstants, readdirSync, readFileSync } from "node:fs"; -import { chmod, copyFile, mkdir } from "node:fs/promises"; +import { chmod, copyFile, mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import * as readline from "node:readline"; import type { BackendEvent, BackendProfile, BackendTurnResult, CommandRecord, FailureKind, InitialPromptAssembly, JsonRecord, JsonValue, RunRecord, TerminalStatus } from "../common/types.js"; @@ -852,6 +852,7 @@ async function prepareProjectedCodexHome(codexHome: string, projectedHome: strin await copyFile(path.join(projectedHome, fileName), path.join(codexHome, fileName)); await chmod(path.join(codexHome, fileName), 0o600); } + await normalizeCopiedCodexConfig(codexHome); return null; } catch (error) { const payload = { @@ -875,6 +876,29 @@ async function prepareProjectedCodexHome(codexHome: string, projectedHome: strin } } +async function normalizeCopiedCodexConfig(codexHome: string): Promise { + const configPath = path.join(codexHome, "config.toml"); + const modelCatalogPath = path.join(codexHome, "model-catalog.json"); + if (!fileReadable(modelCatalogPath).readable) return; + let configToml = ""; + try { + configToml = readFileSync(configPath, "utf8"); + } catch { + return; + } + const normalized = configToml.replace( + /(^\s*model_catalog_json\s*=\s*")([^"]+)("\s*$)/mu, + (_match, prefix: string, _oldValue: string, suffix: string) => `${prefix}${tomlBasicStringValue(modelCatalogPath)}${suffix}`, + ); + if (normalized === configToml) return; + await writeFile(configPath, normalized, { mode: 0o600 }); + await chmod(configPath, 0o600); +} + +function tomlBasicStringValue(value: string): string { + return value.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\""); +} + function codexHomeReadiness(codexHome: string, profile: BackendProfile): BackendTurnResult | null { const auth = fileReadable(`${codexHome}/auth.json`); const config = fileReadable(`${codexHome}/config.toml`); diff --git a/src/runner/k8s-job.ts b/src/runner/k8s-job.ts index 573891b..6863e28 100644 --- a/src/runner/k8s-job.ts +++ b/src/runner/k8s-job.ts @@ -250,7 +250,7 @@ export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { mani function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string; jobName: string; runnerJobId: string; runnerId: string; attemptId: string; sourceCommit: string; secretRefs: CredentialProjection[]; toolCredentials: ToolCredentialProjection[]; sessionPvc: RunnerSessionPvcOptions | undefined; runnerIdleTimeoutMs: number; missingTerminalAfterToolTimeoutMs: number; backendRetryMaxAttempts: number; backendRetryInitialBackoffMs: number; backendRetryMaxBackoffMs: number }): JsonRecord[] { const selectedSecret = context.secretRefs.find((item) => item.profile === options.run.backendProfile); - const codexHome = selectedSecret?.runtimeMountPath ?? defaultRuntimeHome(options.run.backendProfile); + const codexHome = runnerCodexHome(options.run.backendProfile, selectedSecret, context.sessionPvc); const bootRepoUrl = optionalString(options.bootRepoUrl) ?? defaultBootRepoUrl; return dedupeEnvVars([ { name: "AGENTRUN_MGR_URL", value: options.managerUrl }, @@ -285,6 +285,7 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string { name: "AGENTRUN_RUNNER_POLL_INTERVAL_MS", value: "250" }, { name: "HOME", value: "/home/agentrun" }, { name: "CODEX_HOME", value: codexHome }, + { name: "AGENTRUN_CODEX_HOME_STORAGE_KIND", value: context.sessionPvc ? "session-pvc" : "runner-home" }, ...runnerOtelEnvVars(process.env), ...(selectedSecret ? [{ name: "AGENTRUN_CODEX_SECRET_HOME", value: selectedSecret.projectionMountPath }] : []), ...(context.sessionPvc ? [ @@ -580,6 +581,11 @@ function normalizeMountPath(value: string | undefined, profile: string): string return value; } +function runnerCodexHome(profile: string, selectedSecret: CredentialProjection | undefined, sessionPvc: RunnerSessionPvcOptions | undefined): string { + if (!sessionPvc) return selectedSecret?.runtimeMountPath ?? defaultRuntimeHome(profile); + return `${sessionPvc.mountPath}/codex-home/${sanitizeVolumeName(profile)}`; +} + function defaultRuntimeHome(profile: string): string { return `/home/agentrun/.codex-${sanitizeVolumeName(profile)}`; }