feat: 自动选择已配置的 runtime 策略账号

This commit is contained in:
Codex
2026-07-16 08:07:33 +02:00
parent f612f55902
commit d36eef5370
8 changed files with 242 additions and 19 deletions
@@ -65,7 +65,12 @@
- 错误列表没有账号全部路由尝试数时,命令只报告错误数量,不伪造错误率;
- Ops 记录与 runtime 日志是独立证据,命令明确披露采样数量,不按日志位置强行关联;
- `runtime apply|delete` 支持 `temp-unschedulable``model-mapping` 两类 runtime 配置;
- 单账号必须显式使用 `--account`,精准批量必须显式使用 `--accounts <id-or-exact-name,...>`,不提供隐式 `all`
- 单账号必须显式使用 `--account`已知精准批量必须显式使用 `--accounts <id-or-exact-name,...>`,不提供隐式 `all`
- 更新已配置同类 runtime 策略的手工账号时,可显式使用 `--select-configured-policy --account-type apikey` 自动选集,并用 `--exclude-accounts <id-or-exact-name,...>` 排除例外:
- 该入口只选择当前 credentials 已存在临时不可调度字段的账号,不把未配置账号自动纳入;
- 不以 `matchingTemplate` 作为更新选集,因为模板内容变化后旧 runtime 不再与新 YAML 精确相等;
- dry-run 必须披露候选数、已配置数、类型匹配数、排除账号、最终名称/ID 和选中来源;
- confirm 继续复用批量预检、顺序写入和自动回读对账;
- 批量操作先校验全部 selector 存在且账号类型可修改,再顺序调用 Sub2API admin API
- 选择错误不会产生部分写入;
- 上游 admin API 在批次中途失败时仍需逐账号 `get` 对账;
@@ -94,6 +99,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <name-or-id> --template <template-id> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --template <template-id>
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --template <template-id> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --template <template-id> --select-configured-policy --account-type apikey --exclude-accounts <id-or-exact-name,...>
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --kind priority --priority <0-1000> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <id-or-exact-name> --kind account-name --name <new-name> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account <name-or-id> --kind temp-unschedulable
@@ -0,0 +1,68 @@
# R4 任务报告
## 目标
- 关联 [UniDesk #2281](https://github.com/pikasTech/unidesk/issues/2281)。
- 将模板更新流程从以下三步:
- `runtime list`
- 人工抄写账号 ID
- `runtime apply --accounts ...`
- 收敛为一个受控 dry-run 命令。
## 实现
- 新增显式自动选集参数:
- `--select-configured-policy`
- `--account-type apikey`
- `--exclude-accounts <id-or-exact-name,...>`
- 自动选集只覆盖当前 credentials 已存在临时不可调度字段的账号:
- 不把未配置账号隐式纳入;
- 不提供隐式 `all`
- 不带 `--confirm` 时保持 dry-run
- confirm 复用既有批量预检、顺序写入和自动回读对账。
- 默认 Kubernetes 风格输出新增:
- 候选账号数;
- 已配置策略数;
- 类型匹配数;
- 请求排除数与实际从候选移除数;
- 排除账号名称、ID、selector 和原因;
- 每个最终账号的 `SELECTED_BY`
- 显式 JSON 增加 mutation 专用紧凑投影:
- 保留 request selector、selection provenance、汇总和每账号结果;
- dry-run noop 不重复输出相同的 before、desired、actual 三份策略;
- update 和 confirmed 对账仍保留必要差异。
## 设计修正
- 没有使用 `matchingTemplate` 自动选集:
- owning YAML 模板内容变化后,旧 runtime 与新模板不再精确相等;
- 本次真实运行面在更新前已显示 `TEMPLATE=-`
- 以精确模板匹配选集会错误选中 0 个待更新账号。
- 因此使用“已配置同类策略 + 显式账号类型 + 显式排除”的通用选集,并要求先看 dry-run 最终名单。
## 验证
- 单命令 PK01 dry-run
- `candidateAccounts=11`
- `configuredPolicyMatches=9`
- `accountTypeMatches=9`
- `selected=9`
- `changed=0`
- 9 个账号全部 `noop`
- `mutation=false`
- `--exclude-accounts 15` 明确显示:
- `lyon9801 0.0`
- ID 15
- 原因 `explicit-exclusion`
- 缺少 `--account-type` 时命令在本地 fail-closed
- 非零退出;
- 明确提示 `--select-configured-policy requires --account-type apikey`
- 不访问写入路径。
- 显式 JSON 最终完整返回:
- 未触发 10 KiB 输出保护;
- 保留所有 9 个账号和 selection provenance。
## 边界
- 本任务所有真实 Sub2API 调用均未携带 `--confirm`
- 未修改 Sub2API 源码、版本、runtime、YAML、账号配置或外部哨兵。
@@ -275,3 +275,7 @@
### R3.7 [completed]
在 Sub2API 公告 skill 与 CLI create 预览中固化明确范围规则:补偿公告必须在用户可见正文列出故障或补偿时间窗、全部实际接收用户、每人金额和总额;邮箱允许脱敏但必须保留稳定用户 ID,`targeting` 仅表示投递范围、不能替代补偿资格名单。本次只记录并验证 dry-run 展示,不重新发布或修改既有公告,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R3.7_Task_Report.md)。
## R4 [completed]
关联 [UniDesk #2281](https://github.com/pikasTech/unidesk/issues/2281),增强 Sub2API runtime 模板批量 CRUD:支持通过显式参数按原生 runtime 已配置同类策略和账号类型自动选集,并支持精确排除 selector;dry-run 直接输出最终账号名称、ID、选中来源与排除摘要,confirm 复用既有写后自动回读对账,避免先 list、人工抄账号 ID、再 apply 的重复调用;显式 JSON 使用紧凑 mutation 投影保留 provenance 且不触发输出保护,不修改 Sub2API 源码、版本、runtime、YAML 或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R4_Task_Report.md)。
@@ -5,6 +5,7 @@ import type { RuntimeOptions } from "./runtime-options";
export function renderRuntimeMutationResult(ok: boolean, options: RuntimeOptions, parsed: Record<string, unknown> | null, exitCode: number): RenderedCliResult {
const summary = parsed !== null && isRecord(parsed.summary) ? parsed.summary : {};
const selection = parsed !== null && isRecord(parsed.selection) ? parsed.selection : {};
const items = parsed !== null && Array.isArray(parsed.items) ? parsed.items.filter(isRecord) : [];
const lines = [[
"SUB2API RUNTIME", `operation=${options.action}`, `mode=${stringValue(parsed?.mode) ?? (options.confirm ? "confirmed" : "dry-run")}`,
@@ -12,6 +13,22 @@ export function renderRuntimeMutationResult(ok: boolean, options: RuntimeOptions
`writeSucceeded=${summary.writeSucceeded ?? "?"}`, `writeFailed=${summary.writeFailed ?? "?"}`,
`reconciled=${summary.reconciled ?? "?"}`, `mismatched=${summary.mismatched ?? "?"}`,
].join(" ")];
if (typeof selection.mode === "string") {
lines.push([
"SELECTION", `mode=${selection.mode}`, `accountType=${selection.accountType ?? "-"}`,
`candidates=${selection.candidateAccounts ?? selection.requestedSelectors ?? "?"}`,
`configured=${selection.configuredPolicyMatches ?? "-"}`, `typeMatched=${selection.accountTypeMatches ?? "-"}`,
`excludeRequested=${selection.explicitExclusionsRequested ?? 0}`, `excludedSelected=${selection.explicitlyExcluded ?? 0}`,
].join(" "));
}
const excludedAccounts = Array.isArray(selection.excludedAccounts) ? selection.excludedAccounts.filter(isRecord) : [];
if (excludedAccounts.length > 0) lines.push(renderTable([
["EXCLUDED_ACCOUNT", "ID", "SELECTOR", "REASON"],
...excludedAccounts.map((item) => [
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"),
stringValue(item.selector) ?? "-", stringValue(item.reason) ?? "-",
]),
]));
if (items.length > 0) {
const fields = items.map((item) => ({
item,
@@ -21,29 +38,29 @@ export function renderRuntimeMutationResult(ok: boolean, options: RuntimeOptions
actual: isRecord(item.actual) ? item.actual : {},
}));
if (options.kind === "priority") lines.push(renderTable([
["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE_PRIORITY", "DESIRED_PRIORITY", "ACTUAL_PRIORITY", "FAILURE"],
["ACCOUNT", "ID", "SELECTED_BY", "CHANGE", "WRITE", "RECONCILED", "BEFORE_PRIORITY", "DESIRED_PRIORITY", "ACTUAL_PRIORITY", "FAILURE"],
...fields.map(({ item, write, before, desired, actual }) => [
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.change) ?? "-",
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.selectionReason) ?? "-", stringValue(item.change) ?? "-",
stringValue(write.status) ?? "-", item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-",
String(before.priority ?? "-"), String(desired.priority ?? "-"), String(actual.priority ?? "-"),
stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-",
]),
]));
else if (options.kind === "account-name") lines.push(renderTable([
["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE_NAME", "DESIRED_NAME", "ACTUAL_NAME", "FAILURE"],
["ACCOUNT", "ID", "SELECTED_BY", "CHANGE", "WRITE", "RECONCILED", "BEFORE_NAME", "DESIRED_NAME", "ACTUAL_NAME", "FAILURE"],
...fields.map(({ item, write, before, desired, actual }) => [
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.change) ?? "-",
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.selectionReason) ?? "-", stringValue(item.change) ?? "-",
stringValue(write.status) ?? "-", item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-",
stringValue(before.name) ?? "-", stringValue(desired.name) ?? "-", stringValue(actual.name) ?? "-",
stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-",
]),
]));
else lines.push(renderTable([
["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE", "DESIRED", "ACTUAL", "BEFORE_CODES", "DESIRED_CODES", "ACTUAL_CODES", "FAILURE"],
["ACCOUNT", "ID", "SELECTED_BY", "CHANGE", "WRITE", "RECONCILED", "BEFORE", "DESIRED", "ACTUAL", "BEFORE_CODES", "DESIRED_CODES", "ACTUAL_CODES", "FAILURE"],
...fields.map(({ item, write, before, desired, actual }) => {
const codes = (value: unknown): string => Array.isArray(value) ? value.join(",") || "-" : "-";
return [
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.change) ?? "-",
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.selectionReason) ?? "-", stringValue(item.change) ?? "-",
stringValue(write.status) ?? "-", item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-",
String(before.ruleCount ?? "-"), String(desired.ruleCount ?? "-"), String(actual.ruleCount ?? "-"),
codes(before.statusCodes), codes(desired.statusCodes), codes(actual.statusCodes),
@@ -6,6 +6,9 @@ export interface RuntimeOptions {
action: RuntimeAction;
account: string | null;
accounts: string[];
selectConfiguredPolicy: boolean;
accountType: "apikey" | null;
excludeAccounts: string[];
group: string | null;
allGroups: boolean;
platform: string | null;
@@ -26,10 +29,13 @@ export interface RuntimeOptions {
export function parseRuntimeOptions(args: string[]): RuntimeOptions {
const [actionRaw = "list", ...rest] = args;
if (!(["list", "get", "errors", "infrastructure", "apply", "delete"] as string[]).includes(actionRaw)) {
throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable|priority|account-name] [--priority <0-1000>|--name <account-name>] [--confirm] [--target <id>] [--json|--full|--raw]");
throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>|--select-configured-policy --account-type apikey [--exclude-accounts <selectors>]] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable|priority|account-name] [--priority <0-1000>|--name <account-name>] [--confirm] [--target <id>] [--json|--full|--raw]");
}
let account: string | null = null;
let accounts: string[] = [];
let selectConfiguredPolicy = false;
let accountType: "apikey" | null = null;
let excludeAccounts: string[] = [];
let group: string | null = null;
let allGroups = false;
let platform: string | null = null;
@@ -62,6 +68,11 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
else if (arg.startsWith("--account=")) account = arg.slice("--account=".length).trim();
else if (arg === "--accounts") accounts = parseAccountSelectors(readValue("--accounts"));
else if (arg.startsWith("--accounts=")) accounts = parseAccountSelectors(arg.slice("--accounts=".length));
else if (arg === "--select-configured-policy") selectConfiguredPolicy = true;
else if (arg === "--account-type") accountType = parseAccountType(readValue("--account-type"));
else if (arg.startsWith("--account-type=")) accountType = parseAccountType(arg.slice("--account-type=".length));
else if (arg === "--exclude-accounts") excludeAccounts = parseAccountSelectors(readValue("--exclude-accounts"), "--exclude-accounts");
else if (arg.startsWith("--exclude-accounts=")) excludeAccounts = parseAccountSelectors(arg.slice("--exclude-accounts=".length), "--exclude-accounts");
else if (arg === "--group") group = readValue("--group");
else if (arg.startsWith("--group=")) group = arg.slice("--group=".length).trim();
else if (arg === "--all-groups") allGroups = true;
@@ -87,8 +98,11 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
}
const action = actionRaw as RuntimeAction;
if (account !== null && accounts.length > 0) throw new Error("use only one of --account or --accounts");
if (selectConfiguredPolicy && (account !== null || accounts.length > 0)) throw new Error("--select-configured-policy cannot be combined with --account or --accounts");
if ((action === "get" || action === "infrastructure") && !account) throw new Error(`runtime ${action} requires --account <name-or-id>`);
if ((action === "apply" || action === "delete") && account === null && accounts.length === 0) throw new Error(`runtime ${action} requires --account or --accounts`);
if ((action === "apply" || action === "delete") && account === null && accounts.length === 0 && !selectConfiguredPolicy) {
throw new Error(`runtime ${action} requires --account, --accounts, or --select-configured-policy`);
}
if (accounts.length > 0 && action !== "apply" && action !== "delete") throw new Error(`runtime ${action} does not accept --accounts`);
if (account !== null && (account.length > 256 || /[\r\n]/u.test(account))) throw new Error("--account has an unsupported format");
if (group !== null && (group.length > 256 || /[\r\n]/u.test(group))) throw new Error("--group has an unsupported format");
@@ -99,6 +113,12 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
if ((group !== null || platform !== null) && action !== "errors" && action !== "infrastructure") throw new Error(`runtime ${action} does not accept group scope options`);
if (pageToken !== null && !allGroups) throw new Error("--page-token requires --all-groups");
if (account !== null && allGroups) throw new Error("--account cannot be combined with --all-groups; use --group first");
if (selectConfiguredPolicy && !((action === "apply" || action === "delete") && kind === "temp-unschedulable")) {
throw new Error("--select-configured-policy only supports runtime apply|delete --kind temp-unschedulable");
}
if (selectConfiguredPolicy && accountType === null) throw new Error("--select-configured-policy requires --account-type apikey");
if (!selectConfiguredPolicy && accountType !== null) throw new Error("--account-type requires --select-configured-policy");
if (!selectConfiguredPolicy && excludeAccounts.length > 0) throw new Error("--exclude-accounts requires --select-configured-policy");
if (action === "apply" && kind === "temp-unschedulable" && !template) throw new Error("runtime apply --kind temp-unschedulable requires --template <id>");
if (kind === "priority" && action !== "apply") throw new Error("runtime --kind priority only supports apply");
if (kind === "priority" && priority === null) throw new Error("runtime apply --kind priority requires --priority <0-1000>");
@@ -116,17 +136,22 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
if (!/^\d+[mhd]$/u.test(since)) throw new Error("--since must use <number>m, <number>h, or <number>d");
if (["list", "get", "errors", "infrastructure"].includes(action) && confirm) throw new Error(`runtime ${action} does not accept --confirm`);
if ([full, raw, json].filter(Boolean).length > 1) throw new Error("use only one of --json, --full, or --raw");
return { action, account, accounts, group, allGroups, platform, pageToken, template, kind, priority, name, confirm, full, raw, json, since, tail, targetId };
return { action, account, accounts, selectConfiguredPolicy, accountType, excludeAccounts, group, allGroups, platform, pageToken, template, kind, priority, name, confirm, full, raw, json, since, tail, targetId };
}
function parseAccountSelectors(value: string): string[] {
function parseAccountSelectors(value: string, option = "--accounts"): string[] {
const selectors = value.split(",").map((item) => item.trim()).filter(Boolean);
if (selectors.length === 0 || selectors.length > 100) throw new Error("--accounts requires 1 to 100 comma-separated selectors");
if (selectors.some((item) => item.length > 256 || /[\r\n]/u.test(item))) throw new Error("--accounts has an unsupported selector");
if (new Set(selectors).size !== selectors.length) throw new Error("--accounts contains duplicate selectors");
if (selectors.length === 0 || selectors.length > 100) throw new Error(`${option} requires 1 to 100 comma-separated selectors`);
if (selectors.some((item) => item.length > 256 || /[\r\n]/u.test(item))) throw new Error(`${option} has an unsupported selector`);
if (new Set(selectors).size !== selectors.length) throw new Error(`${option} contains duplicate selectors`);
return selectors;
}
function parseAccountType(value: string): "apikey" {
if (value !== "apikey") throw new Error("--account-type currently supports only apikey");
return value;
}
function parseRuntimeKind(value: string): "temp-unschedulable" | "priority" | "account-name" {
if (value !== "temp-unschedulable" && value !== "priority" && value !== "account-name") throw new Error("--kind must be temp-unschedulable, priority, or account-name");
return value;
@@ -1053,6 +1053,50 @@ def observed_runtime_errors(token, accounts, group_id, platform=None):
}
def selected_account_details(token, accounts):
if PAYLOAD.get("selectConfiguredPolicy") is True:
account_type = PAYLOAD.get("accountType")
details = [account_detail(token, item["id"]) for item in accounts]
configured = []
for detail in details:
credentials = detail.get("credentials") if isinstance(detail.get("credentials"), dict) else {}
if "temp_unschedulable_enabled" in credentials or "temp_unschedulable_rules" in credentials:
configured.append(detail)
type_matches = [detail for detail in configured if detail.get("type") == account_type]
excluded_ids = set()
excluded_accounts = []
for raw_selector in PAYLOAD.get("excludeAccounts") or []:
excluded = find_account(token, accounts, str(raw_selector))
account_id = excluded.get("id")
if account_id in excluded_ids:
raise RuntimeError("duplicate-excluded-account-selector: " + str(raw_selector))
excluded_ids.add(account_id)
excluded_accounts.append({
"accountName": excluded.get("name"),
"accountId": account_id,
"selector": str(raw_selector),
"reason": "explicit-exclusion",
})
selected = []
for detail in type_matches:
if detail.get("id") in excluded_ids:
continue
detail["_selectionReason"] = "configured-policy"
selected.append(detail)
return {
"details": selected,
"selection": {
"mode": "configured-policy",
"accountType": account_type,
"candidateAccounts": len(details),
"configuredPolicyMatches": len(configured),
"accountTypeMatches": len(type_matches),
"accountTypeFiltered": len(configured) - len(type_matches),
"explicitExclusionsRequested": len(excluded_accounts),
"explicitlyExcluded": len([item for item in type_matches if item.get("id") in excluded_ids]),
"selected": len(selected),
"excludedAccounts": excluded_accounts,
},
}
selectors = PAYLOAD.get("accounts")
if not isinstance(selectors, list) or not selectors:
selector = PAYLOAD.get("account")
@@ -1067,9 +1111,18 @@ def selected_account_details(token, accounts):
raise RuntimeError("duplicate-account-selector: " + selector)
if PAYLOAD.get("kind") == "temp-unschedulable" and detail.get("type") != "apikey":
raise RuntimeError("runtime temp-unschedulable policy requires an apikey account: " + selector)
detail["_selectionReason"] = "explicit-selector"
selected_ids.add(account_id)
details.append(detail)
return details
return {
"details": details,
"selection": {
"mode": "explicit-selector",
"requestedSelectors": len(selectors),
"selected": len(details),
"excludedAccounts": [],
},
}
def mutation_plan(detail, action, include_rules):
if PAYLOAD.get("kind") == "account-name":
@@ -1078,6 +1131,7 @@ def mutation_plan(detail, action, include_rules):
return {
"accountName": before_name,
"accountId": detail.get("id"),
"selectionReason": detail.get("_selectionReason"),
"change": "noop" if before_name == desired_name else "update",
"before": {"name": before_name},
"desired": {"name": desired_name},
@@ -1091,6 +1145,7 @@ def mutation_plan(detail, action, include_rules):
return {
"accountName": detail.get("name"),
"accountId": detail.get("id"),
"selectionReason": detail.get("_selectionReason"),
"change": "noop" if before_priority == desired_priority else "update",
"before": {"priority": before_priority},
"desired": {"priority": desired_priority},
@@ -1118,6 +1173,7 @@ def mutation_plan(detail, action, include_rules):
return {
"accountName": detail.get("name"),
"accountId": detail.get("id"),
"selectionReason": detail.get("_selectionReason"),
"change": change,
"before": policy_summary(before, include_rules),
"desired": policy_summary(desired_policy, include_rules),
@@ -1219,7 +1275,9 @@ def runtime_result():
"infrastructure": infrastructure_snapshot(token, detail),
}
include_plan_rules = PAYLOAD.get("full") is True
details = selected_account_details(token, accounts)
selected_accounts = selected_account_details(token, accounts)
details = selected_accounts["details"]
selection = selected_accounts["selection"]
plans = [mutation_plan(item, action, include_plan_rules) for item in details]
changed = sum(1 for plan in plans if plan["change"] != "noop")
if PAYLOAD.get("confirm") is not True:
@@ -1232,7 +1290,7 @@ def runtime_result():
item["reconcileError"] = None
items.append(item)
summary = {"selected": len(items), "changed": changed, "writeSucceeded": 0, "writeFailed": 0, "reconciled": 0, "mismatched": 0}
return {**base, "ok": True, "operation": action, "mode": "dry-run", "mutation": False, "partialWrite": False, "summary": summary, "items": items, "credentialsPrinted": False}
return {**base, "ok": True, "operation": action, "mode": "dry-run", "mutation": False, "partialWrite": False, "selection": selection, "summary": summary, "items": items, "credentialsPrinted": False}
writes = [write_mutation_plan(token, plan) for plan in plans]
items = [reconcile_mutation_plan(token, plan, write, include_plan_rules) for plan, write in zip(plans, writes)]
write_succeeded = sum(1 for item in items if item["write"]["status"] == "succeeded")
@@ -1241,7 +1299,7 @@ def runtime_result():
mismatched = sum(1 for item in items if item["reconciled"] is False)
summary = {"selected": len(items), "changed": changed, "writeSucceeded": write_succeeded, "writeFailed": write_failed, "reconciled": reconciled, "mismatched": mismatched}
ok = write_failed == 0 and mismatched == 0
return {**base, "ok": ok, "operation": action, "mode": "confirmed", "mutation": write_succeeded > 0, "partialWrite": write_succeeded > 0 and write_failed > 0, "summary": summary, "items": items, "credentialsPrinted": False}
return {**base, "ok": ok, "operation": action, "mode": "confirmed", "mutation": write_succeeded > 0, "partialWrite": write_succeeded > 0 and write_failed > 0, "selection": selection, "summary": summary, "items": items, "credentialsPrinted": False}
try:
result = runtime_result()
@@ -2,6 +2,7 @@ import type { UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { desiredAccountNames } from "./accounts";
import { readCodexPoolConfig } from "./config";
import { isRecord } from "./config-utils";
import { protectedManualAccountsForTarget } from "./public-exposure";
import { renderSub2ApiTempUnschedulableCredentials } from "./redaction";
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
@@ -22,6 +23,9 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
action: options.action,
account: options.account,
accounts: options.accounts,
selectConfiguredPolicy: options.selectConfiguredPolicy,
accountType: options.accountType,
excludeAccounts: options.excludeAccounts,
group: options.group,
allGroups: options.allGroups,
platform: options.platform,
@@ -78,6 +82,9 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
operation: options.action,
account: options.account,
accounts: options.accounts,
selectConfiguredPolicy: options.selectConfiguredPolicy,
accountType: options.accountType,
excludeAccounts: options.excludeAccounts,
group: options.group,
allGroups: options.allGroups,
platform: options.platform,
@@ -90,7 +97,9 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
since: options.action === "errors" || options.action === "infrastructure" ? options.since : undefined,
tail: options.action === "errors" || options.action === "infrastructure" ? options.tail : undefined,
},
runtime: compactRuntimeErrors(parsed, options),
runtime: options.json && (options.action === "apply" || options.action === "delete")
? compactRuntimeMutationJson(parsed)
: compactRuntimeErrors(parsed, options),
remote: compactCapture(result, { full: result.exitCode !== 0 || parsed === null }),
disclosure: options.action === "list"
? {
@@ -102,3 +111,38 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
};
return options.full || options.json ? response : renderRuntimeResult(response, options);
}
function compactRuntimeMutationJson(parsed: Record<string, unknown> | null): Record<string, unknown> | null {
if (parsed === null) return null;
const items = Array.isArray(parsed.items) ? parsed.items.filter(isRecord) : [];
const confirmed = parsed.mode === "confirmed";
return {
ok: parsed.ok,
operation: parsed.operation,
mode: parsed.mode,
mutation: parsed.mutation,
partialWrite: parsed.partialWrite,
selection: parsed.selection,
summary: parsed.summary,
items: items.map((item) => {
const write = isRecord(item.write) ? item.write : {};
const desiredDiffers = JSON.stringify(item.desired) !== JSON.stringify(item.before);
return {
accountName: item.accountName,
accountId: item.accountId,
selectionReason: item.selectionReason,
change: item.change,
template: item.template,
before: item.before,
desired: desiredDiffers ? item.desired : null,
actual: confirmed ? item.actual : undefined,
writeStatus: write.status,
writeError: write.error,
reconciled: item.reconciled,
reconcileError: item.reconcileError,
};
}),
credentialsPrinted: false,
valuesPrinted: false,
};
}
@@ -412,6 +412,7 @@ export function codexPoolHelp(): unknown {
"bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough list|apply|delete [--template <id>] [--target PK01] [--confirm] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --template <id> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --template <id> [--target PK01] [--confirm] [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --template <id> --select-configured-policy --account-type apikey [--exclude-accounts <name-or-id,...>] [--target PK01] [--confirm] [--json|--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --kind priority --priority <0-1000> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --kind account-name --name <new-name> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --account <name-or-id> --kind temp-unschedulable [--target PK01] [--confirm]",