From 7d157d7a942c1caf3ebd0fe50c7a8cdfa4809768 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 07:51:56 +0000 Subject: [PATCH] chore: sync post-task workspace updates --- AGENTS.md | 2 +- config/platform-infra/sub2api-codex-pool.yaml | 12 +- docs/reference/platform-infra.md | 2 + docs/reference/secretary-reference.md | 11 + scripts/src/platform-infra-sub2api-codex.ts | 236 +++++++++++++++++- 5 files changed, 252 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dd5b58f1..abbca694 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - P0: 单纯文档、AGENTS.md、docs/reference、SPEC、短连接 CLI、trans/tran/helper 这类轻量变更,禁止为了“隔离开发”新建 git worktree;必须在当前项目固定主 worktree 直接修改、提交并 push 到对应 remote 分支。 - P0: 只有业务代码、运行面、发布链路、CI/CD、Secret、权限、数据迁移、PROD 或明确高风险改动,才按项目规则创建隔离 worktree 或走 PR;不要把重型开发流程套到文档/CLI 轻量变更上。 -- P0: 若固定主 worktree 已有非本次任务的未提交修改或未推送提交,默认视为可共同交付状态,一律可以一起提交并直接 push;必要时使用非破坏性 rebase/autostash 或等价方式对齐 remote 后直接 push,禁止绕到新 worktree 掩盖主 worktree 状态。 +- P0: 若固定主 worktree 已有「不属于这次任务的未提交修改」或未推送提交,默认视为可共同交付状态,一律一起提交并直接 push;必要时使用非破坏性 rebase/autostash 或等价方式对齐 remote 后直接 push,禁止绕到新 worktree 掩盖主 worktree 状态。 ## P0 最高优先级:可见性先修 diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index 85357e7a..ef8115de 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -13,10 +13,12 @@ profiles: authFile: auth.json.pre-sub2api fallbackConfigFile: config.toml fallbackAuthFile: auth.json + openaiResponsesWebSocketsV2Mode: ctx_pool - profile: HY accountName: unidesk-codex-hy configFile: config.toml.HY authFile: auth.json.HY + openaiResponsesWebSocketsV2Mode: passthrough - profile: gptclub accountName: unidesk-codex-gptclub configFile: config.toml.gptclub @@ -31,7 +33,7 @@ profiles: accountName: unidesk-codex-fixwikihub configFile: config.toml.fixwikihub authFile: auth.json.fixwikihub - openaiResponsesWebSocketsV2Mode: ctx_pool + openaiResponsesWebSocketsV2Mode: off - profile: zakuzaku accountName: unidesk-codex-zakuzaku configFile: config.toml.zakuzaku @@ -52,11 +54,17 @@ publicExposure: remotePort: 21880 localIP: sub2api.platform-infra.svc.cluster.local localPort: 8080 - publicBaseUrl: http://74.48.78.17:21880 + publicBaseUrl: https://sub2api.74-48-78-17.nip.io masterBaseUrl: http://127.0.0.1:21880 masterFrps: 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 providerName: OpenAI diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index 00f1904a..c11c8c58 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -31,6 +31,8 @@ - `publicExposure` controls the optional FRP bridge from master server to the G14 ClusterIP service. - `localCodex` controls how the master server's current `~/.codex` consumer files are backed up and rewritten. Codex consumers using Sub2API must keep `supportsWebSockets` and `responsesWebSocketsV2` enabled so compacted long sessions can continue through the Responses WebSocket v2 response chain instead of falling back to HTTP-only summary context. +Enable account-level WebSocket v2 only for upstream profiles that have passed a direct Codex WSv2 probe. `codex-pool validate` reports each managed account's runtime WebSocket v2 mode and whether it matches YAML, so stale `ctx_pool` settings cannot silently keep routing Codex WS sessions to an upstream that closes with `no available account`. + The request path is: 1. A client sends an OpenAI-compatible request to the configured consumer base URL, normally master-local `http://127.0.0.1:/v1/...`, with the unified API key. diff --git a/docs/reference/secretary-reference.md b/docs/reference/secretary-reference.md index 8d443acd..29c21825 100644 --- a/docs/reference/secretary-reference.md +++ b/docs/reference/secretary-reference.md @@ -43,6 +43,17 @@ 给用户下一步时,秘书默认输出三个要素:一个明确当前动作、一个时间盒、一个反馈格式。反馈格式应简单,例如“完成/未完成 + 卡点”“路径 + 当前状态 + 下一步阻塞”。用户反馈后,只根据真实进展滚动调整下一段安排,不补写无依据的完成状态。 +**用户反馈后秘书必须立即同步到 Todo Note。** 用户按反馈格式给出进展后,秘书的下一步不是直接调整日程,而是:先把进展同步写入 Todo Note(标记已完成 todo / 新增子 todo 拆分剩余工作 / 更新标题反映最新出发状态 / 调整 reminder 时间),再根据 Todo Note 的新状态滚动安排下一段日程。 + +同步规则细节: +- 若用户报告「完成」,对应 todo 必须 `toggleTodoCompleted`,不可只口头接着往下排而漏标。 +- 若用户报告「未完成 + 卡点」,秘书必须把剩余工作量拆成新的子 todo 或更新原 todo 标题反映最新进展和阻塞原因。 +- 若用户新增口头待办、调整优先级或给出截止时间变化,秘书必须立即新增/更新/改 reminder,不能等下次排程才做。 +- 同步必须在同一轮交互中完成(即用户反馈后、秘书输出下一段日程前),禁止攒到一天结束时批量补写。 +- 若 Todo Note 写路径因外部故障阻塞,秘书必须立即透明告知用户「当前无法同步到 Todo Note,你的进展请你自己记一下」,并把这个阻塞升级到日程(作为待修问题),不能跳过同步直接给下一步。 + +这条规则不替代「秘书的本份是维护 Todo Note;Decision Center diary 只读不写」。反馈同步只写 Todo Note,不写到 diary。 + **秘书做日程只在对话里给时间块,配合决策日记做承载层,不开 TaskList/TaskCreate。** TaskList 是 Claude Code 内部的进度跟踪工具,不是给用户看的日程;用户要的是「下一段做什么 + 几点到几点」一句话,不是 task id / 依赖图 / completed 状态那一套。任务跟踪和触发依赖(「等杨邮件到了再启动回炉」)用对话口头说 + 决策日记的「进展」段记录,不开 TaskUpdate blockedBy 图。唯一的例外:工程任务(写代码、跑 CI、提交 PR)才用 TaskCreate,因为那是 Claude Code 自己的多步工作,不是用户日程。 **时间分配由秘书主导,不向用户要选择。** 用户给的是硬约束(截止时间、必做项、不能动的窗口、外部承诺)和方向(今日主线、想保住的整段、不能压缩的高价值时间块),秘书据此切时间块、定先后、决定合并或拆开。把多个时间块候选方案列给用户挑,等于把决策成本推回去,与秘书替用户节省决策带宽的定位相悖。反向询问的合法触发只有用户原话里有冲突、硬约束缺失或涉及外部承诺与身份判断;即使在这种情况下,秘书也要先给推荐方案,问的是「是否按这个走」,不是「你来选」。 diff --git a/scripts/src/platform-infra-sub2api-codex.ts b/scripts/src/platform-infra-sub2api-codex.ts index 5f49fbff..e390a638 100644 --- a/scripts/src/platform-infra-sub2api-codex.ts +++ b/scripts/src/platform-infra-sub2api-codex.ts @@ -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 { 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> { + 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 | 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> { - 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> { + 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 { + const codexDir = join(homedir(), ".codex"); + const seenAccountNames = new Set(); + const configs = pool.profiles.length > 0 ? pool.profiles : discoverCodexProfileConfigs(codexDir); + const modes: Record = {}; + 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}, }