feat: activate D601 sub2api external edge

This commit is contained in:
Codex
2026-06-12 07:56:46 +00:00
parent f5a6610fe9
commit c23d9b5ea0
8 changed files with 2375 additions and 164 deletions
@@ -80,6 +80,10 @@ export interface CodexPoolSentinelManifestOptions {
serviceName: string;
serviceDns: string;
appSecretName: string;
proxy?: {
httpProxy: string;
noProxy: string;
} | null;
}
export function defaultCodexPoolSentinelConfig(): CodexPoolSentinelConfig {
@@ -317,6 +321,25 @@ export function renderCodexPoolSentinelManifest(
const activeDeadlineSeconds = Math.max(300, Math.min(3600, config.probe.timeoutSeconds + 240));
const command = sentinelContainerShellCommand(config);
const runtimeImage = codexPoolSentinelRuntimeImage(config).runtimeImage;
const proxyEnv = options.proxy?.httpProxy
? ` - name: HTTP_PROXY
value: ${JSON.stringify(options.proxy.httpProxy)}
- name: HTTPS_PROXY
value: ${JSON.stringify(options.proxy.httpProxy)}
- name: ALL_PROXY
value: ${JSON.stringify(options.proxy.httpProxy)}
- name: http_proxy
value: ${JSON.stringify(options.proxy.httpProxy)}
- name: https_proxy
value: ${JSON.stringify(options.proxy.httpProxy)}
- name: all_proxy
value: ${JSON.stringify(options.proxy.httpProxy)}
- name: NO_PROXY
value: ${JSON.stringify(options.proxy.noProxy)}
- name: no_proxy
value: ${JSON.stringify(options.proxy.noProxy)}
`
: "";
return `apiVersion: v1
kind: Secret
metadata:
@@ -446,6 +469,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
${proxyEnv}
volumeMounts:
- name: sentinel-config
mountPath: /opt/sentinel
+222 -80
View File
@@ -17,11 +17,13 @@ import {
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
const g14K3sRoute = "G14:k3s";
const defaultTargetId = "G14";
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 sub2apiConfigPath = rootPath("config", "platform-infra", "sub2api.yaml");
const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");
const sentinelImageDockerfilePath = rootPath("src", "components", "platform-infra", "sub2api", "sentinel.Dockerfile");
const defaultPoolGroupName = "unidesk-codex-pool";
@@ -37,6 +39,7 @@ const remoteJobPollMs = 5_000;
interface DisclosureOptions {
full: boolean;
raw: boolean;
targetId: string;
}
interface SyncOptions extends DisclosureOptions {
@@ -70,6 +73,21 @@ interface SentinelImageOptions extends DisclosureOptions {
dryRun: boolean;
}
interface CodexPoolRuntimeTarget {
id: string;
route: string;
namespace: string;
serviceName: string;
serviceDns: string;
appSecretName: string;
egressProxy: {
enabled: boolean;
applyToSentinel: boolean;
httpProxy: string;
noProxy: string;
} | null;
}
interface CodexProfile {
profile: string;
accountName: string;
@@ -204,14 +222,15 @@ export function codexPoolHelp(): unknown {
output: "json, except trace and sentinel-report default to low-noise text tables",
usage: [
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
"bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm [--prune-removed]",
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report [--events 20|--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool plan --target D601",
"bun scripts/cli.ts platform-infra sub2api codex-pool sync [--target D601] --confirm [--prune-removed]",
"bun scripts/cli.ts platform-infra sub2api codex-pool validate [--target D601] [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool trace [--target D601] --request-id <requestId> [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status [--target D601]",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build [--target D601] --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe [--target D601] --account unidesk-codex-hy --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-report [--target D601] [--events 20|--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes [--target D601] --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool expose --confirm",
"bun scripts/cli.ts platform-infra sub2api codex-pool configure-local --confirm",
],
@@ -254,14 +273,14 @@ export async function runCodexPoolCommand(config: UniDeskConfig, args: string[])
}
function parseSyncOptions(args: string[]): SyncOptions {
validateOptions(args, new Set(["--confirm", "--prune-removed", "--full", "--raw"]));
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm" && arg !== "--prune-removed"));
validateOptions(args, new Set(["--confirm", "--prune-removed", "--full", "--raw", "--target"]));
const disclosure = parseDisclosureOptions(stripBooleanOptions(args, new Set(["--confirm", "--prune-removed"])));
return { ...disclosure, confirm: args.includes("--confirm"), pruneRemoved: args.includes("--prune-removed") };
}
function parseConfirmOptions(args: string[]): ConfirmOptions {
validateOptions(args, new Set(["--confirm", "--full", "--raw"]));
const disclosure = parseDisclosureOptions(args.filter((arg) => arg !== "--confirm"));
validateOptions(args, new Set(["--confirm", "--full", "--raw", "--target"]));
const disclosure = parseDisclosureOptions(stripBooleanOptions(args, new Set(["--confirm"])));
return { ...disclosure, confirm: args.includes("--confirm") };
}
@@ -271,7 +290,8 @@ function parseSentinelImageOptions(args: string[]): SentinelImageOptions {
let confirm = false;
let explicitDryRun = false;
const disclosureArgs: string[] = [];
for (const arg of rest) {
for (let index = 0; index < rest.length; index += 1) {
const arg = rest[index]!;
if (arg === "--confirm") {
confirm = true;
continue;
@@ -280,8 +300,14 @@ function parseSentinelImageOptions(args: string[]): SentinelImageOptions {
explicitDryRun = true;
continue;
}
if (arg === "--full" || arg === "--raw") {
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
disclosureArgs.push(arg);
if (arg === "--target") {
const value = rest[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
disclosureArgs.push(value);
index += 1;
}
continue;
}
throw new Error(`unsupported option: ${arg}`);
@@ -306,8 +332,14 @@ function parseSentinelProbeOptions(args: string[]): SentinelProbeOptions {
confirm = true;
continue;
}
if (arg === "--full" || arg === "--raw") {
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
disclosureArgs.push(arg);
if (arg === "--target") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
disclosureArgs.push(value);
index += 1;
}
continue;
}
if (arg === "--account") {
@@ -336,8 +368,14 @@ function parseSentinelReportOptions(args: string[]): SentinelReportOptions {
const disclosureArgs: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]!;
if (arg === "--full" || arg === "--raw") {
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
disclosureArgs.push(arg);
if (arg === "--target") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
disclosureArgs.push(value);
index += 1;
}
continue;
}
if (arg === "--events") {
@@ -369,8 +407,14 @@ function parseTraceOptions(args: string[]): TraceOptions {
const disclosureArgs: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]!;
if (arg === "--full" || arg === "--raw") {
if (arg === "--full" || arg === "--raw" || arg === "--target" || arg.startsWith("--target=")) {
disclosureArgs.push(arg);
if (arg === "--target") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
disclosureArgs.push(value);
index += 1;
}
continue;
}
if (arg === "--show-lines") {
@@ -452,9 +496,26 @@ function readReportEventLimit(raw: string, option: string): number {
}
function parseDisclosureOptions(args: string[]): DisclosureOptions {
validateOptions(args, new Set(["--full", "--raw"]));
validateOptions(args, new Set(["--full", "--raw", "--target"]));
const raw = args.includes("--raw");
return { full: raw || args.includes("--full"), raw };
return { full: raw || args.includes("--full"), raw, targetId: parseTargetId(args) };
}
function parseTargetId(args: string[]): string {
let targetId = defaultTargetId;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]!;
if (arg === "--target") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--target requires a value");
targetId = value;
index += 1;
continue;
}
if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length);
}
if (!/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error("--target must be a simple target id");
return targetId;
}
function splitAccountNames(value: string): string[] {
@@ -462,14 +523,68 @@ function splitAccountNames(value: string): string[] {
}
function validateOptions(args: string[], booleanOptions: Set<string>): void {
for (const arg of args) {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]!;
if (arg === "--target") {
index += 1;
continue;
}
if (arg.startsWith("--target=") && booleanOptions.has("--target")) continue;
if (booleanOptions.has(arg)) continue;
throw new Error(`unsupported option: ${arg}`);
}
}
function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false }): Record<string, unknown> {
function stripBooleanOptions(args: string[], stripped: Set<string>): string[] {
return args.filter((arg) => !stripped.has(arg));
}
function codexPoolRuntimeTarget(targetId: string = defaultTargetId): CodexPoolRuntimeTarget {
const parsed = Bun.YAML.parse(readFileSync(sub2apiConfigPath, "utf8")) as unknown;
if (!isRecord(parsed) || !Array.isArray(parsed.targets)) throw new Error(`${sub2apiConfigPath}.targets must be a list`);
const raw = parsed.targets.find((item) => isRecord(item) && String(item.id ?? "").toLowerCase() === targetId.toLowerCase());
if (!isRecord(raw)) throw new Error(`${sub2apiConfigPath}.targets does not contain target ${targetId}`);
const id = stringValue(raw.id) ?? targetId;
const route = stringValue(raw.route) ?? (id === defaultTargetId ? g14K3sRoute : "");
const targetNamespace = stringValue(raw.namespace) ?? namespace;
if (route.length === 0) throw new Error(`${sub2apiConfigPath}.targets[${id}].route is required`);
validateKubernetesName(targetNamespace, `${sub2apiConfigPath}.targets[${id}].namespace`, true);
let egressProxy: CodexPoolRuntimeTarget["egressProxy"] = null;
if (isRecord(raw.egressProxy) && raw.egressProxy.enabled === true) {
const proxyServiceName = stringValue(raw.egressProxy.serviceName);
const listenPort = numberValue(raw.egressProxy.listenPort);
if (proxyServiceName === null || listenPort === null) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName/listenPort are required`);
validateKubernetesName(proxyServiceName, `${sub2apiConfigPath}.targets[${id}].egressProxy.serviceName`, true);
if (!Number.isInteger(listenPort) || listenPort < 1 || listenPort > 65535) throw new Error(`${sub2apiConfigPath}.targets[${id}].egressProxy.listenPort must be a TCP port`);
const noProxyRaw = Array.isArray(raw.egressProxy.noProxy) ? raw.egressProxy.noProxy : [];
const noProxy = noProxyRaw.map((entry) => stringValue(entry)).filter((entry): entry is string => entry !== null && entry.length > 0).join(",");
egressProxy = {
enabled: true,
applyToSentinel: raw.egressProxy.applyToSentinel === undefined ? true : raw.egressProxy.applyToSentinel === true,
httpProxy: `http://${proxyServiceName}.${targetNamespace}.svc.cluster.local:${listenPort}`,
noProxy,
};
}
return {
id,
route,
namespace: targetNamespace,
serviceName,
serviceDns: `${serviceName}.${targetNamespace}.svc.cluster.local:8080`,
appSecretName,
egressProxy,
};
}
function targetFlag(target: CodexPoolRuntimeTarget): string {
return target.id === defaultTargetId ? "" : ` --target ${target.id}`;
}
function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false, targetId: defaultTargetId }): Record<string, unknown> {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const profiles = collectCodexProfiles();
const ok = profiles.length > 0 && profiles.every((profile) => profile.ok);
return {
@@ -481,7 +596,7 @@ function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false })
authPattern: "YAML-selected auth files under ~/.codex",
valuesPrinted: false,
},
target: poolTarget(),
target: poolTarget(pool, runtimeTarget),
config: {
path: codexPoolConfigPath,
pool: options.full ? pool : codexPoolConfigSummary(pool),
@@ -490,9 +605,9 @@ function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false })
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 ${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}.`,
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 ${namespace}/${pool.sentinel.cronJobName}; actions.enabled=${pool.sentinel.actions.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: pool.publicExposure.enabled
? `Default Codex consumers use ${codexConsumerBaseUrl(pool)}; bounded master-local probes may use ${pool.publicExposure.masterBaseUrl}. FRP proxy ${pool.publicExposure.proxyName} maps public ${pool.publicExposure.publicBaseUrl} to ${pool.publicExposure.localIP}:${pool.publicExposure.localPort}.`
@@ -501,13 +616,14 @@ function codexPoolPlan(options: DisclosureOptions = { full: false, raw: false })
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" }
? { 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." },
};
}
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) {
@@ -522,7 +638,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
}
const sentinelImage = pool.sentinel.monitor.enabled
? await runCodexPoolSentinelImage(config, pool, { action: "build", confirm: true, dryRun: false, full: options.full, raw: false })
? await runCodexPoolSentinelImage(config, pool, { action: "build", confirm: true, dryRun: false, full: options.full, raw: false, targetId: options.targetId })
: { ok: true, mode: "skipped-monitor-disabled" };
if (sentinelImage.ok !== true) {
return {
@@ -531,7 +647,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
mode: "blocked-sentinel-image",
sentinelImage,
next: {
image: "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm",
image: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build${targetFlag(runtimeTarget)} --confirm`,
},
};
}
@@ -540,10 +656,14 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
pruneRemoved: options.pruneRemoved,
sentinel: {
manifest: renderCodexPoolSentinelManifest(pool.sentinel, sentinelProfileSecrets(profiles), {
namespace,
serviceName,
serviceDns,
appSecretName,
namespace: runtimeTarget.namespace,
serviceName: runtimeTarget.serviceName,
serviceDns: runtimeTarget.serviceDns,
appSecretName: runtimeTarget.appSecretName,
proxy: runtimeTarget.egressProxy?.applyToSentinel ? {
httpProxy: runtimeTarget.egressProxy.httpProxy,
noProxy: runtimeTarget.egressProxy.noProxy,
} : null,
}),
summary: codexPoolSentinelSummary(pool.sentinel),
},
@@ -589,7 +709,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
tempUnschedulableCredentials: renderSub2ApiTempUnschedulableCredentials(profile.tempUnschedulable),
})),
};
const result = await runRemoteCodexPoolScript(config, "sync", syncScript(payload, pool));
const result = await runRemoteCodexPoolScript(config, "sync", syncScript(payload, pool, runtimeTarget), runtimeTarget);
const parsed = parseJsonOutput(result.stdout);
if (options.raw) {
return {
@@ -614,7 +734,7 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi
? 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",
validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`,
},
};
}
@@ -625,6 +745,7 @@ async function codexPoolSentinelImage(config: UniDeskConfig, options: SentinelIm
}
async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolConfig, options: SentinelImageOptions): Promise<Record<string, unknown>> {
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const target = codexPoolSentinelRuntimeImage(pool.sentinel);
if (options.action === "build" && options.dryRun) {
return {
@@ -635,13 +756,13 @@ async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolC
dockerfile: sentinelImageDockerfilePath,
mutation: false,
next: {
confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm",
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) : sentinelImageBuildScript(pool);
const result = await runRemoteCodexPoolScript(config, mode, script);
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 {
@@ -665,7 +786,8 @@ async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: CodexPoolC
async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
const pool = readCodexPoolConfig();
const result = await runRemoteCodexPoolScript(config, "validate", validateScript(pool));
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const result = await runRemoteCodexPoolScript(config, "validate", validateScript(pool, runtimeTarget), runtimeTarget);
const parsed = parseJsonOutput(result.stdout);
if (options.raw) {
return {
@@ -685,7 +807,8 @@ async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptio
async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
const pool = readCodexPoolConfig();
const result = await capture(config, g14K3sRoute, ["script"], traceScript(pool, options));
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const result = await capture(config, runtimeTarget.route, ["script"], traceScript(pool, options, runtimeTarget));
const parsed = parseJsonOutput(result.stdout);
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
if (options.raw) {
@@ -707,7 +830,8 @@ async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Pro
async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelReportOptions): Promise<Record<string, unknown> | RenderedCliResult> {
const pool = readCodexPoolConfig();
const result = await capture(config, g14K3sRoute, ["script"], sentinelReportScript(pool, options.events));
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const result = await capture(config, runtimeTarget.route, ["script"], sentinelReportScript(pool, options.events, runtimeTarget));
const parsed = parseJsonOutput(result.stdout);
const ok = result.exitCode === 0 && boolField(parsed, "ok", false);
if (options.raw) {
@@ -729,6 +853,7 @@ async function codexPoolSentinelReport(config: UniDeskConfig, options: SentinelR
async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelProbeOptions): Promise<Record<string, unknown>> {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const configuredAccounts = desiredAccountNames(pool);
const missing = options.accounts.filter((account) => !configuredAccounts.includes(account));
if (missing.length > 0) {
@@ -746,11 +871,11 @@ async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelPr
ok: true,
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
mode: "dry-run",
target: poolTarget(pool),
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 --account ${options.accounts.join(",")} --confirm`,
confirm: `bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe${targetFlag(runtimeTarget)} --account ${options.accounts.join(",")} --confirm`,
},
valuesPrinted: false,
};
@@ -758,7 +883,7 @@ async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelPr
const payload = {
accounts: options.accounts,
};
const result = await runRemoteCodexPoolScript(config, "sentinel-probe", sentinelProbeScript(payload, pool));
const result = await runRemoteCodexPoolScript(config, "sentinel-probe", sentinelProbeScript(payload, pool, runtimeTarget), runtimeTarget);
const parsed = parseJsonOutput(result.stdout);
if (options.raw) {
return {
@@ -777,19 +902,20 @@ async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelPr
}
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(),
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 --confirm" },
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, g14K3sRoute, ["script"], cleanupProbesScript(pool));
const result = await capture(config, runtimeTarget.route, ["script"], cleanupProbesScript(pool, runtimeTarget));
const parsed = parseJsonOutput(result.stdout);
if (options.raw) {
return {
@@ -2485,16 +2611,17 @@ function codexPoolSyncSummary(parsed: Record<string, unknown> | null): Record<st
};
}
function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRuntimeTarget(defaultTargetId)): Record<string, unknown> {
return {
route: g14K3sRoute,
namespace,
id: target.id,
route: target.route,
namespace: target.namespace,
service: serviceName,
serviceDns,
serviceDns: target.serviceDns,
configPath: codexPoolConfigPath,
groupName: pool.groupName,
apiKeyName: pool.apiKeyName,
apiKeySecret: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
apiKeySecret: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey}`,
minOwnerConcurrency: pool.minOwnerConcurrency,
minOwnerConcurrencySource: pool.minOwnerConcurrencySource,
accountCapacityTotal: desiredAccountCapacityTotal(pool),
@@ -2507,6 +2634,12 @@ function poolTarget(pool = readCodexPoolConfig()): Record<string, unknown> {
cronJobName: pool.sentinel.cronJobName,
stateConfigMapName: pool.sentinel.stateConfigMapName,
},
egressProxy: target.egressProxy === null ? null : {
enabled: target.egressProxy.enabled,
applyToSentinel: target.egressProxy.applyToSentinel,
httpProxy: target.egressProxy.httpProxy,
noProxy: target.egressProxy.noProxy,
},
valuesPrinted: false,
};
}
@@ -2902,10 +3035,10 @@ spec:
`;
}
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig): Promise<{ apiKey: string | null; error: string | null }> {
const result = await capture(config, g14K3sRoute, ["script"], `
async function fetchPoolApiKey(config: UniDeskConfig, pool: CodexPoolConfig, target = codexPoolRuntimeTarget(defaultTargetId)): Promise<{ apiKey: string | null; error: string | null }> {
const result = await capture(config, target.route, ["script"], `
set -u
kubectl -n ${namespace} get secret ${pool.apiKeySecretName} -o json
kubectl -n ${target.namespace} get secret ${pool.apiKeySecretName} -o json
`);
if (result.exitCode !== 0) {
return { apiKey: null, error: `read pool API key secret failed: ${result.stderr.slice(-1000)}` };
@@ -2913,7 +3046,7 @@ kubectl -n ${namespace} get secret ${pool.apiKeySecretName} -o json
const parsed = parseJsonOutput(result.stdout);
const data = isRecord(parsed?.data) ? parsed.data : null;
const encoded = typeof data?.[pool.apiKeySecretKey] === "string" ? data[pool.apiKeySecretKey] : null;
if (encoded === null) return { apiKey: null, error: `${namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey} missing` };
if (encoded === null) return { apiKey: null, error: `${target.namespace}/${pool.apiKeySecretName}.${pool.apiKeySecretKey} missing` };
try {
const apiKey = Buffer.from(encoded, "base64").toString("utf8");
return apiKey.length > 0 ? { apiKey, error: null } : { apiKey: null, error: "decoded API key is empty" };
@@ -3208,16 +3341,16 @@ export function codexPoolSentinelProbeConfigFingerprint(input: {
}));
}
function syncScript(payload: unknown, pool: CodexPoolConfig): string {
function syncScript(payload: unknown, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return remotePythonScript("sync", encoded, pool);
return remotePythonScript("sync", encoded, pool, target);
}
function validateScript(pool: CodexPoolConfig): string {
return remotePythonScript("validate", "", pool);
function validateScript(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
return remotePythonScript("validate", "", pool, target);
}
function traceScript(pool: CodexPoolConfig, options: TraceOptions): string {
function traceScript(pool: CodexPoolConfig, options: TraceOptions, target: CodexPoolRuntimeTarget): string {
const encoded = Buffer.from(JSON.stringify({
requestId: options.requestId,
since: options.since,
@@ -3225,15 +3358,15 @@ function traceScript(pool: CodexPoolConfig, options: TraceOptions): string {
contextSeconds: options.contextSeconds,
showLines: options.showLines,
}), "utf8").toString("base64");
return remotePythonScript("trace", encoded, pool);
return remotePythonScript("trace", encoded, pool, target);
}
function sentinelProbeScript(payload: unknown, pool: CodexPoolConfig): string {
function sentinelProbeScript(payload: unknown, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return remotePythonScript("sentinel-probe", encoded, pool);
return remotePythonScript("sentinel-probe", encoded, pool, target);
}
function sentinelReportScript(pool: CodexPoolConfig, events: number): string {
function sentinelReportScript(pool: CodexPoolConfig, events: number, target: CodexPoolRuntimeTarget): string {
const stateName = pool.sentinel.stateConfigMapName;
const cronJobName = pool.sentinel.cronJobName;
return `
@@ -3243,7 +3376,7 @@ import json
import subprocess
from datetime import datetime, timezone, timedelta
NAMESPACE = ${JSON.stringify(namespace)}
NAMESPACE = ${JSON.stringify(target.namespace)}
STATE_NAME = ${JSON.stringify(stateName)}
CRONJOB_NAME = ${JSON.stringify(cronJobName)}
EVENT_LIMIT = ${JSON.stringify(events)}
@@ -3445,22 +3578,22 @@ PY
`;
}
function cleanupProbesScript(pool: CodexPoolConfig): string {
return remotePythonScript("cleanup-probes", "", pool);
function cleanupProbesScript(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
return remotePythonScript("cleanup-probes", "", pool, target);
}
function sentinelImageStatusScript(pool: CodexPoolConfig): string {
function sentinelImageStatusScript(pool: CodexPoolConfig, targetRuntime: CodexPoolRuntimeTarget): string {
const target = codexPoolSentinelRuntimeImage(pool.sentinel);
return remoteSentinelImageScript("status", target, pool.sentinel, null);
return remoteSentinelImageScript("status", target, pool.sentinel, null, targetRuntime);
}
function sentinelImageBuildScript(pool: CodexPoolConfig): string {
function sentinelImageBuildScript(pool: CodexPoolConfig, targetRuntime: CodexPoolRuntimeTarget): string {
const target = codexPoolSentinelRuntimeImage(pool.sentinel);
const dockerfile = readFileSync(sentinelImageDockerfilePath, "utf8");
return remoteSentinelImageScript("build", target, pool.sentinel, dockerfile);
return remoteSentinelImageScript("build", target, pool.sentinel, dockerfile, targetRuntime);
}
function remoteSentinelImageScript(mode: "status" | "build", target: ReturnType<typeof codexPoolSentinelRuntimeImage>, sentinel: CodexPoolSentinelConfig, dockerfile: string | null): string {
function remoteSentinelImageScript(mode: "status" | "build", target: ReturnType<typeof codexPoolSentinelRuntimeImage>, sentinel: CodexPoolSentinelConfig, dockerfile: string | null, targetRuntime: CodexPoolRuntimeTarget): string {
const dockerfileB64 = dockerfile === null ? "" : Buffer.from(dockerfile, "utf8").toString("base64");
return `
set -eu
@@ -3470,6 +3603,7 @@ repo=${shQuote("platform-infra/sub2api-account-sentinel")}
tag=${shQuote(target.tag)}
base_image=${shQuote(target.baseImage)}
openai_version=${shQuote(sentinel.sdk.openaiPythonVersion)}
runtime_target=${shQuote(targetRuntime.id)}
work=/tmp/unidesk-sub2api-sentinel-image
mkdir -p "$work"
dockerfile_path="$work/sentinel.Dockerfile"
@@ -3527,7 +3661,13 @@ ${dockerfileB64}
UNIDESK_SENTINEL_DOCKERFILE_B64
export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000
export no_proxy=$NO_PROXY
docker build --pull \\
set -- --pull
base_image_source="registry"
if [ "$runtime_target" != "G14" ] && docker image inspect "$base_image" >/dev/null 2>&1; then
set --
base_image_source="local-cache"
fi
docker build "$@" \\
--build-arg BASE_IMAGE="$base_image" \\
--build-arg OPENAI_PYTHON_VERSION="$openai_version" \\
--build-arg HTTP_PROXY= --build-arg HTTPS_PROXY= --build-arg http_proxy= --build-arg https_proxy= \\
@@ -3546,6 +3686,7 @@ print(json.dumps({
"image": "${target.runtimeImage}",
"baseImage": "${target.baseImage}",
"tag": "${target.tag}",
"baseImageSource": "${"${base_image_source}"}",
"digest": "${"${digest}"}" or None,
}, ensure_ascii=False, indent=2))
PY
@@ -3620,7 +3761,7 @@ function desiredAccountTempUnschedulableMap(pool: CodexPoolConfig): Record<strin
return policies;
}
function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig): string {
function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
return `
set -u
python3 - <<'PY'
@@ -3635,11 +3776,12 @@ import time
from datetime import datetime, timezone, timedelta
from urllib.parse import quote
NAMESPACE = "${namespace}"
SERVICE_NAME = "${serviceName}"
SERVICE_DNS = "${serviceDns}"
TARGET_ID = ${JSON.stringify(target.id)}
NAMESPACE = ${JSON.stringify(target.namespace)}
SERVICE_NAME = ${JSON.stringify(target.serviceName)}
SERVICE_DNS = ${JSON.stringify(target.serviceDns)}
FIELD_MANAGER = "${fieldManager}"
APP_SECRET_NAME = "${appSecretName}"
APP_SECRET_NAME = ${JSON.stringify(target.appSecretName)}
POOL_GROUP_NAME = "${pool.groupName}"
POOL_API_KEY_NAME = "${pool.apiKeyName}"
POOL_API_KEY_SECRET_NAME = "${pool.apiKeySecretName}"
@@ -6099,17 +6241,17 @@ async function capture(config: UniDeskConfig, target: string, args: string[], in
type RemoteCodexPoolMode = "sync" | "validate" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build";
async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string): Promise<SshCaptureResult> {
async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget(defaultTargetId)): Promise<SshCaptureResult> {
const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63);
const startedAtMs = Date.now();
const start = await capture(config, g14K3sRoute, ["script"], remoteJobStartScript(jobName, script));
const start = await capture(config, target.route, ["script"], remoteJobStartScript(jobName, script));
const started = parseJsonOutput(start.stdout);
if (start.exitCode !== 0 || boolField(started, "ok", false) !== true) return start;
let latest: RemoteCodexPoolJobStatus | null = null;
while (Date.now() - startedAtMs <= remoteJobTimeoutMs) {
await sleep(remoteJobPollMs);
const probe = await capture(config, g14K3sRoute, ["script"], remoteJobStatusScript(jobName));
const probe = await capture(config, target.route, ["script"], remoteJobStatusScript(jobName));
const parsed = parseJsonOutput(probe.stdout);
latest = normalizeRemoteJobStatus(parsed);
process.stderr.write(`${JSON.stringify({
File diff suppressed because it is too large Load Diff