feat: manage sub2api codex pool

This commit is contained in:
Codex
2026-06-09 03:33:12 +00:00
parent 9973b0cb9c
commit b0fef5b44d
3 changed files with 836 additions and 25 deletions
@@ -4,3 +4,43 @@ pool:
apiKeySecretName: sub2api-codex-pool-api-key
apiKeySecretKey: API_KEY
minOwnerBalanceUsd: 1000
profiles:
entries:
- profile: default
accountName: unidesk-codex-default
configFile: config.toml.pre-sub2api
authFile: auth.json.pre-sub2api
fallbackConfigFile: config.toml
fallbackAuthFile: auth.json
- profile: HY
accountName: unidesk-codex-hy
configFile: config.toml.HY
authFile: auth.json.HY
- profile: gptclub
accountName: unidesk-codex-gptclub
configFile: config.toml.gptclub
authFile: auth.json.gptclub
- profile: pinche
accountName: unidesk-codex-pinche
configFile: config.toml.pinche
authFile: auth.json.pinche
publicExposure:
enabled: true
proxyName: platform-infra-sub2api
configMapName: sub2api-frpc-config
deploymentName: sub2api-frpc
frpcImage: fatedier/frpc:v0.68.1
serverAddr: 74.48.78.17
serverPort: 7000
remotePort: 21880
localIP: sub2api.platform-infra.svc.cluster.local
localPort: 8080
publicBaseUrl: http://74.48.78.17:21880
masterBaseUrl: http://127.0.0.1:21880
masterFrps:
configPath: /opt/hwlab-frp/frps.dev.toml
containerName: hwlab-frps-dev
localCodex:
backupSuffix: pre-sub2api
providerName: OpenAI
wireApi: responses
+1 -1
View File
@@ -63,7 +63,7 @@ export function rootHelp(): unknown {
{ command: "hwlab nodes control-plane|git-mirror|secret --node G14 --lane v03", description: "Manage HWLAB node/lane runtime prerequisites for v0.3+ with the node identity passed as data instead of a command family." },
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync|flush | hwlab g14 tools-image status|build", description: "Start the legacy G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror/relay maintenance, or fixed HWLAB CI tools image actions; long confirmed trigger/sync/flush actions return async jobs by default." },
{ command: "agentrun v01 queue|sessions|control-plane|git-mirror", description: "Use AgentRun v0.1 Queue and Sessions as the active commander entry through the official G14 CLI bridge, plus bounded Tekton/Argo and git-mirror operations." },
{ command: "platform-infra sub2api plan|apply|status|validate", description: "Deploy and validate the G14 k3s internal-only Sub2API service in the platform-infra namespace through the controlled UniDesk CLI." },
{ command: "platform-infra sub2api plan|apply|status|validate|codex-pool", description: "Deploy Sub2API in G14 platform-infra, manage the YAML-controlled Codex upstream pool, expose the unified API through FRP when needed, and configure master ~/.codex without printing API keys." },
{ command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Legacy D601 HWLAB DEV CD wrapper kept for explicit old-path diagnostics; current HWLAB rollout uses G14 GitOps." },
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
+795 -24
View File
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { UniDeskConfig } from "./config";
@@ -28,6 +28,10 @@ interface SyncOptions extends DisclosureOptions {
confirm: boolean;
}
interface ConfirmOptions extends DisclosureOptions {
confirm: boolean;
}
interface CodexProfile {
profile: string;
accountName: string;
@@ -51,19 +55,58 @@ interface CodexPoolConfig {
apiKeySecretName: string;
apiKeySecretKey: string;
minOwnerBalanceUsd: number;
profiles: CodexPoolProfileConfig[];
publicExposure: CodexPoolPublicExposureConfig;
localCodex: CodexPoolLocalCodexConfig;
}
interface CodexPoolProfileConfig {
profile: string;
accountName: string | null;
configFile: string;
authFile: string;
fallbackConfigFile: string | null;
fallbackAuthFile: string | null;
}
interface CodexPoolPublicExposureConfig {
enabled: boolean;
proxyName: string;
configMapName: string;
deploymentName: string;
frpcImage: string;
serverAddr: string;
serverPort: number;
remotePort: number;
localIP: string;
localPort: number;
publicBaseUrl: string;
masterBaseUrl: string;
masterFrps: {
configPath: string;
containerName: string;
};
}
interface CodexPoolLocalCodexConfig {
backupSuffix: string;
providerName: string;
wireApi: string;
}
export function codexPoolHelp(): unknown {
const pool = readCodexPoolConfig();
return {
command: "platform-infra sub2api codex-pool plan|sync|validate",
command: "platform-infra sub2api codex-pool plan|sync|validate|expose|configure-local",
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
],
description: "Import ~/.codex/config.toml plus config.toml.* API-key profiles into one Sub2API OpenAI pool and expose one internal API_KEY stored in a k3s Secret.",
description: "Import YAML-selected ~/.codex API-key profiles into one Sub2API OpenAI pool, expose one unified API_KEY, and optionally configure this master server's ~/.codex consumer endpoint.",
target: {
route: g14K3sRoute,
namespace,
@@ -72,6 +115,8 @@ export function codexPoolHelp(): unknown {
poolGroupName: pool.groupName,
poolApiKeySecretName: pool.apiKeySecretName,
poolApiKeySecretKey: pool.apiKeySecretKey,
publicBaseUrl: pool.publicExposure.enabled ? pool.publicExposure.publicBaseUrl : null,
masterBaseUrl: pool.publicExposure.enabled ? pool.publicExposure.masterBaseUrl : null,
secretValuesPrinted: false,
},
};
@@ -82,6 +127,8 @@ export async function runCodexPoolCommand(config: UniDeskConfig, args: string[])
if (action === "plan") return codexPoolPlan();
if (action === "sync") return await codexPoolSync(config, parseSyncOptions(args.slice(1)));
if (action === "validate") return await codexPoolValidate(config, parseDisclosureOptions(args.slice(1)));
if (action === "expose") return await codexPoolExpose(config, parseConfirmOptions(args.slice(1)));
if (action === "configure-local") return await codexPoolConfigureLocal(config, parseConfirmOptions(args.slice(1)));
return {
ok: false,
error: "unsupported-platform-infra-sub2api-codex-pool-command",
@@ -96,6 +143,12 @@ function parseSyncOptions(args: string[]): SyncOptions {
return { ...disclosure, confirm: args.includes("--confirm") };
}
function parseConfirmOptions(args: string[]): ConfirmOptions {
validateOptions(args, new Set(["--confirm", "--full", "--raw"]));
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm"));
return { ...disclosure, confirm: args.includes("--confirm") };
}
function parseDisclosureOptions(args: string[]): DisclosureOptions {
validateOptions(args, new Set(["--full", "--raw"]));
const raw = args.includes("--raw");
@@ -118,8 +171,8 @@ function codexPoolPlan(): Record<string, unknown> {
action: "platform-infra-sub2api-codex-pool-plan",
source: {
directory: join(homedir(), ".codex"),
configPattern: "config.toml and config.toml.*",
authPattern: "auth.json and auth.json.*",
configPattern: "YAML-selected config files under ~/.codex",
authPattern: "YAML-selected auth files under ~/.codex",
valuesPrinted: false,
},
target: poolTarget(),
@@ -132,6 +185,9 @@ function codexPoolPlan(): Record<string, unknown> {
accountType: "openai/apikey",
grouping: `All discovered Codex profiles are bound to one Sub2API group named ${pool.groupName}.`,
unifiedApiKey: `The client-facing API_KEY is controlled by k3s Secret ${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`,
publicExposure: pool.publicExposure.enabled
? `Master server consumers use ${pool.publicExposure.masterBaseUrl}; FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.`
: "Public FRP exposure is disabled by YAML.",
idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files.",
configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.",
},
@@ -222,22 +278,126 @@ async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptio
};
}
async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
const pool = readCodexPoolConfig();
if (!pool.publicExposure.enabled) {
return {
ok: true,
action: "platform-infra-sub2api-codex-pool-expose",
mode: "disabled-by-yaml",
target: poolTarget(pool),
};
}
if (!options.confirm) {
return {
ok: true,
action: "platform-infra-sub2api-codex-pool-expose",
mode: "dry-run",
target: poolTarget(pool),
publicExposure: publicExposureSummary(pool),
next: {
confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm",
},
};
}
const masterResult = await applyMasterFrpsAllowPort(pool);
const remoteResult = await capture(config, g14K3sRoute, ["script"], exposeScript(pool));
const parsed = parseJsonOutput(remoteResult.stdout);
const publicProbe = await probePublicModels(pool, "without-api-key");
const ok = masterResult.ok && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
if (options.raw) {
return {
ok,
action: "platform-infra-sub2api-codex-pool-expose",
masterFrps: masterResult,
remote: compactCapture(remoteResult, { full: true }),
parsed,
publicProbe,
};
}
return {
ok,
action: "platform-infra-sub2api-codex-pool-expose",
mode: "confirmed",
publicExposure: publicExposureSummary(pool),
masterFrps: masterResult,
remote: parsed ?? compactCapture(remoteResult, { full: options.full || remoteResult.exitCode !== 0 }),
publicProbe,
next: {
configureLocal: "bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
validate: "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
},
};
}
async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
const pool = readCodexPoolConfig();
const codexDir = join(homedir(), ".codex");
const configPath = join(codexDir, "config.toml");
const authPath = join(codexDir, "auth.json");
const backupConfigPath = join(codexDir, `config.toml.${pool.localCodex.backupSuffix}`);
const backupAuthPath = join(codexDir, `auth.json.${pool.localCodex.backupSuffix}`);
if (!options.confirm) {
return {
ok: true,
action: "platform-infra-sub2api-codex-pool-configure-local",
mode: "dry-run",
target: {
codexDir,
configPath,
authPath,
backupConfigPath,
backupAuthPath,
baseUrl: pool.publicExposure.masterBaseUrl,
providerName: pool.localCodex.providerName,
wireApi: pool.localCodex.wireApi,
valuesPrinted: false,
},
next: {
confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
},
};
}
if (!pool.publicExposure.enabled) throw new Error(`${codexPoolConfigPath}.publicExposure.enabled must be true before configure-local`);
const keyResult = await fetchPoolApiKey(config, pool);
if (keyResult.apiKey === null) {
return {
ok: false,
action: "platform-infra-sub2api-codex-pool-configure-local",
error: keyResult.error ?? "pool API key missing",
valuesPrinted: false,
};
}
const writeResult = writeLocalCodexConfig(pool, keyResult.apiKey);
const validateResult = await validatePublicGatewayWithKey(pool, keyResult.apiKey);
return {
ok: writeResult.ok && validateResult.ok,
action: "platform-infra-sub2api-codex-pool-configure-local",
mode: "confirmed",
local: writeResult,
validation: validateResult,
apiKey: {
secret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
keyPreview: apiKeyPreview(keyResult.apiKey),
apiKeyFingerprint: fingerprint(keyResult.apiKey),
valuesPrinted: false,
},
};
}
function collectCodexProfiles(): CodexProfile[] {
const codexDir = join(homedir(), ".codex");
const pool = readCodexPoolConfig();
if (!existsSync(codexDir)) return [];
const configFiles = readdirSync(codexDir)
.filter((file) => file === "config.toml" || file.startsWith("config.toml."))
.sort((a, b) => {
if (a === "config.toml") return -1;
if (b === "config.toml") return 1;
return a.localeCompare(b);
});
const seenAccountNames = new Set<string>();
return configFiles.map((configFile) => {
const suffix = configFile === "config.toml" ? "" : configFile.slice("config.toml.".length);
const profile = suffix === "" ? "default" : suffix;
const accountName = uniqueAccountName(profile, seenAccountNames);
const authFile = suffix === "" ? "auth.json" : `auth.json.${suffix}`;
const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir);
return configs.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 = {
@@ -258,6 +418,7 @@ function collectCodexProfiles(): CodexProfile[] {
};
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;
@@ -295,14 +456,40 @@ function collectCodexProfiles(): CodexProfile[] {
});
}
function discoverCodexProfileConfigs(codexDir: string): CodexPoolProfileConfig[] {
return readdirSync(codexDir)
.filter((file) => file === "config.toml" || file.startsWith("config.toml."))
.sort((a, b) => {
if (a === "config.toml") return -1;
if (b === "config.toml") return 1;
return a.localeCompare(b);
})
.map((configFile) => {
const suffix = configFile === "config.toml" ? "" : configFile.slice("config.toml.".length);
const profile = suffix === "" ? "default" : suffix;
return {
profile,
accountName: null,
configFile,
authFile: suffix === "" ? "auth.json" : `auth.json.${suffix}`,
fallbackConfigFile: null,
fallbackAuthFile: null,
};
});
}
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 };
}
function readCodexPoolConfig(): CodexPoolConfig {
const defaults: CodexPoolConfig = {
groupName: defaultPoolGroupName,
apiKeyName: defaultPoolApiKeyName,
apiKeySecretName: defaultPoolApiKeySecretName,
apiKeySecretKey: defaultPoolApiKeySecretKey,
minOwnerBalanceUsd: defaultMinOwnerBalanceUsd,
};
const defaults = defaultCodexPoolConfig();
if (!existsSync(codexPoolConfigPath)) return defaults;
const parsed = Bun.YAML.parse(readFileSync(codexPoolConfigPath, "utf8")) as unknown;
if (!isRecord(parsed)) throw new Error(`${codexPoolConfigPath} must contain a YAML object`);
@@ -314,6 +501,9 @@ function readCodexPoolConfig(): CodexPoolConfig {
apiKeySecretName: stringValue(pool.apiKeySecretName) ?? defaults.apiKeySecretName,
apiKeySecretKey: stringValue(pool.apiKeySecretKey) ?? defaults.apiKeySecretKey,
minOwnerBalanceUsd: numberValue(pool.minOwnerBalanceUsd) ?? defaults.minOwnerBalanceUsd,
profiles: readProfileConfig(parsed.profiles, defaults.profiles),
publicExposure: readPublicExposureConfig(parsed.publicExposure, defaults.publicExposure),
localCodex: readLocalCodexConfig(parsed.localCodex, defaults.localCodex),
};
validateKubernetesName(config.groupName, "pool.groupName", false);
validateKubernetesName(config.apiKeySecretName, "pool.apiKeySecretName", true);
@@ -324,6 +514,133 @@ function readCodexPoolConfig(): CodexPoolConfig {
return config;
}
function defaultCodexPoolConfig(): CodexPoolConfig {
return {
groupName: defaultPoolGroupName,
apiKeyName: defaultPoolApiKeyName,
apiKeySecretName: defaultPoolApiKeySecretName,
apiKeySecretKey: defaultPoolApiKeySecretKey,
minOwnerBalanceUsd: defaultMinOwnerBalanceUsd,
profiles: [],
publicExposure: {
enabled: false,
proxyName: "platform-infra-sub2api",
configMapName: "sub2api-frpc-config",
deploymentName: "sub2api-frpc",
frpcImage: "fatedier/frpc:v0.68.1",
serverAddr: "74.48.78.17",
serverPort: 7000,
remotePort: 21880,
localIP: serviceDns.split(":")[0],
localPort: 8080,
publicBaseUrl: "http://74.48.78.17:21880",
masterBaseUrl: "http://127.0.0.1:21880",
masterFrps: {
configPath: "/opt/hwlab-frp/frps.dev.toml",
containerName: "hwlab-frps-dev",
},
},
localCodex: {
backupSuffix: "pre-sub2api",
providerName: "OpenAI",
wireApi: "responses",
},
};
}
function readProfileConfig(value: unknown, defaults: CodexPoolProfileConfig[]): CodexPoolProfileConfig[] {
if (!isRecord(value)) return defaults;
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`);
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`);
return {
profile,
accountName,
configFile,
authFile,
fallbackConfigFile,
fallbackAuthFile,
};
});
}
function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExposureConfig): CodexPoolPublicExposureConfig {
if (!isRecord(value)) return defaults;
const masterFrpsValue = isRecord(value.masterFrps) ? value.masterFrps : {};
const config: CodexPoolPublicExposureConfig = {
enabled: booleanValue(value.enabled) ?? defaults.enabled,
proxyName: stringValue(value.proxyName) ?? defaults.proxyName,
configMapName: stringValue(value.configMapName) ?? defaults.configMapName,
deploymentName: stringValue(value.deploymentName) ?? defaults.deploymentName,
frpcImage: stringValue(value.frpcImage) ?? defaults.frpcImage,
serverAddr: stringValue(value.serverAddr) ?? defaults.serverAddr,
serverPort: numberValue(value.serverPort) ?? defaults.serverPort,
remotePort: numberValue(value.remotePort) ?? defaults.remotePort,
localIP: stringValue(value.localIP) ?? defaults.localIP,
localPort: numberValue(value.localPort) ?? defaults.localPort,
publicBaseUrl: normalizeBaseUrl(stringValue(value.publicBaseUrl)) ?? defaults.publicBaseUrl,
masterBaseUrl: normalizeBaseUrl(stringValue(value.masterBaseUrl)) ?? defaults.masterBaseUrl,
masterFrps: {
configPath: stringValue(masterFrpsValue.configPath) ?? defaults.masterFrps.configPath,
containerName: stringValue(masterFrpsValue.containerName) ?? defaults.masterFrps.containerName,
},
};
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");
return config;
}
function readLocalCodexConfig(value: unknown, defaults: CodexPoolLocalCodexConfig): CodexPoolLocalCodexConfig {
if (!isRecord(value)) return defaults;
const config = {
backupSuffix: stringValue(value.backupSuffix) ?? defaults.backupSuffix,
providerName: stringValue(value.providerName) ?? defaults.providerName,
wireApi: stringValue(value.wireApi) ?? defaults.wireApi,
};
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");
return config;
}
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`);
}
}
function validateProxyName(value: string, key: string): void {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`);
}
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`);
}
function readAuthAPIKey(authPath: string): { apiKey: string | null; shape: string } {
if (!existsSync(authPath)) return { apiKey: null, shape: "missing" };
const parsed = JSON.parse(readFileSync(authPath, "utf8")) as unknown;
@@ -385,6 +702,451 @@ function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
};
}
function publicExposureSummary(pool: CodexPoolConfig): Record<string, unknown> {
return {
enabled: pool.publicExposure.enabled,
proxyName: pool.publicExposure.proxyName,
namespace,
configMapName: pool.publicExposure.configMapName,
deploymentName: pool.publicExposure.deploymentName,
frpcImage: pool.publicExposure.frpcImage,
frps: {
serverAddr: pool.publicExposure.serverAddr,
serverPort: pool.publicExposure.serverPort,
remotePort: pool.publicExposure.remotePort,
masterConfigPath: pool.publicExposure.masterFrps.configPath,
masterContainerName: pool.publicExposure.masterFrps.containerName,
},
upstream: {
localIP: pool.publicExposure.localIP,
localPort: pool.publicExposure.localPort,
serviceDns,
},
urls: {
publicBaseUrl: pool.publicExposure.publicBaseUrl,
masterBaseUrl: pool.publicExposure.masterBaseUrl,
},
};
}
async function applyMasterFrpsAllowPort(pool: CodexPoolConfig): Promise<Record<string, unknown>> {
const path = pool.publicExposure.masterFrps.configPath;
const port = pool.publicExposure.remotePort;
const container = pool.publicExposure.masterFrps.containerName;
if (!existsSync(path)) {
return { ok: false, error: "master-frps-config-missing", path, valuesPrinted: false };
}
const before = readFileSync(path, "utf8");
const alreadyAllowed = frpsAllowPortExists(before, port);
let backupPath: string | null = null;
let action = "kept-existing";
if (!alreadyAllowed) {
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
backupPath = `${path}.bak-sub2api-${stamp}`;
copyFileSync(path, backupPath);
const next = before.replace(/\s*$/u, "") + `\n\n[[allowPorts]]\nstart = ${port}\nend = ${port}\n`;
writeFileSync(path, next, "utf8");
chmodSync(path, statSync(backupPath).mode & 0o777);
action = "added-allow-port";
}
const restart = alreadyAllowed ? null : Bun.spawnSync(["docker", "restart", container]);
const inspect = Bun.spawnSync(["docker", "inspect", "-f", "{{.State.Running}}", container]);
const ok = alreadyAllowed || (restart?.exitCode === 0 && inspect.exitCode === 0 && String(inspect.stdout).trim() === "true");
return {
ok,
action,
path,
backupPath,
remotePort: port,
containerName: container,
restart: restart === null ? null : {
exitCode: restart.exitCode,
stdoutTail: Buffer.from(restart.stdout).toString("utf8").slice(-1000),
stderrTail: Buffer.from(restart.stderr).toString("utf8").slice(-1000),
},
inspect: {
exitCode: inspect.exitCode,
running: String(inspect.stdout).trim() === "true",
stderrTail: Buffer.from(inspect.stderr).toString("utf8").slice(-1000),
},
valuesPrinted: false,
};
}
function frpsAllowPortExists(toml: string, port: number): boolean {
const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
return sections.some((section) => {
if (!section.includes("[[allowPorts]]")) return false;
const start = section.match(/^\s*start\s*=\s*(\d+)\s*$/mu);
const end = section.match(/^\s*end\s*=\s*(\d+)\s*$/mu);
return Number(start?.[1]) === port && Number(end?.[1]) === port;
});
}
function exposeScript(pool: CodexPoolConfig): string {
const manifest = frpcManifest(pool);
const encoded = Buffer.from(manifest, "utf8").toString("base64");
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/sub2api-frpc.yaml"
printf '%s' '${encoded}' | base64 -d > "$manifest"
ns_out="$tmp/ns.out"
ns_err="$tmp/ns.err"
apply_out="$tmp/apply.out"
apply_err="$tmp/apply.err"
rollout_out="$tmp/rollout.out"
rollout_err="$tmp/rollout.err"
kubectl get namespace ${namespace} >"$ns_out" 2>"$ns_err"
ns_rc=$?
apply_rc=1
rollout_rc=1
if [ "$ns_rc" -eq 0 ]; then
kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f "$manifest" >"$apply_out" 2>"$apply_err"
apply_rc=$?
if [ "$apply_rc" -eq 0 ]; then
kubectl -n ${namespace} rollout status deployment/${pool.publicExposure.deploymentName} --timeout=30s >"$rollout_out" 2>"$rollout_err"
rollout_rc=$?
else
printf '%s\\n' 'skipped because apply failed' >"$rollout_err"
fi
else
: >"$apply_out"
printf '%s\\n' 'skipped because namespace is missing' >"$apply_err"
: >"$rollout_out"
printf '%s\\n' 'skipped because namespace is missing' >"$rollout_err"
fi
pods_json="$tmp/pods.json"
pods_err="$tmp/pods.err"
kubectl -n ${namespace} get pods -l app.kubernetes.io/name=${pool.publicExposure.deploymentName} -o json >"$pods_json" 2>"$pods_err"
pods_rc=$?
logs_out="$tmp/logs.out"
logs_err="$tmp/logs.err"
kubectl -n ${namespace} logs deployment/${pool.publicExposure.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err"
logs_rc=$?
python3 - "$tmp" "$ns_rc" "$apply_rc" "$rollout_rc" "$pods_rc" "$logs_rc" <<'PY'
import json
import os
import sys
tmp = sys.argv[1]
ns_rc, apply_rc, rollout_rc, pods_rc, logs_rc = [int(value) for value in sys.argv[2:]]
def text(name, limit=4000):
path = os.path.join(tmp, name)
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
def load_json(name):
path = os.path.join(tmp, name)
try:
return json.load(open(path, encoding="utf-8"))
except Exception:
return None
pods_obj = load_json("pods.json")
pods = []
if isinstance(pods_obj, dict):
for item in pods_obj.get("items") or []:
status = item.get("status") or {}
statuses = status.get("containerStatuses") or []
pods.append({
"name": item.get("metadata", {}).get("name"),
"phase": status.get("phase"),
"ready": all(cs.get("ready") is True for cs in statuses) if statuses else False,
"restarts": sum(int(cs.get("restartCount") or 0) for cs in statuses),
})
logs = text("logs.out", 4000)
payload = {
"ok": ns_rc == 0 and apply_rc == 0 and rollout_rc == 0 and pods_rc == 0 and len(pods) > 0 and all(p["ready"] for p in pods),
"namespace": "${namespace}",
"proxy": {
"name": "${pool.publicExposure.proxyName}",
"remotePort": ${JSON.stringify(pool.publicExposure.remotePort)},
"publicBaseUrl": "${pool.publicExposure.publicBaseUrl}",
"masterBaseUrl": "${pool.publicExposure.masterBaseUrl}",
"localIP": "${pool.publicExposure.localIP}",
"localPort": ${JSON.stringify(pool.publicExposure.localPort)},
},
"resources": {
"configMap": "${pool.publicExposure.configMapName}",
"deployment": "${pool.publicExposure.deploymentName}",
"image": "${pool.publicExposure.frpcImage}",
},
"steps": {
"namespace": {"exitCode": ns_rc, "stdout": text("ns.out"), "stderr": text("ns.err")},
"apply": {"exitCode": apply_rc, "stdout": text("apply.out"), "stderr": text("apply.err")},
"rollout": {"exitCode": rollout_rc, "stdout": text("rollout.out"), "stderr": text("rollout.err")},
"pods": {"exitCode": pods_rc, "items": pods, "stderr": text("pods.err")},
"logs": {
"exitCode": logs_rc,
"startProxySuccess": "start proxy success" in logs,
"stdoutTail": logs,
"stderrTail": text("logs.err"),
},
},
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
function frpcManifest(pool: CodexPoolConfig): string {
return `apiVersion: v1
kind: ConfigMap
metadata:
name: ${pool.publicExposure.configMapName}
namespace: ${namespace}
labels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
data:
frpc.toml: |
serverAddr = "${pool.publicExposure.serverAddr}"
serverPort = ${pool.publicExposure.serverPort}
loginFailExit = true
[[proxies]]
name = "${pool.publicExposure.proxyName}"
type = "tcp"
localIP = "${pool.publicExposure.localIP}"
localPort = ${pool.publicExposure.localPort}
remotePort = ${pool.publicExposure.remotePort}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${pool.publicExposure.deploymentName}
namespace: ${namespace}
labels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
template:
metadata:
labels:
app.kubernetes.io/name: ${pool.publicExposure.deploymentName}
app.kubernetes.io/part-of: platform-infra
app.kubernetes.io/managed-by: unidesk
spec:
containers:
- name: frpc
image: ${pool.publicExposure.frpcImage}
imagePullPolicy: IfNotPresent
args:
- -c
- /etc/frp/frpc.toml
volumeMounts:
- name: config
mountPath: /etc/frp
readOnly: true
volumes:
- name: config
configMap:
name: ${pool.publicExposure.configMapName}
`;
}
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig): Promise<{ apiKey: string | null; error: string | null }> {
const result = await capture(config, g14K3sRoute, ["script"], `
set -u
kubectl -n ${namespace} get secret ${pool.apiKeySecretName} -o json
`);
if (result.exitCode !== 0) {
return { apiKey: null, error: `read pool API key secret failed: ${result.stderr.slice(-1000)}` };
}
const parsed = parseJsonOutput(result.stdout);
const data = isRecord(parsed?.data) ? parsed.data : null;
const encoded = typeof data?.[pool.apiKeySecretKey] === "string" ? data[pool.apiKeySecretKey] : null;
if (encoded === null) return { apiKey: null, error: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey} missing` };
try {
const apiKey = Buffer.from(encoded, "base64").toString("utf8");
return apiKey.length > 0 ? { apiKey, error: null } : { apiKey: null, error: "decoded API key is empty" };
} catch (error) {
return { apiKey: null, error: error instanceof Error ? error.message : String(error) };
}
}
function writeLocalCodexConfig(pool: CodexPoolConfig, apiKey: string): Record<string, unknown> {
const codexDir = join(homedir(), ".codex");
mkdirSync(codexDir, { recursive: true, mode: 0o700 });
const configPath = join(codexDir, "config.toml");
const authPath = join(codexDir, "auth.json");
const backupConfigPath = join(codexDir, `config.toml.${pool.localCodex.backupSuffix}`);
const backupAuthPath = join(codexDir, `auth.json.${pool.localCodex.backupSuffix}`);
if (!existsSync(configPath)) throw new Error(`${configPath} missing`);
if (!existsSync(authPath)) throw new Error(`${authPath} missing`);
const configBackupAction = copyIfMissing(configPath, backupConfigPath);
const authBackupAction = copyIfMissing(authPath, backupAuthPath);
const currentToml = readFileSync(configPath, "utf8");
const nextToml = updateCodexConfigToml(currentToml, pool);
writeFileSync(configPath, nextToml, { encoding: "utf8", mode: 0o600 });
chmodSync(configPath, 0o600);
writeFileSync(authPath, `${JSON.stringify({ OPENAI_API_KEY: apiKey }, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
chmodSync(authPath, 0o600);
return {
ok: true,
codexDir,
config: {
path: configPath,
bytes: statSync(configPath).size,
backupPath: backupConfigPath,
backupAction: configBackupAction,
backupBytes: statSync(backupConfigPath).size,
},
auth: {
path: authPath,
bytes: statSync(authPath).size,
backupPath: backupAuthPath,
backupAction: authBackupAction,
backupBytes: statSync(backupAuthPath).size,
openaiApiKeyShape: "string",
openaiApiKeyBytes: Buffer.byteLength(apiKey, "utf8"),
openaiApiKeyFingerprint: fingerprint(apiKey),
valuesPrinted: false,
},
provider: {
name: pool.localCodex.providerName,
baseUrl: pool.publicExposure.masterBaseUrl,
wireApi: pool.localCodex.wireApi,
},
valuesPrinted: false,
};
}
function copyIfMissing(source: string, target: string): "created" | "kept-existing" {
if (existsSync(target)) return "kept-existing";
copyFileSync(source, target);
chmodSync(target, 0o600);
return "created";
}
function updateCodexConfigToml(current: string, pool: CodexPoolConfig): string {
let next = upsertTopLevelTomlString(current, "model_provider", pool.localCodex.providerName);
next = upsertProviderSection(next, pool.localCodex.providerName, pool.publicExposure.masterBaseUrl, pool.localCodex.wireApi);
return next.endsWith("\n") ? next : `${next}\n`;
}
function upsertTopLevelTomlString(text: string, key: string, value: string): string {
const lines = text.split(/\r?\n/u);
const firstSection = lines.findIndex((line) => /^\s*\[/.test(line));
const end = firstSection === -1 ? lines.length : firstSection;
for (let index = 0; index < end; index += 1) {
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) {
lines[index] = `${key} = ${tomlString(value)}`;
return lines.join("\n");
}
}
lines.splice(0, 0, `${key} = ${tomlString(value)}`);
return lines.join("\n");
}
function upsertProviderSection(text: string, providerName: string, baseUrl: string, wireApi: string): string {
const header = `[model_providers.${providerName}]`;
const lines = text.split(/\r?\n/u);
const start = lines.findIndex((line) => line.trim() === header);
const canonical = [
`name = ${tomlString(providerName)}`,
`base_url = ${tomlString(baseUrl)}`,
`wire_api = ${tomlString(wireApi)}`,
"requires_openai_auth = true",
];
if (start === -1) {
const needsBlank = lines.length > 0 && lines[lines.length - 1].trim() !== "";
return [...lines, ...(needsBlank ? [""] : []), header, ...canonical].join("\n");
}
let end = lines.length;
for (let index = start + 1; index < lines.length; index += 1) {
if (/^\s*\[/.test(lines[index])) {
end = index;
break;
}
}
const preserved = lines.slice(start + 1, end).filter((line) => {
return !/^\s*(name|base_url|wire_api|requires_openai_auth|env_key)\s*=/u.test(line);
});
const replacement = [header, ...canonical, ...preserved.filter((line) => line.trim() !== "")];
return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n");
}
async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: string): Promise<Record<string, unknown>> {
const probe = await probePublicModels(pool, "with-api-key", apiKey);
return {
...probe,
ok: probe.ok === true,
apiKeyFingerprint: fingerprint(apiKey),
keyPreview: apiKeyPreview(apiKey),
valuesPrinted: false,
};
}
async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string): Promise<Record<string, unknown>> {
const url = `${pool.publicExposure.masterBaseUrl.replace(/\/+$/u, "")}/v1/models`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(url, {
method: "GET",
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
signal: controller.signal,
});
const body = await response.text();
let parsed: unknown = null;
try {
parsed = body.trim().length > 0 ? JSON.parse(body) : null;
} catch {
parsed = null;
}
const modelCount = isRecord(parsed) && Array.isArray(parsed.data) ? parsed.data.length : null;
return {
ok: response.ok,
mode,
method: "GET /v1/models",
baseUrl: pool.publicExposure.masterBaseUrl,
httpStatus: response.status,
modelCount,
bodyPreview: response.ok ? "" : body.slice(0, 500),
valuesPrinted: false,
};
} catch (error) {
return {
ok: false,
mode,
method: "GET /v1/models",
baseUrl: pool.publicExposure.masterBaseUrl,
httpStatus: 0,
error: error instanceof Error ? error.message : String(error),
valuesPrinted: false,
};
} finally {
clearTimeout(timeout);
}
}
function apiKeyPreview(apiKey: string): string {
if (apiKey.length <= 14) return "***";
return `${apiKey.slice(0, 10)}...${apiKey.slice(-4)}`;
}
function tomlString(value: string): string {
return JSON.stringify(value);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
function normalizeBaseUrl(value: string | null): string | null {
if (value === null) return null;
const trimmed = value.trim().replace(/\/+$/u, "");
@@ -411,6 +1173,15 @@ function numberValue(value: unknown): number | null {
return null;
}
function booleanValue(value: unknown): boolean | null {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
if (value.trim() === "true") return true;
if (value.trim() === "false") return false;
}
return null;
}
function validateKubernetesName(value: string, key: string, strictDns: boolean): void {
const pattern = strictDns ? /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u : /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
if (!pattern.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`);