Skip to content

Commit 9d19558

Browse files
committed
feat(managed-agent): enforce high-risk confirmation for runtime operations
1 parent a9200a3 commit 9d19558

9 files changed

Lines changed: 146 additions & 86 deletions

File tree

packages/cli/tests/skill-risk-confirmation.test.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@ import { expect, test } from "vite-plus/test";
55

66
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
77
const skillsRoot = join(repositoryRoot, "skills");
8+
const scopedCreateCommands = new Set([
9+
"bl managed-agent agent create",
10+
"bl managed-agent deployment create",
11+
"bl managed-agent environment create",
12+
"bl managed-agent skill create",
13+
"bl managed-agent vault create",
14+
"bl managed-agent vault credential create",
15+
]);
816

9-
test("every generated high-risk command reference requires user confirmation before --yes", () => {
17+
test("generated references distinguish runtime high-risk confirmation from scoped create execution", () => {
1018
let highRiskCommandCount = 0;
19+
const seenScopedCreateCommands = new Set<string>();
1120

1221
for (const skillDirectory of readdirSync(skillsRoot, { withFileTypes: true })) {
1322
if (!skillDirectory.isDirectory()) continue;
@@ -27,9 +36,21 @@ test("every generated high-risk command reference requires user confirmation bef
2736
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
2837

2938
for (const commandSection of commandSections) {
30-
if (!commandSection.includes("`--yes`")) continue;
39+
const commandName = commandSection.match(/^### `([^`]+)`/m)?.[1];
40+
const hasConfirmationFlag = commandSection.includes("`--yes`");
41+
const hasHighRiskMetadata = /\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/.test(commandSection);
42+
43+
if (!hasHighRiskMetadata) {
44+
if (!hasConfirmationFlag) continue;
45+
expect(commandName).toBeDefined();
46+
expect(scopedCreateCommands.has(commandName ?? "")).toBe(true);
47+
expect(commandSection).toMatch(/Without --yes, .*preview/i);
48+
seenScopedCreateCommands.add(commandName ?? "");
49+
continue;
50+
}
51+
3152
highRiskCommandCount += 1;
32-
expect(commandSection).toMatch(/\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/);
53+
expect(hasConfirmationFlag).toBe(true);
3354
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
3455
expect(commandSection).toMatch(/type=.*requires_confirmation/);
3556
const agentSafetyLine = commandSection
@@ -44,4 +65,5 @@ test("every generated high-risk command reference requires user confirmation bef
4465
}
4566

4667
expect(highRiskCommandCount).toBeGreaterThan(0);
68+
expect([...seenScopedCreateCommands].sort()).toEqual([...scopedCreateCommands].sort());
4769
});

packages/commands/src/commands/managed-agent/deployment/_set-paused.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,24 @@ export function createSetDeploymentPausedCommand(paused: boolean) {
1616
? { "en-US": "Pause a Managed Agent deployment", "zh-CN": "暂停托管 Agent Deployment" }
1717
: { "en-US": "Unpause a Managed Agent deployment", "zh-CN": "恢复托管 Agent Deployment" },
1818
auth: "apiKey",
19+
risk: {
20+
level: "high",
21+
message: paused
22+
? {
23+
"en-US":
24+
"This pauses the specified Managed Agent deployment and stops its scheduled executions until resumed.",
25+
"zh-CN": "该操作会暂停指定的托管 Agent Deployment,并停止其定时执行直至恢复。",
26+
}
27+
: {
28+
"en-US":
29+
"This resumes the specified Managed Agent deployment and may restart scheduled executions and related usage.",
30+
"zh-CN":
31+
"该操作会恢复指定的托管 Agent Deployment,可能重新开始定时执行并产生相关用量。",
32+
},
33+
},
1934
usageArgs: "(--deployment <name> | --deployment-id <id>)",
2035
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
21-
exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc`],
36+
exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc --yes`],
2237
notes: CREDENTIALS_NOTE,
2338
validate: validateDeploymentActionTarget,
2439
async run(ctx) {

packages/commands/src/commands/managed-agent/deployment/_shared.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,6 @@ export const DEPLOYMENT_ACTION_TARGET_FLAGS = {
9696
},
9797
} as const;
9898

99-
export const DEPLOYMENT_RUN_ACTION_FLAGS = {
100-
...DEPLOYMENT_ACTION_TARGET_FLAGS,
101-
yes: {
102-
type: "switch",
103-
description: { "en-US": "Confirm deployment run", "zh-CN": "确认运行 Deployment" },
104-
},
105-
} as const;
106-
10799
export function validateDeploymentActionTarget(flags: {
108100
deployment?: string;
109101
deploymentId?: string;

packages/commands/src/commands/managed-agent/deployment/run.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { runRemoteDeployment } from "@openagentpack/sdk";
2-
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
2+
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
33
import { emitBare, emitResult } from "bailian-cli-runtime";
44
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
55
import { withStdoutProtected } from "../_engine/console-capture.ts";
66
import { withAgentErrors } from "../_engine/errors.ts";
77
import {
8-
DEPLOYMENT_RUN_ACTION_FLAGS,
8+
DEPLOYMENT_ACTION_TARGET_FLAGS,
99
resolveDeploymentTarget,
1010
validateDeploymentActionTarget,
1111
} from "./_shared.ts";
@@ -16,8 +16,16 @@ export default defineCommand({
1616
"zh-CN": "立即运行托管 Agent Deployment",
1717
},
1818
auth: "apiKey",
19-
usageArgs: "(--deployment <name> | --deployment-id <id>) --yes",
20-
flags: DEPLOYMENT_RUN_ACTION_FLAGS,
19+
risk: {
20+
level: "high",
21+
message: {
22+
"en-US":
23+
"This immediately starts a run for the specified Managed Agent deployment and may incur usage or trigger configured actions.",
24+
"zh-CN": "该操作会立即运行指定的托管 Agent Deployment,可能产生用量或触发已配置的动作。",
25+
},
26+
},
27+
usageArgs: "(--deployment <name> | --deployment-id <id>)",
28+
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
2129
exampleArgs: ["--deployment daily-report --dry-run", "--deployment-id dep_abc --yes"],
2230
notes: CREDENTIALS_NOTE,
2331
validate: validateDeploymentActionTarget,
@@ -33,13 +41,6 @@ export default defineCommand({
3341
);
3442
return;
3543
}
36-
if (!ctx.flags.yes) {
37-
throw new BailianError(
38-
"Refusing to run the deployment without confirmation.",
39-
ExitCode.USAGE,
40-
"Re-run with --yes or preview with --dry-run.",
41-
);
42-
}
4344
const result = await withAgentErrors(() =>
4445
withStdoutProtected(async () => {
4546
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");

packages/commands/src/commands/managed-agent/file.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
listRemoteFiles,
88
uploadFile,
99
} from "@openagentpack/sdk";
10-
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
10+
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
1111
import { emitBare, emitResult } from "bailian-cli-runtime";
1212
import {
1313
API_TARGET_FLAGS,
@@ -90,10 +90,6 @@ const DOWNLOAD_FLAGS = {
9090
} as const;
9191
const DELETE_FLAGS = {
9292
...GET_FLAGS,
93-
yes: {
94-
type: "switch",
95-
description: { "en-US": "Confirm permanent file deletion", "zh-CN": "确认永久删除文件" },
96-
},
9793
} as const;
9894

9995
function fileRows(files: ProviderFileInfo[]): string[][] {
@@ -298,7 +294,14 @@ export const managedAgentFileDownload = defineCommand({
298294
export const managedAgentFileDelete = defineCommand({
299295
description: { "en-US": "Delete a Managed Agent file", "zh-CN": "删除托管 Agent 文件" },
300296
auth: "apiKey",
301-
usageArgs: "--file-id <id> --yes",
297+
risk: {
298+
level: "high",
299+
message: {
300+
"en-US": "This permanently deletes the specified remote Managed Agent file.",
301+
"zh-CN": "该操作会永久删除指定的远端托管 Agent 文件。",
302+
},
303+
},
304+
usageArgs: "--file-id <id>",
302305
flags: DELETE_FLAGS,
303306
exampleArgs: ["--file-id file_abc --dry-run", "--file-id file_abc --yes"],
304307
notes: CREDENTIALS_NOTE,
@@ -308,13 +311,6 @@ export const managedAgentFileDelete = defineCommand({
308311
emitResult({ would_delete_file: ctx.flags.fileId }, format);
309312
return;
310313
}
311-
if (!ctx.flags.yes) {
312-
throw new BailianError(
313-
`Refusing to delete file ${ctx.flags.fileId} without confirmation.`,
314-
ExitCode.USAGE,
315-
"Re-run with --yes or preview with --dry-run.",
316-
);
317-
}
318314
await withAgentErrors(() =>
319315
withStdoutProtected(async () => {
320316
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");

packages/commands/src/commands/managed-agent/session-management.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,6 @@ const UPDATE_FLAGS = {
9090
const ARCHIVE_FLAGS = {
9191
...API_TARGET_FLAGS,
9292
...SESSION_ID_FLAG,
93-
yes: {
94-
type: "switch",
95-
description: { "en-US": "Confirm session archive", "zh-CN": "确认归档 Session" },
96-
},
9793
} as const;
9894

9995
function sessionRows(sessions: ProviderSessionInfo[]): string[][] {
@@ -217,7 +213,14 @@ export const managedAgentSessionUpdate = defineCommand({
217213
export const managedAgentSessionArchive = defineCommand({
218214
description: { "en-US": "Archive a Managed Agent session", "zh-CN": "归档托管 Agent Session" },
219215
auth: "apiKey",
220-
usageArgs: "--session-id <id> --yes",
216+
risk: {
217+
level: "high",
218+
message: {
219+
"en-US": "This archives the specified remote Managed Agent Session.",
220+
"zh-CN": "该操作会归档指定的远端托管 Agent Session。",
221+
},
222+
},
223+
usageArgs: "--session-id <id>",
221224
flags: ARCHIVE_FLAGS,
222225
exampleArgs: ["--session-id sess_abc --dry-run", "--session-id sess_abc --yes"],
223226
notes: CREDENTIALS_NOTE,
@@ -227,13 +230,6 @@ export const managedAgentSessionArchive = defineCommand({
227230
emitResult({ would_archive_session: ctx.flags.sessionId }, format);
228231
return;
229232
}
230-
if (!ctx.flags.yes) {
231-
throw new BailianError(
232-
`Refusing to archive session ${ctx.flags.sessionId} without confirmation.`,
233-
ExitCode.USAGE,
234-
"Re-run with --yes or preview with --dry-run.",
235-
);
236-
}
237233
const session = await withAgentErrors(() =>
238234
withStdoutProtected(async () => {
239235
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");

packages/commands/tests/e2e/managed-agent.e2e.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,11 @@ vaults:
10461046
test.each([
10471047
["state rm", ["state", "rm"]],
10481048
["session delete", ["session", "delete"]],
1049+
["session archive", ["session", "archive"]],
1050+
["file delete", ["file", "delete"]],
1051+
["deployment run", ["deployment", "run"]],
1052+
["deployment pause", ["deployment", "pause"]],
1053+
["deployment unpause", ["deployment", "unpause"]],
10491054
])("managed-agent %s --help 展示 runtime 注入的 --yes", async (_commandName, commandPath) => {
10501055
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
10511056
"managed-agent",
@@ -1062,6 +1067,11 @@ vaults:
10621067
"session delete",
10631068
["session", "delete", "--session-id", "sess_e2e", "--api-key", "e2e-dummy-key"],
10641069
],
1070+
["session archive", ["session", "archive", "--session-id", "sess_e2e"]],
1071+
["file delete", ["file", "delete", "--file-id", "file_e2e"]],
1072+
["deployment run", ["deployment", "run", "--deployment-id", "dep_e2e"]],
1073+
["deployment pause", ["deployment", "pause", "--deployment-id", "dep_e2e"]],
1074+
["deployment unpause", ["deployment", "unpause", "--deployment-id", "dep_e2e"]],
10651075
])("managed-agent %s 无 --yes 返回确认请求 (7)", async (_commandName, commandArgs) => {
10661076
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
10671077
"managed-agent",

skills/bailian-managed-agent/SKILL.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ description: >-
3030

3131
API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault / Deployment 的 create 命令仍通过
3232
`agents.yaml → scoped plan → scoped apply` 管理;查询命令和 Session、Event、File、Deployment 运行时动作直接调用 API。
33-
`session archive|delete``file delete``deployment run` 也需要先 `--dry-run`,确认后才传 `--yes`
33+
`session archive|delete``file delete``deployment run|pause|unpause` 也需要先 `--dry-run`,确认后才传 `--yes`
3434

35-
`state rm`, `session delete`, and future `risk: high` commands follow the shared protocol: show the
36-
risk message and exact scope, then wait for explicit confirmation before re-running with `--yes`.
35+
`state rm`, `session archive|delete`, `file delete`, `deployment run|pause|unpause`, and future
36+
`risk: high` commands follow the shared protocol: show the risk message and exact scope, then wait
37+
for explicit confirmation before re-running with `--yes`.
3738

3839
## IaC lifecycle
3940

0 commit comments

Comments
 (0)