// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. runtime-target module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:621-819 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 { Sub2ApiRuntimeConfig } from "./options"; import type { CodexPoolRuntimePublicExposure, CodexPoolRuntimeTarget } from "./types"; import { validatePort, validateProxyName } from "./config"; import { booleanValue, isRecord, normalizeBaseUrl, numberValue, stringValue, validateKubernetesName } from "./config-utils"; import { serviceName, sub2apiConfigPath } from "./types"; export function readSub2ApiRuntimeConfig(): Sub2ApiRuntimeConfig { const parsed = Bun.YAML.parse(readFileSync(sub2apiConfigPath, "utf8")) as unknown; if (!isRecord(parsed)) throw new Error(`${sub2apiConfigPath} must contain a YAML object`); const defaults = isRecord(parsed.defaults) ? parsed.defaults : null; const defaultTargetId = defaults === null ? null : stringValue(defaults.targetId); if (defaultTargetId === null || !/^[A-Za-z0-9._-]+$/u.test(defaultTargetId)) throw new Error(`${sub2apiConfigPath}.defaults.targetId must be a simple target id`); const runtime = isRecord(parsed.runtime) ? parsed.runtime : null; const database = runtime !== null && isRecord(runtime.database) ? runtime.database : null; const appSecretName = database === null ? null : stringValue(database.secretName); if (appSecretName === null) throw new Error(`${sub2apiConfigPath}.runtime.database.secretName is required`); validateKubernetesName(appSecretName, `${sub2apiConfigPath}.runtime.database.secretName`, true); const secrets = runtime !== null && isRecord(runtime.secrets) ? runtime.secrets : null; const secretsRoot = secrets === null ? null : stringValue(secrets.root); if (secretsRoot === null || !secretsRoot.startsWith("/")) throw new Error(`${sub2apiConfigPath}.runtime.secrets.root must be an absolute path`); const appSourceRef = secrets === null ? null : stringValue(secrets.appSourceRef); if (appSourceRef === null || !/^[A-Za-z0-9_./-]+$/u.test(appSourceRef)) throw new Error(`${sub2apiConfigPath}.runtime.secrets.appSourceRef has an unsupported format`); const sentinel = runtime !== null && isRecord(runtime.sentinel) ? runtime.sentinel : null; const enabledOnTargets = Array.isArray(sentinel?.enabledOnTargets) ? sentinel.enabledOnTargets.map((entry) => stringValue(entry)).filter((entry): entry is string => entry !== null && entry.length > 0) : []; if (!Array.isArray(parsed.targets) || !parsed.targets.every(isRecord)) throw new Error(`${sub2apiConfigPath}.targets must be a list`); return { defaultTargetId, appSecretName, secretsRoot, appSourceRef, sentinelEnabledOnTargets: enabledOnTargets, targets: parsed.targets, }; } export function defaultCodexPoolRuntimeTargetId(): string { return readSub2ApiRuntimeConfig().defaultTargetId; } export function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarget { const runtimeConfig = readSub2ApiRuntimeConfig(); const resolvedTargetId = targetId ?? runtimeConfig.defaultTargetId; const raw = runtimeConfig.targets.find((item) => String(item.id ?? "").toLowerCase() === resolvedTargetId.toLowerCase()); if (!isRecord(raw)) throw new Error(`${sub2apiConfigPath}.targets does not contain target ${targetId}`); const id = stringValue(raw.id) ?? resolvedTargetId; const route = stringValue(raw.route) ?? ""; const runtimeMode = stringValue(raw.runtimeMode) ?? "k3s"; if (runtimeMode !== "k3s" && runtimeMode !== "host-docker") throw new Error(`${sub2apiConfigPath}.targets[${id}].runtimeMode must be k3s or host-docker`); const targetNamespace = stringValue(raw.namespace); if (route.length === 0) throw new Error(`${sub2apiConfigPath}.targets[${id}].route is required`); if (targetNamespace === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].namespace is required`); validateKubernetesName(targetNamespace, `${sub2apiConfigPath}.targets[${id}].namespace`, true); const sentinelEnabled = runtimeConfig.sentinelEnabledOnTargets.some((entry) => entry.toLowerCase() === id.toLowerCase()); const sentinelImageBuild = readTargetSentinelImageBuild(raw, id, sentinelEnabled); let egressProxy: CodexPoolRuntimeTarget["egressProxy"] = null; if (isRecord(raw.egressProxy) && raw.egressProxy.enabled === true) { const proxyServiceName = stringValue(raw.egressProxy.serviceName); const listenPort = numberValue(raw.egressProxy.listenPort); if (proxyServiceName === null || listenPort === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName/listenPort are required`); validateKubernetesName(proxyServiceName, `${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName`, true); if (!Number.isInteger(listenPort) || listenPort < 1 || listenPort > 65535) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.listenPort must be a TCP port`); const noProxyRaw = Array.isArray(raw.egressProxy.noProxy) ? raw.egressProxy.noProxy : []; const noProxy = noProxyRaw.map((entry) => stringValue(entry)).filter((entry): entry is string => entry !== null && entry.length > 0).join(","); egressProxy = { enabled: true, applyToSentinel: raw.egressProxy.applyToSentinel === undefined ? true : raw.egressProxy.applyToSentinel === true, serviceName: proxyServiceName, listenPort, httpProxy: `http://${proxyServiceName}.${targetNamespace}.svc.cluster.local:${listenPort}`, noProxy, }; } let publicBaseUrl: string | null = null; const publicExposure = readTargetPublicExposure(raw, id); if (publicExposure !== null && publicExposure.enabled) publicBaseUrl = publicExposure.publicBaseUrl; const hostDocker = runtimeMode === "host-docker" && isRecord(raw.hostDocker) ? raw.hostDocker : null; const hostDockerAppPort = hostDocker === null ? null : numberValue(hostDocker.appPort); const hostDockerEnvPath = hostDocker === null ? null : stringValue(hostDocker.envPath); if (runtimeMode === "host-docker" && (hostDockerAppPort === null || !Number.isInteger(hostDockerAppPort) || hostDockerAppPort < 1 || hostDockerAppPort > 65535)) { throw new Error(`${sub2apiConfigPath}.targets[${id}].hostDocker.appPort must be an integer TCP port when runtimeMode=host-docker`); } if (runtimeMode === "host-docker" && (hostDockerEnvPath === null || !hostDockerEnvPath.startsWith("/"))) { throw new Error(`${sub2apiConfigPath}.targets[${id}].hostDocker.envPath must be an absolute path when runtimeMode=host-docker`); } return { id, route, namespace: targetNamespace, runtimeMode, serviceName, serviceDns: runtimeMode === "host-docker" ? `host-docker:127.0.0.1:${hostDockerAppPort}` : `${serviceName}.${targetNamespace}.svc.cluster.local:8080`, publicBaseUrl, publicExposure, appSecretName: runtimeConfig.appSecretName, secretsRoot: runtimeConfig.secretsRoot, appSourceRef: runtimeConfig.appSourceRef, hostDockerAppPort, hostDockerEnvPath, sentinelEnabled, sentinelImageBuild, egressProxy, }; } export function readTargetSentinelImageBuild(raw: Record, targetId: string, required = true): CodexPoolRuntimeTarget["sentinelImageBuild"] { const codexPool = isRecord(raw.codexPool) ? raw.codexPool : null; const imageBuild = codexPool !== null && isRecord(codexPool.sentinelImageBuild) ? codexPool.sentinelImageBuild : null; if (imageBuild === null && !required) return { baseImageCachePolicy: "pull", noProxy: [], proxyEnvPath: null }; if (imageBuild === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild must be a YAML object`); const policy = stringValue(imageBuild.baseImageCachePolicy); if (policy !== "pull" && policy !== "local-if-present") { throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.baseImageCachePolicy must be pull or local-if-present`); } const egressProxy = isRecord(raw.egressProxy) ? raw.egressProxy : null; const proxyEnvPath = egressProxy === null ? null : readProxyEnvPath(egressProxy.proxyEnvPath, `${sub2apiConfigPath}.targets[${targetId}].egressProxy.proxyEnvPath`); return { baseImageCachePolicy: policy, noProxy: readTargetNoProxy(imageBuild.noProxy, `${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.noProxy`), proxyEnvPath, }; } export function readProxyEnvPath(value: unknown, key: string): string | null { if (value === undefined || value === null) return null; const path = stringValue(value); if (path === null) throw new Error(`${key} must be a non-empty string`); if (!/^\/[A-Za-z0-9._/-]+$/u.test(path)) throw new Error(`${key} has an unsupported absolute path format`); return path; } export function readTargetPublicExposure(raw: Record, targetId: string): CodexPoolRuntimePublicExposure | null { if (raw.publicExposure === undefined || raw.publicExposure === null) return null; if (!isRecord(raw.publicExposure)) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure must be a YAML object`); const exposure = raw.publicExposure; const enabled = readRequiredTargetBoolean(exposure.enabled, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.enabled`); const mode = stringValue(exposure.mode) ?? "frp"; if (mode !== "frp" && mode !== "pk01-local") throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure.mode must be frp or pk01-local`); const dns = readRequiredTargetRecord(exposure.dns, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns`); const local = exposure.local === undefined || exposure.local === null ? null : readRequiredTargetRecord(exposure.local, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local`); const frpc = exposure.frpc === undefined || exposure.frpc === null ? null : readRequiredTargetRecord(exposure.frpc, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc`); if (mode === "frp" && frpc === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc is required when mode=frp`); if (mode === "pk01-local" && local === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure.local is required when mode=pk01-local`); const pk01 = readRequiredTargetRecord(exposure.pk01, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01`); const publicBaseUrl = readTargetBaseUrl(exposure.publicBaseUrl, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.publicBaseUrl`, "https:"); const hostname = readRequiredTargetString(dns.hostname, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.hostname`); if (new URL(publicBaseUrl).hostname !== hostname) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure publicBaseUrl hostname must match dns.hostname`); const config: CodexPoolRuntimePublicExposure = { enabled, mode, publicBaseUrl, dns: { hostname, expectedA: readRequiredTargetString(dns.expectedA, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.expectedA`), resolvers: readTargetStringArray(dns.resolvers, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.dns.resolvers`), }, local: local === null ? null : { upstreamHost: readRequiredTargetString(local.upstreamHost, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local.upstreamHost`), upstreamPort: readTargetPort(local.upstreamPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local.upstreamPort`), }, frpc: frpc === null ? null : { deploymentName: readRequiredTargetString(frpc.deploymentName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.deploymentName`), secretName: readRequiredTargetString(frpc.secretName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretName`), secretKey: readRequiredTargetString(frpc.secretKey, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretKey`), image: readRequiredTargetString(frpc.image, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.image`), serverAddr: readRequiredTargetString(frpc.serverAddr, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverAddr`), serverPort: readTargetPort(frpc.serverPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverPort`), proxyName: readRequiredTargetString(frpc.proxyName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.proxyName`), remotePort: readTargetPort(frpc.remotePort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.remotePort`), localIP: readRequiredTargetString(frpc.localIP, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localIP`), localPort: readTargetPort(frpc.localPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localPort`), tokenSourceRef: readRequiredTargetString(frpc.tokenSourceRef, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.tokenSourceRef`), tokenSourceKey: readRequiredTargetString(frpc.tokenSourceKey, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.tokenSourceKey`), }, pk01: { route: readRequiredTargetString(pk01.route, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.route`), caddyConfigPath: readAbsoluteTargetPath(pk01.caddyConfigPath, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.caddyConfigPath`), caddyServiceName: readRequiredTargetString(pk01.caddyServiceName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.caddyServiceName`), responseHeaderTimeoutSeconds: readPositiveTargetInteger(pk01.responseHeaderTimeoutSeconds, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.pk01.responseHeaderTimeoutSeconds`), }, }; if (config.frpc !== null) { validateKubernetesName(config.frpc.deploymentName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.deploymentName`, true); validateKubernetesName(config.frpc.secretName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.secretName`, true); validateProxyName(config.frpc.proxyName, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.proxyName`); validatePort(config.frpc.serverPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.serverPort`); validatePort(config.frpc.remotePort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.remotePort`); validatePort(config.frpc.localPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.frpc.localPort`); } if (config.local !== null) validatePort(config.local.upstreamPort, `${sub2apiConfigPath}.targets[${targetId}].publicExposure.local.upstreamPort`); return config; } export function readRequiredTargetRecord(value: unknown, path: string): Record { if (!isRecord(value)) throw new Error(`${path} must be a YAML object`); return value; } export function readRequiredTargetString(value: unknown, path: string): string { const text = stringValue(value); if (text === null) throw new Error(`${path} must be a non-empty string`); if (/[\r\n]/u.test(text)) throw new Error(`${path} must not contain newlines`); return text; } export function readRequiredTargetBoolean(value: unknown, path: string): boolean { const parsed = booleanValue(value); if (parsed === null) throw new Error(`${path} must be a boolean`); return parsed; } export function readTargetBaseUrl(value: unknown, path: string, protocol: "http:" | "https:" | null = null): string { const baseUrl = normalizeBaseUrl(stringValue(value)); if (baseUrl === null) throw new Error(`${path} must be a valid http(s) URL`); if (protocol !== null && new URL(baseUrl).protocol !== protocol) throw new Error(`${path} must use ${protocol}//`); return baseUrl; } export function readTargetPort(value: unknown, path: string): number { const port = numberValue(value); if (port === null || !Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${path} must be an integer TCP port`); return port; } export function readPositiveTargetInteger(value: unknown, path: string): number { const number = numberValue(value); if (number === null || !Number.isInteger(number) || number < 1) throw new Error(`${path} must be a positive integer`); return number; } export function readAbsoluteTargetPath(value: unknown, path: string): string { const text = readRequiredTargetString(value, path); if (!text.startsWith("/")) throw new Error(`${path} must be an absolute path`); return text; } export function readTargetStringArray(value: unknown, path: string): string[] { if (!Array.isArray(value)) throw new Error(`${path} must be a YAML array`); const result = value.map((item, index) => readRequiredTargetString(item, `${path}[${index}]`)); if (result.length === 0) throw new Error(`${path} must not be empty`); return result; } export function readTargetNoProxy(value: unknown, path: string): string { return readTargetStringArray(value, path).join(","); } export function targetFlag(target: CodexPoolRuntimeTarget): string { return target.id === defaultCodexPoolRuntimeTargetId() ? "" : ` --target ${target.id}`; }