// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. local-codex module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:3422-3685 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 { CodexLocalConsumerTomlOptions, CodexPoolConfig } from "./types"; import { fingerprint, isRecord } from "./config-utils"; import { capture, parseJsonOutput } from "./remote"; import { codexPoolRuntimeTarget } from "./runtime-target"; import { sub2apiConfigPath } from "./types"; export async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): Promise<{ apiKey: string | null; error: string | null }> { const result = await capture(config, target.route, ["sh"], ` set -u kubectl -n ${target.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: `${target.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) }; } } export function writeLocalCodexConfig(pool: CodexPoolConfig, apiKey: string, target = codexPoolRuntimeTarget()): Record { 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, target); 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: codexConsumerBaseUrl(pool, target), wireApi: pool.localCodex.wireApi, modelContextWindow: pool.localCodex.modelContextWindow, modelAutoCompactTokenLimit: pool.localCodex.modelAutoCompactTokenLimit, supportsWebSockets: pool.localCodex.supportsWebSockets, responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2, }, valuesPrinted: false, }; } export function copyIfMissing(source: string, target: string): "created" | "kept-existing" { if (existsSync(target)) return "kept-existing"; copyFileSync(source, target); chmodSync(target, 0o600); return "created"; } export function updateCodexConfigToml(current: string, pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): string { return renderCodexLocalConsumerToml(current, { providerName: pool.localCodex.providerName, baseUrl: codexConsumerBaseUrl(pool, target), wireApi: pool.localCodex.wireApi, modelContextWindow: pool.localCodex.modelContextWindow, modelAutoCompactTokenLimit: pool.localCodex.modelAutoCompactTokenLimit, supportsWebSockets: pool.localCodex.supportsWebSockets, responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2, }); } export function codexConsumerBaseUrl(pool: CodexPoolConfig, target = codexPoolRuntimeTarget()): string { void pool; const baseUrl = target.publicBaseUrl; if (baseUrl === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure.enabled must be true before resolving the Codex consumer base URL`); return `${baseUrl.replace(/\/+$/u, "")}/`; } export function renderCodexLocalConsumerToml(current: string, options: CodexLocalConsumerTomlOptions): string { let next = upsertTopLevelTomlString(current, "model_provider", options.providerName); next = upsertTopLevelTomlInteger(next, "model_context_window", options.modelContextWindow); next = upsertTopLevelTomlInteger(next, "model_auto_compact_token_limit", options.modelAutoCompactTokenLimit); next = upsertProviderSection(next, options); next = upsertTomlSectionBoolean(next, "features", "responses_websockets_v2", options.responsesWebSocketsV2); return next.endsWith("\n") ? next : `${next}\n`; } export 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"); } export function upsertTopLevelTomlInteger(text: string, key: string, value: number): string { const lines = text.split(/\r?\n/u); const firstSection = lines.findIndex((line) => /^\s*\[/.test(line)); const end = firstSection === -1 ? lines.length : firstSection; const assignment = `${key} = ${value}`; for (let index = 0; index < end; index += 1) { if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) { lines[index] = assignment; return lines.join("\n"); } } lines.splice(end, 0, assignment); return lines.join("\n"); } export function upsertProviderSection(text: string, options: CodexLocalConsumerTomlOptions): string { const { providerName, baseUrl, wireApi, supportsWebSockets } = options; 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", `supports_websockets = ${tomlBoolean(supportsWebSockets)}`, ]; 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|supports_websockets|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"); } export function upsertTomlSectionBoolean(text: string, sectionName: string, key: string, value: boolean): string { const header = `[${sectionName}]`; const lines = text.split(/\r?\n/u); const start = lines.findIndex((line) => line.trim() === header); const assignment = `${key} = ${tomlBoolean(value)}`; if (start === -1) { const needsBlank = lines.length > 0 && lines[lines.length - 1].trim() !== ""; return [...lines, ...(needsBlank ? [""] : []), header, assignment].join("\n"); } let end = lines.length; for (let index = start + 1; index < lines.length; index += 1) { if (/^\s*\[/.test(lines[index])) { end = index; break; } } for (let index = start + 1; index < end; index += 1) { if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[index])) { lines[index] = assignment; return lines.join("\n"); } } return [...lines.slice(0, start + 1), assignment, ...lines.slice(start + 1)].join("\n"); } export async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: string, target = codexPoolRuntimeTarget()): Promise> { const probe = await probePublicModels(pool, "with-api-key", apiKey, target); return { ...probe, ok: probe.ok === true, apiKeyFingerprint: fingerprint(apiKey), keyPreview: apiKeyPreview(apiKey), valuesPrinted: false, }; } export async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string, target = codexPoolRuntimeTarget()): Promise> { void pool; if (target.publicBaseUrl === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure.enabled must be true before probing the public gateway`); const baseUrl = target.publicBaseUrl; const url = `${baseUrl.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, httpStatus: response.status, modelCount, bodyPreview: response.ok ? "" : body.slice(0, 500), valuesPrinted: false, }; } catch (error) { return { ok: false, mode, method: "GET /v1/models", baseUrl, httpStatus: 0, error: error instanceof Error ? error.message : String(error), valuesPrinted: false, }; } finally { clearTimeout(timeout); } } export function apiKeyPreview(apiKey: string): string { if (apiKey.length <= 14) return "***"; return `${apiKey.slice(0, 10)}...${apiKey.slice(-4)}`; } export function tomlString(value: string): string { return JSON.stringify(value); } export function tomlBoolean(value: boolean): string { return value ? "true" : "false"; } export function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); }