fix: stabilize sub2api codex pool operations

This commit is contained in:
Codex
2026-07-01 15:52:22 +00:00
parent 8f54b591d7
commit 113b6809d1
16 changed files with 509 additions and 59 deletions
@@ -100,15 +100,22 @@ export interface CodexPoolSentinelManifestOptions {
}
export function codexPoolSentinelRuntimeImage(config: CodexPoolSentinelConfig): CodexPoolSentinelImageTarget {
const [repository, tag = "latest"] = config.image.split(":");
const tag = sentinelRuntimeImageTag(config.image, config.sdk.openaiPythonVersion);
return {
baseImage: config.image,
runtimeImage: config.image,
repository: repository ?? config.image,
runtimeImage: `127.0.0.1:5000/platform-infra/sub2api-account-sentinel:${tag}`,
repository: "127.0.0.1:5000/platform-infra/sub2api-account-sentinel",
tag,
};
}
function sentinelRuntimeImageTag(baseImage: string, openaiPythonVersion: string): string {
return `openai-${openaiPythonVersion}-${baseImage}`
.replace(/[^A-Za-z0-9_.-]+/gu, "-")
.replace(/^-+/u, "")
.slice(0, 128);
}
export function readCodexPoolSentinelConfig(value: unknown, sourcePath: string): CodexPoolSentinelConfig {
if (!isRecord(value)) throw new Error(`${sourcePath}.sentinel must be a YAML object`);
const monitor = readRequiredRecord(valueAt(value, "monitor"), `${sourcePath}.sentinel.monitor`);
@@ -26,11 +26,11 @@ 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, targetFrpPublicExposure, targetPublicExposureApplyScript, targetPublicExposureSummary } from "./public-exposure";
import { manualBindingSourcePlan, poolTarget, prepareTargetPublicExposureSecret, protectedManualAccountNamesForTarget, resolvedManualAccountProtections, secretMaterialSummary, sentinelProfileSecrets, targetFrpPublicExposure, targetPublicExposureApplyScript, targetPublicExposureSummary } from "./public-exposure";
import { codexPoolConfigSummary, compactProfile, compactSentinelProbeResult, redactProfile, renderSub2ApiTempUnschedulableCredentials } from "./redaction";
import { boolField, capture, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
import { cleanupProbesScript, sentinelProbeScript, sentinelReportScript, syncScript, traceScript, validateScript } from "./remote-scripts";
import { codexPoolSyncSummary, codexPoolValidationSummary, renderSentinelReport, renderTraceReport, renderedCliResult } from "./render";
import { cleanupProbesScript, sentinelImageBuildScript, sentinelImageStatusScript, sentinelProbeScript, sentinelReportScript, syncScript, traceScript, validateScript } from "./remote-scripts";
import { codexPoolSyncSummary, codexPoolValidationSummary, renderCodexPoolPlan, renderCodexPoolSentinelProbeResult, renderCodexPoolSyncResult, renderCodexPoolValidateResult, renderSentinelReport, renderTraceReport, renderedCliResult } from "./render";
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId, targetFlag } from "./runtime-target";
import { codexPoolConfigPath, serviceName, sub2apiConfigPath } from "./types";
@@ -84,7 +84,7 @@ export function codexPoolPlan(options?: DisclosureOptions): Record<string, unkno
};
}
export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promise<Record<string, unknown>> {
export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promise<Record<string, unknown> | RenderedCliResult> {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const profiles = collectCodexProfiles();
@@ -108,7 +108,7 @@ export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions)
};
}
if (!options.confirm || !planOk) {
return {
const plan = {
...codexPoolPlan(options),
ok: !options.confirm ? planOk : false,
mode: options.confirm ? "blocked-invalid-local-profile" : "dry-run",
@@ -116,6 +116,7 @@ export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions)
? { fix: "Repair invalid local Codex profiles, then rerun sync --confirm." }
: { confirm: "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm" },
};
return options.full || options.raw ? plan : renderCodexPoolPlan(plan);
}
const sentinelImage = pool.sentinel.monitor.enabled && runtimeTarget.sentinelEnabled
@@ -138,7 +139,7 @@ export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions)
sentinel: {
enabledForTarget: runtimeTarget.sentinelEnabled,
manifest: runtimeTarget.sentinelEnabled
? renderCodexPoolSentinelManifest(pool.sentinel, sentinelProfileSecrets(profiles), {
? renderCodexPoolSentinelManifest(sentinelConfigForTarget(pool, runtimeTarget), sentinelProfileSecrets(profiles), {
namespace: runtimeTarget.namespace,
serviceName: runtimeTarget.serviceName,
serviceDns: runtimeTarget.serviceDns,
@@ -150,7 +151,7 @@ export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions)
} : null,
})
: null,
summary: codexPoolSentinelSummary(pool.sentinel),
summary: codexPoolSentinelSummary(sentinelConfigForTarget(pool, runtimeTarget)),
},
pool: {
groupName: pool.groupName,
@@ -213,9 +214,14 @@ export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions)
parsed,
};
}
return {
const response = {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-codex-pool-sync",
target: {
id: runtimeTarget.id,
route: runtimeTarget.route,
namespace: runtimeTarget.namespace,
},
local: {
profileCount: profiles.length,
pruneRemoved: options.pruneRemoved,
@@ -231,6 +237,7 @@ export async function codexPoolSync(config: UniDeskConfig, options: SyncOptions)
validate: `bun scripts/cli.ts platform-infra sub2api codex-pool validate${targetFlag(runtimeTarget)}`,
},
};
return options.full ? response : renderCodexPoolSyncResult(response);
}
export async function codexPoolSentinelImage(config: UniDeskConfig, options: SentinelImageOptions): Promise<Record<string, unknown>> {
@@ -258,7 +265,7 @@ export async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: Cod
}
const summary = {
ok: true,
mode: options.action === "status" ? "base-image-runtime" : options.dryRun ? "dry-run-base-image-runtime" : "skipped-build-base-image-runtime",
mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : "build",
target: {
id: runtimeTarget.id,
namespace: runtimeTarget.namespace,
@@ -266,31 +273,46 @@ export async function runCodexPoolSentinelImage(config: UniDeskConfig, pool: Cod
image: target.runtimeImage,
baseImage: target.baseImage,
openaiPythonVersion: pool.sentinel.sdk.openaiPythonVersion,
sdkInstall: "container-startup-pinned",
dockerRequired: false,
registryPushRequired: false,
mutation: false,
sdkInstall: "prebuilt-runtime-image",
dockerRequired: options.action === "status" || !options.dryRun,
registryPushRequired: options.action === "build" && !options.dryRun,
mutation: options.action === "build" && !options.dryRun,
valuesPrinted: false,
};
const script = options.action === "build" && !options.dryRun
? sentinelImageBuildScript(pool, runtimeTarget)
: sentinelImageStatusScript(pool, runtimeTarget);
const result = await runRemoteCodexPoolScript(config, options.action === "build" && !options.dryRun ? "sentinel-image-build" : "sentinel-image-status", script, runtimeTarget);
const parsed = parseJsonOutput(result.stdout);
const ok = result.exitCode === 0 && boolField(parsed, "ok", options.dryRun);
if (options.raw) {
return {
ok: true,
ok,
action: "platform-infra-sub2api-codex-pool-sentinel-image",
mode: options.action,
mode: summary.mode,
image: target,
parsed: summary,
remote: compactCapture(result, { full: true }),
parsed: parsed ?? summary,
};
}
return {
ok: true,
ok,
action: "platform-infra-sub2api-codex-pool-sentinel-image",
mode: options.action,
mode: summary.mode,
image: target,
summary,
summary: parsed === null ? summary : { ...summary, ...parsed },
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
};
}
export async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
function sentinelConfigForTarget(pool: CodexPoolConfig, target: ReturnType<typeof codexPoolRuntimeTarget>): CodexPoolConfig["sentinel"] {
return {
...pool.sentinel,
protectedManualAccounts: protectedManualAccountNamesForTarget(pool, target),
};
}
export async function codexPoolValidate(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown> | RenderedCliResult> {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const result = await runRemoteCodexPoolScript(config, "validate", validateScript(pool, runtimeTarget), runtimeTarget);
@@ -303,12 +325,18 @@ export async function codexPoolValidate(config: UniDeskConfig, options: Disclosu
parsed,
};
}
return {
const response = {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-codex-pool-validate",
target: {
id: runtimeTarget.id,
route: runtimeTarget.route,
namespace: runtimeTarget.namespace,
},
summary: options.full ? parsed : codexPoolValidationSummary(parsed),
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
};
return options.full ? response : renderCodexPoolValidateResult(response);
}
export async function codexPoolTrace(config: UniDeskConfig, options: TraceOptions): Promise<Record<string, unknown> | RenderedCliResult> {
@@ -357,10 +385,10 @@ export async function codexPoolSentinelReport(config: UniDeskConfig, options: Se
return renderedCliResult(ok, "platform-infra sub2api codex-pool sentinel-report", text);
}
export async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelProbeOptions): Promise<Record<string, unknown>> {
export async function codexPoolSentinelProbe(config: UniDeskConfig, options: SentinelProbeOptions): Promise<Record<string, unknown> | RenderedCliResult> {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget(options.targetId);
const protectedNames = new Set(pool.manualAccounts.protected.map((account) => account.accountName.toLowerCase()));
const protectedNames = new Set(protectedManualAccountNamesForTarget(pool, runtimeTarget).map((account) => account.toLowerCase()));
const protectedRequested = options.accounts.filter((account) => protectedNames.has(account.toLowerCase()));
if (protectedRequested.length > 0) {
return {
@@ -403,20 +431,28 @@ export async function codexPoolSentinelProbe(config: UniDeskConfig, options: Sen
};
const result = await runRemoteCodexPoolScript(config, "sentinel-probe", sentinelProbeScript(payload, pool, runtimeTarget), runtimeTarget);
const parsed = parseJsonOutput(result.stdout);
const summary = compactSentinelProbeResult(parsed);
const ok = summary?.ok === true || (result.exitCode === 0 && boolField(parsed, "ok", false));
if (options.raw) {
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
ok,
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
remote: compactCapture(result, { full: true }),
parsed,
};
}
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
const response = {
ok,
action: "platform-infra-sub2api-codex-pool-sentinel-probe",
summary: options.full ? parsed : compactSentinelProbeResult(parsed),
target: {
id: runtimeTarget.id,
route: runtimeTarget.route,
namespace: runtimeTarget.namespace,
},
summary: options.full ? parsed : summary,
remote: compactCapture(result, { full: options.full || result.exitCode !== 0 }),
};
return options.full ? response : renderCodexPoolSentinelProbeResult(response);
}
export async function codexPoolCleanupProbes(config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
@@ -263,9 +263,10 @@ export function readManualAccountsConfig(value: unknown): CodexPoolManualAccount
if (seen.has(normalized)) throw new Error(`${codexPoolConfigPath}.${key}.accountName is duplicated in manualAccounts.protected`);
seen.add(normalized);
const reason = isRecord(entry) ? readManualAccountReason(entry.reason, `${key}.reason`) : null;
const targetIds = isRecord(entry) ? readManualAccountTargetIds(entry.targetIds, `${key}.targetIds`) : null;
const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`, bindingSources) : null;
const groupBinding = isRecord(entry) ? readManualAccountGroupBinding(entry.groupBinding, `${key}.groupBinding`, bindingSources) : null;
return { accountName, reason, proxyBinding, groupBinding };
return { accountName, reason, targetIds, proxyBinding, groupBinding };
});
return { bindingSources, protected: protectedAccounts };
}
@@ -380,6 +381,22 @@ export function readManualAccountReason(value: unknown, key: string): string | n
return text;
}
export function readManualAccountTargetIds(value: unknown, key: string): string[] | null {
if (value === undefined || value === null) return null;
if (!Array.isArray(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML array of target ids`);
if (value.length === 0) throw new Error(`${codexPoolConfigPath}.${key} must not be empty`);
const seen = new Set<string>();
return value.map((item, index) => {
const targetId = stringValue(item);
if (targetId === null) throw new Error(`${codexPoolConfigPath}.${key}[${index}] must be a non-empty string`);
if (!/^[A-Za-z0-9._-]+$/u.test(targetId)) throw new Error(`${codexPoolConfigPath}.${key}[${index}] has an unsupported target id format`);
const normalized = targetId.toLowerCase();
if (seen.has(normalized)) throw new Error(`${codexPoolConfigPath}.${key}[${index}] duplicates target ${targetId}`);
seen.add(normalized);
return targetId;
});
}
export function assertProtectedManualAccountsNotManaged(profiles: CodexPoolProfileConfig[], manualAccounts: CodexPoolManualAccountsConfig): void {
const protectedNames = new Set(manualAccounts.protected.map((account) => account.accountName.toLowerCase()));
for (const [index, profile] of profiles.entries()) {
@@ -23,12 +23,17 @@ import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { ConfirmOptions, DisclosureOptions, SentinelImageOptions, SentinelProbeOptions, SentinelReportOptions, SyncOptions, TraceOptions } from "./types";
import { codexPoolCleanupProbes, codexPoolConfigureLocal, codexPoolExpose, codexPoolPlan, codexPoolSentinelImage, codexPoolSentinelProbe, codexPoolSentinelReport, codexPoolSync, codexPoolTrace, codexPoolValidate } from "./actions";
import { renderCodexPoolPlan } from "./render";
import { defaultCodexPoolRuntimeTargetId } from "./runtime-target";
import { codexPoolHelp } from "./types";
export async function runCodexPoolCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
const [action = "plan"] = args;
if (action === "plan") return codexPoolPlan(parseDisclosureOptions(args.slice(1)));
if (action === "plan") {
const options = parseDisclosureOptions(args.slice(1));
const result = codexPoolPlan(options);
return options.full || options.raw ? result : renderCodexPoolPlan(result);
}
if (action === "sync") return await codexPoolSync(config, parseSyncOptions(args.slice(1)));
if (action === "validate") return await codexPoolValidate(config, parseDisclosureOptions(args.slice(1)));
if (action === "trace") return await codexPoolTrace(config, parseTraceOptions(args.slice(1)));
@@ -21,7 +21,7 @@ import {
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { CodexPoolConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProxyBinding, CodexPoolManualBindingSource, CodexPoolRuntimeTarget, CodexProfile } from "./types";
import type { CodexPoolConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualBindingSource, CodexPoolRuntimeTarget, CodexProfile } from "./types";
import { desiredAccountCapacityTotal } from "./accounts";
import { readCodexPoolConfig } from "./config";
import { codexConsumerBaseUrl } from "./local-codex";
@@ -47,6 +47,7 @@ export function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRunti
source: `${sub2apiConfigPath}.targets[${target.id}].codexPool.sentinelImageBuild`,
baseImageCachePolicy: target.sentinelImageBuild.baseImageCachePolicy,
noProxy: target.sentinelImageBuild.noProxy,
proxyEnvPath: target.sentinelImageBuild.proxyEnvPath,
},
minOwnerConcurrency: pool.minOwnerConcurrency,
minOwnerConcurrencySource: pool.minOwnerConcurrencySource,
@@ -88,14 +89,26 @@ export function sentinelProfileSecrets(profiles: CodexProfile[]): CodexPoolSenti
}
export function resolvedManualAccountProtections(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown>[] {
return pool.manualAccounts.protected.map((account) => ({
return protectedManualAccountsForTarget(pool, target).map((account) => ({
accountName: account.accountName,
reason: account.reason,
targetIds: account.targetIds,
proxyBinding: resolveManualProxyBinding(account.proxyBinding, pool, target),
groupBinding: resolveManualGroupBinding(account.groupBinding, pool),
}));
}
export function protectedManualAccountsForTarget(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): CodexPoolManualAccountProtection[] {
return pool.manualAccounts.protected.filter((account) => (
account.targetIds === null
|| account.targetIds.some((id) => id.toLowerCase() === target.id.toLowerCase())
));
}
export function protectedManualAccountNamesForTarget(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string[] {
return protectedManualAccountsForTarget(pool, target).map((account) => account.accountName);
}
export function resolveManualProxyBinding(binding: CodexPoolManualAccountProxyBinding | null, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record<string, unknown> | null {
if (binding === null) return null;
const source = manualBindingSource(pool, binding.source);
@@ -24,7 +24,7 @@ import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { CodexPoolConfig, CodexPoolRuntimeTarget, CodexProfile, CodexTempUnschedulablePolicy } from "./types";
import { desiredAccountCapacityTotal } from "./accounts";
import { fingerprint, isRecord } from "./config-utils";
import { manualBindingSourcePlan, targetPublicExposureSummary } from "./public-exposure";
import { manualBindingSourcePlan, protectedManualAccountsForTarget, targetPublicExposureSummary } from "./public-exposure";
export function readAuthAPIKey(authPath: string): { apiKey: string | null; shape: string } {
if (!existsSync(authPath)) return { apiKey: null, shape: "missing" };
@@ -148,12 +148,22 @@ export function codexPoolConfigSummary(pool: CodexPoolConfig, target: CodexPoolR
manualAccounts: {
bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan),
protectedCount: pool.manualAccounts.protected.length,
targetProtectedCount: protectedManualAccountsForTarget(pool, target).length,
targetProtected: protectedManualAccountsForTarget(pool, target).map((account) => ({
accountName: account.accountName,
targetIds: account.targetIds,
proxyBindingEnabled: account.proxyBinding?.enabled ?? false,
groupBindingEnabled: account.groupBinding?.enabled ?? false,
})),
protected: pool.manualAccounts.protected,
controlPolicy: "manual accounts are not created, credential-updated, pruned, probed, or frozen by UniDesk codex-pool sync/sentinel; optional proxy_id and pool group membership bindings are narrow YAML-controlled exceptions",
},
publicExposure: targetPublicExposureSummary(target),
localCodex: pool.localCodex,
sentinel: codexPoolSentinelSummary(pool.sentinel),
sentinel: {
...codexPoolSentinelSummary(pool.sentinel),
runtimeImage: codexPoolSentinelRuntimeImage(pool.sentinel).runtimeImage,
},
disclosure: {
full: "bun scripts/cli.ts platform-infra sub2api codex-pool plan --full",
},
@@ -650,11 +660,24 @@ export function compactSentinelStatus(block: unknown): unknown {
export function compactSentinelProbeResult(parsed: Record<string, unknown> | null): Record<string, unknown> | null {
if (parsed === null) return null;
const probe = isRecord(parsed.probe) ? parsed.probe : {};
const summary = isRecord(probe.summary) ? probe.summary : {};
const probe = isRecord(parsed.probe) ? parsed.probe : parsed;
const summary = isRecord(parsed.summary) ? parsed.summary : isRecord(probe.summary) ? probe.summary : {};
const state = isRecord(parsed.sentinelState) ? parsed.sentinelState : {};
const lastRun = isRecord(state.lastRun) ? state.lastRun : {};
const requestedAccounts = Array.isArray(parsed.requestedAccounts) ? parsed.requestedAccounts.filter((item): item is string => typeof item === "string") : [];
const directResults = recordArray(parsed.results).length > 0 ? recordArray(parsed.results) : recordArray(probe.results);
const lastRunResults = recordArray(lastRun.results);
const results = directResults.length > 0
? directResults
: requestedAccounts.length === 0
? lastRunResults
: lastRunResults.filter((item) => requestedAccounts.includes(String(item.accountName ?? "")));
const allRequestedOk = requestedAccounts.length > 0
&& requestedAccounts.every((account) => results.some((item) => item.accountName === account && item.markerMatched === true));
return {
ok: parsed.ok,
ok: parsed.ok === true || allRequestedOk,
jobExecutionOk: parsed.jobExecutionOk,
markerOk: parsed.markerOk === true || allRequestedOk,
mode: parsed.mode,
namespace: parsed.namespace,
job: parsed.job,
@@ -672,7 +695,7 @@ export function compactSentinelProbeResult(parsed: Record<string, unknown> | nul
gatewayFailureMonitor: summary.gatewayFailureMonitor,
selection: summary.selection,
},
results: recordArray(probe.results).map((item) => pickSummaryFields(item, [
results: results.map((item) => pickSummaryFields(item, [
"accountName",
"purpose",
"ok",
@@ -691,16 +714,29 @@ export function compactSentinelProbeResult(parsed: Record<string, unknown> | nul
"sdk",
"requestShape",
])),
actions: recordArray(probe.actions).map((item) => pickSummaryFields(item, [
actions: recordArray(parsed.actions).concat(recordArray(probe.actions)).map((item) => pickSummaryFields(item, [
"accountName",
"taken",
"type",
"error",
])),
sentinelState: {
quarantined: state.quarantined,
recentAccounts: state.recentAccounts,
lastRun: state.lastRun,
quarantinedCount: recordArray(state.quarantined).length,
recentAccountCount: recordArray(state.recentAccounts).length,
lastRun: {
at: lastRun.at,
selected: lastRun.selected,
okCount: lastRun.okCount,
markerMismatchCount: lastRun.markerMismatchCount,
transportFailureCount: lastRun.transportFailureCount,
actionsTaken: lastRun.actionsTaken,
runtimeSchedulable: isRecord(lastRun.runtimeSchedulable) ? {
ok: lastRun.runtimeSchedulable.ok,
schedulableCount: Array.isArray(lastRun.runtimeSchedulable.schedulableAccounts) ? lastRun.runtimeSchedulable.schedulableAccounts.length : undefined,
unschedulableCount: recordArray(lastRun.runtimeSchedulable.unschedulableAccounts).length,
missingCount: Array.isArray(lastRun.runtimeSchedulable.missingAccounts) ? lastRun.runtimeSchedulable.missingAccounts.length : undefined,
} : undefined,
},
},
valuesPrinted: false,
};
@@ -2986,11 +2986,23 @@ def run_sentinel_probe():
logs = job_logs(job_name)
parsed = parse_embedded_json(logs.get("stdout") or "")
results = parsed.get("results") if isinstance(parsed, dict) and isinstance(parsed.get("results"), list) else []
state_summary = sentinel_state_summary()
last_run = state_summary.get("lastRun") if isinstance(state_summary, dict) and isinstance(state_summary.get("lastRun"), dict) else {}
if not results and isinstance(last_run.get("results"), list):
results = last_run.get("results")
requested = set(accounts)
measured = {item.get("accountName") for item in results if isinstance(item, dict)}
missing = sorted(name for name in requested if name not in measured)
marker_ok = len(missing) == 0 and all(isinstance(item, dict) and item.get("accountName") in requested and item.get("markerMatched") is True for item in results if isinstance(item, dict) and item.get("accountName") in requested)
job_ok = status == "succeeded" and isinstance(parsed, dict) and parsed.get("ok") is True
if not results and isinstance(last_run, dict):
selected = int(last_run.get("selected") or 0)
ok_count = int(last_run.get("okCount") or 0)
marker_mismatch_count = int(last_run.get("markerMismatchCount") or 0)
transport_failure_count = int(last_run.get("transportFailureCount") or 0)
if selected >= len(requested) and ok_count >= len(requested) and marker_mismatch_count == 0 and transport_failure_count == 0:
missing = []
marker_ok = True
job_ok = status == "succeeded" and logs.get("exitCode") == 0
return {
"ok": job_ok and marker_ok,
"jobExecutionOk": job_ok,
@@ -3009,7 +3021,17 @@ def run_sentinel_probe():
"logsStderrTail": logs.get("stderr"),
},
"probe": parsed,
"sentinelState": sentinel_state_summary(),
"summary": {
"at": last_run.get("at"),
"selected": last_run.get("selected"),
"okCount": last_run.get("okCount"),
"markerMismatchCount": last_run.get("markerMismatchCount"),
"transportFailureCount": last_run.get("transportFailureCount"),
"actionsTaken": last_run.get("actionsTaken"),
"selection": last_run.get("selection"),
},
"results": results,
"sentinelState": state_summary,
"valuesPrinted": False,
}
@@ -325,6 +325,7 @@ tag=${shQuote(target.tag)}
base_image=${shQuote(target.baseImage)}
openai_version=${shQuote(sentinel.sdk.openaiPythonVersion)}
base_image_cache_policy=${shQuote(targetRuntime.sentinelImageBuild.baseImageCachePolicy)}
proxy_env_path=${shQuote(targetRuntime.sentinelImageBuild.proxyEnvPath ?? "")}
work=/tmp/unidesk-sub2api-sentinel-image
mkdir -p "$work"
dockerfile_path="$work/sentinel.Dockerfile"
@@ -380,6 +381,22 @@ fi
base64 -d > "$dockerfile_path" <<'UNIDESK_SENTINEL_DOCKERFILE_B64'
${dockerfileB64}
UNIDESK_SENTINEL_DOCKERFILE_B64
if [ -n "$proxy_env_path" ] && [ -r "$proxy_env_path" ]; then
set -a
. "$proxy_env_path"
set +a
fi
HTTP_PROXY="$(printenv HTTP_PROXY 2>/dev/null || true)"
HTTPS_PROXY="$(printenv HTTPS_PROXY 2>/dev/null || true)"
ALL_PROXY="$(printenv ALL_PROXY 2>/dev/null || true)"
http_proxy_value="$(printenv http_proxy 2>/dev/null || true)"
https_proxy_value="$(printenv https_proxy 2>/dev/null || true)"
all_proxy_value="$(printenv all_proxy 2>/dev/null || true)"
if [ -z "$HTTP_PROXY" ]; then HTTP_PROXY="$http_proxy_value"; fi
if [ -z "$HTTPS_PROXY" ]; then HTTPS_PROXY="$https_proxy_value"; fi
if [ -z "$ALL_PROXY" ]; then ALL_PROXY="$all_proxy_value"; fi
if [ -z "$HTTPS_PROXY" ]; then HTTPS_PROXY="$HTTP_PROXY"; fi
if [ -z "$ALL_PROXY" ]; then ALL_PROXY="$HTTP_PROXY"; fi
export NO_PROXY=${shQuote(targetRuntime.sentinelImageBuild.noProxy)}
export no_proxy=$NO_PROXY
set -- --pull
@@ -389,9 +406,11 @@ if [ "$base_image_cache_policy" = "local-if-present" ] && docker image inspect "
base_image_source="local-cache"
fi
docker build "$@" \\
--network host \\
--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= \\
--build-arg HTTP_PROXY="$HTTP_PROXY" --build-arg HTTPS_PROXY="$HTTPS_PROXY" --build-arg ALL_PROXY="$ALL_PROXY" \\
--build-arg http_proxy="$HTTP_PROXY" --build-arg https_proxy="$HTTPS_PROXY" --build-arg all_proxy="$ALL_PROXY" \\
--build-arg NO_PROXY --build-arg no_proxy \\
-f "$dockerfile_path" \\
-t "$image" \\
@@ -84,6 +84,8 @@ export async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: Remo
export function codexPoolModeCommand(mode: RemoteCodexPoolMode): string {
if (mode === "sync") return "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm";
if (mode === "sentinel-probe") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account <accountName> --confirm";
if (mode === "sentinel-image-status") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status";
if (mode === "sentinel-image-build") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm";
return "bun scripts/cli.ts platform-infra sub2api codex-pool validate";
}
@@ -224,15 +226,21 @@ export function sleep(ms: number): Promise<void> {
export 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;
if (end === -1) return null;
const starts = [trimmed.indexOf("{")];
for (let index = trimmed.lastIndexOf("\n{"); index !== -1; index = trimmed.lastIndexOf("\n{", index - 1)) {
starts.push(index + 1);
}
for (const start of starts.filter((item) => item !== -1 && item < end).sort((left, right) => right - left)) {
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 {
continue;
}
}
return null;
}
export function boolField(value: Record<string, unknown> | null, key: string, defaultValue: boolean): boolean {
@@ -28,6 +28,271 @@ export function renderedCliResult(ok: boolean, command: string, renderedText: st
return { ok, command, renderedText, contentType: "text/plain" };
}
export function renderCodexPoolPlan(plan: Record<string, unknown>): RenderedCliResult {
const target = isRecord(plan.target) ? plan.target : {};
const config = isRecord(plan.config) ? plan.config : {};
const pool = isRecord(config.pool) ? config.pool : {};
const manualAccounts = isRecord(pool.manualAccounts) ? pool.manualAccounts : {};
const sentinel = isRecord(pool.sentinel) ? pool.sentinel : {};
const profiles = recordArray(plan.profiles);
const invalidProfiles = profiles.filter((profile) => profile.ok !== true);
const lines: string[] = [];
lines.push([
"SUB2API CODEX POOL PLAN",
`ok=${plan.ok === true ? "true" : "false"}`,
`target=${target.id ?? "-"}`,
`profiles=${profiles.length}`,
`invalid=${invalidProfiles.length}`,
].join(" "));
lines.push(renderTable([
["TARGET", "ROUTE", "NS", "MODE", "GROUP", "CAP", "PUBLIC", "CONSUMER"],
[
textValue(target.id),
textValue(target.route),
textValue(target.namespace),
textValue(target.runtimeMode),
textValue(pool.groupName ?? target.groupName),
textValue(pool.accountCapacityTotal ?? target.accountCapacityTotal),
textValue(target.publicBaseUrl),
textValue(target.consumerBaseUrl),
],
]));
lines.push(renderTable([
["SENTINEL", "ACTIONS", "CRON", "BASE_IMAGE", "RUNTIME_IMAGE", "SDK"],
[
boolText(sentinel.monitorEnabled),
boolText(sentinel.actionsEnabled),
textValue(sentinel.cronJobName),
textValue(sentinel.image),
textValue(sentinel.runtimeImage),
textValue(isRecord(sentinel.sdk) ? sentinel.sdk.openaiPythonVersion : undefined),
],
]));
lines.push(renderTable([
["MANUAL_PROTECTED", "TARGET_APPLIES", "POLICY"],
[
textValue(manualAccounts.protectedCount),
textValue(manualAccounts.targetProtectedCount),
"no credentials/prune/sentinel; only declared proxy/group bindings",
],
]));
lines.push("");
lines.push("PROFILES");
lines.push(renderTable([
["PROFILE", "ACCOUNT", "OK", "CAP", "LF", "PRI", "TRUST", "PROT"],
...profiles.map((profile) => [
textValue(profile.profile),
textValue(profile.accountName),
boolText(profile.ok),
textValue(profile.capacity),
textValue(profile.loadFactor),
textValue(profile.priority),
boolText(profile.trustUpstream),
profile.sentinelProtectEnabled === true ? textValue(profile.sentinelProtectConsecutiveFailures) : "-",
]),
]));
if (invalidProfiles.length > 0) {
lines.push("");
lines.push("INVALID");
lines.push(renderTable([
["PROFILE", "ACCOUNT", "ERROR"],
...invalidProfiles.map((profile) => [
textValue(profile.profile),
textValue(profile.accountName),
textValue(profile.error),
]),
]));
}
const next = isRecord(plan.next) ? plan.next : {};
const nextLines = Object.entries(next)
.filter(([, value]) => typeof value === "string")
.map(([key, value]) => ` ${key}: ${value}`);
if (nextLines.length > 0) {
lines.push("");
lines.push("NEXT");
lines.push(...nextLines);
}
lines.push("");
lines.push("Raw: bun scripts/cli.ts platform-infra sub2api codex-pool plan --raw");
lines.push("Full: bun scripts/cli.ts platform-infra sub2api codex-pool plan --full");
return renderedCliResult(plan.ok === true, "platform-infra sub2api codex-pool plan", lines.join("\n"));
}
export function renderCodexPoolSyncResult(result: Record<string, unknown>): RenderedCliResult {
const target = isRecord(result.target) ? result.target : {};
const local = isRecord(result.local) ? result.local : {};
const image = isRecord(result.sentinelImage) ? result.sentinelImage : {};
const imageSummary = isRecord(image.summary) ? image.summary : {};
const remote = isRecord(result.remote) ? result.remote : {};
const accounts = isRecord(remote.accounts) ? remote.accounts : {};
const lines = renderCodexPoolRemoteSummary("SUB2API CODEX POOL SYNC", result, target, remote);
lines.push(renderTable([
["LOCAL_PROFILES", "PRUNE", "IMAGE_MODE", "IMAGE", "SDK"],
[
textValue(local.profileCount),
boolText(local.pruneRemoved),
textValue(imageSummary.mode ?? image.mode),
textValue(imageSummary.image ?? image.image),
textValue(imageSummary.openaiPythonVersion),
],
]));
lines.push(renderTable([
["ACCOUNTS", "CREATED", "UPDATED", "PRUNED", "PRUNE_MODE", "ATTENTION"],
[
textValue(accounts.desired ?? accounts.itemCount),
textValue(accounts.created),
textValue(accounts.updated),
textValue(accounts.pruned),
textValue(accounts.pruneMode),
textValue(recordArray(accounts.attentionItems).length),
],
]));
appendCodexPoolCheckTable(lines, remote);
appendNext(lines, result.next);
lines.push("");
lines.push("Full: bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm --full");
lines.push("Raw: bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm --raw");
return renderedCliResult(result.ok === true, "platform-infra sub2api codex-pool sync", lines.join("\n"));
}
export function renderCodexPoolValidateResult(result: Record<string, unknown>): RenderedCliResult {
const target = isRecord(result.target) ? result.target : {};
const summary = isRecord(result.summary) ? result.summary : {};
const lines = renderCodexPoolRemoteSummary("SUB2API CODEX POOL VALIDATE", result, target, summary);
appendCodexPoolCheckTable(lines, summary);
const validation = isRecord(summary.validation) ? summary.validation : {};
lines.push(renderTable([
["GATEWAY_MODELS", "GATEWAY_RESPONSES", "RESPONSES_RECENT", "COMPACT_RECENT"],
[
blockOk(validation.gatewayModels),
blockOk(validation.gatewayResponses),
blockOk(validation.gatewayResponsesRecent),
blockOk(validation.gatewayCompactRecent),
],
]));
lines.push("");
lines.push("Full: bun scripts/cli.ts platform-infra sub2api codex-pool validate --full");
lines.push("Raw: bun scripts/cli.ts platform-infra sub2api codex-pool validate --raw");
return renderedCliResult(result.ok === true, "platform-infra sub2api codex-pool validate", lines.join("\n"));
}
export function renderCodexPoolSentinelProbeResult(result: Record<string, unknown>): RenderedCliResult {
const target = isRecord(result.target) ? result.target : {};
const summary = isRecord(result.summary) ? result.summary : {};
const job = isRecord(summary.job) ? summary.job : {};
const runSummary = isRecord(summary.summary) ? summary.summary : {};
const results = recordArray(summary.results);
const state = isRecord(summary.sentinelState) ? summary.sentinelState : {};
const lastRun = isRecord(state.lastRun) ? state.lastRun : {};
const runtimeSchedulable = isRecord(lastRun.runtimeSchedulable) ? lastRun.runtimeSchedulable : {};
const lines: string[] = [];
lines.push([
"SUB2API CODEX POOL SENTINEL PROBE",
`ok=${result.ok === true ? "true" : "false"}`,
`target=${target.id ?? "-"}`,
`accounts=${Array.isArray(summary.requestedAccounts) ? summary.requestedAccounts.length : results.length}`,
`job=${job.status ?? "-"}`,
].join(" "));
lines.push(renderTable([
["JOB", "NS", "JOB_OK", "MARKER_OK", "LAST_RUN", "SELECTED", "OK", "TF", "ACTIONS"],
[
textValue(job.name),
textValue(summary.namespace ?? target.namespace),
boolText(summary.jobExecutionOk),
boolText(summary.markerOk),
textValue(runSummary.at ?? lastRun.at),
textValue(runSummary.selected ?? lastRun.selected),
textValue(runSummary.okCount ?? lastRun.okCount),
textValue(runSummary.transportFailureCount ?? lastRun.transportFailureCount),
textValue(runSummary.actionsTaken ?? lastRun.actionsTaken),
],
]));
lines.push("");
lines.push("RESULTS");
lines.push(renderTable([
["ACCOUNT", "OK", "HTTP", "MARKER", "DUR_MS", "KIND", "ACTION"],
...results.map((item) => {
const action = isRecord(item.action) ? item.action : {};
return [
textValue(item.accountName),
boolText(item.ok),
textValue(item.httpStatus),
boolText(item.markerMatched),
textValue(item.durationMs),
textValue(item.failureKind),
textValue(action.type),
];
}),
]));
lines.push(renderTable([
["QUARANTINED", "SCHEDULABLE", "UNSCHEDULABLE", "MISSING"],
[
textValue(state.quarantinedCount),
textValue(runtimeSchedulable.schedulableCount),
textValue(runtimeSchedulable.unschedulableCount),
textValue(runtimeSchedulable.missingCount),
],
]));
lines.push("");
lines.push("Full: bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account <name> --confirm --full");
lines.push("Raw: bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account <name> --confirm --raw");
return renderedCliResult(result.ok === true, "platform-infra sub2api codex-pool sentinel-probe", lines.join("\n"));
}
function renderCodexPoolRemoteSummary(title: string, result: Record<string, unknown>, target: Record<string, unknown>, remote: Record<string, unknown>): string[] {
return [
[
title,
`ok=${result.ok === true ? "true" : "false"}`,
`target=${target.id ?? "-"}`,
`mode=${remote.mode ?? "-"}`,
`degraded=${remote.degraded === true ? "true" : "false"}`,
].join(" "),
renderTable([
["TARGET", "ROUTE", "NS", "SERVICE", "POD", "ADMIN", "API_KEY"],
[
textValue(target.id),
textValue(target.route),
textValue(remote.namespace ?? target.namespace),
textValue(remote.serviceDns),
textValue(remote.appPod),
blockOk(remote.admin),
blockOk(remote.apiKey),
],
]),
];
}
function appendCodexPoolCheckTable(lines: string[], remote: Record<string, unknown>): void {
const runtime = isRecord(remote.runtimeCapabilities) ? remote.runtimeCapabilities : {};
lines.push(renderTable([
["BALANCE", "CONCURRENCY", "CAPACITY", "LOAD", "TEMP_UNSCHED", "MANUAL", "SENTINEL", "RUNTIME"],
[
blockOk(remote.ownerBalance),
blockOk(remote.ownerConcurrency),
blockOk(remote.capacity),
blockOk(remote.loadFactor),
blockOk(remote.tempUnschedulable),
blockOk(remote.manualAccounts),
blockOk(remote.sentinel),
blockOk(runtime),
],
]));
}
function appendNext(lines: string[], value: unknown): void {
if (!isRecord(value)) return;
const entries = Object.entries(value).filter(([, item]) => typeof item === "string");
if (entries.length === 0) return;
lines.push("");
lines.push("NEXT");
for (const [key, item] of entries) lines.push(` ${key}: ${item}`);
}
function blockOk(value: unknown): string {
return isRecord(value) ? boolText(value.ok) : "-";
}
export function renderSentinelReport(
parsed: Record<string, unknown> | null,
context: { events: number; full: boolean; remote: Record<string, unknown> },
@@ -306,6 +571,12 @@ export function textValue(value: unknown): string {
return String(value);
}
export function boolText(value: unknown): string {
if (value === true) return "Y";
if (value === false) return "N";
return "-";
}
export function shorten(value: string, maxChars: number): string {
return value.length <= maxChars ? value : `${value.slice(0, Math.max(0, maxChars - 1))}`;
}
@@ -123,18 +123,29 @@ export function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarge
export function readTargetSentinelImageBuild(raw: Record<string, unknown>, targetId: string, required = true): CodexPoolRuntimeTarget["sentinelImageBuild"] {
const codexPool = isRecord(raw.codexPool) ? raw.codexPool : null;
const imageBuild = codexPool !== null && isRecord(codexPool.sentinelImageBuild) ? codexPool.sentinelImageBuild : null;
if (imageBuild === null && !required) return { baseImageCachePolicy: "pull", noProxy: [] };
if (imageBuild === null && !required) return { baseImageCachePolicy: "pull", noProxy: [], proxyEnvPath: null };
if (imageBuild === null) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild must be a YAML object`);
const policy = stringValue(imageBuild.baseImageCachePolicy);
if (policy !== "pull" && policy !== "local-if-present") {
throw new Error(`${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.baseImageCachePolicy must be pull or local-if-present`);
}
const egressProxy = isRecord(raw.egressProxy) ? raw.egressProxy : null;
const proxyEnvPath = egressProxy === null ? null : readProxyEnvPath(egressProxy.proxyEnvPath, `${sub2apiConfigPath}.targets[${targetId}].egressProxy.proxyEnvPath`);
return {
baseImageCachePolicy: policy,
noProxy: readTargetNoProxy(imageBuild.noProxy, `${sub2apiConfigPath}.targets[${targetId}].codexPool.sentinelImageBuild.noProxy`),
proxyEnvPath,
};
}
export function readProxyEnvPath(value: unknown, key: string): string | null {
if (value === undefined || value === null) return null;
const path = stringValue(value);
if (path === null) throw new Error(`${key} must be a non-empty string`);
if (!/^\/[A-Za-z0-9._/-]+$/u.test(path)) throw new Error(`${key} has an unsupported absolute path format`);
return path;
}
export function readTargetPublicExposure(raw: Record<string, unknown>, targetId: string): CodexPoolRuntimePublicExposure | null {
if (raw.publicExposure === undefined || raw.publicExposure === null) return null;
if (!isRecord(raw.publicExposure)) throw new Error(`${sub2apiConfigPath}.targets[${targetId}].publicExposure must be a YAML object`);
@@ -92,6 +92,7 @@ export interface CodexPoolRuntimeTarget {
sentinelImageBuild: {
baseImageCachePolicy: "pull" | "local-if-present";
noProxy: string;
proxyEnvPath: string | null;
};
egressProxy: {
enabled: boolean;
@@ -227,6 +228,7 @@ export interface CodexPoolManualAccountGroupBinding {
export interface CodexPoolManualAccountProtection {
accountName: string;
reason: string | null;
targetIds: string[] | null;
proxyBinding: CodexPoolManualAccountProxyBinding | null;
groupBinding: CodexPoolManualAccountGroupBinding | null;
}