fix: bind protected sub2api manual accounts to pool group

This commit is contained in:
Codex
2026-06-14 14:55:28 +00:00
parent 16e7284bdb
commit d6638655cc
4 changed files with 208 additions and 20 deletions
+193 -13
View File
@@ -164,10 +164,16 @@ interface CodexPoolManualAccountProxyBinding {
proxyName: string;
}
interface CodexPoolManualAccountGroupBinding {
enabled: boolean;
source: "pool-group";
}
interface CodexPoolManualAccountProtection {
accountName: string;
reason: string | null;
proxyBinding: CodexPoolManualAccountProxyBinding | null;
groupBinding: CodexPoolManualAccountGroupBinding | null;
}
interface CodexPoolProfileConfig {
@@ -703,11 +709,11 @@ function codexPoolPlan(options?: DisclosureOptions): Record<string, unknown> {
: runtimeTarget.publicBaseUrl === null
? "Public FRP exposure is disabled by YAML."
: `Legacy Codex-pool FRP exposure is disabled by YAML; Codex consumers for target ${runtimeTarget.id} use target-level public exposure ${consumerBaseUrl}.`,
idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files; managed accounts missing from YAML are preserved unless --prune-removed is explicitly provided.",
idempotency: "sync reuses the group, account names, and k3s Secret when they already exist; credentials are updated from the current local Codex files for YAML-managed profiles only; managed accounts missing from YAML are preserved unless --prune-removed is explicitly provided.",
configPolicy: "UniDesk-owned durable configuration remains YAML-first; local ~/.codex files and runtime Secrets are not committed.",
manualAccountProtection: pool.manualAccounts.protected.length === 0
? "No manual Sub2API accounts are protected by YAML."
: `${pool.manualAccounts.protected.length} manual Sub2API account(s) are protected from UniDesk-managed sync, prune, sentinel probe, and sentinel freeze paths.`,
: `${pool.manualAccounts.protected.length} manual Sub2API account(s) are protected from UniDesk-managed credentials, prune, sentinel probe, and sentinel freeze paths; only explicitly declared proxy/group bindings are reconciled.`,
},
next: ok
? { sync: `bun scripts/cli.ts platform-infra sub2api codex-pool sync${targetFlag(runtimeTarget)} --confirm` }
@@ -1520,7 +1526,8 @@ function readManualAccountsConfig(value: unknown, defaults: CodexPoolManualAccou
seen.add(normalized);
const reason = isRecord(entry) ? readManualAccountReason(entry.reason, `${key}.reason`) : null;
const proxyBinding = isRecord(entry) ? readManualAccountProxyBinding(entry.proxyBinding, `${key}.proxyBinding`) : null;
return { accountName, reason, proxyBinding };
const groupBinding = isRecord(entry) ? readManualAccountGroupBinding(entry.groupBinding, `${key}.groupBinding`) : null;
return { accountName, reason, proxyBinding, groupBinding };
});
return { protected: protectedAccounts };
}
@@ -1541,6 +1548,18 @@ function readManualAccountProxyBinding(value: unknown, key: string): CodexPoolMa
};
}
function readManualAccountGroupBinding(value: unknown, key: string): 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) ?? "pool-group";
if (source !== "pool-group") throw new Error(`${codexPoolConfigPath}.${key}.source must be pool-group`);
return {
enabled,
source,
};
}
function readManualAccountName(value: unknown, key: string): string | null {
const text = stringValue(value)?.trim() ?? null;
if (text === null || text.length === 0) return null;
@@ -2051,7 +2070,7 @@ function codexPoolConfigSummary(pool: CodexPoolConfig): Record<string, unknown>
manualAccounts: {
protectedCount: pool.manualAccounts.protected.length,
protected: pool.manualAccounts.protected,
controlPolicy: "manual accounts are not created, updated, pruned, probed, or frozen by UniDesk codex-pool sync/sentinel",
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: publicExposureSummary(pool),
localCodex: pool.localCodex,
@@ -2158,6 +2177,7 @@ function compactManualAccounts(block: unknown): Record<string, unknown> | null {
"inYamlProfiles",
"runtimeMarkedUnideskManaged",
"proxyBinding",
"groupBinding",
"controlPolicy",
]));
const proxySync = isRecord(block.proxySync)
@@ -2181,11 +2201,31 @@ function compactManualAccounts(block: unknown): Record<string, unknown> | null {
valuesPrinted: false,
}
: undefined;
const groupSync = isRecord(block.groupSync)
? {
ok: block.groupSync.ok,
itemCount: block.groupSync.itemCount,
items: recordArray(block.groupSync.items).map((item) => pickSummaryFields(item, [
"accountName",
"accountId",
"enabled",
"ok",
"action",
"source",
"poolGroupName",
"poolGroupId",
"bindingAligned",
"controlPolicy",
])),
valuesPrinted: false,
}
: undefined;
return {
ok: block.ok,
protectedCount: block.protectedCount,
items,
proxySync,
groupSync,
valuesPrinted: false,
};
}
@@ -4494,9 +4534,12 @@ def group_payload():
"rpm_limit": 0,
}
def list_groups(token):
data = ensure_success(curl_api("GET", "/api/v1/admin/groups/all?platform=openai", bearer=token), "list groups")
return extract_items(data)
def ensure_group(token):
existing_data = ensure_success(curl_api("GET", "/api/v1/admin/groups/all?platform=openai", bearer=token), "list groups")
existing = next((item for item in extract_items(existing_data) if item.get("name") == POOL_GROUP_NAME), None)
existing = next((item for item in list_groups(token) if item.get("name") == POOL_GROUP_NAME), None)
payload = group_payload()
if existing is None:
created = ensure_success(curl_api("POST", "/api/v1/admin/groups", bearer=token, payload=payload), "create group")
@@ -4508,6 +4551,26 @@ def ensure_group(token):
updated = ensure_success(curl_api("PUT", f"/api/v1/admin/groups/{group_id}", bearer=token, payload=payload), "update group")
return updated if isinstance(updated, dict) else existing, "updated"
def list_accounts_for_group(token, group_id):
path = f"/api/v1/admin/accounts?group_id={group_id}&page=1&page_size=500&platform=openai"
data = ensure_success(curl_api("GET", path, bearer=token), f"list accounts for group {group_id}")
return extract_items(data)
def account_group_ids(token, account):
if not isinstance(account, dict) or account.get("id") is None:
return []
account_id = account.get("id")
account_name = account.get("name")
ids = []
for group in list_groups(token):
group_id = group.get("id") if isinstance(group, dict) else None
if group_id is None:
continue
members = list_accounts_for_group(token, group_id)
if any(item.get("id") == account_id or item.get("name") == account_name for item in members if isinstance(item, dict)):
ids.append(group_id)
return sorted(set(ids))
def list_accounts(token):
path = "/api/v1/admin/accounts?page=1&page_size=200&platform=openai&type=apikey&search=" + quote("unidesk-codex-")
data = ensure_success(curl_api("GET", path, bearer=token), "list accounts")
@@ -4689,7 +4752,119 @@ def ensure_manual_account_proxy_bindings(token):
"valuesPrinted": False,
}
def manual_account_protection_status(token):
def manual_group_binding_enabled(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
def manual_group_status(token, account, protection, group_id):
enabled = manual_group_binding_enabled(protection)
if not enabled:
return {
"enabled": False,
"ok": True,
"action": "not-configured",
"valuesPrinted": False,
}
group_accounts = list_accounts_for_group(token, group_id)
account_id = account.get("id") if isinstance(account, dict) else None
account_name = account.get("name") if isinstance(account, dict) else None
binding_aligned = any(
item.get("id") == account_id or item.get("name") == account_name
for item in group_accounts
if isinstance(item, dict)
)
return {
"enabled": True,
"ok": binding_aligned,
"action": "validate",
"source": "pool-group",
"poolGroupName": POOL_GROUP_NAME,
"poolGroupId": group_id,
"bindingAligned": binding_aligned,
"valuesPrinted": False,
}
def ensure_manual_account_group_bindings(token, group_id):
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
if not manual_group_binding_enabled(protection):
items.append({
"accountName": name,
"enabled": False,
"action": "not-configured",
"ok": True,
"valuesPrinted": False,
})
continue
account = find_account_by_name(token, name)
if not isinstance(account, dict):
items.append({
"accountName": name,
"enabled": True,
"action": "account-missing",
"ok": False,
"poolGroupName": POOL_GROUP_NAME,
"poolGroupId": group_id,
"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,
"poolGroupName": POOL_GROUP_NAME,
"poolGroupId": group_id,
"valuesPrinted": False,
})
continue
existing_group_ids = account_group_ids(token, account)
desired_group_ids = sorted(set(existing_group_ids + [group_id]))
action = "unchanged"
if group_id not in existing_group_ids:
updated = ensure_success(curl_api("PUT", f"/api/v1/admin/accounts/{account['id']}", bearer=token, payload={"group_ids": desired_group_ids}), f"bind manual account group {name}")
account = updated if isinstance(updated, dict) else account
action = "bound"
binding_aligned = any(
item.get("id") == account.get("id") or item.get("name") == name
for item in list_accounts_for_group(token, group_id)
if isinstance(item, dict)
)
items.append({
"accountName": name,
"accountId": account.get("id"),
"enabled": True,
"ok": binding_aligned,
"action": action,
"source": "pool-group",
"poolGroupName": POOL_GROUP_NAME,
"poolGroupId": group_id,
"previousGroupIds": existing_group_ids,
"desiredGroupIds": desired_group_ids,
"bindingAligned": binding_aligned,
"controlPolicy": "manual-protected: only pool group membership is YAML-controlled; credentials/status/schedulable are untouched and sentinel does not probe it",
"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, group_id=None):
items = []
desired_names = set(EXPECTED_ACCOUNT_CAPACITIES.keys())
for protection in MANUAL_ACCOUNT_PROTECTIONS:
@@ -4701,6 +4876,7 @@ def manual_account_protection_status(token):
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)
group_status = manual_group_status(token, account, protection, group_id) if group_id is not None else {"enabled": False, "ok": True, "action": "not-checked", "valuesPrinted": False}
items.append({
"accountName": name,
"reason": protection.get("reason") if isinstance(protection.get("reason"), str) else None,
@@ -4711,8 +4887,9 @@ def manual_account_protection_status(token):
"inYamlProfiles": name in desired_names,
"runtimeMarkedUnideskManaged": extra.get("unidesk_managed") is True,
"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",
"groupBinding": group_status,
"ok": proxy_status.get("ok") is True and group_status.get("ok") is True,
"controlPolicy": "manual-protected: no create/update/prune/probe/freeze; optional proxy_id and pool group membership binding only when configured",
"valuesPrinted": False,
})
return {
@@ -6334,7 +6511,8 @@ def run_sync():
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)
manual_account_group_bindings = ensure_manual_account_group_bindings(token, group_id)
manual_account_protections = manual_account_protection_status(token, group_id)
capacity_status = account_capacity_status(token)
load_factor_status = account_load_factor_status(token)
ws_v2_status = account_ws_v2_status(token)
@@ -6352,7 +6530,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 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,
"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_group_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,
@@ -6371,7 +6549,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, "proxySync": manual_account_proxy_bindings},
"manualAccounts": {**manual_account_protections, "proxySync": manual_account_proxy_bindings, "groupSync": manual_account_group_bindings},
"capacity": capacity_status,
"loadFactor": load_factor_status,
"webSocketsV2": ws_v2_status,
@@ -6410,7 +6588,8 @@ def run_validate():
load_factor_status = account_load_factor_status(token)
ws_v2_status = account_ws_v2_status(token)
temp_unschedulable_status = account_temp_unschedulable_status(token)
manual_account_protections = manual_account_protection_status(token)
pool_group_id = key_item.get("group_id") if isinstance(key_item, dict) else None
manual_account_protections = manual_account_protection_status(token, pool_group_id)
gateway = validate_gateway(api_key)
responses_smoke = validate_gateway_responses(api_key)
compact_evidence = recent_compact_gateway_evidence()
@@ -6429,6 +6608,7 @@ def run_validate():
"secret": f"{NAMESPACE}/{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY}",
"sub2apiId": key_item.get("id") if isinstance(key_item, dict) else None,
"userId": key_item.get("user_id") if isinstance(key_item, dict) else None,
"groupId": key_item.get("group_id") if isinstance(key_item, dict) else None,
"keyPreview": api_key_preview(api_key),
"valuesPrinted": False,
},