From c00f22eab56f7e72e59be38a90b7033305601f7e Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 14 Jun 2026 12:41:10 +0000 Subject: [PATCH] Bind protected Sub2API manual accounts to proxy --- config/platform-infra/sub2api-codex-pool.yaml | 4 + scripts/src/platform-infra-sub2api-codex.ts | 243 +++++++++++++++++- 2 files changed, 239 insertions(+), 8 deletions(-) diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index 2dad53c5..d6191622 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -138,6 +138,10 @@ manualAccounts: protected: - accountName: lucianepidgeon@gmail.com reason: Manually configured in Sub2API; keep outside UniDesk-managed Codex pool and sentinel control. + proxyBinding: + enabled: true + source: target-egress-proxy + proxyName: platform-infra-sub2api-egress-proxy publicExposure: enabled: false proxyName: platform-infra-sub2api diff --git a/scripts/src/platform-infra-sub2api-codex.ts b/scripts/src/platform-infra-sub2api-codex.ts index 2636e164..254bf9fa 100644 --- a/scripts/src/platform-infra-sub2api-codex.ts +++ b/scripts/src/platform-infra-sub2api-codex.ts @@ -81,6 +81,8 @@ interface CodexPoolRuntimeTarget { egressProxy: { enabled: boolean; applyToSentinel: boolean; + serviceName: string; + listenPort: number; httpProxy: string; noProxy: string; } | null; @@ -156,9 +158,16 @@ interface CodexPoolManualAccountsConfig { protected: CodexPoolManualAccountProtection[]; } +interface CodexPoolManualAccountProxyBinding { + enabled: boolean; + source: "target-egress-proxy"; + proxyName: string; +} + interface CodexPoolManualAccountProtection { accountName: string; reason: string | null; + proxyBinding: CodexPoolManualAccountProxyBinding | null; } interface CodexPoolProfileConfig { @@ -631,6 +640,8 @@ function codexPoolRuntimeTarget(targetId?: string): CodexPoolRuntimeTarget { egressProxy = { enabled: true, applyToSentinel: raw.egressProxy.applyToSentinel === undefined ? true : raw.egressProxy.applyToSentinel === true, + serviceName: proxyServiceName, + listenPort, httpProxy: `http://${proxyServiceName}.${targetNamespace}.svc.cluster.local:${listenPort}`, noProxy, }; @@ -1508,11 +1519,28 @@ function readManualAccountsConfig(value: unknown, defaults: CodexPoolManualAccou 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; - return { accountName, reason }; + const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`) : null; + return { accountName, reason, proxyBinding }; }); return { protected: protectedAccounts }; } +function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolManualAccountProxyBinding | null { + if (value === undefined || value === null) return null; + if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); + const enabled = value.enabled === undefined ? true : value.enabled === true; + const source = stringValue(value.source) ?? "target-egress-proxy"; + if (source !== "target-egress-proxy") throw new Error(`${codexPoolConfigPath}.${key}.source must be target-egress-proxy`); + const proxyName = stringValue(value.proxyName); + if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`); + validateProxyName(proxyName, `${key}.proxyName`); + return { + enabled, + source, + proxyName, + }; +} + function readManualAccountName(value: unknown, key: string): string | null { const text = stringValue(value)?.trim() ?? null; if (text === null || text.length === 0) return null; @@ -2122,18 +2150,42 @@ function compactManualAccounts(block: unknown): Record | null { const items = recordArray(block.items).map((item) => pickSummaryFields(item, [ "accountName", "reason", + "ok", "exists", "accountId", "status", "schedulable", "inYamlProfiles", "runtimeMarkedUnideskManaged", + "proxyBinding", "controlPolicy", ])); + const proxySync = isRecord(block.proxySync) + ? { + ok: block.proxySync.ok, + itemCount: block.proxySync.itemCount, + items: recordArray(block.proxySync.items).map((item) => pickSummaryFields(item, [ + "accountName", + "accountId", + "enabled", + "ok", + "action", + "proxyAction", + "expectedProxyName", + "proxyId", + "runtimeProxyId", + "runtimeProxyName", + "bindingAligned", + "controlPolicy", + ])), + valuesPrinted: false, + } + : undefined; return { ok: block.ok, protectedCount: block.protectedCount, items, + proxySync, valuesPrinted: false, }; } @@ -2946,6 +2998,8 @@ function poolTarget(pool = readCodexPoolConfig(), target = codexPoolRuntimeTarge egressProxy: target.egressProxy === null ? null : { enabled: target.egressProxy.enabled, applyToSentinel: target.egressProxy.applyToSentinel, + serviceName: target.egressProxy.serviceName, + listenPort: target.egressProxy.listenPort, httpProxy: target.egressProxy.httpProxy, noProxy: target.egressProxy.noProxy, }, @@ -4152,6 +4206,7 @@ EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAc EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredAccountTempUnschedulableMap(pool)))}) MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(pool.manualAccounts.protected))}) SENTINEL_CONFIG = json.loads(${JSON.stringify(JSON.stringify(pool.sentinel))}) +TARGET_EGRESS_PROXY = json.loads(${JSON.stringify(JSON.stringify(target.egressProxy))}) MODE = "${mode}" PAYLOAD_B64 = "${encodedPayload}" @@ -4459,13 +4514,181 @@ def list_accounts(token): return extract_items(data) def find_account_by_name(token, name): - path = "/api/v1/admin/accounts?page=1&page_size=20&platform=openai&type=apikey&search=" + quote(str(name)) + path = "/api/v1/admin/accounts?page=1&page_size=50&platform=openai&search=" + quote(str(name)) data = ensure_success(curl_api("GET", path, bearer=token), "find account " + str(name)) for item in extract_items(data): if isinstance(item, dict) and item.get("name") == name: return item return None +def list_proxy_candidates(token, search=""): + path = "/api/v1/admin/proxies?page=1&page_size=200" + if search: + path += "&search=" + quote(str(search)) + data = ensure_success(curl_api("GET", path, bearer=token), "list proxies") + return extract_items(data) + +def find_proxy_by_name(token, name): + for item in list_proxy_candidates(token, name): + if isinstance(item, dict) and item.get("name") == name: + return item + return None + +def desired_manual_proxy_payload(protection): + binding = protection.get("proxyBinding") if isinstance(protection, dict) else None + if not isinstance(binding, dict) or binding.get("enabled") is not True: + return None + if binding.get("source") != "target-egress-proxy": + raise RuntimeError("manual account proxyBinding source must be target-egress-proxy") + if not isinstance(TARGET_EGRESS_PROXY, dict) or TARGET_EGRESS_PROXY.get("enabled") is not True: + raise RuntimeError("manual account proxyBinding requires an enabled target egressProxy") + service_name = TARGET_EGRESS_PROXY.get("serviceName") + listen_port = TARGET_EGRESS_PROXY.get("listenPort") + proxy_name = binding.get("proxyName") + if not isinstance(service_name, str) or not service_name: + raise RuntimeError("target egressProxy serviceName is missing") + if not isinstance(listen_port, int) or listen_port <= 0: + raise RuntimeError("target egressProxy listenPort is missing") + if not isinstance(proxy_name, str) or not proxy_name: + raise RuntimeError("manual account proxyBinding proxyName is missing") + return { + "name": proxy_name, + "protocol": "http", + "host": f"{service_name}.{NAMESPACE}.svc.cluster.local", + "port": listen_port, + "fallback_mode": "none", + "expiry_warn_days": 0, + } + +def proxy_needs_update(proxy, payload): + if not isinstance(proxy, dict): + return True + for key in ("name", "protocol", "host", "port", "fallback_mode", "expiry_warn_days"): + if proxy.get(key) != payload.get(key): + return True + return proxy.get("status") != "active" + +def ensure_manual_proxy(token, payload): + current = find_proxy_by_name(token, payload["name"]) + if current is None: + created = ensure_success(curl_api("POST", "/api/v1/admin/proxies", bearer=token, payload=payload), f"create proxy {payload['name']}") + return created, "created" + if proxy_needs_update(current, payload): + update_payload = dict(payload) + update_payload["status"] = "active" + updated = ensure_success(curl_api("PUT", f"/api/v1/admin/proxies/{current['id']}", bearer=token, payload=update_payload), f"update proxy {payload['name']}") + return updated if isinstance(updated, dict) else current, "updated" + return current, "unchanged" + +def manual_proxy_status(token, account, protection): + payload = desired_manual_proxy_payload(protection) + if payload is None: + return { + "enabled": False, + "ok": True, + "action": "not-configured", + "valuesPrinted": False, + } + proxy = find_proxy_by_name(token, payload["name"]) + runtime_proxy = account.get("proxy") if isinstance(account, dict) and isinstance(account.get("proxy"), dict) else None + binding_aligned = ( + isinstance(account, dict) + and isinstance(proxy, dict) + and account.get("proxy_id") == proxy.get("id") + ) + return { + "enabled": True, + "ok": binding_aligned, + "action": "validate", + "source": "target-egress-proxy", + "expectedProxyName": payload["name"], + "expectedProtocol": payload["protocol"], + "expectedHost": payload["host"], + "expectedPort": payload["port"], + "proxyRecordExists": isinstance(proxy, dict), + "proxyId": proxy.get("id") if isinstance(proxy, dict) else None, + "runtimeProxyId": account.get("proxy_id") if isinstance(account, dict) else None, + "runtimeProxyName": runtime_proxy.get("name") if isinstance(runtime_proxy, dict) else None, + "bindingAligned": binding_aligned, + "valuesPrinted": False, + } + +def ensure_manual_account_proxy_bindings(token): + items = [] + for protection in MANUAL_ACCOUNT_PROTECTIONS: + if not isinstance(protection, dict): + continue + name = protection.get("accountName") + if not isinstance(name, str) or not name: + continue + account = find_account_by_name(token, name) + payload = desired_manual_proxy_payload(protection) + if payload is None: + items.append({ + "accountName": name, + "enabled": False, + "action": "not-configured", + "ok": True, + "valuesPrinted": False, + }) + continue + if not isinstance(account, dict): + items.append({ + "accountName": name, + "enabled": True, + "action": "account-missing", + "ok": False, + "expectedProxyName": payload["name"], + "valuesPrinted": False, + }) + continue + extra = account.get("extra") if isinstance(account.get("extra"), dict) else {} + if extra.get("unidesk_managed") is True: + items.append({ + "accountName": name, + "accountId": account.get("id"), + "enabled": True, + "action": "refused-managed-runtime-account", + "ok": False, + "expectedProxyName": payload["name"], + "valuesPrinted": False, + }) + continue + proxy, proxy_action = ensure_manual_proxy(token, payload) + proxy_id = proxy.get("id") if isinstance(proxy, dict) else None + if proxy_id is None: + raise RuntimeError(f"proxy {payload['name']} has no id") + action = "unchanged" + if account.get("proxy_id") != proxy_id: + updated = ensure_success(curl_api("PUT", f"/api/v1/admin/accounts/{account['id']}", bearer=token, payload={"proxy_id": proxy_id}), f"bind manual account proxy {name}") + account = updated if isinstance(updated, dict) else account + action = "bound" + runtime_proxy = account.get("proxy") if isinstance(account.get("proxy"), dict) else None + items.append({ + "accountName": name, + "accountId": account.get("id"), + "enabled": True, + "action": action, + "proxyAction": proxy_action, + "ok": account.get("proxy_id") == proxy_id, + "expectedProxyName": payload["name"], + "expectedProtocol": payload["protocol"], + "expectedHost": payload["host"], + "expectedPort": payload["port"], + "proxyId": proxy_id, + "runtimeProxyId": account.get("proxy_id"), + "runtimeProxyName": runtime_proxy.get("name") if isinstance(runtime_proxy, dict) else None, + "bindingAligned": account.get("proxy_id") == proxy_id, + "controlPolicy": "manual-protected: only proxy_id binding is YAML-controlled; credentials/status/schedulable are untouched", + "valuesPrinted": False, + }) + return { + "ok": all(item.get("ok") is True for item in items), + "itemCount": len(items), + "items": items, + "valuesPrinted": False, + } + def manual_account_protection_status(token): items = [] desired_names = set(EXPECTED_ACCOUNT_CAPACITIES.keys()) @@ -4477,6 +4700,7 @@ def manual_account_protection_status(token): continue account = find_account_by_name(token, name) extra = account.get("extra") if isinstance(account, dict) and isinstance(account.get("extra"), dict) else {} + proxy_status = manual_proxy_status(token, account, protection) items.append({ "accountName": name, "reason": protection.get("reason") if isinstance(protection.get("reason"), str) else None, @@ -4486,11 +4710,13 @@ def manual_account_protection_status(token): "schedulable": account.get("schedulable") if isinstance(account, dict) else None, "inYamlProfiles": name in desired_names, "runtimeMarkedUnideskManaged": extra.get("unidesk_managed") is True, - "controlPolicy": "manual-protected: no create/update/prune/probe/freeze", + "proxyBinding": proxy_status, + "ok": proxy_status.get("ok") is True, + "controlPolicy": "manual-protected: no create/update/prune/probe/freeze; optional proxy_id binding only when proxyBinding is configured", "valuesPrinted": False, }) return { - "ok": True, + "ok": all(item.get("ok") is True for item in items), "protectedCount": len(items), "items": items, "valuesPrinted": False, @@ -6101,13 +6327,14 @@ def run_sync(): if group_id is None: raise RuntimeError("pool group id missing after ensure") existing_accounts = list_accounts(token) - manual_account_protections = manual_account_protection_status(token) planned_account_results = planned_sentinel_account_results(profiles, existing_accounts) sentinel_quality_prepare = ensure_sentinel_state_for_sync(planned_account_results, True) if sentinel_quality_prepare.get("ok") is not True: raise RuntimeError("prepare sentinel pending probe failed: " + json.dumps(sentinel_quality_prepare, ensure_ascii=False)) protected_frozen_names = active_sentinel_quarantine_names() account_results, pruned_account_results = ensure_accounts(token, profiles, group_id, prune_removed, protected_frozen_names, existing_accounts) + manual_account_proxy_bindings = ensure_manual_account_proxy_bindings(token) + manual_account_protections = manual_account_protection_status(token) capacity_status = account_capacity_status(token) load_factor_status = account_load_factor_status(token) ws_v2_status = account_ws_v2_status(token) @@ -6125,7 +6352,7 @@ def run_sync(): sentinel_quality = ensure_sentinel_state_for_sync(account_results) sentinel_reassert = reassert_sentinel_freezes_after_sync(token) return { - "ok": gateway["ok"] is True and responses_smoke["ok"] is True and owner_concurrency["ok"] is True and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and sentinel.get("ok") is True and sentinel_quality_prepare.get("ok") is True and sentinel_quality.get("ok") is True and sentinel_reassert.get("ok") is True and runtime_capabilities.get("ok") is True, + "ok": gateway["ok"] is True and responses_smoke["ok"] is True and owner_concurrency["ok"] is True and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and manual_account_proxy_bindings.get("ok") is True and manual_account_protections.get("ok") is True and sentinel.get("ok") is True and sentinel_quality_prepare.get("ok") is True and sentinel_quality.get("ok") is True and sentinel_reassert.get("ok") is True and runtime_capabilities.get("ok") is True, "degraded": bool(responses_smoke.get("degraded")) or bool(compact_evidence.get("degraded")) or bool(responses_evidence.get("degraded")) or runtime_capabilities.get("ok") is not True, "mode": "sync", "namespace": NAMESPACE, @@ -6144,7 +6371,7 @@ def run_sync(): "processControl": {"schedulableRestore": "sentinel marker probe only; sync does not restore schedulable for existing accounts", "durableConfig": False}, "valuesPrinted": False, }, - "manualAccounts": manual_account_protections, + "manualAccounts": {**manual_account_protections, "proxySync": manual_account_proxy_bindings}, "capacity": capacity_status, "loadFactor": load_factor_status, "webSocketsV2": ws_v2_status, @@ -6191,7 +6418,7 @@ def run_validate(): runtime_capabilities = validate_runtime_capabilities(token) sentinel = sentinel_runtime_status() return { - "ok": gateway["ok"] is True and responses_smoke["ok"] is True and (owner_concurrency is None or owner_concurrency["ok"] is True) and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and sentinel.get("ok") is True and runtime_capabilities.get("ok") is True, + "ok": gateway["ok"] is True and responses_smoke["ok"] is True and (owner_concurrency is None or owner_concurrency["ok"] is True) and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and manual_account_protections.get("ok") is True and sentinel.get("ok") is True and runtime_capabilities.get("ok") is True, "degraded": bool(responses_smoke.get("degraded")) or bool(compact_evidence.get("degraded")) or bool(responses_evidence.get("degraded")) or runtime_capabilities.get("ok") is not True, "mode": "validate", "namespace": NAMESPACE,