chore: sync post-task workspace updates

This commit is contained in:
Codex
2026-06-09 07:51:56 +00:00
parent dab7db6ea7
commit 7d157d7a94
5 changed files with 252 additions and 11 deletions
+228 -8
View File
@@ -97,6 +97,13 @@ interface CodexPoolPublicExposureConfig {
configPath: string;
containerName: string;
};
masterCaddy: {
enabled: boolean;
domain: string;
configPath: string;
serviceName: string;
upstreamBaseUrl: string;
};
}
interface CodexPoolLocalCodexConfig {
@@ -327,15 +334,17 @@ async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions):
};
}
const masterResult = await applyMasterFrpsAllowPort(pool);
const caddyResult = await applyMasterCaddySite(pool);
const remoteResult = await capture(config, g14K3sRoute, ["script"], exposeScript(pool));
const parsed = parseJsonOutput(remoteResult.stdout);
const publicProbe = await probePublicModels(pool, "without-api-key");
const ok = masterResult.ok && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
const publicProbe = await probePublicModels(pool, "without-api-key", undefined, "public");
const ok = masterResult.ok && caddyResult.ok && remoteResult.exitCode === 0 && boolField(parsed, "ok", false) && publicProbe.httpStatus === 401;
if (options.raw) {
return {
ok,
action: "platform-infra-sub2api-codex-pool-expose",
masterFrps: masterResult,
masterCaddy: caddyResult,
remote: compactCapture(remoteResult, { full: true }),
parsed,
publicProbe,
@@ -347,6 +356,7 @@ async function codexPoolExpose(config: UniDeskConfig, options: ConfirmOptions):
mode: "confirmed",
publicExposure: publicExposureSummary(pool),
masterFrps: masterResult,
masterCaddy: caddyResult,
remote: parsed ?? compactCapture(remoteResult, { full: options.full || remoteResult.exitCode !== 0 }),
publicProbe,
next: {
@@ -577,6 +587,13 @@ function defaultCodexPoolConfig(): CodexPoolConfig {
configPath: "/opt/hwlab-frp/frps.dev.toml",
containerName: "hwlab-frps-dev",
},
masterCaddy: {
enabled: true,
domain: "sub2api.74-48-78-17.nip.io",
configPath: "/etc/caddy/Caddyfile",
serviceName: "caddy",
upstreamBaseUrl: "http://127.0.0.1:21880",
},
},
localCodex: {
backupSuffix: "pre-sub2api",
@@ -671,6 +688,7 @@ function readBooleanConfig(value: unknown, key: string, fallback: boolean): bool
function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExposureConfig): CodexPoolPublicExposureConfig {
if (!isRecord(value)) return defaults;
const masterFrpsValue = isRecord(value.masterFrps) ? value.masterFrps : {};
const masterCaddyValue = isRecord(value.masterCaddy) ? value.masterCaddy : {};
const config: CodexPoolPublicExposureConfig = {
enabled: booleanValue(value.enabled) ?? defaults.enabled,
proxyName: stringValue(value.proxyName) ?? defaults.proxyName,
@@ -688,6 +706,13 @@ function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExpos
configPath: stringValue(masterFrpsValue.configPath) ?? defaults.masterFrps.configPath,
containerName: stringValue(masterFrpsValue.containerName) ?? defaults.masterFrps.containerName,
},
masterCaddy: {
enabled: booleanValue(masterCaddyValue.enabled) ?? defaults.masterCaddy.enabled,
domain: stringValue(masterCaddyValue.domain) ?? defaults.masterCaddy.domain,
configPath: stringValue(masterCaddyValue.configPath) ?? defaults.masterCaddy.configPath,
serviceName: stringValue(masterCaddyValue.serviceName) ?? defaults.masterCaddy.serviceName,
upstreamBaseUrl: normalizeBaseUrl(stringValue(masterCaddyValue.upstreamBaseUrl)) ?? defaults.masterCaddy.upstreamBaseUrl,
},
};
validateKubernetesName(config.configMapName, "publicExposure.configMapName", true);
validateKubernetesName(config.deploymentName, "publicExposure.deploymentName", true);
@@ -700,6 +725,11 @@ function readPublicExposureConfig(value: unknown, defaults: CodexPoolPublicExpos
if (!/^[A-Za-z0-9._:-]+$/u.test(config.localIP)) throw new Error(`${codexPoolConfigPath}.publicExposure.localIP has an unsupported format`);
if (!config.masterFrps.configPath.startsWith("/")) throw new Error(`${codexPoolConfigPath}.publicExposure.masterFrps.configPath must be absolute`);
validateProxyName(config.masterFrps.containerName, "publicExposure.masterFrps.containerName");
validatePublicHostname(config.masterCaddy.domain, "publicExposure.masterCaddy.domain");
if (!config.masterCaddy.configPath.startsWith("/")) throw new Error(`${codexPoolConfigPath}.publicExposure.masterCaddy.configPath must be absolute`);
validateProxyName(config.masterCaddy.serviceName, "publicExposure.masterCaddy.serviceName");
const upstream = new URL(config.masterCaddy.upstreamBaseUrl);
if (upstream.protocol !== "http:") throw new Error(`${codexPoolConfigPath}.publicExposure.masterCaddy.upstreamBaseUrl must use http:// for the local upstream`);
return config;
}
@@ -728,6 +758,12 @@ function validateProxyName(value: string, key: string): void {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`);
}
function validatePublicHostname(value: string, key: string): void {
if (value.length > 253 || !/^[A-Za-z0-9.-]+$/u.test(value) || value.startsWith(".") || value.endsWith(".")) {
throw new Error(`${codexPoolConfigPath}.${key} has an unsupported hostname format`);
}
}
function validatePort(value: number, key: string): void {
if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error(`${codexPoolConfigPath}.${key} must be an integer port`);
}
@@ -813,6 +849,13 @@ function publicExposureSummary(pool: CodexPoolConfig): Record<string, unknown> {
masterConfigPath: pool.publicExposure.masterFrps.configPath,
masterContainerName: pool.publicExposure.masterFrps.containerName,
},
caddy: {
enabled: pool.publicExposure.masterCaddy.enabled,
domain: pool.publicExposure.masterCaddy.domain,
configPath: pool.publicExposure.masterCaddy.configPath,
serviceName: pool.publicExposure.masterCaddy.serviceName,
upstreamBaseUrl: pool.publicExposure.masterCaddy.upstreamBaseUrl,
},
upstream: {
localIP: pool.publicExposure.localIP,
localPort: pool.publicExposure.localPort,
@@ -869,6 +912,97 @@ async function applyMasterFrpsAllowPort(pool: CodexPoolConfig): Promise<Record<s
};
}
async function applyMasterCaddySite(pool: CodexPoolConfig): Promise<Record<string, unknown>> {
const caddy = pool.publicExposure.masterCaddy;
if (!caddy.enabled) return { ok: true, action: "disabled-by-yaml", valuesPrinted: false };
const path = caddy.configPath;
if (!existsSync(path)) return { ok: false, error: "master-caddy-config-missing", path, valuesPrinted: false };
const before = readFileSync(path, "utf8");
const desiredBlock = renderCaddySiteBlock(caddy.domain, caddy.upstreamBaseUrl);
const existing = caddySiteBlock(before, caddy.domain);
const alreadyConfigured = existing === desiredBlock;
let backupPath: string | null = null;
let action = "kept-existing";
if (!alreadyConfigured) {
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
backupPath = `${path}.bak-sub2api-https-${stamp}`;
copyFileSync(path, backupPath);
const next = existing === null
? `${before.replace(/\s*$/u, "")}\n\n${desiredBlock}\n`
: before.replace(existing, desiredBlock);
writeFileSync(path, next, "utf8");
chmodSync(path, statSync(backupPath).mode & 0o777);
action = existing === null ? "added-site" : "updated-site";
}
const validate = Bun.spawnSync(["caddy", "validate", "--config", path, "--adapter", "caddyfile"]);
let reload: ReturnType<typeof Bun.spawnSync> | null = null;
if (validate.exitCode === 0 && !alreadyConfigured) {
reload = Bun.spawnSync(["systemctl", "reload", caddy.serviceName]);
}
const active = Bun.spawnSync(["systemctl", "is-active", caddy.serviceName]);
const ok = validate.exitCode === 0 && (reload === null || reload.exitCode === 0) && active.exitCode === 0 && String(active.stdout).trim() === "active";
return {
ok,
action,
path,
backupPath,
domain: caddy.domain,
upstreamBaseUrl: caddy.upstreamBaseUrl,
serviceName: caddy.serviceName,
validate: {
exitCode: validate.exitCode,
stdoutTail: Buffer.from(validate.stdout).toString("utf8").slice(-1000),
stderrTail: Buffer.from(validate.stderr).toString("utf8").slice(-2000),
},
reload: reload === null ? null : {
exitCode: reload.exitCode,
stdoutTail: Buffer.from(reload.stdout).toString("utf8").slice(-1000),
stderrTail: Buffer.from(reload.stderr).toString("utf8").slice(-1000),
},
active: {
exitCode: active.exitCode,
stdoutTail: Buffer.from(active.stdout).toString("utf8").slice(-1000),
stderrTail: Buffer.from(active.stderr).toString("utf8").slice(-1000),
},
valuesPrinted: false,
};
}
function renderCaddySiteBlock(domain: string, upstreamBaseUrl: string): string {
const upstream = new URL(upstreamBaseUrl);
const upstreamHost = `${upstream.hostname}${upstream.port ? `:${upstream.port}` : ""}`;
return `${domain} {
encode zstd gzip
reverse_proxy ${upstreamHost} {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
dial_timeout 5s
response_header_timeout 30s
}
}
}`;
}
function caddySiteBlock(text: string, domain: string): string | null {
const startMatch = new RegExp(`(^|\\n)${escapeRegExp(domain)}\\s*\\{`, "u").exec(text);
if (startMatch === null) return null;
const start = startMatch.index + (startMatch[1] === "\n" ? 1 : 0);
const open = text.indexOf("{", start);
if (open < 0) return null;
let depth = 0;
for (let index = open; index < text.length; index += 1) {
const ch = text[index];
if (ch === "{") depth += 1;
if (ch === "}") {
depth -= 1;
if (depth === 0) return text.slice(start, index + 1);
}
}
return null;
}
function frpsAllowPortExists(toml: string, port: number): boolean {
const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
return sections.some((section) => {
@@ -1227,8 +1361,9 @@ async function validatePublicGatewayWithKey(pool: CodexPoolConfig, apiKey: strin
};
}
async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string): Promise<Record<string, unknown>> {
const url = `${pool.publicExposure.masterBaseUrl.replace(/\/+$/u, "")}/v1/models`;
async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "without-api-key", apiKey?: string, base: "master" | "public" = "master"): Promise<Record<string, unknown>> {
const baseUrl = base === "public" ? pool.publicExposure.publicBaseUrl : pool.publicExposure.masterBaseUrl;
const url = `${baseUrl.replace(/\/+$/u, "")}/v1/models`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
@@ -1249,7 +1384,7 @@ async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "
ok: response.ok,
mode,
method: "GET /v1/models",
baseUrl: pool.publicExposure.masterBaseUrl,
baseUrl,
httpStatus: response.status,
modelCount,
bodyPreview: response.ok ? "" : body.slice(0, 500),
@@ -1260,7 +1395,7 @@ async function probePublicModels(pool: CodexPoolConfig, mode: "with-api-key" | "
ok: false,
mode,
method: "GET /v1/models",
baseUrl: pool.publicExposure.masterBaseUrl,
baseUrl,
httpStatus: 0,
error: error instanceof Error ? error.message : String(error),
valuesPrinted: false,
@@ -1357,6 +1492,19 @@ function desiredAccountCapacityMap(pool: CodexPoolConfig): Record<string, number
return capacities;
}
function desiredAccountWebSocketsV2ModeMap(pool: CodexPoolConfig): Record<string, OpenAIResponsesWebSocketsV2Mode | null> {
const codexDir = join(homedir(), ".codex");
const seenAccountNames = new Set<string>();
const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir);
const modes: Record<string, OpenAIResponsesWebSocketsV2Mode | null> = {};
for (const entry of configs) {
const accountName = entry.accountName ?? uniqueAccountName(entry.profile, seenAccountNames);
seenAccountNames.add(accountName);
modes[accountName] = entry.openaiResponsesWebSocketsV2Mode;
}
return modes;
}
function remotePythonScript(mode: "sync" | "validate", encodedPayload: string, pool: CodexPoolConfig): string {
return `
set -u
@@ -1382,6 +1530,7 @@ POOL_API_KEY_SECRET_KEY = "${pool.apiKeySecretKey}"
MIN_OWNER_BALANCE_USD = ${JSON.stringify(pool.minOwnerBalanceUsd)}
POOL_DEFAULT_ACCOUNT_CAPACITY = ${JSON.stringify(pool.defaultAccountCapacity)}
EXPECTED_ACCOUNT_CAPACITIES = ${JSON.stringify(desiredAccountCapacityMap(pool))}
EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAccountWebSocketsV2ModeMap(pool)))})
MODE = "${mode}"
PAYLOAD_B64 = "${encodedPayload}"
@@ -1835,6 +1984,73 @@ def account_capacity_status(token):
"valuesPrinted": False,
}
def account_ws_v2_status(token):
accounts = list_accounts(token)
by_name = {item.get("name"): item for item in accounts if isinstance(item.get("name"), str)}
items = []
missing = []
mismatched = []
enabled_names = []
unschedulable_enabled = []
schedulable_enabled = []
for name in sorted(EXPECTED_ACCOUNT_WS_MODES):
expected_mode = EXPECTED_ACCOUNT_WS_MODES[name]
expected_enabled = expected_mode not in (None, "", "off")
account = by_name.get(name)
if account is None:
missing.append(name)
items.append({
"accountName": name,
"accountId": None,
"expectedMode": expected_mode,
"expectedEnabled": expected_enabled,
"runtimeMode": None,
"runtimeEnabled": None,
"ok": False,
})
continue
extra = account.get("extra") if isinstance(account.get("extra"), dict) else {}
runtime_mode = extra.get("openai_apikey_responses_websockets_v2_mode")
runtime_enabled = extra.get("openai_apikey_responses_websockets_v2_enabled")
schedulable = account.get("schedulable")
if expected_mode == "off":
ok = runtime_mode == "off" and runtime_enabled is False
elif expected_mode is None:
ok = runtime_mode in (None, "", "off") and runtime_enabled in (None, False)
else:
ok = runtime_mode == expected_mode and runtime_enabled is True
if not ok:
mismatched.append(name)
if expected_enabled:
enabled_names.append(name)
if schedulable is True:
schedulable_enabled.append(name)
else:
unschedulable_enabled.append(name)
items.append({
"accountName": name,
"accountId": account.get("id"),
"expectedMode": expected_mode,
"expectedEnabled": expected_enabled,
"runtimeMode": runtime_mode,
"runtimeEnabled": runtime_enabled,
"status": account.get("status"),
"schedulable": schedulable,
"ok": ok and ((not expected_enabled) or schedulable is True),
})
availability_ok = len(enabled_names) == 0 or len(schedulable_enabled) > 0
return {
"ok": len(missing) == 0 and len(mismatched) == 0 and availability_ok,
"desired": len(EXPECTED_ACCOUNT_WS_MODES),
"enabled": enabled_names,
"schedulableEnabled": schedulable_enabled,
"unschedulableEnabled": unschedulable_enabled,
"missing": missing,
"mismatched": mismatched,
"items": items,
"valuesPrinted": False,
}
def api_key_preview(api_key):
if len(api_key) <= 14:
return "***"
@@ -1852,12 +2068,13 @@ def run_sync():
raise RuntimeError("pool group id missing after ensure")
account_results, pruned_account_results = ensure_accounts(token, profiles, group_id)
capacity_status = account_capacity_status(token)
ws_v2_status = account_ws_v2_status(token)
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)
owner_balance = ensure_pool_owner_balance(token, api_key_result["userId"])
gateway = validate_gateway(api_key)
return {
"ok": gateway["ok"] is True and capacity_status["ok"] is True,
"ok": gateway["ok"] is True and capacity_status["ok"] is True and ws_v2_status["ok"] is True,
"mode": "sync",
"namespace": NAMESPACE,
"serviceDns": SERVICE_DNS,
@@ -1874,6 +2091,7 @@ def run_sync():
"valuesPrinted": False,
},
"capacity": capacity_status,
"webSocketsV2": ws_v2_status,
"apiKey": {
"name": POOL_API_KEY_NAME,
"secret": f"{NAMESPACE}/{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY}",
@@ -1900,9 +2118,10 @@ def run_validate():
if key_item is not None and key_item.get("user_id") is not None:
owner_balance = ensure_pool_owner_balance(token, key_item["user_id"])
capacity_status = account_capacity_status(token)
ws_v2_status = account_ws_v2_status(token)
gateway = validate_gateway(api_key)
return {
"ok": gateway["ok"] is True and capacity_status["ok"] is True,
"ok": gateway["ok"] is True and capacity_status["ok"] is True and ws_v2_status["ok"] is True,
"mode": "validate",
"namespace": NAMESPACE,
"serviceDns": SERVICE_DNS,
@@ -1917,6 +2136,7 @@ def run_validate():
},
"ownerBalance": owner_balance,
"capacity": capacity_status,
"webSocketsV2": ws_v2_status,
"validation": {"gatewayModels": gateway},
}