refactor(commands): migrate remaining confirmations to runtime gate

This commit is contained in:
若麒
2026-08-28 14:35:17 +08:00
parent 8906108744
commit afb547e0c8
7 changed files with 112 additions and 93 deletions
@@ -34,13 +34,6 @@ const APPLY_FLAGS = {
"zh-CN": "目标 Provider默认全部已配置项", "zh-CN": "目标 Provider默认全部已配置项",
}, },
}, },
yes: {
type: "switch",
description: {
"en-US": "Confirm and apply without an interactive prompt (required to mutate)",
"zh-CN": "无需交互提示直接确认并应用(执行变更时必填)",
},
},
noRefresh: { noRefresh: {
type: "switch", type: "switch",
description: { description: {
@@ -64,7 +57,15 @@ export default defineCommand({
"zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源", "zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源",
}, },
auth: "apiKey", auth: "apiKey",
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]", risk: {
level: "high",
message: {
"en-US":
"This applies the current plan and may create, update, or delete remote managed Agent resources.",
"zh-CN": "该操作会应用当前计划,可能创建、更新或删除远端托管 Agent 资源。",
},
},
usageArgs: "[--file <path>] [--provider <name>] [--concurrency <n>]",
flags: APPLY_FLAGS, flags: APPLY_FLAGS,
exampleArgs: ["--yes", "--provider bailian --yes"], exampleArgs: ["--yes", "--provider bailian --yes"],
notes: CREDENTIALS_NOTE, notes: CREDENTIALS_NOTE,
@@ -124,23 +125,11 @@ export default defineCommand({
return; return;
} }
const creates = actionable.filter((action) => action.action === "create").length;
const updates = actionable.filter((action) => action.action === "update").length;
const deletes = planned.destructiveActions;
for (const action of actionable) { for (const action of actionable) {
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-"; const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitProgress(` ${icon} ${formatResourceLabel(action.address)}`); emitProgress(` ${icon} ${formatResourceLabel(action.address)}`);
} }
if (!flags.yes) {
throw new BailianError(
`Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`,
ExitCode.USAGE,
"Review with `bl managed-agent plan`, then re-run with --yes to apply.",
);
}
const result = await withAgentErrors(() => const result = await withAgentErrors(() =>
withStdoutProtected(() => withStdoutProtected(() =>
executePlannedProject(planned, { executePlannedProject(planned, {
@@ -21,13 +21,6 @@ const DESTROY_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml", "zh-CN": "配置文件路径默认agents.yaml",
}, },
}, },
yes: {
type: "switch",
description: {
"en-US": "Confirm and destroy without an interactive prompt (required)",
"zh-CN": "无需交互提示直接确认并销毁(必填)",
},
},
cascade: { cascade: {
type: "switch", type: "switch",
description: { description: {
@@ -43,7 +36,15 @@ export default defineCommand({
"zh-CN": "销毁 State 中跟踪的全部托管 Agent 资源", "zh-CN": "销毁 State 中跟踪的全部托管 Agent 资源",
}, },
auth: "apiKey", auth: "apiKey",
usageArgs: "[--file <path>] [--yes] [--cascade]", risk: {
level: "high",
message: {
"en-US":
"This deletes every managed Agent resource tracked in state; --cascade may also delete dependent resources.",
"zh-CN": "该操作会删除 State 中跟踪的全部托管 Agent 资源;--cascade 还可能删除依赖资源。",
},
},
usageArgs: "[--file <path>] [--cascade]",
flags: DESTROY_FLAGS, flags: DESTROY_FLAGS,
exampleArgs: ["--yes", "--yes --cascade"], exampleArgs: ["--yes", "--yes --cascade"],
notes: CREDENTIALS_NOTE, notes: CREDENTIALS_NOTE,
@@ -86,14 +87,6 @@ export default defineCommand({
else emitBare(line); else emitBare(line);
} }
if (!flags.yes) {
throw new BailianError(
`Refusing to destroy ${resources.length} resource(s) without confirmation.`,
ExitCode.USAGE,
"Re-run with --yes to destroy (add --cascade to remove dependents).",
);
}
const result = await withAgentErrors(() => const result = await withAgentErrors(() =>
withStdoutProtected(() => withStdoutProtected(() =>
destroyPlannedProjectResources(planned, { destroyPlannedProjectResources(planned, {
@@ -1,4 +1,4 @@
import { defineCommand, BailianError, ExitCode } from "bailian-cli-core"; import { defineCommand } from "bailian-cli-core";
import { runPermissionChange, validatePermissionChange } from "./shared.ts"; import { runPermissionChange, validatePermissionChange } from "./shared.ts";
export default defineCommand({ export default defineCommand({
@@ -7,7 +7,16 @@ export default defineCommand({
"zh-CN": "撤销模型权限(推理 / 微调 / 部署)", "zh-CN": "撤销模型权限(推理 / 微调 / 部署)",
}, },
auth: "apiKey", auth: "apiKey",
usageArgs: "--model <models> [--action <actions>] | --all --yes", risk: {
level: "high",
message: {
"en-US":
"This revokes model permissions and may interrupt inference, fine-tuning, or deployment workloads. With --all, it also clears all historical inference grants.",
"zh-CN":
"该操作会撤销模型权限,可能导致推理、精调或部署任务中断;使用 --all 时会清除全部历史推理授权。",
},
},
usageArgs: "--model <models> [--action <actions>] | --all [flags]",
flags: { flags: {
model: { model: {
type: "string", type: "string",
@@ -33,17 +42,10 @@ export default defineCommand({
"zh-CN": "关闭一键授权并清除所有历史推理授权", "zh-CN": "关闭一键授权并清除所有历史推理授权",
}, },
}, },
yes: {
type: "switch",
description: {
"en-US": "Confirm --all without an interactive prompt (required)",
"zh-CN": "无需交互提示确认执行 --all必填",
},
},
}, },
exampleArgs: [ exampleArgs: [
"--model qwen-plus", "--model qwen-plus --yes",
"--model qwen-plus,qwen3-max --action inference,finetune", "--model qwen-plus,qwen3-max --action inference,finetune --yes",
"--all --yes", "--all --yes",
"--model qwen-plus --dry-run --output json", "--model qwen-plus --dry-run --output json",
], ],
@@ -52,6 +54,11 @@ export default defineCommand({
"en-US": "Grants apply to the business workspace your API key belongs to.", "en-US": "Grants apply to the business workspace your API key belongs to.",
"zh-CN": "授权将应用于 API Key 所属的业务 Workspace。", "zh-CN": "授权将应用于 API Key 所属的业务 Workspace。",
}, },
{
"en-US":
"All revoke operations require --yes; use --dry-run to preview the request without confirmation.",
"zh-CN": "所有撤权操作均需使用 --yes可通过 --dry-run 免确认预览请求。",
},
{ {
"en-US": "en-US":
"--all maps to the server one-key switch (access_all_entities: CLOSE): it clears every historical inference grant and cannot be undone, so it requires --yes.", "--all maps to the server one-key switch (access_all_entities: CLOSE): it clears every historical inference grant and cannot be undone, so it requires --yes.",
@@ -65,14 +72,6 @@ export default defineCommand({
], ],
validate: (flags) => validatePermissionChange(flags), validate: (flags) => validatePermissionChange(flags),
async run(ctx) { async run(ctx) {
const { flags, settings } = ctx; await runPermissionChange(ctx, ctx.flags, false);
if (flags.all && !flags.yes && !settings.dryRun) {
throw new BailianError(
"Refusing to clear all historical inference grants without confirmation.",
ExitCode.USAGE,
"Re-run with --yes to close one-key authorization (or preview with --dry-run).",
);
}
await runPermissionChange(ctx, flags, false);
}, },
}); });
@@ -132,6 +132,23 @@ describe("e2e: managed-agent", () => {
expect(stderr).toMatch(/--file|--provider|--yes/i); expect(stderr).toMatch(/--file|--provider|--yes/i);
}); });
test.each(["apply", "destroy"])("managed-agent %s 无 --yes 返回确认请求 (7)", async (command) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
command,
"--file",
"agents.e2e-missing.yaml",
"--api-key",
"e2e-dummy-key",
"--output",
"json",
]);
expect(exitCode).toBe(7);
expect(JSON.parse(stderr)).toMatchObject({
error: { code: 7, type: "requires_confirmation" },
});
});
test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => { test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent", "managed-agent",
@@ -223,7 +240,6 @@ describe("e2e: managed-agent--dry-run 短路,不联网不写盘)", () =>
"managed-agent", "managed-agent",
"apply", "apply",
"--dry-run", "--dry-run",
"--yes",
"--output", "--output",
"json", "json",
]); ]);
@@ -98,18 +98,39 @@ describe("e2e: permission", () => {
expect(stderr).toContain("at most 20"); expect(stderr).toContain("at most 20");
}); });
test("permission revoke --all --yes 拒绝执行", async () => { test("permission revoke --all --yes 返回确认请求 (7)", async () => {
// --yes 护栏在 run() 开头、任何网络调用之前抛出;带 dummy key 让用例不依赖环境凭证(否则 auth stage 先报 AUTH(3))。
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [ const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
"permission", "permission",
"revoke", "revoke",
"--all", "--all",
"--api-key", "--api-key",
"e2e-dummy-key", "e2e-dummy-key",
"--output",
"json",
]); ]);
expect(exitCode).toBe(2); expect(exitCode).toBe(7);
expect(stderr).toContain("Refusing"); expect(JSON.parse(stderr)).toMatchObject({
expect(stderr).toContain("--yes"); error: { code: 7, type: "requires_confirmation" },
});
});
test("permission revoke --model 无 --yes 返回确认请求 (7)", async () => {
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
"permission",
"revoke",
"--model",
"qwen-plus",
"--api-key",
"e2e-dummy-key",
"--base-url",
"http://127.0.0.1:1",
"--output",
"json",
]);
expect(exitCode).toBe(7);
expect(JSON.parse(stderr)).toMatchObject({
error: { code: 7, type: "requires_confirmation" },
});
}); });
// --dry-run 跳过 auth stage见 runtime middleware无需凭证即可断言请求形状。 // --dry-run 跳过 auth stage见 runtime middleware无需凭证即可断言请求形状。
+10 -9
View File
@@ -109,12 +109,12 @@ bl permission list --output text
### `bl permission revoke` ### `bl permission revoke`
| Field | Value | | Field | Value |
| ------------------ | --------------------------------------------------------------------------- | | ------------------ | ----------------------------------------------------------------------------- |
| **Name** | `permission revoke` | | **Name** | `permission revoke` |
| **Description** | Revoke model permissions (inference / finetune / deploy) | | **Description** | Revoke model permissions (inference / finetune / deploy) |
| **Authentication** | API Key | | **Authentication** | API Key |
| **Usage** | `bl permission revoke --model <models> [--action <actions>] \| --all --yes` | | **Usage** | `bl permission revoke --model <models> [--action <actions>] \| --all [flags]` |
#### Flags #### Flags
@@ -123,24 +123,25 @@ bl permission list --output text
| `--model <models>` | string | no | Model ID(s), comma-separated (max 20) | | `--model <models>` | string | no | Model ID(s), comma-separated (max 20) |
| `--action <actions>` | string | no | Permission action(s), comma-separated: inference, finetune, deploy (default: inference) | | `--action <actions>` | string | no | Permission action(s), comma-separated: inference, finetune, deploy (default: inference) |
| `--all` | switch | no | Close one-key authorization and clear ALL historical inference grants | | `--all` | switch | no | Close one-key authorization and clear ALL historical inference grants |
| `--yes` | switch | no | Confirm --all without an interactive prompt (required) | | `--yes` | switch | no | Confirm this high-risk operation |
| `--api-key <key>` | string | no | API key | | `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL | | `--base-url <url>` | string | no | API base URL |
#### Notes #### Notes
- Grants apply to the business workspace your API key belongs to. - Grants apply to the business workspace your API key belongs to.
- All revoke operations require --yes; use --dry-run to preview the request without confirmation.
- --all maps to the server one-key switch (access_all_entities: CLOSE): it clears every historical inference grant and cannot be undone, so it requires --yes. - --all maps to the server one-key switch (access_all_entities: CLOSE): it clears every historical inference grant and cannot be undone, so it requires --yes.
- Actions you omit keep their current grants (server-side tri-state patch). - Actions you omit keep their current grants (server-side tri-state patch).
#### Examples #### Examples
```bash ```bash
bl permission revoke --model qwen-plus bl permission revoke --model qwen-plus --yes
``` ```
```bash ```bash
bl permission revoke --model qwen-plus,qwen3-max --action inference,finetune bl permission revoke --model qwen-plus,qwen3-max --action inference,finetune --yes
``` ```
```bash ```bash
@@ -31,24 +31,24 @@ Index: [index.md](index.md)
### `bl managed-agent apply` ### `bl managed-agent apply`
| Field | Value | | Field | Value |
| ------------------ | ---------------------------------------------------------------------------------------- | | ------------------ | -------------------------------------------------------------------------------- |
| **Name** | `managed-agent apply` | | **Name** | `managed-agent apply` |
| **Description** | Apply planned changes to create/update/delete agent resources | | **Description** | Apply planned changes to create/update/delete agent resources |
| **Authentication** | API Key | | **Authentication** | API Key |
| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes] [--concurrency <n>]` | | **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--concurrency <n>]` |
#### Flags #### Flags
| Flag | Type | Required | Description | | Flag | Type | Required | Description |
| ------------------- | ------ | -------- | -------------------------------------------------------------------- | | ------------------- | ------ | -------- | ------------------------------------------------------------------ |
| `--file <path>` | string | no | Config file path (default: agents.yaml) | | `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--provider <name>` | string | no | Target provider (default: all configured) | | `--provider <name>` | string | no | Target provider (default: all configured) |
| `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) | | `--no-refresh` | switch | no | Skip refreshing state from remote before planning |
| `--no-refresh` | switch | no | Skip refreshing state from remote before planning | | `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) |
| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) | | `--yes` | switch | no | Confirm this high-risk operation |
| `--api-key <key>` | string | no | API key | | `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL | | `--base-url <url>` | string | no | API base URL |
#### Notes #### Notes
@@ -68,20 +68,20 @@ bl managed-agent apply --provider bailian --yes
### `bl managed-agent destroy` ### `bl managed-agent destroy`
| Field | Value | | Field | Value |
| ------------------ | -------------------------------------------------------------- | | ------------------ | ------------------------------------------------------ |
| **Name** | `managed-agent destroy` | | **Name** | `managed-agent destroy` |
| **Description** | Destroy all managed agent resources tracked in state | | **Description** | Destroy all managed agent resources tracked in state |
| **Authentication** | API Key | | **Authentication** | API Key |
| **Usage** | `bl managed-agent destroy [--file <path>] [--yes] [--cascade]` | | **Usage** | `bl managed-agent destroy [--file <path>] [--cascade]` |
#### Flags #### Flags
| Flag | Type | Required | Description | | Flag | Type | Required | Description |
| ------------------ | ------ | -------- | -------------------------------------------------------------------------- | | ------------------ | ------ | -------- | -------------------------------------------------------------------------- |
| `--file <path>` | string | no | Config file path (default: agents.yaml) | | `--file <path>` | string | no | Config file path (default: agents.yaml) |
| `--yes` | switch | no | Confirm and destroy without an interactive prompt (required) |
| `--cascade` | switch | no | Auto-delete dependent resources (e.g. sessions referencing an environment) | | `--cascade` | switch | no | Auto-delete dependent resources (e.g. sessions referencing an environment) |
| `--yes` | switch | no | Confirm this high-risk operation |
| `--api-key <key>` | string | no | API key | | `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL | | `--base-url <url>` | string | no | API base URL |