From dabadffb992bf85a3305de26402302e4fa7ab32f Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 14 Jun 2026 17:07:35 +0000 Subject: [PATCH] refactor: name sub2api manual binding sources in yaml --- config/platform-infra/sub2api-codex-pool.yaml | 15 +- scripts/src/platform-infra-sub2api-codex.ts | 241 +++++++++++++++--- 2 files changed, 221 insertions(+), 35 deletions(-) diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index 336eb6f9..e0125497 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -137,16 +137,27 @@ profiles: configFile: config.toml.socap authFile: auth.json.socap manualAccounts: + bindingSources: + active-target-egress-proxy: + enabled: true + kind: proxy + provider: target-egress-proxy + description: Bind a protected manual account to the selected target's YAML-declared egress proxy. + unified-pool-group: + enabled: true + kind: group + provider: pool-group + description: Attach a protected manual account to the unified Codex pool group. 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 + source: active-target-egress-proxy proxyName: platform-infra-sub2api-egress-proxy groupBinding: enabled: true - source: pool-group + source: unified-pool-group 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 0fbec7e9..a874207c 100644 --- a/scripts/src/platform-infra-sub2api-codex.ts +++ b/scripts/src/platform-infra-sub2api-codex.ts @@ -156,18 +156,35 @@ interface CodexPoolConfig { } interface CodexPoolManualAccountsConfig { + bindingSources: CodexPoolManualBindingSourcesConfig; protected: CodexPoolManualAccountProtection[]; } +type CodexPoolManualBindingKind = "proxy" | "group"; +type CodexPoolManualBindingProvider = "target-egress-proxy" | "pool-group"; + +interface CodexPoolManualBindingSource { + id: string; + enabled: boolean; + kind: CodexPoolManualBindingKind; + provider: CodexPoolManualBindingProvider; + description: string | null; +} + +interface CodexPoolManualBindingSourcesConfig { + items: CodexPoolManualBindingSource[]; + byId: Record; +} + interface CodexPoolManualAccountProxyBinding { enabled: boolean; - source: "target-egress-proxy"; + source: string; proxyName: string; } interface CodexPoolManualAccountGroupBinding { enabled: boolean; - source: "pool-group"; + source: string; } interface CodexPoolManualAccountProtection { @@ -905,6 +922,10 @@ async function codexPoolSync(config: UniDeskConfig, options: SyncOptions): Promi defaultAccountLoadFactor: pool.defaultAccountLoadFactor, defaultSentinelProtect: pool.defaultSentinelProtect, }, + manualAccounts: { + bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan), + protected: resolvedManualAccountProtections(pool, runtimeTarget), + }, profiles: profiles.map((profile) => ({ profile: profile.profile, accountName: profile.accountName, @@ -1514,6 +1535,7 @@ function desiredProfileCapacityTotal(profiles: CodexPoolProfileConfig[], default function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig { if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.manualAccounts must be a YAML object`); + const bindingSources = readManualBindingSources(value.bindingSources, "manualAccounts.bindingSources"); const protectedRaw = value.protected; if (!Array.isArray(protectedRaw)) throw new Error(`${codexPoolConfigPath}.manualAccounts.protected must be a YAML array`); const seen = new Set(); @@ -1529,20 +1551,46 @@ function readManualAccountsConfig(value: unknown): CodexPoolManualAccountsConfig 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 proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`) : null; - const groupBinding = isRecord(entry) ? readManualAccountGroupBinding(entry.groupBinding, `${key}.groupBinding`) : 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 { protected: protectedAccounts }; + return { bindingSources, protected: protectedAccounts }; } -function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolManualAccountProxyBinding | null { +function readManualBindingSources(value: unknown, key: string): CodexPoolManualBindingSourcesConfig { + if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${key} must be a YAML object`); + const items: CodexPoolManualBindingSource[] = []; + const byId: Record = {}; + for (const [id, rawSource] of Object.entries(value)) { + const path = `${key}.${id}`; + validateManualBindingSourceId(id, path); + if (!isRecord(rawSource)) throw new Error(`${codexPoolConfigPath}.${path} must be a YAML object`); + const source: CodexPoolManualBindingSource = { + id, + enabled: readBooleanConfig(rawSource.enabled, `${path}.enabled`), + kind: readManualBindingKind(rawSource.kind, `${path}.kind`), + provider: readManualBindingProvider(rawSource.provider, `${path}.provider`), + description: readManualAccountReason(rawSource.description, `${path}.description`), + }; + if (source.kind === "proxy" && source.provider !== "target-egress-proxy") { + throw new Error(`${codexPoolConfigPath}.${path}.provider must be target-egress-proxy for kind=proxy`); + } + if (source.kind === "group" && source.provider !== "pool-group") { + throw new Error(`${codexPoolConfigPath}.${path}.provider must be pool-group for kind=group`); + } + byId[id] = source; + items.push(source); + } + if (items.length === 0) throw new Error(`${codexPoolConfigPath}.${key} must declare at least one source`); + return { items, byId }; +} + +function readManualAccountProxyBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): 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); - if (source === null) throw new Error(`${codexPoolConfigPath}.${key}.source is required`); - if (source !== "target-egress-proxy") throw new Error(`${codexPoolConfigPath}.${key}.source must be target-egress-proxy`); + const enabled = readBooleanConfig(value.enabled, `${key}.enabled`); + const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "proxy", enabled); const proxyName = stringValue(value.proxyName); if (proxyName === null || proxyName.trim().length === 0) throw new Error(`${codexPoolConfigPath}.${key}.proxyName is required`); validateProxyName(proxyName, `${key}.proxyName`); @@ -1553,19 +1601,44 @@ function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolMa }; } -function readManualAccountGroupBinding(value: unknown, key: string): CodexPoolManualAccountGroupBinding | null { +function readManualAccountGroupBinding(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig): CodexPoolManualAccountGroupBinding | 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); - if (source === null) throw new Error(`${codexPoolConfigPath}.${key}.source is required`); - if (source !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key}.source must be pool-group`); + const enabled = readBooleanConfig(value.enabled, `${key}.enabled`); + const source = readManualBindingSourceRef(value.source, `${key}.source`, bindingSources, "group", enabled); return { enabled, source, }; } +function readManualBindingSourceRef(value: unknown, key: string, bindingSources: CodexPoolManualBindingSourcesConfig, expectedKind: CodexPoolManualBindingKind, bindingEnabled: boolean): string { + const sourceId = stringValue(value); + if (sourceId === null) throw new Error(`${codexPoolConfigPath}.${key} is required`); + validateManualBindingSourceId(sourceId, key); + const source = bindingSources.byId[sourceId]; + if (source === undefined) throw new Error(`${codexPoolConfigPath}.${key} references unknown manualAccounts.bindingSources id ${sourceId}`); + if (source.kind !== expectedKind) throw new Error(`${codexPoolConfigPath}.${key} references ${sourceId} with kind=${source.kind}; expected ${expectedKind}`); + if (bindingEnabled && !source.enabled) throw new Error(`${codexPoolConfigPath}.${key} references disabled manualAccounts.bindingSources.${sourceId}`); + return sourceId; +} + +function readManualBindingKind(value: unknown, key: string): CodexPoolManualBindingKind { + const text = stringValue(value); + if (text !== "proxy" && text !== "group") throw new Error(`${codexPoolConfigPath}.${key} must be proxy or group`); + return text; +} + +function readManualBindingProvider(value: unknown, key: string): CodexPoolManualBindingProvider { + const text = stringValue(value); + if (text !== "target-egress-proxy" && text !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key} must be target-egress-proxy or pool-group`); + return text; +} + +function validateManualBindingSourceId(value: string, key: string): void { + if (!/^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported source id format`); +} + function readManualAccountName(value: unknown, key: string): string | null { const text = stringValue(value)?.trim() ?? null; if (text === null || text.length === 0) return null; @@ -2079,6 +2152,7 @@ function codexPoolConfigSummary(pool: CodexPoolConfig, target: CodexPoolRuntimeT defaultSentinelProtect: pool.defaultSentinelProtect, profileCount: pool.profiles.length, manualAccounts: { + bindingSources: pool.manualAccounts.bindingSources.items.map(manualBindingSourcePlan), protectedCount: pool.manualAccounts.protected.length, 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", @@ -3078,6 +3152,72 @@ function sentinelProfileSecrets(profiles: CodexProfile[]): CodexPoolSentinelProf })); } +function resolvedManualAccountProtections(pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record[] { + return pool.manualAccounts.protected.map((account) => ({ + accountName: account.accountName, + reason: account.reason, + proxyBinding: resolveManualProxyBinding(account.proxyBinding, pool, target), + groupBinding: resolveManualGroupBinding(account.groupBinding, pool), + })); +} + +function resolveManualProxyBinding(binding: CodexPoolManualAccountProxyBinding | null, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): Record | null { + if (binding === null) return null; + const source = manualBindingSource(pool, binding.source); + if (source.kind !== "proxy") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=proxy`); + if (binding.enabled && source.provider === "target-egress-proxy" && (target.egressProxy === null || target.egressProxy.enabled !== true)) { + throw new Error(`${codexPoolConfigPath}.manualAccounts.bindingSources.${source.id} requires ${sub2apiConfigPath}.targets[${target.id}].egressProxy.enabled=true`); + } + return { + enabled: binding.enabled, + source: source.id, + proxyName: binding.proxyName, + sourcePlan: { + ...manualBindingSourcePlan(source), + targetEgressProxy: source.provider === "target-egress-proxy" && target.egressProxy !== null + ? { + targetId: target.id, + namespace: target.namespace, + serviceName: target.egressProxy.serviceName, + listenPort: target.egressProxy.listenPort, + httpProxy: target.egressProxy.httpProxy, + noProxy: target.egressProxy.noProxy, + } + : null, + }, + }; +} + +function resolveManualGroupBinding(binding: CodexPoolManualAccountGroupBinding | null, pool: CodexPoolConfig): Record | null { + if (binding === null) return null; + const source = manualBindingSource(pool, binding.source); + if (source.kind !== "group") throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${source.id} must have kind=group`); + return { + enabled: binding.enabled, + source: source.id, + sourcePlan: { + ...manualBindingSourcePlan(source), + poolGroupName: source.provider === "pool-group" ? pool.groupName : null, + }, + }; +} + +function manualBindingSource(pool: CodexPoolConfig, sourceId: string): CodexPoolManualBindingSource { + const source = pool.manualAccounts.bindingSources.byId[sourceId]; + if (source === undefined) throw new Error(`${codexPoolConfigPath}.manualAccounts binding source ${sourceId} is not declared`); + return source; +} + +function manualBindingSourcePlan(source: CodexPoolManualBindingSource): Record { + return { + id: source.id, + enabled: source.enabled, + kind: source.kind, + provider: source.provider, + description: source.description, + }; +} + function targetPublicExposureSummary(target: CodexPoolRuntimeTarget): Record | null { const exposure = target.publicExposure; if (exposure === null) return null; @@ -4160,7 +4300,7 @@ EXPECTED_ACCOUNT_CAPACITIES = ${JSON.stringify(desiredAccountCapacityMap(pool))} EXPECTED_ACCOUNT_LOAD_FACTORS = ${JSON.stringify(desiredAccountLoadFactorMap(pool))} EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAccountWebSocketsV2ModeMap(pool)))}) EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredAccountTempUnschedulableMap(pool)))}) -MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(pool.manualAccounts.protected))}) +MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(resolvedManualAccountProtections(pool, target)))}) SENTINEL_CONFIG = json.loads(${JSON.stringify(JSON.stringify(pool.sentinel))}) TARGET_EGRESS_PROXY = json.loads(${JSON.stringify(JSON.stringify(target.egressProxy))}) MODE = "${mode}" @@ -4513,16 +4653,31 @@ def find_proxy_by_name(token, name): return item return None +def manual_binding_plan(binding, expected_kind): + if not isinstance(binding, dict): + return None + plan = binding.get("sourcePlan") + if not isinstance(plan, dict): + raise RuntimeError("manual account binding is missing resolved sourcePlan") + if plan.get("enabled") is not True: + raise RuntimeError(f"manual account binding source {plan.get('id')} is disabled") + if plan.get("kind") != expected_kind: + raise RuntimeError(f"manual account binding source {plan.get('id')} kind must be {expected_kind}") + return plan + 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") + plan = manual_binding_plan(binding, "proxy") + if plan.get("provider") != "target-egress-proxy": + raise RuntimeError(f"manual account proxyBinding source {plan.get('id')} uses unsupported provider {plan.get('provider')}") + target_proxy = plan.get("targetEgressProxy") + if not isinstance(target_proxy, dict): + raise RuntimeError(f"manual account proxyBinding source {plan.get('id')} requires an enabled target egressProxy") + service_name = target_proxy.get("serviceName") + listen_port = target_proxy.get("listenPort") + namespace = target_proxy.get("namespace") if isinstance(target_proxy.get("namespace"), str) and target_proxy.get("namespace") else NAMESPACE proxy_name = binding.get("proxyName") if not isinstance(service_name, str) or not service_name: raise RuntimeError("target egressProxy serviceName is missing") @@ -4533,7 +4688,7 @@ def desired_manual_proxy_payload(protection): return { "name": proxy_name, "protocol": "http", - "host": f"{service_name}.{NAMESPACE}.svc.cluster.local", + "host": f"{service_name}.{namespace}.svc.cluster.local", "port": listen_port, "fallback_mode": "none", "expiry_warn_days": 0, @@ -4568,6 +4723,8 @@ def manual_proxy_status(token, account, protection): "action": "not-configured", "valuesPrinted": False, } + binding = protection.get("proxyBinding") if isinstance(protection, dict) else None + plan = manual_binding_plan(binding, "proxy") 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 = ( @@ -4579,7 +4736,8 @@ def manual_proxy_status(token, account, protection): "enabled": True, "ok": binding_aligned, "action": "validate", - "source": "target-egress-proxy", + "source": plan.get("id"), + "sourceProvider": plan.get("provider"), "expectedProxyName": payload["name"], "expectedProtocol": payload["protocol"], "expectedHost": payload["host"], @@ -4634,6 +4792,8 @@ def ensure_manual_account_proxy_bindings(token): }) continue proxy, proxy_action = ensure_manual_proxy(token, payload) + binding = protection.get("proxyBinding") if isinstance(protection, dict) else None + plan = manual_binding_plan(binding, "proxy") 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") @@ -4649,6 +4809,8 @@ def ensure_manual_account_proxy_bindings(token): "enabled": True, "action": action, "proxyAction": proxy_action, + "source": plan.get("id"), + "sourceProvider": plan.get("provider"), "ok": account.get("proxy_id") == proxy_id, "expectedProxyName": payload["name"], "expectedProtocol": payload["protocol"], @@ -4668,17 +4830,21 @@ def ensure_manual_account_proxy_bindings(token): "valuesPrinted": False, } -def manual_group_binding_enabled(protection): +def manual_group_binding_plan(protection): binding = protection.get("groupBinding") if isinstance(protection, dict) else None if not isinstance(binding, dict) or binding.get("enabled") is not True: - return False - if binding.get("source") != "pool-group": - raise RuntimeError("manual account groupBinding source must be pool-group") - return True + return None + plan = manual_binding_plan(binding, "group") + if plan.get("provider") != "pool-group": + raise RuntimeError(f"manual account groupBinding source {plan.get('id')} uses unsupported provider {plan.get('provider')}") + return plan + +def manual_group_binding_enabled(protection): + return manual_group_binding_plan(protection) is not None def manual_group_status(token, account, protection, group_id): - enabled = manual_group_binding_enabled(protection) - if not enabled: + plan = manual_group_binding_plan(protection) + if plan is None: return { "enabled": False, "ok": True, @@ -4697,7 +4863,8 @@ def manual_group_status(token, account, protection, group_id): "enabled": True, "ok": binding_aligned, "action": "validate", - "source": "pool-group", + "source": plan.get("id"), + "sourceProvider": plan.get("provider"), "poolGroupName": POOL_GROUP_NAME, "poolGroupId": group_id, "bindingAligned": binding_aligned, @@ -4721,6 +4888,7 @@ def ensure_manual_account_group_bindings(token, group_id): "valuesPrinted": False, }) continue + plan = manual_group_binding_plan(protection) account = find_account_by_name(token, name) if not isinstance(account, dict): items.append({ @@ -4764,7 +4932,8 @@ def ensure_manual_account_group_bindings(token, group_id): "enabled": True, "ok": binding_aligned, "action": action, - "source": "pool-group", + "source": plan.get("id") if isinstance(plan, dict) else None, + "sourceProvider": plan.get("provider") if isinstance(plan, dict) else None, "poolGroupName": POOL_GROUP_NAME, "poolGroupId": group_id, "previousGroupIds": existing_group_ids, @@ -6408,7 +6577,13 @@ def api_key_preview(api_key): return api_key[:10] + "..." + api_key[-4:] def run_sync(): + global MANUAL_ACCOUNT_PROTECTIONS payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8")) + manual_accounts_payload = payload.get("manualAccounts") if isinstance(payload.get("manualAccounts"), dict) else {} + resolved_manual_protections = manual_accounts_payload.get("protected") + if not isinstance(resolved_manual_protections, list): + raise RuntimeError("sync payload has no manualAccounts.protected binding plan") + MANUAL_ACCOUNT_PROTECTIONS = resolved_manual_protections profiles = payload.get("profiles") or [] prune_removed = bool(payload.get("pruneRemoved")) sentinel_payload = payload.get("sentinel") if isinstance(payload.get("sentinel"), dict) else {}