feat: add Sub2API runtime configuration CRUD

This commit is contained in:
Codex
2026-07-13 20:17:08 +02:00
parent b8d6b7be03
commit 4d63c8e922
12 changed files with 696 additions and 284 deletions
@@ -21,7 +21,7 @@ import {
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 type { CodexPoolCaddyEdgeRetryConfig, CodexPoolConfig, CodexPoolLocalCodexConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualAccountsConfig, CodexPoolManualBindingKind, CodexPoolManualBindingProvider, CodexPoolManualBindingSource, CodexPoolManualBindingSourcesConfig, CodexPoolProfileConfig, CodexPoolPublicExposureConfig, CodexProfile, CodexRuntimeConfig, CodexRuntimeTemplate, 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";
@@ -59,6 +59,7 @@ export function collectCodexProfiles(): CodexProfile[] {
priority: entry.priority,
capacity: entry.capacity ?? pool.defaultAccountCapacity,
loadFactor: entry.loadFactor ?? pool.defaultAccountLoadFactor,
tempUnschedulableTemplate: entry.tempUnschedulableTemplate,
tempUnschedulable: entry.tempUnschedulable,
authOpenAIKeyShape: existsSync(authPath) ? "unknown" : "missing",
ok: false,
@@ -127,13 +128,21 @@ export function readCodexPoolConfig(): CodexPoolConfig {
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 runtime = readRuntimeConfig(parsed.runtime);
const defaultTempUnschedulableTemplate = readRuntimeTemplateRef(
pool.defaultTempUnschedulableTemplate,
"pool.defaultTempUnschedulableTemplate",
runtime,
);
const defaultTempUnschedulable = runtime.templatesById[defaultTempUnschedulableTemplate]!.tempUnschedulable;
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,
runtime,
defaultTempUnschedulableTemplate,
defaultTempUnschedulable,
defaultSentinelProtect,
defaultAccountPriorityValue,
@@ -165,9 +174,11 @@ export function readCodexPoolConfig(): CodexPoolConfig {
defaultAccountPriority: defaultAccountPriorityValue,
defaultAccountCapacity: defaultAccountCapacityValue,
defaultAccountLoadFactor: defaultAccountLoadFactorValue,
defaultTempUnschedulableTemplate,
defaultTempUnschedulable,
defaultSentinelProtect,
profiles,
runtime,
manualAccounts,
publicExposure: readPublicExposureConfig(parsed.publicExposure),
localCodex: readLocalCodexConfig(parsed.localCodex),
@@ -188,8 +199,46 @@ export function readCodexPoolConfig(): CodexPoolConfig {
return config;
}
export function readRuntimeConfig(value: unknown): CodexRuntimeConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.runtime must be a YAML object`);
if (!Array.isArray(value.templates)) throw new Error(`${codexPoolConfigPath}.runtime.templates must be a YAML array`);
const templates: CodexRuntimeTemplate[] = value.templates.map((item, index) => {
if (!isRecord(item)) throw new Error(`${codexPoolConfigPath}.runtime.templates[${index}] must be a YAML object`);
const id = requiredStringConfigField(item, "id", `runtime.templates[${index}]`);
if (!/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u.test(id)) {
throw new Error(`${codexPoolConfigPath}.runtime.templates[${index}].id has an unsupported format`);
}
const kind = requiredStringConfigField(item, "kind", `runtime.templates[${index}]`);
if (kind !== "temp-unschedulable") {
throw new Error(`${codexPoolConfigPath}.runtime.templates[${index}].kind must be temp-unschedulable`);
}
return {
id,
kind,
description: readRequiredDescription(item.description, `runtime.templates[${index}].description`, 300),
tempUnschedulable: readTempUnschedulablePolicy(item.spec, `runtime.templates[${index}].spec`),
};
});
if (templates.length === 0) throw new Error(`${codexPoolConfigPath}.runtime.templates must not be empty`);
const templatesById: Record<string, CodexRuntimeTemplate> = {};
for (const template of templates) {
if (templatesById[template.id] !== undefined) throw new Error(`${codexPoolConfigPath}.runtime.templates duplicates id ${template.id}`);
templatesById[template.id] = template;
}
return { templates, templatesById };
}
export function readRuntimeTemplateRef(value: unknown, key: string, runtime: CodexRuntimeConfig): string {
const id = stringValue(value);
if (id === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty template id`);
if (runtime.templatesById[id] === undefined) throw new Error(`${codexPoolConfigPath}.${key} references missing runtime template ${id}`);
return id;
}
export function readProfileConfig(
value: unknown,
runtime: CodexRuntimeConfig,
defaultTempUnschedulableTemplate: string,
defaultTempUnschedulable: CodexTempUnschedulablePolicy,
defaultSentinelProtect: CodexSentinelProtectPolicy,
defaultPriority: number,
@@ -221,7 +270,13 @@ export function readProfileConfig(
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);
if (entry.tempUnschedulable !== undefined) {
throw new Error(`${codexPoolConfigPath}.profiles.entries[${index}].tempUnschedulable was replaced by tempUnschedulableTemplate`);
}
const tempUnschedulableTemplate = entry.tempUnschedulableTemplate === undefined || entry.tempUnschedulableTemplate === null
? defaultTempUnschedulableTemplate
: readRuntimeTemplateRef(entry.tempUnschedulableTemplate, `profiles.entries[${index}].tempUnschedulableTemplate`, runtime);
const tempUnschedulable = runtime.templatesById[tempUnschedulableTemplate]?.tempUnschedulable ?? defaultTempUnschedulable;
return {
profile,
accountName,
@@ -236,6 +291,7 @@ export function readProfileConfig(
priority,
capacity,
loadFactor,
tempUnschedulableTemplate,
tempUnschedulable,
};
});