// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:1317-2034 for #903. import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import type { RenderedCliResult } from "../output"; import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service"; import { shortSha256Fingerprint } from "../platform-infra-ops-library"; import { codexPoolSentinelSummary, codexPoolSentinelRuntimeImage, readCodexPoolSentinelConfig, renderCodexPoolSentinelManifest, type CodexPoolSentinelConfig, type CodexPoolSentinelProfileSecret, } from "../platform-infra-sub2api-codex-sentinel"; import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import { runSshCommandCapture, type SshCaptureResult } from "../ssh"; import type { CodexPoolCaddyEdgeRetryConfig, CodexPoolConfig, CodexPoolLocalCodexConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualAccountsConfig, CodexPoolManualBindingKind, CodexPoolManualBindingProvider, CodexPoolManualBindingSource, CodexPoolManualBindingSourcesConfig, CodexPoolProfileConfig, CodexPoolPublicExposureConfig, CodexProfile, CodexSentinelProtectPolicy, CodexTempUnschedulablePolicy, CodexTempUnschedulableRule, OpenAIResponsesWebSocketsV2Mode } from "./types"; import { booleanValue, integerArrayConfigField, integerConfigField, isRecord, normalizeBaseUrl, numberValue, readRequiredBaseUrl, readRequiredDescription, readRequiredEmail, readRequiredPort, readRequiredPositiveNumber, requiredRecordConfigField, requiredStringConfigField, stringValue, validateKubernetesName } from "./config-utils"; import { readAuthAPIKey, uniqueAccountName } from "./redaction"; import { codexPoolConfigPath } from "./types"; export function collectCodexProfiles(): CodexProfile[] { const codexDir = join(homedir(), ".codex"); const pool = readCodexPoolConfig(); if (!existsSync(codexDir)) return []; const seenAccountNames = new Set(); return pool.profiles.map((entry) => { const resolved = resolveProfileFiles(codexDir, entry); const profile = entry.profile; const accountName = entry.accountName ?? uniqueAccountName(profile, seenAccountNames); seenAccountNames.add(accountName); const configFile = resolved.configFile; const authFile = resolved.authFile; const configPath = join(codexDir, configFile); const authPath = join(codexDir, authFile); const base: CodexProfile = { profile, accountName, configFile, authFile, provider: "", baseUrl: "", wireApi: null, model: null, envKey: null, apiKey: null, apiKeySource: null, openaiResponsesWebSocketsV2Mode: entry.openaiResponsesWebSocketsV2Mode, upstreamUserAgent: entry.upstreamUserAgent, trustUpstream: entry.trustUpstream, sentinelProtect: entry.sentinelProtect, priority: entry.priority, capacity: entry.capacity ?? pool.defaultAccountCapacity, loadFactor: entry.loadFactor ?? pool.defaultAccountLoadFactor, tempUnschedulable: entry.tempUnschedulable, authOpenAIKeyShape: existsSync(authPath) ? "unknown" : "missing", ok: false, error: null, }; try { if (!existsSync(configPath)) throw new Error(`config file ${configFile} is missing`); const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as unknown; if (!isRecord(parsed)) throw new Error("config is not a TOML object"); const providers = parsed.model_providers; if (!isRecord(providers)) throw new Error("model_providers is missing"); const provider = stringValue(parsed.model_provider) ?? Object.keys(providers)[0] ?? ""; if (provider === "") throw new Error("model_provider is missing"); const providerConfig = providers[provider]; if (!isRecord(providerConfig)) throw new Error(`model provider ${provider} is missing`); const baseUrl = normalizeBaseUrl(stringValue(providerConfig.base_url)); if (baseUrl === null) throw new Error(`model provider ${provider} base_url is missing or invalid`); base.provider = provider; base.baseUrl = baseUrl; base.envKey = stringValue(providerConfig.env_key); base.wireApi = stringValue(providerConfig.wire_api); base.model = stringValue(parsed.model); const auth = readAuthAPIKey(authPath); base.authOpenAIKeyShape = auth.shape; if (auth.apiKey !== null) { base.apiKey = auth.apiKey; base.apiKeySource = "auth-json"; } else if (base.envKey !== null && typeof process.env[base.envKey] === "string" && process.env[base.envKey]!.length > 0) { base.apiKey = process.env[base.envKey]!; base.apiKeySource = "env"; } if (base.apiKey === null || base.apiKey.length === 0) { throw new Error(base.envKey === null ? "auth OPENAI_API_KEY is missing or empty" : `auth OPENAI_API_KEY is missing and env ${base.envKey} is not present`); } base.ok = true; return base; } catch (error) { base.error = error instanceof Error ? error.message : String(error); return base; } }); } export function resolveProfileFiles(codexDir: string, profile: CodexPoolProfileConfig): { configFile: string; authFile: string } { const configFile = existsSync(join(codexDir, profile.configFile)) || profile.fallbackConfigFile === null ? profile.configFile : profile.fallbackConfigFile; const authFile = existsSync(join(codexDir, profile.authFile)) || profile.fallbackAuthFile === null ? profile.authFile : profile.fallbackAuthFile; return { configFile, authFile }; } export function readCodexPoolConfig(): CodexPoolConfig { if (!existsSync(codexPoolConfigPath)) throw new Error(`${codexPoolConfigPath} is required`); const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown; if (!isRecord(parsed)) throw new Error(`${codexPoolConfigPath} must contain a YAML object`); const version = integerConfigField(parsed, "version", ""); if (version !== 1) throw new Error(`${codexPoolConfigPath}.version must be 1`); const kind = requiredStringConfigField(parsed, "kind", ""); if (kind !== "platform-infra-sub2api-codex-pool") throw new Error(`${codexPoolConfigPath}.kind must be platform-infra-sub2api-codex-pool`); const metadata = requiredRecordConfigField(parsed, "metadata", ""); const pool = parsed.pool; if (!isRecord(pool)) throw new Error(`${codexPoolConfigPath}.pool must be a YAML object`); if (!isRecord(parsed.profiles)) throw new Error(`${codexPoolConfigPath}.profiles must be a YAML object`); rejectSchedulableYamlField(pool, "pool"); const defaultTempUnschedulable = readTempUnschedulablePolicy(pool.defaultTempUnschedulable, "pool.defaultTempUnschedulable"); const defaultAccountPriorityValue = readAccountPriority(pool.defaultAccountPriority, "pool.defaultAccountPriority"); const defaultAccountCapacityValue = readAccountCapacity(pool.defaultAccountCapacity, "pool.defaultAccountCapacity"); const defaultAccountLoadFactorValue = readAccountLoadFactor(pool.defaultAccountLoadFactor, "pool.defaultAccountLoadFactor"); const defaultSentinelProtect = readSentinelProtectPolicy(pool.defaultSentinelProtect, "pool.defaultSentinelProtect"); const profiles = readProfileConfig( parsed.profiles, defaultTempUnschedulable, defaultSentinelProtect, defaultAccountPriorityValue, ); const manualAccounts = readManualAccountsConfig(parsed.manualAccounts); assertProtectedManualAccountsNotManaged(profiles, manualAccounts); const declaredAccountCapacity = desiredProfileCapacityTotal(profiles, defaultAccountCapacityValue); const minOwnerConcurrencySource = pool.minOwnerConcurrency === undefined || pool.minOwnerConcurrency === null ? "auto" : "yaml"; const minOwnerConcurrency = minOwnerConcurrencySource === "auto" ? Math.max(1, declaredAccountCapacity) : readOwnerConcurrency(pool.minOwnerConcurrency, "pool.minOwnerConcurrency"); const config: CodexPoolConfig = { version, kind, metadata: { id: requiredStringConfigField(metadata, "id", "metadata"), owner: requiredStringConfigField(metadata, "owner", "metadata"), relatedIssues: integerArrayConfigField(metadata, "relatedIssues", "metadata"), }, groupName: requiredStringConfigField(pool, "groupName", "pool"), groupDescription: readRequiredDescription(pool.groupDescription, "pool.groupDescription", 300), apiKeyName: requiredStringConfigField(pool, "apiKeyName", "pool"), apiKeySecretName: requiredStringConfigField(pool, "apiKeySecretName", "pool"), apiKeySecretKey: requiredStringConfigField(pool, "apiKeySecretKey", "pool"), adminEmailDefault: readRequiredEmail(pool.adminEmailDefault, "pool.adminEmailDefault"), minOwnerBalanceUsd: readRequiredPositiveNumber(pool.minOwnerBalanceUsd, "pool.minOwnerBalanceUsd"), minOwnerConcurrency, minOwnerConcurrencySource, defaultAccountPriority: defaultAccountPriorityValue, defaultAccountCapacity: defaultAccountCapacityValue, defaultAccountLoadFactor: defaultAccountLoadFactorValue, defaultTempUnschedulable, defaultSentinelProtect, profiles, manualAccounts, publicExposure: readPublicExposureConfig(parsed.publicExposure), localCodex: readLocalCodexConfig(parsed.localCodex), sentinel: { ...readCodexPoolSentinelConfig(parsed.sentinel, codexPoolConfigPath), protectedManualAccounts: manualAccounts.protected.map((account) => account.accountName), }, }; validateKubernetesName(config.groupName, "pool.groupName", false); validateKubernetesName(config.apiKeySecretName, "pool.apiKeySecretName", true); if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(config.apiKeySecretKey)) { throw new Error(`${codexPoolConfigPath}.pool.apiKeySecretKey must be a valid secret key name`); } if (config.minOwnerBalanceUsd <= 0) throw new Error(`${codexPoolConfigPath}.pool.minOwnerBalanceUsd must be > 0`); if (declaredAccountCapacity > 0 && config.minOwnerConcurrency < declaredAccountCapacity) { throw new Error(`${codexPoolConfigPath}.pool.minOwnerConcurrency must be >= the sum of declared account capacities (${declaredAccountCapacity})`); } return config; } export function readProfileConfig( value: unknown, defaultTempUnschedulable: CodexTempUnschedulablePolicy, defaultSentinelProtect: CodexSentinelProtectPolicy, defaultPriority: number, ): CodexPoolProfileConfig[] { if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.profiles must be a YAML object`); const entries = value.entries; if (!Array.isArray(entries)) throw new Error(`${codexPoolConfigPath}.profiles.entries must be a YAML array`); return entries.map((entry, index) => { if (!isRecord(entry)) throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}] must be an object`); rejectSchedulableYamlField(entry, `profiles.entries[${index}]`); const profile = stringValue(entry.profile); const configFile = stringValue(entry.configFile); const authFile = stringValue(entry.authFile); if (profile === null || configFile === null || authFile === null) { throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}] requires profile, configFile, and authFile`); } const accountName = stringValue(entry.accountName); if (accountName !== null) validateKubernetesName(accountName, `profiles.entries[${index}].accountName`, false); validateCodexFileName(configFile, `profiles.entries[${index}].configFile`); validateCodexFileName(authFile, `profiles.entries[${index}].authFile`); const fallbackConfigFile = stringValue(entry.fallbackConfigFile); const fallbackAuthFile = stringValue(entry.fallbackAuthFile); if (fallbackConfigFile !== null) validateCodexFileName(fallbackConfigFile, `profiles.entries[${index}].fallbackConfigFile`); if (fallbackAuthFile !== null) validateCodexFileName(fallbackAuthFile, `profiles.entries[${index}].fallbackAuthFile`); const openaiResponsesWebSocketsV2Mode = readOpenAIResponsesWebSocketsV2Mode(entry.openaiResponsesWebSocketsV2Mode, `profiles.entries[${index}].openaiResponsesWebSocketsV2Mode`); const upstreamUserAgent = readUpstreamUserAgent(entry.upstreamUserAgent, `profiles.entries[${index}].upstreamUserAgent`); const trustUpstream = readTrustUpstream(entry.trustUpstream, `profiles.entries[${index}].trustUpstream`); const sentinelProtect = readSentinelProtectPolicy(entry.sentinelProtect, `profiles.entries[${index}].sentinelProtect`, defaultSentinelProtect); const priority = readAccountPriority(entry.priority, `profiles.entries[${index}].priority`, defaultPriority); const capacity = entry.capacity === undefined || entry.capacity === null ? null : readAccountCapacity(entry.capacity, `profiles.entries[${index}].capacity`); const loadFactor = entry.loadFactor === undefined || entry.loadFactor === null ? null : readAccountLoadFactor(entry.loadFactor, `profiles.entries[${index}].loadFactor`); const tempUnschedulable = readTempUnschedulablePolicy(entry.tempUnschedulable, `profiles.entries[${index}].tempUnschedulable`, defaultTempUnschedulable); return { profile, accountName, configFile, authFile, fallbackConfigFile, fallbackAuthFile, openaiResponsesWebSocketsV2Mode, upstreamUserAgent, trustUpstream, sentinelProtect, priority, capacity, loadFactor, tempUnschedulable, }; }); } export function desiredProfileCapacityTotal(profiles: CodexPoolProfileConfig[], defaultCapacity: number): number { return profiles.reduce((total, profile) => total + (profile.capacity ?? defaultCapacity), 0); } export function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig { if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.manualAccounts must be a YAML object`); const bindingSources = readManualBindingSources(value.bindingSources, "manualAccounts.bindingSources"); const protectedRaw = value.protected; if (!Array.isArray(protectedRaw)) throw new Error(`${codexPoolConfigPath}.manualAccounts.protected must be a YAML array`); const seen = new Set(); const protectedAccounts = protectedRaw.map((entry, index): CodexPoolManualAccountProtection => { const key = `manualAccounts.protected[${index}]`; const accountName = typeof entry === "string" ? readManualAccountName(entry, key) : isRecord(entry) ? readManualAccountName(entry.accountName, `${key}.accountName`) : null; if (accountName === null) throw new Error(`${codexPoolConfigPath}.${key} must be an account name string or object with accountName`); const normalized = accountName.toLowerCase(); if (seen.has(normalized)) throw new Error(`${codexPoolConfigPath}.${key}.accountName is duplicated in manualAccounts.protected`); seen.add(normalized); const reason = isRecord(entry) ? readManualAccountReason(entry.reason, `${key}.reason`) : null; const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`, bindingSources) : null; const groupBinding = isRecord(entry) ? readManualAccountGroupBinding(entry.groupBinding, `${key}.groupBinding`, bindingSources) : null; return { accountName, reason, proxyBinding, groupBinding }; }); return { bindingSources, protected: protectedAccounts }; } export function readManualBindingSources(value: unknown, key: string): CodexPoolManualBindingSourcesConfig { if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); const items: CodexPoolManualBindingSource[] = []; const byId: Record = {}; for (const [id, rawSource] of Object.entries(value)) { const path = `${key}.${id}`; validateManualBindingSourceId(id, path); if (!isRecord(rawSource)) throw new Error(`${codexPoolConfigPath}.${path} must be a YAML object`); const source: CodexPoolManualBindingSource = { id, enabled: readBooleanConfig(rawSource.enabled, `${path}.enabled`), kind: readManualBindingKind(rawSource.kind, `${path}.kind`), provider: readManualBindingProvider(rawSource.provider, `${path}.provider`), description: readManualAccountReason(rawSource.description, `${path}.description`), }; if (source.kind === "proxy" && source.provider !== "target-egress-proxy") { throw new Error(`${codexPoolConfigPath}.${path}.provider must be target-egress-proxy for kind=proxy`); } if (source.kind === "group" && source.provider !== "pool-group") { throw new Error(`${codexPoolConfigPath}.${path}.provider must be pool-group for kind=group`); } byId[id] = source; items.push(source); } if (items.length === 0) throw new Error(`${codexPoolConfigPath}.${key} must declare at least one source`); return { items, byId }; } export function readManualAccountProxyBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): CodexPoolManualAccountProxyBinding | null { if (value === undefined || value === null) return null; if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); const enabled = readBooleanConfig(value.enabled, `${key}.enabled`); const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "proxy", enabled); const proxyName = stringValue(value.proxyName); if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`); validateProxyName(proxyName, `${key}.proxyName`); return { enabled, source, proxyName, }; } export function readManualAccountGroupBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): CodexPoolManualAccountGroupBinding | null { if (value === undefined || value === null) return null; if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); const enabled = readBooleanConfig(value.enabled, `${key}.enabled`); const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "group", enabled); return { enabled, source, }; } export function readManualBindingSourceRef(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig, expectedKind: CodexPoolManualBindingKind, bindingEnabled: boolean): string { const sourceId = stringValue(value); if (sourceId === null) throw new Error(`${codexPoolConfigPath}.${key} is required`); validateManualBindingSourceId(sourceId, key); const source = bindingSources.byId[sourceId]; if (source === undefined) throw new Error(`${codexPoolConfigPath}.${key} references unknown manualAccounts.bindingSources id ${sourceId}`); if (source.kind !== expectedKind) throw new Error(`${codexPoolConfigPath}.${key} references ${sourceId} with kind=${source.kind}; expected ${expectedKind}`); if (bindingEnabled && !source.enabled) throw new Error(`${codexPoolConfigPath}.${key} references disabled manualAccounts.bindingSources.${sourceId}`); return sourceId; } export function readManualBindingKind(value: unknown, key: string): CodexPoolManualBindingKind { const text = stringValue(value); if (text !== "proxy" && text !== "group") throw new Error(`${codexPoolConfigPath}.${key} must be proxy or group`); return text; } export function readManualBindingProvider(value: unknown, key: string): CodexPoolManualBindingProvider { const text = stringValue(value); if (text !== "target-egress-proxy" && text !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key} must be target-egress-proxy or pool-group`); return text; } export function validateManualBindingSourceId(value: string, key: string): void { if (!/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported source id format`); } export function readManualAccountName(value: unknown, key: string): string | null { const text = stringValue(value)?.trim() ?? null; if (text === null || text.length === 0) return null; if (text.length > 256) throw new Error(`${codexPoolConfigPath}.${key} must be at most 256 characters`); if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`); if (!/^[^<>"'`\\]+$/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} contains unsupported characters`); return text; } export function readManualAccountReason(value: unknown, key: string): string | null { if (value === undefined || value === null) return null; const text = stringValue(value)?.trim() ?? null; if (text === null || text.length === 0) return null; if (text.length > 240) throw new Error(`${codexPoolConfigPath}.${key} must be at most 240 characters`); if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`); return text; } export function assertProtectedManualAccountsNotManaged(profiles: CodexPoolProfileConfig[], manualAccounts: CodexPoolManualAccountsConfig): void { const protectedNames = new Set(manualAccounts.protected.map((account) => account.accountName.toLowerCase())); for (const [index, profile] of profiles.entries()) { const accountName = profile.accountName; if (accountName !== null && protectedNames.has(accountName.toLowerCase())) { throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}].accountName is listed in manualAccounts.protected; protected manual accounts must not be YAML-managed`); } } } export function rejectSchedulableYamlField(value: Record, key: string): void { if (Object.prototype.hasOwnProperty.call(value, "schedulable")) { throw new Error(`${codexPoolConfigPath}.${key}.schedulable is process control, not durable YAML config; use codex-pool sync actions instead`); } } export function readOpenAIResponsesWebSocketsV2Mode(value: unknown, key: string): OpenAIResponsesWebSocketsV2Mode | null { if (value === undefined || value === null) return null; const text = stringValue(value); if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a string`); if (text === "off" || text === "ctx_pool" || text === "passthrough") return text; throw new Error(`${codexPoolConfigPath}.${key} must be one of off, ctx_pool, passthrough`); } export function readUpstreamUserAgent(value: unknown, key: string): string | null { if (value === undefined || value === null) return null; const text = stringValue(value); if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a string`); if (text.length > 512) throw new Error(`${codexPoolConfigPath}.${key} must be at most 512 characters`); if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`); return text; } export function readTrustUpstream(value: unknown, key: string): boolean { if (value === undefined || value === null) return false; const parsed = booleanValue(value); if (parsed === null) throw new Error(`${codexPoolConfigPath}.${key} must be a boolean`); return parsed; } export function readSentinelProtectPolicy(value: unknown, key: string, fallback?: CodexSentinelProtectPolicy): CodexSentinelProtectPolicy { if (value === undefined || value === null) { if (fallback !== undefined) return fallback; throw new Error(`${codexPoolConfigPath}.${key} is required`); } if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback?.enabled); const required = ["consecutiveFailures", "initialRetryDelaySeconds", "maxRetryDelaySeconds", "backoffMultiplier"]; for (const field of required) { if (fallback === undefined && (value[field] === undefined || value[field] === null)) { throw new Error(`${codexPoolConfigPath}.${key}.${field} is required`); } if (enabled && value[field] === undefined && fallback === undefined) { throw new Error(`${codexPoolConfigPath}.${key}.${field} is required when enabled=true`); } } const consecutiveFailures = value.consecutiveFailures === undefined || value.consecutiveFailures === null ? fallback?.consecutiveFailures : readBoundedInteger(value.consecutiveFailures, `${key}.consecutiveFailures`, 1, 20); const initialRetryDelaySeconds = value.initialRetryDelaySeconds === undefined || value.initialRetryDelaySeconds === null ? fallback?.initialRetryDelaySeconds : readBoundedInteger(value.initialRetryDelaySeconds, `${key}.initialRetryDelaySeconds`, 1, 3600); const maxRetryDelaySeconds = value.maxRetryDelaySeconds === undefined || value.maxRetryDelaySeconds === null ? fallback?.maxRetryDelaySeconds : readBoundedInteger(value.maxRetryDelaySeconds, `${key}.maxRetryDelaySeconds`, 1, 3600); const backoffMultiplier = value.backoffMultiplier === undefined || value.backoffMultiplier === null ? fallback?.backoffMultiplier : readBoundedInteger(value.backoffMultiplier, `${key}.backoffMultiplier`, 1, 10); if (consecutiveFailures === undefined || initialRetryDelaySeconds === undefined || maxRetryDelaySeconds === undefined || backoffMultiplier === undefined) { throw new Error(`${codexPoolConfigPath}.${key} is missing required sentinel protection fields`); } if (maxRetryDelaySeconds < initialRetryDelaySeconds) { throw new Error(`${codexPoolConfigPath}.${key}.maxRetryDelaySeconds must be >= initialRetryDelaySeconds`); } return { enabled, consecutiveFailures, initialRetryDelaySeconds, maxRetryDelaySeconds, backoffMultiplier, }; } export function readBoundedInteger(value: unknown, key: string, min: number, max: number): number { const parsed = numberValue(value); if (parsed === null || !Number.isInteger(parsed) || parsed < min || parsed > max) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer from ${min} to ${max}`); } return parsed; } export function readAccountPriority(value: unknown, key: string, fallback?: number): number { if (value === undefined || value === null) { if (fallback !== undefined) return fallback; throw new Error(`${codexPoolConfigPath}.${key} is required`); } const priority = numberValue(value); if (priority === null || !Number.isInteger(priority) || priority < 0 || priority > 1000) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 0 to 1000`); } return priority; } export function readAccountCapacity(value: unknown, key: string): number { const capacity = numberValue(value); if (capacity === null || !Number.isInteger(capacity) || capacity < 1 || capacity > 1000) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 1 to 1000`); } return capacity; } export function readOwnerConcurrency(value: unknown, key: string): number { const concurrency = numberValue(value); if (concurrency === null || !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100000) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 1 to 100000`); } return concurrency; } export function readAccountLoadFactor(value: unknown, key: string): number { const loadFactor = numberValue(value); if (loadFactor === null || !Number.isInteger(loadFactor) || loadFactor < 1 || loadFactor > 1000) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 1 to 1000`); } return loadFactor; } export function readCaddyTimeoutSeconds(value: unknown, key: string): number { if (value === undefined || value === null) throw new Error(`${codexPoolConfigPath}.${key} is required`); const seconds = numberValue(value); if (seconds === null || !Number.isInteger(seconds) || seconds < 30 || seconds > 900) { throw new Error(`${codexPoolConfigPath}.${key} must be an integer from 30 to 900`); } return seconds; } export function readCaddyEdgeRetryConfig(value: unknown, key: string): CodexPoolCaddyEdgeRetryConfig { if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); const enabled = readBooleanConfig(value.enabled, `${key}.enabled`); const tryDurationSeconds = readPositiveInteger(value.tryDurationSeconds, `${key}.tryDurationSeconds`); const tryIntervalMilliseconds = readPositiveInteger(value.tryIntervalMilliseconds, `${key}.tryIntervalMilliseconds`); const retryMatchValue = isRecord(value.retryMatch) ? value.retryMatch : null; if (retryMatchValue === null) throw new Error(`${codexPoolConfigPath}.${key}.retryMatch must be a YAML object`); const retryMatch = { methods: readCaddyRetryMethods(retryMatchValue.methods, `${key}.retryMatch.methods`), paths: readCaddyRetryPaths(retryMatchValue.paths, `${key}.retryMatch.paths`), }; if (retryMatch.methods.length === 0 && retryMatch.paths.length === 0) { throw new Error(`${codexPoolConfigPath}.${key}.retryMatch must include at least one method or path matcher when enabled=true`); } return { enabled, tryDurationSeconds, tryIntervalMilliseconds, retryMatch }; } export function readPositiveInteger(value: unknown, key: string): number { const parsed = numberValue(value); if (parsed === null || !Number.isInteger(parsed) || parsed < 1) { throw new Error(`${codexPoolConfigPath}.${key} must be a positive integer`); } return parsed; } export function readPositiveIntegerConfig(value: unknown, key: string): number { if (value === undefined || value === null) throw new Error(`${codexPoolConfigPath}.${key} is required`); return readPositiveInteger(value, key); } export function readCaddyRetryMethods(value: unknown, key: string): string[] { if (value === undefined || value === null) return []; if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`); const seen = new Set(); const methods: string[] = []; for (const item of value) { const method = stringValue(item)?.toUpperCase() ?? null; if (method === null || !/^[A-Z][A-Z0-9_-]*$/u.test(method)) throw new Error(`${codexPoolConfigPath}.${key} entries must be HTTP method tokens`); if (seen.has(method)) continue; seen.add(method); methods.push(method); } return methods; } export function readCaddyRetryPaths(value: unknown, key: string): string[] { if (value === undefined || value === null) return []; if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`); const seen = new Set(); const paths: string[] = []; for (const item of value) { const path = stringValue(item); if (path === null || !path.startsWith("/") || /[\r\n]/u.test(path)) { throw new Error(`${codexPoolConfigPath}.${key} entries must be Caddy path matchers starting with / and without newlines`); } if (seen.has(path)) continue; seen.add(path); paths.push(path); } return paths; } export function readTempUnschedulablePolicy(value: unknown, key: string, fallback?: CodexTempUnschedulablePolicy): CodexTempUnschedulablePolicy { if (value === undefined || value === null) { if (fallback !== undefined) return cloneTempUnschedulablePolicy(fallback); throw new Error(`${codexPoolConfigPath}.${key} is required`); } if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); const enabled = readBooleanConfig(value.enabled, `${key}.enabled`, fallback?.enabled); let rules: CodexTempUnschedulableRule[]; if (value.rules === undefined || value.rules === null) { if (fallback === undefined) throw new Error(`${codexPoolConfigPath}.${key}.rules is required`); rules = cloneTempUnschedulablePolicy(fallback).rules; } else { rules = readTempUnschedulableRules(value.rules, `${key}.rules`); } if (enabled && rules.length === 0) throw new Error(`${codexPoolConfigPath}.${key}.rules must not be empty when enabled=true`); return { enabled, rules }; } export function readTempUnschedulableRules(value: unknown, key: string): CodexTempUnschedulableRule[] { if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`); return value.map((entry, index) => { if (!isRecord(entry)) throw new Error(`${codexPoolConfigPath}.${key}[${index}] must be an object`); const statusCode = numberValue(entry.statusCode ?? entry.errorCode); if (statusCode === null || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) { throw new Error(`${codexPoolConfigPath}.${key}[${index}].statusCode must be an HTTP status code from 100 to 599`); } const durationMinutes = numberValue(entry.durationMinutes); if (durationMinutes === null || !Number.isInteger(durationMinutes) || durationMinutes < 1 || durationMinutes > 1440) { throw new Error(`${codexPoolConfigPath}.${key}[${index}].durationMinutes must be an integer from 1 to 1440`); } const keywords = readTempUnschedulableKeywords(entry.keywords, `${key}[${index}].keywords`); const description = stringValue(entry.description); if (description !== null && description.length > 240) throw new Error(`${codexPoolConfigPath}.${key}[${index}].description must be at most 240 characters`); return { statusCode, keywords, durationMinutes, description }; }); } export function readTempUnschedulableKeywords(value: unknown, key: string): string[] { if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array`); const seen = new Set(); const keywords: string[] = []; for (const item of value) { const keyword = stringValue(item); if (keyword === null) throw new Error(`${codexPoolConfigPath}.${key} entries must be non-empty strings`); if (keyword.length > 120) throw new Error(`${codexPoolConfigPath}.${key} entries must be at most 120 characters`); if (/[\r\n]/u.test(keyword)) throw new Error(`${codexPoolConfigPath}.${key} entries must not contain newlines`); const normalized = keyword.toLowerCase(); if (seen.has(normalized)) continue; seen.add(normalized); keywords.push(keyword); } if (keywords.length === 0) throw new Error(`${codexPoolConfigPath}.${key} must not be empty`); return keywords; } export function cloneTempUnschedulablePolicy(policy: CodexTempUnschedulablePolicy): CodexTempUnschedulablePolicy { return { enabled: policy.enabled, rules: policy.rules.map((rule) => ({ statusCode: rule.statusCode, keywords: [...rule.keywords], durationMinutes: rule.durationMinutes, description: rule.description, })), }; } export function readBooleanConfig(value: unknown, key: string, fallback?: boolean): boolean { if (value === undefined || value === null) { if (fallback !== undefined) return fallback; throw new Error(`${codexPoolConfigPath}.${key} is required`); } const parsed = booleanValue(value); if (parsed === null) throw new Error(`${codexPoolConfigPath}.${key} must be a boolean`); return parsed; } export function readPublicExposureConfig(value: unknown): CodexPoolPublicExposureConfig { if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.publicExposure must be a YAML object`); const masterFrpsValue = requiredRecordConfigField(value, "masterFrps", "publicExposure"); const masterCaddyValue = requiredRecordConfigField(value, "masterCaddy", "publicExposure"); const config: CodexPoolPublicExposureConfig = { enabled: readBooleanConfig(value.enabled, "publicExposure.enabled"), proxyName: requiredStringConfigField(value, "proxyName", "publicExposure"), configMapName: requiredStringConfigField(value, "configMapName", "publicExposure"), deploymentName: requiredStringConfigField(value, "deploymentName", "publicExposure"), frpcImage: requiredStringConfigField(value, "frpcImage", "publicExposure"), serverAddr: requiredStringConfigField(value, "serverAddr", "publicExposure"), serverPort: readRequiredPort(value.serverPort, "publicExposure.serverPort"), remotePort: readRequiredPort(value.remotePort, "publicExposure.remotePort"), localIP: requiredStringConfigField(value, "localIP", "publicExposure"), localPort: readRequiredPort(value.localPort, "publicExposure.localPort"), publicBaseUrl: readRequiredBaseUrl(value.publicBaseUrl, "publicExposure.publicBaseUrl"), masterBaseUrl: readRequiredBaseUrl(value.masterBaseUrl, "publicExposure.masterBaseUrl"), masterFrps: { configPath: requiredStringConfigField(masterFrpsValue, "configPath", "publicExposure.masterFrps"), containerName: requiredStringConfigField(masterFrpsValue, "containerName", "publicExposure.masterFrps"), }, masterCaddy: { enabled: readBooleanConfig(masterCaddyValue.enabled, "publicExposure.masterCaddy.enabled"), domain: requiredStringConfigField(masterCaddyValue, "domain", "publicExposure.masterCaddy"), configPath: requiredStringConfigField(masterCaddyValue, "configPath", "publicExposure.masterCaddy"), serviceName: requiredStringConfigField(masterCaddyValue, "serviceName", "publicExposure.masterCaddy"), upstreamBaseUrl: readRequiredBaseUrl(masterCaddyValue.upstreamBaseUrl, "publicExposure.masterCaddy.upstreamBaseUrl"), responseHeaderTimeoutSeconds: readCaddyTimeoutSeconds( masterCaddyValue.responseHeaderTimeoutSeconds, "publicExposure.masterCaddy.responseHeaderTimeoutSeconds", ), edgeRetry: readCaddyEdgeRetryConfig( masterCaddyValue.edgeRetry, "publicExposure.masterCaddy.edgeRetry", ), }, }; validateKubernetesName(config.configMapName, "publicExposure.configMapName", true); validateKubernetesName(config.deploymentName, "publicExposure.deploymentName", true); validateProxyName(config.proxyName, "publicExposure.proxyName"); validatePort(config.serverPort, "publicExposure.serverPort"); validatePort(config.remotePort, "publicExposure.remotePort"); validatePort(config.localPort, "publicExposure.localPort"); if (!/^[A-Za-z0-9._:/-]+$/u.test(config.frpcImage)) throw new Error(`${codexPoolConfigPath}.publicExposure.frpcImage has an unsupported format`); if (!/^[A-Za-z0-9._:-]+$/u.test(config.serverAddr)) throw new Error(`${codexPoolConfigPath}.publicExposure.serverAddr has an unsupported format`); if (!/^[A-Za-z0-9._:-]+$/u.test(config.localIP)) throw new Error(`${codexPoolConfigPath}.publicExposure.localIP has an unsupported format`); if (!config.masterFrps.configPath.startsWith("/")) throw new Error(`${codexPoolConfigPath}.publicExposure.masterFrps.configPath must be absolute`); validateProxyName(config.masterFrps.containerName, "publicExposure.masterFrps.containerName"); validatePublicHostname(config.masterCaddy.domain, "publicExposure.masterCaddy.domain"); if (!config.masterCaddy.configPath.startsWith("/")) throw new Error(`${codexPoolConfigPath}.publicExposure.masterCaddy.configPath must be absolute`); validateProxyName(config.masterCaddy.serviceName, "publicExposure.masterCaddy.serviceName"); const upstream = new URL(config.masterCaddy.upstreamBaseUrl); if (upstream.protocol !== "http:") throw new Error(`${codexPoolConfigPath}.publicExposure.masterCaddy.upstreamBaseUrl must use http:// for the local upstream`); return config; } export function readLocalCodexConfig(value: unknown): CodexPoolLocalCodexConfig { if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.localCodex must be a YAML object`); const config = { backupSuffix: requiredStringConfigField(value, "backupSuffix", "localCodex"), providerName: requiredStringConfigField(value, "providerName", "localCodex"), wireApi: requiredStringConfigField(value, "wireApi", "localCodex"), modelContextWindow: readPositiveIntegerConfig(value.modelContextWindow, "localCodex.modelContextWindow"), modelAutoCompactTokenLimit: readPositiveIntegerConfig(value.modelAutoCompactTokenLimit, "localCodex.modelAutoCompactTokenLimit"), supportsWebSockets: readBooleanConfig(value.supportsWebSockets, "localCodex.supportsWebSockets"), responsesWebSocketsV2: readBooleanConfig(value.responsesWebSocketsV2, "localCodex.responsesWebSocketsV2"), responsesSmokeModel: requiredStringConfigField(value, "responsesSmokeModel", "localCodex"), }; if (!/^[A-Za-z0-9._-]+$/u.test(config.backupSuffix)) throw new Error(`${codexPoolConfigPath}.localCodex.backupSuffix has an unsupported format`); validateProxyName(config.providerName, "localCodex.providerName"); validateProxyName(config.wireApi, "localCodex.wireApi"); validateModelName(config.responsesSmokeModel, "localCodex.responsesSmokeModel"); return config; } export function validateCodexFileName(value: string, key: string): void { if (!/^(config\.toml|config\.toml\.[A-Za-z0-9._-]+|auth\.json|auth\.json\.[A-Za-z0-9._-]+)$/u.test(value)) { throw new Error(`${codexPoolConfigPath}.${key} has an unsupported file name`); } } export function validateProxyName(value: string, key: string): void { if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`); } export function validateModelName(value: string, key: string): void { if (!/^[A-Za-z0-9._:-]+$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported model name`); } export function validatePublicHostname(value: string, key: string): void { if (value.length > 253 || !/^[A-Za-z0-9.-]+$/u.test(value) || value.startsWith(".") || value.endsWith(".")) { throw new Error(`${codexPoolConfigPath}.${key} has an unsupported hostname format`); } } export function validatePort(value: number, key: string): void { if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error(`${codexPoolConfigPath}.${key} must be an integer port`); }