Files
pikasTech-unidesk/scripts/src/platform-infra-sub2api-codex/config.ts
T
2026-06-25 16:16:25 +00:00

746 lines
41 KiB
TypeScript

// 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<string>();
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<string>();
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<string, CodexPoolManualBindingSource> = {};
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<string, unknown>, 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<string>();
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<string>();
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<string>();
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`);
}