551 lines
26 KiB
TypeScript
551 lines
26 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. actions module for scripts/src/platform-infra-sub2api-codex.ts.
|
|
|
|
// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:820-1316 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 { RemoteCodexPoolMode } from "./remote";
|
|
import type { CodexPoolConfig, ConfirmOptions, DisclosureOptions, SentinelImageOptions, SentinelProbeOptions, SentinelReportOptions, SyncOptions, TraceOptions } from "./types";
|
|
import { desiredAccountNames } from "./accounts";
|
|
import { collectCodexProfiles, readCodexPoolConfig } from "./config";
|
|
import { codexPoolSentinelProbeConfigFingerprint, fingerprint } from "./config-utils";
|
|
import { apiKeyPreview, codexConsumerBaseUrl, fetchPoolApiKey, probePublicModels, validatePublicGatewayWithKey, writeLocalCodexConfig } from "./local-codex";
|
|
import { manualBindingSourcePlan, poolTarget, prepareTargetPublicExposureSecret, resolvedManualAccountProtections, secretMaterialSummary, sentinelProfileSecrets, targetPublicExposureApplyScript, targetPublicExposureSummary } from "./public-exposure";
|
|
import { codexPoolConfigSummary, compactProfile, compactSentinelProbeResult, redactProfile, renderSub2ApiTempUnschedulableCredentials } from "./redaction";
|
|
import { boolField, capture, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
|
|
import { cleanupProbesScript, sentinelImageBuildScript, sentinelImageStatusScript, sentinelProbeScript, sentinelReportScript, syncScript, traceScript, validateScript } from "./remote-scripts";
|
|
import { codexPoolSyncSummary, codexPoolValidationSummary, renderSentinelReport, renderTraceReport, renderedCliResult } from "./render";
|
|
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId, targetFlag } from "./runtime-target";
|
|
import { codexPoolConfigPath, sentinelImageDockerfilePath, serviceName, sub2apiConfigPath } from "./types";
|
|
|
|
export function codexPoolPlan(options?: DisclosureOptions): Record<string, unknown> {
|
|
const resolvedOptions = options ?? { full: false, raw: false, targetId: defaultCodexPoolRuntimeTargetId() };
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(resolvedOptions.targetId);
|
|
const profiles = collectCodexProfiles();
|
|
const ok = profiles.length > 0 && profiles.every((profile) => profile.ok);
|
|
const consumerBaseUrl = runtimeTarget.publicBaseUrl === null ? null : codexConsumerBaseUrl(pool, runtimeTarget);
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-plan",
|
|
source: {
|
|
directory: join(homedir(), ".codex"),
|
|
configPattern: "YAML-selected config files under ~/.codex",
|
|
authPattern: "YAML-selected auth files under ~/.codex",
|
|
valuesPrinted: false,
|
|
},
|
|
target: poolTarget(pool, runtimeTarget),
|
|
config: {
|
|
path: codexPoolConfigPath,
|
|
pool: options.full ? pool : codexPoolConfigSummary(pool, runtimeTarget),
|
|
},
|
|
profiles: options.full ? profiles.map(redactProfile) : profiles.map(compactProfile),
|
|
decision: {
|
|
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 ${runtimeTarget.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`,
|
|
sentinel: pool.sentinel.monitor.enabled
|
|
? `Account sentinel is enabled as k8s CronJob ${runtimeTarget.namespace}/${pool.sentinel.cronJobName}; actions.enabled=${pool.sentinel.actions.enabled}.`
|
|
: "Account sentinel monitoring is disabled by YAML.",
|
|
publicExposure: runtimeTarget.publicBaseUrl === null
|
|
? `Target-level public exposure is disabled or absent in ${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure.`
|
|
: `Codex consumers for target ${runtimeTarget.id} use target-level public exposure ${consumerBaseUrl}.`,
|
|
idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files for YAML-managed profiles only; managed accounts missing from YAML are preserved unless --prune-removed is explicitly provided.",
|
|
configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.",
|
|
manualAccountProtection: pool.manualAccounts.protected.length === 0
|
|
? "No manual Sub2API accounts are protected by YAML."
|
|
: `${pool.manualAccounts.protected.length} manual Sub2API account(s) are protected from UniDesk-managed credentials, prune, sentinel probe, and sentinel freeze paths; only explicitly declared proxy/group bindings are reconciled.`,
|
|
},
|
|
next: ok
|
|
? { sync: `bun scripts/cli.ts platform-infra sub2api codex-pool sync${targetFlag(runtimeTarget)} --confirm` }
|
|
: { fix: "Ensure every discovered config.toml profile has a base_url and either auth.json OPENAI_API_KEY or the configured env_key present in this shell." },
|
|
};
|
|
}
|
|
|
|
export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
const profiles = collectCodexProfiles();
|
|
const planOk = profiles.length > 0 && profiles.every((profile) => profile.ok);
|
|
if (!options.confirm || !planOk) {
|
|
return {
|
|
...codexPoolPlan(options),
|
|
ok: !options.confirm ? planOk : false,
|
|
mode: options.confirm ? "blocked-invalid-local-profile" : "dry-run",
|
|
next: options.confirm
|
|
? { fix: "Repair invalid local Codex profiles, then rerun sync --confirm." }
|
|
: { confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm" },
|
|
};
|
|
}
|
|
|
|
const sentinelImage = pool.sentinel.monitor.enabled && runtimeTarget.sentinelEnabled
|
|
? await runCodexPoolSentinelImage(config, pool, { action: "build", confirm: true, dryRun: false, full: options.full, raw: false, targetId: options.targetId })
|
|
: { ok: true, mode: runtimeTarget.sentinelEnabled ? "skipped-monitor-disabled" : "skipped-target-disabled" };
|
|
if (sentinelImage.ok !== true) {
|
|
return {
|
|
ok: false,
|
|
action: "platform-infra-sub2api-codex-pool-sync",
|
|
mode: "blocked-sentinel-image",
|
|
sentinelImage,
|
|
next: {
|
|
image: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build${targetFlag(runtimeTarget)} --confirm`,
|
|
},
|
|
};
|
|
}
|
|
|
|
const payload = {
|
|
pruneRemoved: options.pruneRemoved,
|
|
sentinel: {
|
|
enabledForTarget: runtimeTarget.sentinelEnabled,
|
|
manifest: runtimeTarget.sentinelEnabled
|
|
? renderCodexPoolSentinelManifest(pool.sentinel, sentinelProfileSecrets(profiles), {
|
|
namespace: runtimeTarget.namespace,
|
|
serviceName: runtimeTarget.serviceName,
|
|
serviceDns: runtimeTarget.serviceDns,
|
|
appSecretName: runtimeTarget.appSecretName,
|
|
adminEmailDefault: pool.adminEmailDefault,
|
|
proxy: runtimeTarget.egressProxy?.applyToSentinel ? {
|
|
httpProxy: runtimeTarget.egressProxy.httpProxy,
|
|
noProxy: runtimeTarget.egressProxy.noProxy,
|
|
} : null,
|
|
})
|
|
: null,
|
|
summary: codexPoolSentinelSummary(pool.sentinel),
|
|
},
|
|
pool: {
|
|
groupName: pool.groupName,
|
|
groupDescription: pool.groupDescription,
|
|
apiKeyName: pool.apiKeyName,
|
|
apiKeySecretName: pool.apiKeySecretName,
|
|
apiKeySecretKey: pool.apiKeySecretKey,
|
|
adminEmailDefault: pool.adminEmailDefault,
|
|
minOwnerBalanceUsd: pool.minOwnerBalanceUsd,
|
|
minOwnerConcurrency: pool.minOwnerConcurrency,
|
|
defaultAccountPriority: pool.defaultAccountPriority,
|
|
defaultAccountCapacity: pool.defaultAccountCapacity,
|
|
defaultAccountLoadFactor: pool.defaultAccountLoadFactor,
|
|
defaultSentinelProtect: pool.defaultSentinelProtect,
|
|
},
|
|
manualAccounts: {
|
|
bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan),
|
|
protected: resolvedManualAccountProtections(pool, runtimeTarget),
|
|
},
|
|
profiles: profiles.map((profile) => ({
|
|
profile: profile.profile,
|
|
accountName: profile.accountName,
|
|
configFile: profile.configFile,
|
|
authFile: profile.authFile,
|
|
provider: profile.provider,
|
|
baseUrl: profile.baseUrl,
|
|
wireApi: profile.wireApi,
|
|
model: profile.model,
|
|
apiKey: profile.apiKey,
|
|
apiKeySource: profile.apiKeySource,
|
|
apiKeyFingerprint: fingerprint(profile.apiKey ?? ""),
|
|
sentinelProbeConfigFingerprint: codexPoolSentinelProbeConfigFingerprint({
|
|
accountName: profile.accountName,
|
|
profile: profile.profile,
|
|
baseUrl: profile.baseUrl,
|
|
apiKeyFingerprint: fingerprint(profile.apiKey ?? ""),
|
|
upstreamUserAgent: profile.upstreamUserAgent,
|
|
openaiResponsesWebSocketsV2Mode: profile.openaiResponsesWebSocketsV2Mode,
|
|
trustUpstream: profile.trustUpstream,
|
|
sentinelProtect: profile.sentinelProtect,
|
|
}),
|
|
openaiResponsesWebSocketsV2Mode: profile.openaiResponsesWebSocketsV2Mode,
|
|
upstreamUserAgent: profile.upstreamUserAgent,
|
|
trustUpstream: profile.trustUpstream,
|
|
sentinelProtect: profile.sentinelProtect,
|
|
priority: profile.priority,
|
|
capacity: profile.capacity,
|
|
loadFactor: profile.loadFactor,
|
|
tempUnschedulable: profile.tempUnschedulable,
|
|
tempUnschedulableCredentials: renderSub2ApiTempUnschedulableCredentials(profile.tempUnschedulable),
|
|
})),
|
|
};
|
|
const result = await runRemoteCodexPoolScript(config, "sync", syncScript(payload, pool, runtimeTarget), runtimeTarget);
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sync",
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sync",
|
|
local: {
|
|
profileCount: profiles.length,
|
|
pruneRemoved: options.pruneRemoved,
|
|
invalidProfiles: profiles.filter((profile) => !profile.ok).map(compactProfile),
|
|
profiles: options.full ? profiles.map(redactProfile) : undefined,
|
|
valuesPrinted: false,
|
|
},
|
|
sentinelImage,
|
|
remote: parsed === null
|
|
? compactCapture(result, { full: options.full || result.exitCode !== 0 })
|
|
: options.full ? parsed : codexPoolSyncSummary(parsed),
|
|
next: {
|
|
validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function codexPoolSentinelImage(config: UniDeskConfig, options: SentinelImageOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
return await runCodexPoolSentinelImage(config, pool, options);
|
|
}
|
|
|
|
export async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolConfig, options: SentinelImageOptions): Promise<Record<string, unknown>> {
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
const target = codexPoolSentinelRuntimeImage(pool.sentinel);
|
|
if (!runtimeTarget.sentinelEnabled) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-image",
|
|
mode: "skipped-target-disabled",
|
|
target: {
|
|
id: runtimeTarget.id,
|
|
namespace: runtimeTarget.namespace,
|
|
},
|
|
image: target,
|
|
mutation: false,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
if (options.action === "build" && options.dryRun) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-image",
|
|
mode: "dry-run",
|
|
image: target,
|
|
dockerfile: sentinelImageDockerfilePath,
|
|
mutation: false,
|
|
next: {
|
|
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build${targetFlag(runtimeTarget)} --confirm`,
|
|
},
|
|
};
|
|
}
|
|
const mode: RemoteCodexPoolMode = options.action === "status" ? "sentinel-image-status" : "sentinel-image-build";
|
|
const script = options.action === "status" ? sentinelImageStatusScript(pool, runtimeTarget) : sentinelImageBuildScript(pool, runtimeTarget);
|
|
const result = await runRemoteCodexPoolScript(config, mode, script, runtimeTarget);
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-image",
|
|
mode: options.action,
|
|
image: target,
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-image",
|
|
mode: options.action,
|
|
image: target,
|
|
summary: parsed,
|
|
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
|
|
};
|
|
}
|
|
|
|
export async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
const result = await runRemoteCodexPoolScript(config, "validate", validateScript(pool, runtimeTarget), runtimeTarget);
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-validate",
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-validate",
|
|
summary: options.full ? parsed : codexPoolValidationSummary(parsed),
|
|
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
|
|
};
|
|
}
|
|
|
|
export async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
const result = await capture(config, runtimeTarget.route, ["sh"], traceScript(pool, options, runtimeTarget));
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
|
if (options.raw) {
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-trace",
|
|
remote: compactCapture(result, { full: true }),
|
|
trace: parsed,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
const text = renderTraceReport(parsed, {
|
|
requestId: options.requestId ?? "",
|
|
showLines: options.showLines,
|
|
remote: compactCapture(result, { full: result.exitCode !== 0 || parsed === null }),
|
|
});
|
|
return renderedCliResult(ok, "platform-infra sub2api codex-pool trace", text);
|
|
}
|
|
|
|
export async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelReportOptions): Promise<Record<string, unknown> | RenderedCliResult> {
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
const result = await capture(config, runtimeTarget.route, ["sh"], sentinelReportScript(pool, options.events, runtimeTarget));
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
|
|
if (options.raw) {
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-report",
|
|
remote: compactCapture(result, { full: true }),
|
|
report: parsed,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
const text = renderSentinelReport(parsed, {
|
|
events: options.events,
|
|
full: options.full,
|
|
remote: compactCapture(result, { full: result.exitCode !== 0 || parsed === null }),
|
|
});
|
|
return renderedCliResult(ok, "platform-infra sub2api codex-pool sentinel-report", text);
|
|
}
|
|
|
|
export async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelProbeOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
const protectedNames = new Set(pool.manualAccounts.protected.map((account) => account.accountName.toLowerCase()));
|
|
const protectedRequested = options.accounts.filter((account) => protectedNames.has(account.toLowerCase()));
|
|
if (protectedRequested.length > 0) {
|
|
return {
|
|
ok: false,
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
|
|
error: "account-protected-manual",
|
|
protected: protectedRequested,
|
|
message: "Protected manual Sub2API accounts are not YAML-managed and must not be probed by the UniDesk sentinel.",
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
const configuredAccounts = desiredAccountNames(pool);
|
|
const missing = options.accounts.filter((account) => !configuredAccounts.includes(account));
|
|
if (missing.length > 0) {
|
|
return {
|
|
ok: false,
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
|
|
error: "account-not-in-yaml",
|
|
missing,
|
|
configuredAccounts,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
if (!options.confirm) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
|
|
mode: "dry-run",
|
|
target: poolTarget(pool, runtimeTarget),
|
|
accounts: options.accounts,
|
|
effect: "Would create one Kubernetes Job from the managed sentinel CronJob and force an immediate marker probe for the requested account(s).",
|
|
next: {
|
|
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe${targetFlag(runtimeTarget)} --account ${options.accounts.join(",")} --confirm`,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
const payload = {
|
|
accounts: options.accounts,
|
|
};
|
|
const result = await runRemoteCodexPoolScript(config, "sentinel-probe", sentinelProbeScript(payload, pool, runtimeTarget), runtimeTarget);
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
|
|
summary: options.full ? parsed : compactSentinelProbeResult(parsed),
|
|
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
|
|
};
|
|
}
|
|
|
|
export async function codexPoolCleanupProbes(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
if (!options.confirm) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-cleanup-probes",
|
|
mode: "dry-run",
|
|
target: poolTarget(readCodexPoolConfig(), runtimeTarget),
|
|
scope: "Only deletes temporary resources whose names start with unidesk-probe-.",
|
|
next: { confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes${targetFlag(runtimeTarget)} --confirm` },
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
const pool = readCodexPoolConfig();
|
|
const result = await capture(config, runtimeTarget.route, ["sh"], cleanupProbesScript(pool, runtimeTarget));
|
|
const parsed = parseJsonOutput(result.stdout);
|
|
if (options.raw) {
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-cleanup-probes",
|
|
remote: compactCapture(result, { full: true }),
|
|
parsed,
|
|
};
|
|
}
|
|
return {
|
|
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
|
action: "platform-infra-sub2api-codex-pool-cleanup-probes",
|
|
summary: parsed,
|
|
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
|
|
};
|
|
}
|
|
|
|
export async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
if (runtimeTarget.publicExposure === null || !runtimeTarget.publicExposure.enabled) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
mode: "disabled-by-yaml",
|
|
target: poolTarget(pool, runtimeTarget),
|
|
source: `${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure`,
|
|
};
|
|
}
|
|
if (!options.confirm) {
|
|
return {
|
|
ok: true,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
mode: "dry-run",
|
|
target: poolTarget(pool, runtimeTarget),
|
|
publicExposure: targetPublicExposureSummary(runtimeTarget),
|
|
next: {
|
|
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool expose${targetFlag(runtimeTarget)} --confirm`,
|
|
},
|
|
};
|
|
}
|
|
const secretMaterial = prepareTargetPublicExposureSecret(runtimeTarget);
|
|
const caddyResult = await applyPk01CaddyBlock(config, serviceName, runtimeTarget.publicExposure);
|
|
const remoteResult = await capture(config, runtimeTarget.route, ["sh"], targetPublicExposureApplyScript(runtimeTarget, secretMaterial));
|
|
const parsed = parseJsonOutput(remoteResult.stdout);
|
|
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, runtimeTarget);
|
|
const ok = caddyResult.ok === true && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
|
|
if (options.raw) {
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
frpcSecret: secretMaterialSummary(secretMaterial),
|
|
pk01Caddy: caddyResult,
|
|
remote: compactCapture(remoteResult, { full: true }),
|
|
parsed,
|
|
publicProbe,
|
|
};
|
|
}
|
|
return {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-expose",
|
|
mode: "confirmed",
|
|
publicExposure: targetPublicExposureSummary(runtimeTarget),
|
|
frpcSecret: secretMaterialSummary(secretMaterial),
|
|
pk01Caddy: caddyResult,
|
|
remote: parsed ?? compactCapture(remoteResult, { full: options.full || remoteResult.exitCode !== 0 }),
|
|
publicProbe,
|
|
next: {
|
|
configureLocal: `bun scripts/cli.ts platform-infra sub2api codex-pool configure-local${targetFlag(runtimeTarget)} --confirm`,
|
|
validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function codexPoolConfigureLocal(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
|
|
const pool = readCodexPoolConfig();
|
|
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
|
|
const consumerExposureAvailable = runtimeTarget.publicBaseUrl !== null;
|
|
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: consumerExposureAvailable ? codexConsumerBaseUrl(pool, runtimeTarget) : null,
|
|
providerName: pool.localCodex.providerName,
|
|
wireApi: pool.localCodex.wireApi,
|
|
modelContextWindow: pool.localCodex.modelContextWindow,
|
|
modelAutoCompactTokenLimit: pool.localCodex.modelAutoCompactTokenLimit,
|
|
supportsWebSockets: pool.localCodex.supportsWebSockets,
|
|
responsesWebSocketsV2: pool.localCodex.responsesWebSocketsV2,
|
|
responsesSmokeModel: pool.localCodex.responsesSmokeModel,
|
|
valuesPrinted: false,
|
|
},
|
|
next: {
|
|
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool configure-local${targetFlag(runtimeTarget)} --confirm`,
|
|
},
|
|
};
|
|
}
|
|
if (!consumerExposureAvailable) throw new Error(`${sub2apiConfigPath}.targets[${runtimeTarget.id}].publicExposure.enabled must be true; configure-local needs one consumer URL`);
|
|
const keyResult = await fetchPoolApiKey(config, pool, runtimeTarget);
|
|
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, runtimeTarget);
|
|
const validateResult = await validatePublicGatewayWithKey(pool, keyResult.apiKey, runtimeTarget);
|
|
return {
|
|
ok: writeResult.ok && validateResult.ok,
|
|
action: "platform-infra-sub2api-codex-pool-configure-local",
|
|
mode: "confirmed",
|
|
local: writeResult,
|
|
validation: validateResult,
|
|
apiKey: {
|
|
secret: `${runtimeTarget.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
|
|
keyPreview: apiKeyPreview(keyResult.apiKey),
|
|
apiKeyFingerprint: fingerprint(keyResult.apiKey),
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|