feat: 自动选择已配置的 runtime 策略账号
This commit is contained in:
@@ -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]",
|
||||
|
||||
Reference in New Issue
Block a user