// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. public-exposure module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:3097-3421 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 { CodexPoolConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProxyBinding, CodexPoolManualBindingSource, CodexPoolRuntimeTarget, CodexProfile } from "./types"; import { desiredAccountCapacityTotal } from "./accounts"; import { readCodexPoolConfig } from "./config"; import { codexConsumerBaseUrl } from "./local-codex"; import { codexPoolRuntimeTarget } from "./runtime-target"; import { codexPoolConfigPath, fieldManager, serviceName, sub2apiConfigPath } from "./types"; export function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRuntimeTarget()): Record { return { id: target.id, route: target.route, namespace: target.namespace, service: serviceName, serviceDns: target.serviceDns, publicBaseUrl: target.publicBaseUrl, consumerBaseUrl: target.publicBaseUrl === null ? null : codexConsumerBaseUrl(pool, target), configPath: codexPoolConfigPath, groupName: pool.groupName, apiKeyName: pool.apiKeyName, apiKeySecret: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`, publicExposure: targetPublicExposureSummary(target), sentinelImageBuild: { source: `${sub2apiConfigPath}.targets[${target.id}].codexPool.sentinelImageBuild`, baseImageCachePolicy: target.sentinelImageBuild.baseImageCachePolicy, noProxy: target.sentinelImageBuild.noProxy, }, minOwnerConcurrency: pool.minOwnerConcurrency, minOwnerConcurrencySource: pool.minOwnerConcurrencySource, accountCapacityTotal: desiredAccountCapacityTotal(pool), defaultAccountPriority: pool.defaultAccountPriority, defaultAccountCapacity: pool.defaultAccountCapacity, defaultAccountLoadFactor: pool.defaultAccountLoadFactor, defaultSentinelProtect: pool.defaultSentinelProtect, sentinel: { monitorEnabled: pool.sentinel.monitor.enabled, actionsEnabled: pool.sentinel.actions.enabled, cronJobName: pool.sentinel.cronJobName, stateConfigMapName: pool.sentinel.stateConfigMapName, }, egressProxy: target.egressProxy === null ? null : { enabled: target.egressProxy.enabled, applyToSentinel: target.egressProxy.applyToSentinel, serviceName: target.egressProxy.serviceName, listenPort: target.egressProxy.listenPort, httpProxy: target.egressProxy.httpProxy, noProxy: target.egressProxy.noProxy, }, valuesPrinted: false, }; } export function sentinelProfileSecrets(profiles: CodexProfile[]): CodexPoolSentinelProfileSecret[] { return profiles .filter((profile) => profile.ok && profile.apiKey !== null && profile.apiKey.length > 0) .map((profile) => ({ accountName: profile.accountName, profile: profile.profile, baseUrl: profile.baseUrl, apiKey: profile.apiKey ?? "", upstreamUserAgent: profile.upstreamUserAgent, trustUpstream: profile.trustUpstream, sentinelProtect: profile.sentinelProtect, })); } export function resolvedManualAccountProtections(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record[] { return pool.manualAccounts.protected.map((account) => ({ accountName: account.accountName, reason: account.reason, proxyBinding: resolveManualProxyBinding(account.proxyBinding, pool, target), groupBinding: resolveManualGroupBinding(account.groupBinding, pool), })); } export function resolveManualProxyBinding(binding: CodexPoolManualAccountProxyBinding | null, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record | null { if (binding === null) return null; const source = manualBindingSource(pool, binding.source); if (source.kind !== "proxy") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=proxy`); if (binding.enabled && source.provider === "target-egress-proxy" && (target.egressProxy === null || target.egressProxy.enabled !== true)) { throw new Error(`${codexPoolConfigPath}.manualAccounts.bindingSources.${source.id} requires ${sub2apiConfigPath}.targets[${target.id}].egressProxy.enabled=true`); } return { enabled: binding.enabled, source: source.id, proxyName: binding.proxyName, sourcePlan: { ...manualBindingSourcePlan(source), targetEgressProxy: source.provider === "target-egress-proxy" && target.egressProxy !== null ? { targetId: target.id, namespace: target.namespace, serviceName: target.egressProxy.serviceName, listenPort: target.egressProxy.listenPort, httpProxy: target.egressProxy.httpProxy, noProxy: target.egressProxy.noProxy, } : null, }, }; } export function resolveManualGroupBinding(binding: CodexPoolManualAccountGroupBinding | null, pool: CodexPoolConfig): Record | null { if (binding === null) return null; const source = manualBindingSource(pool, binding.source); if (source.kind !== "group") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=group`); return { enabled: binding.enabled, source: source.id, sourcePlan: { ...manualBindingSourcePlan(source), poolGroupName: source.provider === "pool-group" ? pool.groupName : null, }, }; } export function manualBindingSource(pool: CodexPoolConfig, sourceId: string): CodexPoolManualBindingSource { const source = pool.manualAccounts.bindingSources.byId[sourceId]; if (source === undefined) throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${sourceId} is not declared`); return source; } export function manualBindingSourcePlan(source: CodexPoolManualBindingSource): Record { return { id: source.id, enabled: source.enabled, kind: source.kind, provider: source.provider, description: source.description, }; } export function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Record | null { const exposure = target.publicExposure; if (exposure === null) return null; return { source: `${sub2apiConfigPath}.targets[${target.id}].publicExposure`, enabled: exposure.enabled, target: { id: target.id, route: target.route, namespace: target.namespace, serviceDns: target.serviceDns, }, urls: { publicBaseUrl: exposure.publicBaseUrl, consumerBaseUrl: target.publicBaseUrl === null ? null : `${target.publicBaseUrl.replace(/\/+$/u, "")}/`, }, dns: exposure.dns, frpc: { deploymentName: exposure.frpc.deploymentName, secretName: exposure.frpc.secretName, secretKey: exposure.frpc.secretKey, image: exposure.frpc.image, serverAddr: exposure.frpc.serverAddr, serverPort: exposure.frpc.serverPort, proxyName: exposure.frpc.proxyName, remotePort: exposure.frpc.remotePort, localIP: exposure.frpc.localIP, localPort: exposure.frpc.localPort, tokenSourceRef: exposure.frpc.tokenSourceRef, tokenSourceKey: exposure.frpc.tokenSourceKey, }, pk01: exposure.pk01, valuesPrinted: false, }; } export function prepareTargetPublicExposureSecret(target: CodexPoolRuntimeTarget): ReturnType { if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`); return prepareFrpcSecret({ secretRoot: target.secretsRoot, exposure: target.publicExposure, sourcePathRedactor: redactRepoPath, parseEnvFile, requiredEnvValue, readTextFile: (sourcePath) => { if (!existsSync(sourcePath)) throw new Error(`publicExposure requires ${redactRepoPath(sourcePath)} with ${target.publicExposure?.frpc.tokenSourceKey}; materialize the PK01 frps token source first`); return readTextFile(sourcePath); }, }); } export function secretMaterialSummary(secret: ReturnType): Record { return { sourceRef: secret.sourceRef, sourcePath: secret.sourcePath, secretName: secret.secretName, secretKey: secret.secretKey, fingerprint: secret.fingerprint, valuesPrinted: false, }; } export function targetPublicExposureApplyScript(target: CodexPoolRuntimeTarget, secret: ReturnType): string { if (target.publicExposure === null) throw new Error(`${sub2apiConfigPath}.targets[${target.id}].publicExposure is required`); const manifest = renderFrpcManifest({ id: target.id, route: target.route, namespace: target.namespace, replicas: 1, publicExposure: target.publicExposure, } satisfies PublicServiceTarget); const manifestB64 = Buffer.from(manifest, "utf8").toString("base64"); const frpcTomlB64 = Buffer.from(secret.frpcToml, "utf8").toString("base64"); return ` set -u tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT manifest="$tmp/sub2api-public-exposure.yaml" frpc_toml="$tmp/frpc.toml" printf '%s' '${manifestB64}' | base64 -d > "$manifest" printf '%s' '${frpcTomlB64}' | base64 -d > "$frpc_toml" ns_out="$tmp/ns.out" ns_err="$tmp/ns.err" secret_out="$tmp/secret.out" secret_err="$tmp/secret.err" apply_out="$tmp/apply.out" apply_err="$tmp/apply.err" rollout_out="$tmp/rollout.out" rollout_err="$tmp/rollout.err" pods_json="$tmp/pods.json" pods_err="$tmp/pods.err" logs_out="$tmp/logs.out" logs_err="$tmp/logs.err" kubectl create namespace ${target.namespace} --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$ns_out" 2>"$ns_err" ns_rc=$? secret_rc=1 apply_rc=1 rollout_rc=1 if [ "$ns_rc" -eq 0 ]; then kubectl -n ${target.namespace} create secret generic ${secret.secretName} \\ --from-file=${secret.secretKey}="$frpc_toml" \\ --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=${fieldManager} -f - >"$secret_out" 2>"$secret_err" secret_rc=$? else : >"$secret_out" printf '%s\\n' 'skipped because namespace apply failed' >"$secret_err" fi if [ "$ns_rc" -eq 0 ] && [ "$secret_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 ${target.namespace} rollout status deployment/${target.publicExposure.frpc.deploymentName} --timeout=60s >"$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 or secret step failed' >"$apply_err" : >"$rollout_out" printf '%s\\n' 'skipped because namespace or secret step failed' >"$rollout_err" fi kubectl -n ${target.namespace} get pods -l app.kubernetes.io/name=${target.publicExposure.frpc.deploymentName} -o json >"$pods_json" 2>"$pods_err" pods_rc=$? kubectl -n ${target.namespace} logs deployment/${target.publicExposure.frpc.deploymentName} --tail=80 >"$logs_out" 2>"$logs_err" logs_rc=$? python3 - "$tmp" "$ns_rc" "$secret_rc" "$apply_rc" "$rollout_rc" "$pods_rc" "$logs_rc" <<'PY' import json import os import sys tmp = sys.argv[1] ns_rc, secret_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 secret_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), "target": "${target.id}", "namespace": "${target.namespace}", "source": "${sub2apiConfigPath}.targets[${target.id}].publicExposure", "proxy": { "name": "${target.publicExposure.frpc.proxyName}", "remotePort": ${JSON.stringify(target.publicExposure.frpc.remotePort)}, "publicBaseUrl": "${target.publicExposure.publicBaseUrl}", "localIP": "${target.publicExposure.frpc.localIP}", "localPort": ${JSON.stringify(target.publicExposure.frpc.localPort)}, }, "resources": { "secret": "${secret.secretName}", "secretKey": "${secret.secretKey}", "deployment": "${target.publicExposure.frpc.deploymentName}", "image": "${target.publicExposure.frpc.image}", }, "steps": { "namespace": {"exitCode": ns_rc, "stdout": text("ns.out"), "stderr": text("ns.err")}, "secret": {"exitCode": secret_rc, "stdout": text("secret.out"), "stderr": text("secret.err"), "valuesPrinted": False}, "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"), }, }, "valuesPrinted": False, } print(json.dumps(payload, ensure_ascii=False, indent=2)) sys.exit(0 if payload["ok"] else 1) PY `; }