feat: extend G14 runtime automation

This commit is contained in:
Codex
2026-06-09 03:00:48 +00:00
parent 46dc43c618
commit 97f19d462f
9 changed files with 1967 additions and 132 deletions
+779 -116
View File
File diff suppressed because it is too large Load Diff
+141 -8
View File
@@ -5,8 +5,8 @@ import { startJob } from "./jobs";
import { runHwlabG14Command } from "./hwlab-g14";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane } from "./hwlab-node-lanes";
type SecretAction = "status" | "ensure";
type SecretPreset = "openfga" | "master-server-admin-api-key" | "code-agent-provider" | "cloud-api-db";
type SecretAction = "status" | "ensure" | "cleanup-owned-postgres";
type SecretPreset = "openfga" | "master-server-admin-api-key" | "code-agent-provider" | "cloud-api-db" | "owned-postgres-cleanup";
type DelegatedNodeDomain = "control-plane" | "git-mirror";
interface NodeSecretOptions {
@@ -86,6 +86,8 @@ export function hwlabNodeHelp(): Record<string, unknown> {
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-master-server-admin-api-key --confirm",
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-cloud-api-v03-db",
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-cloud-api-v03-db --confirm",
"bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --dry-run",
"bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node G14 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-code-agent-provider",
"bun scripts/cli.ts hwlab nodes secret ensure --node G14 --lane v03 --name hwlab-v03-code-agent-provider --confirm",
],
@@ -203,8 +205,8 @@ function rewriteDelegatedNodeString(value: string, scoped: ReturnType<typeof par
function parseSecretOptions(args: string[]): NodeSecretOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "ensure") {
throw new Error("secret usage: status|ensure --node NODE --lane vNN --name hwlab-vNN-openfga|hwlab-vNN-master-server-admin-api-key|hwlab-cloud-api-vNN-db|hwlab-vNN-code-agent-provider [--dry-run|--confirm]");
if (actionRaw !== "status" && actionRaw !== "ensure" && actionRaw !== "cleanup-owned-postgres") {
throw new Error("secret usage: status|ensure --node NODE --lane vNN --name hwlab-vNN-openfga|hwlab-vNN-master-server-admin-api-key|hwlab-cloud-api-vNN-db|hwlab-vNN-code-agent-provider [--dry-run|--confirm] | cleanup-owned-postgres --node NODE --lane vNN [--dry-run|--confirm]");
}
const node = requiredOption(args, "--node");
assertNodeId(node);
@@ -216,6 +218,21 @@ function parseSecretOptions(args: string[]): NodeSecretOptions {
const confirm = args.includes("--confirm");
const explicitDryRun = args.includes("--dry-run");
if (confirm && explicitDryRun) throw new Error("secret accepts only one of --confirm or --dry-run");
if (actionRaw === "cleanup-owned-postgres") {
if (lane === "v02") throw new Error("secret cleanup-owned-postgres is only for v0.3+ lanes after migration to G14 platform Postgres");
if (key !== undefined) throw new Error("secret cleanup-owned-postgres does not accept --key");
if (name !== spec.openFgaSecret && name !== spec.postgresSecret) throw new Error(`secret cleanup-owned-postgres for --lane ${lane} targets ${spec.postgresSecret}; omit --name or pass --name ${spec.postgresSecret}`);
return {
action: actionRaw,
node,
lane,
name: spec.postgresSecret,
preset: "owned-postgres-cleanup",
confirm,
dryRun: explicitDryRun || !confirm,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 900),
};
}
if (name === spec.masterAdminApiKeySecret) {
if (key !== undefined && key !== MASTER_ADMIN_API_KEY_KEY) throw new Error(`secret ${name} supports only key ${MASTER_ADMIN_API_KEY_KEY}`);
return {
@@ -317,11 +334,14 @@ function runNodeSecret(options: NodeSecretOptions): Record<string, unknown> {
? masterAdminApiKeySecretScript(options, spec)
: options.preset === "cloud-api-db"
? cloudApiDbSecretScript(options, spec)
: codeAgentProviderSecretScript(options, spec);
: options.preset === "owned-postgres-cleanup"
? ownedPostgresCleanupScript(options, spec)
: codeAgentProviderSecretScript(options, spec);
const result = runTransScript(options.node, script, input, options.timeoutSeconds);
const status = secretStatusFromText(statusText(result), result.exitCode === 0, result.exitCode, result.stderr, spec);
const dryRunOk = options.action === "ensure" && options.dryRun && result.exitCode === 0;
const ok = dryRunOk ? true : status.ok === true;
const cleanupDryRunOk = options.action === "cleanup-owned-postgres" && options.dryRun && result.exitCode === 0;
const ok = dryRunOk || cleanupDryRunOk ? true : status.ok === true;
return {
ok,
command: `hwlab nodes secret ${options.action}`,
@@ -331,13 +351,15 @@ function runNodeSecret(options: NodeSecretOptions): Record<string, unknown> {
secret: options.name,
key: options.key ?? null,
preset: options.preset,
mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : "confirmed-ensure",
mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : options.action === "cleanup-owned-postgres" ? "confirmed-delete" : "confirmed-ensure",
status,
mutation: status.mutation === true,
result: compactCommandResult(result),
valuesRedacted: true,
next: ok && options.action === "status" ? undefined : {
ensure: `bun scripts/cli.ts hwlab nodes secret ensure --node ${options.node} --lane ${options.lane} --name ${options.name}${options.key ? ` --key ${options.key}` : ""} --confirm`,
ensure: options.action === "cleanup-owned-postgres"
? `bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node ${options.node} --lane ${options.lane} --confirm`
: `bun scripts/cli.ts hwlab nodes secret ensure --node ${options.node} --lane ${options.lane} --name ${options.name}${options.key ? ` --key ${options.key}` : ""} --confirm`,
},
};
}
@@ -346,6 +368,76 @@ function runTransScript(node: string, script: string, input: string, timeoutSeco
return runCommand(["/root/.local/bin/trans", `${node}:k3s`, "script", "--", script], repoRoot, { input, timeoutMs: timeoutSeconds * 1000 });
}
function ownedPostgresCleanupScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string {
const pvc = `data-${spec.postgresSecret}-0`;
const platformService = "g14-platform-postgres";
return [
"set +e",
`namespace=${shellQuote(spec.namespace)}`,
`postgres_secret=${shellQuote(spec.postgresSecret)}`,
`postgres_statefulset=${shellQuote(spec.postgresStatefulSet)}`,
`pvc=${shellQuote(pvc)}`,
`platform_service=${shellQuote(platformService)}`,
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
"preset=owned-postgres-cleanup",
"exists_flag() { kind=\"$1\"; item=\"$2\"; kubectl -n \"$namespace\" get \"$kind\" \"$item\" >/dev/null 2>&1 && printf yes || printf no; }",
"pv_name() { kubectl -n \"$namespace\" get pvc \"$pvc\" -o jsonpath='{.spec.volumeName}' 2>/dev/null; }",
"phase_of_pvc() { kubectl -n \"$namespace\" get pvc \"$pvc\" -o jsonpath='{.status.phase}' 2>/dev/null; }",
"before_secret_exists=$(exists_flag secret \"$postgres_secret\")",
"before_pvc_exists=$(exists_flag pvc \"$pvc\")",
"before_pvc_phase=$(phase_of_pvc)",
"before_pv=$(pv_name)",
"before_statefulset_exists=$(exists_flag statefulset \"$postgres_statefulset\")",
"platform_service_exists=$(exists_flag service \"$platform_service\")",
"action=observed",
"mutation=false",
"delete_secret_exit=",
"delete_pvc_exit=",
"if [ \"$dry_run\" = true ]; then",
" if [ \"$before_secret_exists\" = yes ] || [ \"$before_pvc_exists\" = yes ]; then action=would-delete; else action=already-absent; fi",
"else",
" kubectl -n \"$namespace\" delete secret \"$postgres_secret\" --ignore-not-found=true >/tmp/hwlab-owned-postgres-secret-delete.out 2>/tmp/hwlab-owned-postgres-secret-delete.err",
" delete_secret_exit=$?",
" kubectl -n \"$namespace\" delete pvc \"$pvc\" --ignore-not-found=true >/tmp/hwlab-owned-postgres-pvc-delete.out 2>/tmp/hwlab-owned-postgres-pvc-delete.err",
" delete_pvc_exit=$?",
" if [ \"$delete_secret_exit\" -eq 0 ] && [ \"$delete_pvc_exit\" -eq 0 ]; then",
" if [ \"$before_secret_exists\" = yes ] || [ \"$before_pvc_exists\" = yes ]; then action=deleted; mutation=true; else action=already-absent; fi",
" else",
" action=delete-failed",
" fi",
"fi",
"after_secret_exists=$(exists_flag secret \"$postgres_secret\")",
"after_pvc_exists=$(exists_flag pvc \"$pvc\")",
"after_pvc_phase=$(phase_of_pvc)",
"after_pv=$(pv_name)",
"after_statefulset_exists=$(exists_flag statefulset \"$postgres_statefulset\")",
"printf 'namespace\\t%s\\n' \"$namespace\"",
"printf 'secret\\t%s\\n' \"$postgres_secret\"",
"printf 'pvc\\t%s\\n' \"$pvc\"",
"printf 'preset\\t%s\\n' \"$preset\"",
"printf 'action\\t%s\\n' \"$action\"",
"printf 'dryRun\\t%s\\n' \"$dry_run\"",
"printf 'mutation\\t%s\\n' \"$mutation\"",
"printf 'beforeSecretExists\\t%s\\n' \"$before_secret_exists\"",
"printf 'beforePvcExists\\t%s\\n' \"$before_pvc_exists\"",
"printf 'beforePvcPhase\\t%s\\n' \"$before_pvc_phase\"",
"printf 'beforePersistentVolume\\t%s\\n' \"$before_pv\"",
"printf 'beforeStatefulSetExists\\t%s\\n' \"$before_statefulset_exists\"",
"printf 'platformServiceExists\\t%s\\n' \"$platform_service_exists\"",
"printf 'afterSecretExists\\t%s\\n' \"$after_secret_exists\"",
"printf 'afterPvcExists\\t%s\\n' \"$after_pvc_exists\"",
"printf 'afterPvcPhase\\t%s\\n' \"$after_pvc_phase\"",
"printf 'afterPersistentVolume\\t%s\\n' \"$after_pv\"",
"printf 'afterStatefulSetExists\\t%s\\n' \"$after_statefulset_exists\"",
"printf 'deleteSecretExitCode\\t%s\\n' \"$delete_secret_exit\"",
"printf 'deletePvcExitCode\\t%s\\n' \"$delete_pvc_exit\"",
"if [ \"$platform_service_exists\" != yes ]; then exit 44; fi",
"if [ \"$before_statefulset_exists\" = yes ] || [ \"$after_statefulset_exists\" = yes ]; then exit 45; fi",
"if [ -n \"$delete_secret_exit\" ] && [ \"$delete_secret_exit\" != 0 ]; then exit \"$delete_secret_exit\"; fi",
"if [ -n \"$delete_pvc_exit\" ] && [ \"$delete_pvc_exit\" != 0 ]; then exit \"$delete_pvc_exit\"; fi",
].join("\n");
}
function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string {
return [
"set +e",
@@ -803,6 +895,47 @@ function cloudApiDbSecretScript(options: NodeSecretOptions, spec: RuntimeSecretS
function secretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string, spec: RuntimeSecretSpec): Record<string, unknown> {
const fields = keyValueLinesFromText(text);
if (fields.preset === "owned-postgres-cleanup") {
const absent = fields.afterSecretExists !== "yes" && fields.afterPvcExists !== "yes";
const oldWorkloadAbsent = fields.afterStatefulSetExists !== "yes";
const platformServiceReady = fields.platformServiceExists === "yes";
return {
ok: commandOk && absent && oldWorkloadAbsent && platformServiceReady,
namespace: fields.namespace || spec.namespace,
secret: fields.secret || spec.postgresSecret,
pvc: fields.pvc || `data-${spec.postgresSecret}-0`,
preset: "owned-postgres-cleanup",
action: fields.action || null,
dryRun: fields.dryRun === "true",
mutation: fields.mutation === "true",
before: {
secretExists: fields.beforeSecretExists === "yes",
pvcExists: fields.beforePvcExists === "yes",
pvcPhase: fields.beforePvcPhase || null,
persistentVolume: fields.beforePersistentVolume || null,
statefulSetExists: fields.beforeStatefulSetExists === "yes",
},
after: {
secretExists: fields.afterSecretExists === "yes",
pvcExists: fields.afterPvcExists === "yes",
pvcPhase: fields.afterPvcPhase || null,
persistentVolume: fields.afterPersistentVolume || null,
statefulSetExists: fields.afterStatefulSetExists === "yes",
},
platformService: {
name: "g14-platform-postgres",
exists: platformServiceReady,
},
deleteSecretExitCode: numericField(fields.deleteSecretExitCode),
deletePvcExitCode: numericField(fields.deletePvcExitCode),
exitCode,
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
valuesRedacted: true,
summary: absent
? `${fields.secret || spec.postgresSecret} and ${fields.pvc || `data-${spec.postgresSecret}-0`} absent`
: `${fields.secret || spec.postgresSecret} or ${fields.pvc || `data-${spec.postgresSecret}-0`} still exists`,
};
}
if (fields.preset === "master-server-admin-api-key") {
const afterBytes = numericField(fields.afterApiKeyBytes);
const healthy = fields.afterExists === "yes" && fields.afterApiKeyPresent === "yes" && typeof afterBytes === "number" && afterBytes > 0;
+848
View File
@@ -0,0 +1,848 @@
import { createHash } from "node:crypto";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { UniDeskConfig } from "./config";
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
const g14K3sRoute = "G14:k3s";
const namespace = "platform-infra";
const serviceName = "sub2api";
const serviceDns = `${serviceName}.${namespace}.svc.cluster.local:8080`;
const fieldManager = "unidesk-platform-infra";
const appSecretName = "sub2api-secrets";
const poolGroupName = "unidesk-codex-pool";
const poolApiKeyName = "unidesk-codex-pool-api-key";
const poolApiKeySecretName = "sub2api-codex-pool-api-key";
const poolApiKeySecretKey = "API_KEY";
interface DisclosureOptions {
full: boolean;
raw: boolean;
}
interface SyncOptions extends DisclosureOptions {
confirm: boolean;
}
interface CodexProfile {
profile: string;
accountName: string;
configFile: string;
authFile: string;
provider: string;
baseUrl: string;
wireApi: string | null;
model: string | null;
envKey: string | null;
apiKey: string | null;
apiKeySource: "auth-json" | "env" | null;
authOpenAIKeyShape: string;
ok: boolean;
error: string | null;
}
export function codexPoolHelp(): unknown {
return {
command: "platform-infra sub2api codex-pool plan|sync|validate",
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--full|--raw]",
],
description: "Import ~/.codex/config.toml plus config.toml.* API-key profiles into one Sub2API OpenAI pool and expose one internal API_KEY stored in a k3s Secret.",
target: {
route: g14K3sRoute,
namespace,
serviceDns,
poolGroupName,
poolApiKeySecretName,
poolApiKeySecretKey,
secretValuesPrinted: false,
},
};
}
export async function runCodexPoolCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
const [action = "plan"] = args;
if (action === "plan") return codexPoolPlan();
if (action === "sync") return await codexPoolSync(config, parseSyncOptions(args.slice(1)));
if (action === "validate") return await codexPoolValidate(config, parseDisclosureOptions(args.slice(1)));
return {
ok: false,
error: "unsupported-platform-infra-sub2api-codex-pool-command",
args,
help: codexPoolHelp(),
};
}
function parseSyncOptions(args: string[]): SyncOptions {
validateOptions(args, new Set(["--confirm", "--full", "--raw"]));
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm"));
return { ...disclosure, confirm: args.includes("--confirm") };
}
function parseDisclosureOptions(args: string[]): DisclosureOptions {
validateOptions(args, new Set(["--full", "--raw"]));
const raw = args.includes("--raw");
return { full: raw || args.includes("--full"), raw };
}
function validateOptions(args: string[], booleanOptions: Set<string>): void {
for (const arg of args) {
if (booleanOptions.has(arg)) continue;
throw new Error(`unsupported option: ${arg}`);
}
}
function codexPoolPlan(): Record<string, unknown> {
const profiles = collectCodexProfiles();
const ok = profiles.length > 0 && profiles.every((profile) => profile.ok);
return {
ok,
action: "platform-infra-sub2api-codex-pool-plan",
source: {
directory: join(homedir(), ".codex"),
configPattern: "config.toml and config.toml.*",
authPattern: "auth.json and auth.json.*",
valuesPrinted: false,
},
target: poolTarget(),
profiles: profiles.map(redactProfile),
decision: {
accountType: "openai/apikey",
grouping: `All discovered Codex profiles are bound to one Sub2API group named ${poolGroupName}.`,
unifiedApiKey: `The client-facing API_KEY is controlled by k3s Secret ${namespace}/${poolApiKeySecretName}.${poolApiKeySecretKey}.`,
idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files.",
configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.",
},
next: ok
? { sync: "bun scripts/cli.ts platform-infra sub2api codex-pool sync --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." },
};
}
async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promise<Record<string, unknown>> {
const profiles = collectCodexProfiles();
const planOk = profiles.length > 0 && profiles.every((profile) => profile.ok);
if (!options.confirm || !planOk) {
return {
...codexPoolPlan(),
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 payload = {
pool: {
groupName: poolGroupName,
apiKeyName: poolApiKeyName,
apiKeySecretName: poolApiKeySecretName,
apiKeySecretKey: poolApiKeySecretKey,
},
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 ?? ""),
})),
};
const result = await capture(config, g14K3sRoute, ["script"], syncScript(payload));
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: {
profiles: profiles.map(redactProfile),
valuesPrinted: false,
},
remote: parsed ?? compactCapture(result, { full: options.full || result.exitCode !== 0 }),
next: {
validate: "bun scripts/cli.ts platform-infra sub2api codex-pool validate",
},
};
}
async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
const result = await capture(config, g14K3sRoute, ["script"], validateScript());
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: parsed,
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
};
}
function collectCodexProfiles(): CodexProfile[] {
const codexDir = join(homedir(), ".codex");
if (!existsSync(codexDir)) return [];
const configFiles = readdirSync(codexDir)
.filter((file) => file === "config.toml" || file.startsWith("config.toml."))
.sort((a, b) => {
if (a === "config.toml") return -1;
if (b === "config.toml") return 1;
return a.localeCompare(b);
});
const seenAccountNames = new Set<string>();
return configFiles.map((configFile) => {
const suffix = configFile === "config.toml" ? "" : configFile.slice("config.toml.".length);
const profile = suffix === "" ? "default" : suffix;
const accountName = uniqueAccountName(profile, seenAccountNames);
const authFile = suffix === "" ? "auth.json" : `auth.json.${suffix}`;
const configPath = join(codexDir, configFile);
const authPath = join(codexDir, authFile);
const base: CodexProfile = {
profile,
accountName,
configFile,
authFile,
provider: "",
baseUrl: "",
wireApi: null,
model: null,
envKey: null,
apiKey: null,
apiKeySource: null,
authOpenAIKeyShape: existsSync(authPath) ? "unknown" : "missing",
ok: false,
error: null,
};
try {
const parsed = Bun.TOML.parse(readFileSync(configPath, "utf8")) as unknown;
if (!isRecord(parsed)) throw new Error("config is not a TOML object");
const providers = parsed.model_providers;
if (!isRecord(providers)) throw new Error("model_providers is missing");
const provider = stringValue(parsed.model_provider) ?? Object.keys(providers)[0] ?? "";
if (provider === "") throw new Error("model_provider is missing");
const providerConfig = providers[provider];
if (!isRecord(providerConfig)) throw new Error(`model provider ${provider} is missing`);
const baseUrl = normalizeBaseUrl(stringValue(providerConfig.base_url));
if (baseUrl === null) throw new Error(`model provider ${provider} base_url is missing or invalid`);
base.provider = provider;
base.baseUrl = baseUrl;
base.envKey = stringValue(providerConfig.env_key);
base.wireApi = stringValue(providerConfig.wire_api);
base.model = stringValue(parsed.model);
const auth = readAuthAPIKey(authPath);
base.authOpenAIKeyShape = auth.shape;
if (auth.apiKey !== null) {
base.apiKey = auth.apiKey;
base.apiKeySource = "auth-json";
} else if (base.envKey !== null && typeof process.env[base.envKey] === "string" && process.env[base.envKey]!.length > 0) {
base.apiKey = process.env[base.envKey]!;
base.apiKeySource = "env";
}
if (base.apiKey === null || base.apiKey.length === 0) {
throw new Error(base.envKey === null ? "auth OPENAI_API_KEY is missing or empty" : `auth OPENAI_API_KEY is missing and env ${base.envKey} is not present`);
}
base.ok = true;
return base;
} catch (error) {
base.error = error instanceof Error ? error.message : String(error);
return base;
}
});
}
function readAuthAPIKey(authPath: string): { apiKey: string | null; shape: string } {
if (!existsSync(authPath)) return { apiKey: null, shape: "missing" };
const parsed = JSON.parse(readFileSync(authPath, "utf8")) as unknown;
if (!isRecord(parsed)) return { apiKey: null, shape: "non-object" };
const value = parsed.OPENAI_API_KEY;
const shape = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
if (typeof value === "string" && value.length > 0) return { apiKey: value, shape };
return { apiKey: null, shape };
}
function uniqueAccountName(profile: string, seen: Set<string>): string {
const normalized = profile
.toLowerCase()
.replace(/[^a-z0-9._-]+/gu, "-")
.replace(/^-+|-+$/gu, "") || "default";
let candidate = `unidesk-codex-${normalized}`;
let counter = 2;
while (seen.has(candidate)) {
candidate = `unidesk-codex-${normalized}-${counter}`;
counter += 1;
}
seen.add(candidate);
return candidate;
}
function redactProfile(profile: CodexProfile): Record<string, unknown> {
return {
profile: profile.profile,
accountName: profile.accountName,
configFile: profile.configFile,
authFile: profile.authFile,
provider: profile.provider || null,
baseUrl: profile.baseUrl || null,
wireApi: profile.wireApi,
model: profile.model,
envKey: profile.envKey,
apiKeySource: profile.apiKeySource,
apiKeyPresent: profile.apiKey !== null && profile.apiKey.length > 0,
apiKeyBytes: profile.apiKey === null ? 0 : Buffer.byteLength(profile.apiKey, "utf8"),
apiKeyFingerprint: profile.apiKey === null ? null : fingerprint(profile.apiKey),
authOpenAIKeyShape: profile.authOpenAIKeyShape,
ok: profile.ok,
error: profile.error,
valuesPrinted: false,
};
}
function poolTarget(): Record<string, unknown> {
return {
route: g14K3sRoute,
namespace,
service: serviceName,
serviceDns,
groupName: poolGroupName,
apiKeyName: poolApiKeyName,
apiKeySecret: `${namespace}/${poolApiKeySecretName}.${poolApiKeySecretKey}`,
valuesPrinted: false,
};
}
function normalizeBaseUrl(value: string | null): string | null {
if (value === null) return null;
const trimmed = value.trim().replace(/\/+$/u, "");
if (trimmed.length === 0) return null;
try {
const parsed = new URL(trimmed);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
return parsed.toString().replace(/\/+$/u, "");
} catch {
return null;
}
}
function stringValue(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function fingerprint(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 12);
}
function syncScript(payload: unknown): string {
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return remotePythonScript("sync", encoded);
}
function validateScript(): string {
return remotePythonScript("validate", "");
}
function remotePythonScript(mode: "sync" | "validate", encodedPayload: string): string {
return `
set -u
python3 - <<'PY'
import base64
import json
import secrets
import string
import subprocess
import sys
import time
from urllib.parse import quote
NAMESPACE = "${namespace}"
SERVICE_NAME = "${serviceName}"
SERVICE_DNS = "${serviceDns}"
FIELD_MANAGER = "${fieldManager}"
APP_SECRET_NAME = "${appSecretName}"
POOL_GROUP_NAME = "${poolGroupName}"
POOL_API_KEY_NAME = "${poolApiKeyName}"
POOL_API_KEY_SECRET_NAME = "${poolApiKeySecretName}"
POOL_API_KEY_SECRET_KEY = "${poolApiKeySecretKey}"
MODE = "${mode}"
PAYLOAD_B64 = "${encodedPayload}"
def run(cmd, input_bytes=None):
return subprocess.run(cmd, input=input_bytes, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def text(data, limit=4000):
if isinstance(data, bytes):
data = data.decode("utf-8", errors="replace")
return data[-limit:]
def kubectl(args, input_obj=None):
if isinstance(input_obj, str):
input_bytes = input_obj.encode("utf-8")
else:
input_bytes = input_obj
return run(["kubectl", *args], input_bytes)
def require_kubectl(args, input_obj=None, label="kubectl"):
proc = kubectl(args, input_obj)
if proc.returncode != 0:
raise RuntimeError(f"{label} failed: {text(proc.stderr, 1000)}")
return proc.stdout
def kube_json(args, label):
raw = require_kubectl([*args, "-o", "json"], label=label)
return json.loads(raw.decode("utf-8"))
def decode_secret_value(name, key):
data = kube_json(["-n", NAMESPACE, "get", "secret", name], f"secret/{name}").get("data") or {}
if key not in data:
return None
return base64.b64decode(data[key]).decode("utf-8")
def get_config_value(name, key):
data = kube_json(["-n", NAMESPACE, "get", "configmap", name], f"configmap/{name}").get("data") or {}
value = data.get(key)
return value if isinstance(value, str) and value else None
def select_app_pod():
pods = kube_json(["-n", NAMESPACE, "get", "pods", "-l", "app.kubernetes.io/name=sub2api"], "sub2api pods").get("items") or []
for pod in pods:
status = pod.get("status") or {}
if status.get("phase") != "Running":
continue
statuses = status.get("containerStatuses") or []
if statuses and all(item.get("ready") is True for item in statuses):
return pod["metadata"]["name"]
if pods:
return pods[0]["metadata"]["name"]
raise RuntimeError("sub2api app pod not found")
APP_POD = select_app_pod()
def parse_curl_output(proc):
stdout = proc.stdout.decode("utf-8", errors="replace")
marker = "\\n__HTTP_CODE__:"
pos = stdout.rfind(marker)
if pos < 0:
return {
"ok": False,
"httpStatus": 0,
"json": None,
"body": stdout,
"stderr": text(proc.stderr, 1000),
"transportExitCode": proc.returncode,
}
body = stdout[:pos]
status_text = stdout[pos + len(marker):].strip()
try:
http_status = int(status_text[-3:])
except ValueError:
http_status = 0
try:
parsed = json.loads(body) if body.strip() else None
except json.JSONDecodeError:
parsed = None
return {
"ok": proc.returncode == 0 and 200 <= http_status < 300,
"httpStatus": http_status,
"json": parsed,
"body": body,
"stderr": text(proc.stderr, 1000),
"transportExitCode": proc.returncode,
}
def curl_api(method, path, bearer=None, payload=None):
body = b"" if payload is None else json.dumps(payload, separators=(",", ":")).encode("utf-8")
script = r'''
set -eu
method="$1"
url="$2"
token="\${3:-}"
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
cat > "$tmp"
if [ -n "$token" ]; then
if [ "$method" = "GET" ] && [ ! -s "$tmp" ]; then
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H "Authorization: Bearer $token" "$url"
else
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' -H "Authorization: Bearer $token" --data-binary @"$tmp" "$url"
fi
else
if [ "$method" = "GET" ] && [ ! -s "$tmp" ]; then
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" "$url"
else
curl -sS -w '\\n__HTTP_CODE__:%{http_code}' -X "$method" -H 'Content-Type: application/json' --data-binary @"$tmp" "$url"
fi
fi
'''
proc = run([
"kubectl", "-n", NAMESPACE, "exec", "-i", APP_POD,
"--", "sh", "-c", script, "sh", method, f"http://127.0.0.1:8080{path}", bearer or "",
], body)
return parse_curl_output(proc)
def envelope_data(parsed):
if isinstance(parsed, dict) and "data" in parsed:
return parsed.get("data")
return parsed
def ensure_success(resp, label):
parsed = resp.get("json")
code = parsed.get("code") if isinstance(parsed, dict) else None
if not resp.get("ok") or (code is not None and code != 0):
message = parsed.get("message") if isinstance(parsed, dict) else text(resp.get("body", ""), 500)
raise RuntimeError(f"{label} failed: http={resp.get('httpStatus')} message={message}")
return envelope_data(parsed)
def extract_items(data):
if isinstance(data, list):
return data
if isinstance(data, dict):
if isinstance(data.get("items"), list):
return data["items"]
for key in ("groups", "accounts", "api_keys", "keys"):
if isinstance(data.get(key), list):
return data[key]
return []
def find_access_token(data):
if isinstance(data, dict):
for key in ("access_token", "token"):
if isinstance(data.get(key), str) and data[key]:
return data[key]
for value in data.values():
token = find_access_token(value)
if token:
return token
return None
def login():
admin_email = get_config_value("sub2api-config", "ADMIN_EMAIL") or "admin@sub2api.platform-infra.local"
admin_password = decode_secret_value(APP_SECRET_NAME, "ADMIN_PASSWORD")
if not admin_password:
raise RuntimeError("ADMIN_PASSWORD missing from sub2api-secrets")
data = ensure_success(curl_api("POST", "/api/v1/auth/login", payload={"email": admin_email, "password": admin_password}), "admin login")
token = find_access_token(data)
if not token:
raise RuntimeError("admin login response did not contain access_token")
return admin_email, token
def group_payload():
return {
"name": POOL_GROUP_NAME,
"description": "UniDesk-managed Codex API-key pool for G14 k3s internal Sub2API clients.",
"platform": "openai",
"rate_multiplier": 1,
"is_exclusive": False,
"subscription_type": "standard",
"allow_messages_dispatch": True,
"require_oauth_only": False,
"require_privacy_set": False,
"rpm_limit": 0,
}
def ensure_group(token):
existing_data = ensure_success(curl_api("GET", "/api/v1/admin/groups/all?platform=openai", bearer=token), "list groups")
existing = next((item for item in extract_items(existing_data) if item.get("name") == POOL_GROUP_NAME), None)
payload = group_payload()
if existing is None:
created = ensure_success(curl_api("POST", "/api/v1/admin/groups", bearer=token, payload=payload), "create group")
return created, "created"
group_id = existing.get("id")
if group_id is None:
raise RuntimeError("existing group has no id")
payload["status"] = "active"
updated = ensure_success(curl_api("PUT", f"/api/v1/admin/groups/{group_id}", bearer=token, payload=payload), "update group")
return updated if isinstance(updated, dict) else existing, "updated"
def list_accounts(token):
path = "/api/v1/admin/accounts?page=1&page_size=200&platform=openai&type=apikey&search=" + quote("unidesk-codex-")
data = ensure_success(curl_api("GET", path, bearer=token), "list accounts")
return extract_items(data)
def account_payload(profile, group_id):
return {
"name": profile["accountName"],
"notes": f"UniDesk-managed Codex profile {profile['profile']} from {profile['configFile']} and {profile['authFile']}; secret source={profile['apiKeySource']}; fingerprint={profile['apiKeyFingerprint']}.",
"platform": "openai",
"type": "apikey",
"credentials": {
"api_key": profile["apiKey"],
"base_url": profile["baseUrl"],
},
"extra": {
"openai_responses_mode": "force_responses",
"unidesk_codex_profile": profile["profile"],
"unidesk_managed": True,
},
"concurrency": 1,
"priority": 1,
"rate_multiplier": 1,
"load_factor": 1,
"group_ids": [group_id],
"confirm_mixed_channel_risk": True,
}
def ensure_accounts(token, profiles, group_id):
existing = {item.get("name"): item for item in list_accounts(token)}
results = []
for profile in profiles:
payload = account_payload(profile, group_id)
current = existing.get(profile["accountName"])
if current and current.get("id") is not None:
account_id = current["id"]
update_payload = dict(payload)
update_payload.pop("platform", None)
update_payload["status"] = "active"
data = ensure_success(curl_api("PUT", f"/api/v1/admin/accounts/{account_id}", bearer=token, payload=update_payload), f"update account {profile['accountName']}")
action = "updated"
else:
data = ensure_success(curl_api("POST", "/api/v1/admin/accounts", bearer=token, payload=payload), f"create account {profile['accountName']}")
action = "created"
results.append({
"profile": profile["profile"],
"accountName": profile["accountName"],
"accountId": data.get("id") if isinstance(data, dict) else None,
"action": action,
"baseUrl": profile["baseUrl"],
"apiKeySource": profile["apiKeySource"],
"apiKeyFingerprint": profile["apiKeyFingerprint"],
"valuesPrinted": False,
})
return results
def generate_api_key():
alphabet = string.ascii_letters + string.digits
return "sk-unidesk-codex-" + "".join(secrets.choice(alphabet) for _ in range(48))
def ensure_api_key_secret(group_id):
existing = None
try:
existing = decode_secret_value(POOL_API_KEY_SECRET_NAME, POOL_API_KEY_SECRET_KEY)
except Exception:
existing = None
api_key = existing if existing else generate_api_key()
secret_action = "kept-existing" if existing else "created"
manifest = {
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": POOL_API_KEY_SECRET_NAME,
"namespace": NAMESPACE,
"labels": {
"app.kubernetes.io/name": "sub2api",
"app.kubernetes.io/part-of": "platform-infra",
"app.kubernetes.io/managed-by": "unidesk",
"unidesk.ai/secret-purpose": "sub2api-codex-pool-api-key",
},
},
"type": "Opaque",
"stringData": {
POOL_API_KEY_SECRET_KEY: api_key,
"GROUP_ID": str(group_id),
"GROUP_NAME": POOL_GROUP_NAME,
"SERVICE_DNS": SERVICE_DNS,
},
}
proc = kubectl(["apply", "--server-side", "--force-conflicts", f"--field-manager={FIELD_MANAGER}", "-f", "-"], json.dumps(manifest))
if proc.returncode != 0:
raise RuntimeError(f"apply API key secret failed: {text(proc.stderr, 1000)}")
return api_key, secret_action, text(proc.stdout, 1000)
def list_user_keys(token):
data = ensure_success(curl_api("GET", "/api/v1/keys?page=1&page_size=200", bearer=token), "list user keys")
return extract_items(data)
def ensure_sub2api_api_key(token, api_key, group_id):
keys = list_user_keys(token)
existing = next((item for item in keys if item.get("key") == api_key), None)
action = "kept-existing"
if existing is None:
existing = next((item for item in keys if item.get("name") == POOL_API_KEY_NAME), None)
if existing is None or existing.get("key") != api_key:
payload = {
"name": POOL_API_KEY_NAME,
"group_id": group_id,
"custom_key": api_key,
"quota": 0,
"rate_limit_5h": 0,
"rate_limit_1d": 0,
"rate_limit_7d": 0,
}
created = ensure_success(curl_api("POST", "/api/v1/keys", bearer=token, payload=payload), "create pool API key")
existing = created if isinstance(created, dict) else existing
action = "created"
elif existing.get("id") is not None and existing.get("group_id") != group_id:
updated = ensure_success(curl_api("PUT", f"/api/v1/keys/{existing['id']}", bearer=token, payload={"name": POOL_API_KEY_NAME, "group_id": group_id}), "update pool API key group")
existing = updated if isinstance(updated, dict) else existing
action = "updated-group"
return {
"action": action,
"id": existing.get("id") if isinstance(existing, dict) else None,
"name": existing.get("name") if isinstance(existing, dict) else POOL_API_KEY_NAME,
"groupId": existing.get("group_id") if isinstance(existing, dict) else group_id,
}
def validate_gateway(api_key):
resp = curl_api("GET", "/v1/models", bearer=api_key)
parsed = resp.get("json")
model_count = None
if isinstance(parsed, dict) and isinstance(parsed.get("data"), list):
model_count = len(parsed["data"])
return {
"ok": resp.get("ok"),
"httpStatus": resp.get("httpStatus"),
"transportExitCode": resp.get("transportExitCode"),
"modelCount": model_count,
"bodyPreview": text(resp.get("body", ""), 500) if not resp.get("ok") else "",
"stderr": resp.get("stderr", ""),
"method": "GET /v1/models",
"serviceDns": SERVICE_DNS,
"valuesPrinted": False,
}
def api_key_preview(api_key):
if len(api_key) <= 14:
return "***"
return api_key[:10] + "..." + api_key[-4:]
def run_sync():
payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8"))
profiles = payload.get("profiles") or []
if not profiles:
raise RuntimeError("sync payload has no profiles")
admin_email, token = login()
group, group_action = ensure_group(token)
group_id = group.get("id") if isinstance(group, dict) else None
if group_id is None:
raise RuntimeError("pool group id missing after ensure")
account_results = ensure_accounts(token, profiles, group_id)
api_key, secret_action, secret_apply_stdout = ensure_api_key_secret(group_id)
api_key_result = ensure_sub2api_api_key(token, api_key, group_id)
gateway = validate_gateway(api_key)
return {
"ok": gateway["ok"] is True,
"mode": "sync",
"namespace": NAMESPACE,
"serviceDns": SERVICE_DNS,
"appPod": APP_POD,
"admin": {"email": admin_email, "tokenPrinted": False},
"pool": {"name": POOL_GROUP_NAME, "id": group_id, "action": group_action, "platform": group.get("platform") if isinstance(group, dict) else "openai"},
"accounts": {
"desired": len(profiles),
"created": sum(1 for item in account_results if item["action"] == "created"),
"updated": sum(1 for item in account_results if item["action"] == "updated"),
"items": account_results,
"valuesPrinted": False,
},
"apiKey": {
"name": POOL_API_KEY_NAME,
"secret": f"{NAMESPACE}/{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY}",
"secretAction": secret_action,
"secretApply": secret_apply_stdout,
"sub2apiAction": api_key_result["action"],
"sub2apiId": api_key_result["id"],
"groupId": api_key_result["groupId"],
"keyPreview": api_key_preview(api_key),
"valuesPrinted": False,
},
"validation": {"gatewayModels": gateway},
}
def run_validate():
api_key = decode_secret_value(POOL_API_KEY_SECRET_NAME, POOL_API_KEY_SECRET_KEY)
if not api_key:
raise RuntimeError(f"{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY} missing")
gateway = validate_gateway(api_key)
return {
"ok": gateway["ok"] is True,
"mode": "validate",
"namespace": NAMESPACE,
"serviceDns": SERVICE_DNS,
"appPod": APP_POD,
"apiKey": {
"secret": f"{NAMESPACE}/{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY}",
"keyPreview": api_key_preview(api_key),
"valuesPrinted": False,
},
"validation": {"gatewayModels": gateway},
}
try:
result = run_sync() if MODE == "sync" else run_validate()
except Exception as exc:
result = {
"ok": False,
"mode": MODE,
"namespace": NAMESPACE,
"serviceDns": SERVICE_DNS,
"appPod": globals().get("APP_POD"),
"error": str(exc),
"valuesPrinted": False,
}
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0 if result.get("ok") else 1)
PY
`;
}
async function capture(config: UniDeskConfig, target: string, args: string[], input?: string): Promise<SshCaptureResult> {
return await runSshCommandCapture(config, target, args, input);
}
function parseJsonOutput(stdout: string): Record<string, unknown> | null {
const trimmed = stdout.trim();
if (trimmed.length === 0) return null;
const start = trimmed.indexOf("{");
const end = trimmed.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) return null;
try {
const parsed = JSON.parse(trimmed.slice(start, end + 1)) as unknown;
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
} catch {
return null;
}
}
function boolField(value: Record<string, unknown> | null, key: string, defaultValue: boolean): boolean {
if (value === null) return defaultValue;
const field = value[key];
return typeof field === "boolean" ? field : defaultValue;
}
function compactCapture(result: SshCaptureResult, options: { full?: boolean } = {}): Record<string, unknown> {
const full = options.full ?? false;
return {
exitCode: result.exitCode,
stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
stdoutTail: full || result.exitCode !== 0 ? result.stdout.slice(-8000) : "",
stderrTail: full || result.exitCode !== 0 ? result.stderr.slice(-4000) : "",
};
}
+16 -1
View File
@@ -23,7 +23,7 @@ interface Sub2ApiConfig {
export function platformInfraHelp(): unknown {
return {
command: "platform-infra sub2api plan|apply|status|validate",
command: "platform-infra sub2api plan|apply|status|validate|codex-pool",
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2api plan",
@@ -31,6 +31,9 @@ export function platformInfraHelp(): unknown {
"bun scripts/cli.ts platform-infra sub2api apply --confirm",
"bun scripts/cli.ts platform-infra sub2api status [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api validate [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool validate",
],
description: "Operate the G14 k3s internal-only Sub2API deployment in the shared platform-infra namespace. This entry creates no Ingress, NodePort, LoadBalancer, hostPort, hostNetwork, ResourceQuota, LimitRange, or CPU/memory resource requests/limits.",
target: {
@@ -42,6 +45,14 @@ export function platformInfraHelp(): unknown {
resourceLimits: "unset-by-policy",
versionConfigPath: configPath,
},
codexPool: {
usage: [
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool validate",
],
module: "scripts/src/platform-infra-sub2api-codex.ts",
},
};
}
@@ -52,6 +63,10 @@ export async function runPlatformInfraCommand(config: UniDeskConfig, args: strin
if (action === "apply") return await apply(config, parseApplyOptions(args.slice(2)));
if (action === "status") return await status(config, parseDisclosureOptions(args.slice(2)));
if (action === "validate") return await validate(config, parseDisclosureOptions(args.slice(2)));
if (action === "codex-pool") {
const { runCodexPoolCommand } = await import("./platform-infra-sub2api-codex");
return await runCodexPoolCommand(config, args.slice(2));
}
return unsupported(args);
}