Files
pikasTech-unidesk/scripts/src/platform-infra-sub2api-codex/actions.ts
T

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,
},
};
}