diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/CHANGELOG.md b/CHANGELOG.md index 263ecb19..a2c97ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and [中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md) +## [1.23.0] - 2026-09-09 + +### Added + +- **Agent security commands** — `bl security overview` (protection overview for the last 24 hours) and `bl security alerts` (alert list with risk-level, asset-type, status, vendor, pagination and sorting filters). Both call the per-workspace AgentStudio host and honor the shared `text` / `json` / `--quiet` / `--dry-run` contract. The host is derived from `--workspace-id`, or overridden by `--base-url` / `DASHSCOPE_BASE_URL` / `auth login --base-url` pointed at a workspace or pre-release origin (e.g. `https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`). + +### Internal + +- Add Agent security E2E coverage (help, missing-workspace usage error, dry-run host derivation and `--base-url` override, query-string filters, enum fast-fail) and generate the `bailian-cli` skill reference for the new `security` group. + ## [1.22.0] - 2026-09-08 ### Changed diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index 5c82a3f7..65c2dcc1 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -6,6 +6,16 @@ [English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md) +## [1.23.0] - 2026-09-09 + +### 新增 + +- **Agent 安全命令** —— `bl security overview`(最近 24 小时的防护总览)与 `bl security alerts`(告警列表,支持风险等级、资产类型、状态、厂商、分页与排序等筛选)。两者均对接按 workspace 区分的 AgentStudio 域名,遵循统一的 `text` / `json` / `--quiet` / `--dry-run` 约定。域名默认由 `--workspace-id` 推导,也可通过 `--base-url` / `DASHSCOPE_BASE_URL` / `auth login --base-url` 指向某个 workspace 或预发源覆盖(例如 `https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`)。 + +### 内部 + +- 补充 Agent 安全 E2E 覆盖(help、缺 workspace 的 usage 错误、dry-run 域名推导与 `--base-url` 覆盖、query string 筛选、枚举快失败),并为新的 `security` 组生成 `bailian-cli` 技能 reference。 + ## [1.22.0] - 2026-09-08 ### 变更 diff --git a/packages/cli/package.json b/packages/cli/package.json index 08d84fc8..5cc5b50d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli", - "version": "1.22.0", + "version": "1.23.0", "description": "CLI for Aliyun Model Studio (DashScope) AI Platform.", "keywords": [ "agent", diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index c1ee7fb2..1a2afb3f 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -72,6 +72,8 @@ import { speechRecognize, fileUpload, consoleCall, + securityOverview, + securityAlerts, usageFree, usageFreetier, usageStats, @@ -288,6 +290,8 @@ export const commands: Record = { "speech recognize": speechRecognize, "file upload": fileUpload, "console call": consoleCall, + "security overview": securityOverview, + "security alerts": securityAlerts, "usage free": usageFree, "usage freetier": usageFreetier, "usage stats": usageStats, diff --git a/packages/commands/package.json b/packages/commands/package.json index 574f19b9..c15c2b7d 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-commands", - "version": "1.22.0", + "version": "1.23.0", "description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/commands/src/commands/security/alerts.ts b/packages/commands/src/commands/security/alerts.ts new file mode 100644 index 00000000..eaa78deb --- /dev/null +++ b/packages/commands/src/commands/security/alerts.ts @@ -0,0 +1,201 @@ +import { + defineCommand, + detectOutputFormat, + securityAgentLogsEndpoint, + securityGet, + type FlagsDef, + type SecurityAlertList, +} from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { WORKSPACE_FLAG, renderAlert, resolveSecurityHost, setSecurityParam } from "./shared.ts"; + +const ASSET_TYPES = ["agent", "tool", "skill", "knowledge_base", "memory", "channel"] as const; + +const ALERTS_FLAGS = { + ...WORKSPACE_FLAG, + page: { + type: "number", + valueHint: "", + description: { "en-US": "Page number (default: 1)", "zh-CN": "页码(默认:1)" }, + }, + pageSize: { + type: "number", + valueHint: "", + description: { "en-US": "Results per page (default: 20)", "zh-CN": "每页结果数(默认:20)" }, + }, + riskLevel: { + type: "string", + valueHint: "", + choices: ["high", "medium", "low"] as const, + description: { + "en-US": "Filter by risk level: high, medium, low", + "zh-CN": "按风险等级筛选:high、medium、low", + }, + }, + riskName: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by risk name", "zh-CN": "按风险名称筛选" }, + }, + status: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by handling status", "zh-CN": "按处理状态筛选" }, + }, + statusList: { + type: "array", + valueHint: "", + description: { + "en-US": "Filter by multiple statuses (repeatable)", + "zh-CN": "按多个状态筛选(可重复传入)", + }, + }, + appName: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by application name", "zh-CN": "按应用名称筛选" }, + }, + assetType: { + type: "string", + valueHint: "", + choices: ASSET_TYPES, + description: { + "en-US": `Filter by asset type: ${ASSET_TYPES.join(", ")}`, + "zh-CN": `按资产类型筛选:${ASSET_TYPES.join("、")}`, + }, + }, + vendor: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by vendor", "zh-CN": "按厂商筛选" }, + }, + orderBy: { + type: "string", + valueHint: "", + description: { + "en-US": "Sort field (default: check_time)", + "zh-CN": "排序字段(默认:check_time)", + }, + }, + order: { + type: "string", + valueHint: "", + choices: ["asc", "desc"] as const, + description: { + "en-US": "Sort direction: asc, desc (default: desc)", + "zh-CN": "排序方向:asc、desc(默认:desc)", + }, + }, + lang: { + type: "string", + valueHint: "", + choices: ["zh", "en"] as const, + description: { "en-US": "Response language: zh, en", "zh-CN": "响应语言:zh、en" }, + }, +} satisfies FlagsDef; + +export default defineCommand({ + description: { + "en-US": "List Agent security alerts", + "zh-CN": "列出 Agent 安全告警", + }, + auth: "apiKey", + usageArgs: "[flags]", + flags: ALERTS_FLAGS, + notes: [ + { + "en-US": "Auth: uses DashScope API Key (Bearer token).", + "zh-CN": "鉴权:使用 DashScope API Key(Bearer Token)。", + }, + { + "en-US": "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id.", + "zh-CN": "`--workspace-id` 可通过 BAILIAN_WORKSPACE_ID 环境变量或配置项 workspace_id 设置。", + }, + { + "en-US": + "Filters, pagination and sorting go in the query string; enum flags are validated before any request is sent.", + "zh-CN": "筛选、分页与排序参数走 query string;枚举类 flag 在发起请求前校验。", + }, + { + "en-US": + "AgentStudio host: derived from --workspace-id by default; point --base-url / DASHSCOPE_BASE_URL (or `auth login --base-url`) at a workspace or pre-release origin such as https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio to override it, and --workspace-id is then not required.", + "zh-CN": + "AgentStudio 域名:默认由 --workspace-id 推导;将 --base-url / DASHSCOPE_BASE_URL(或 `auth login --base-url`)指向某个 workspace 或预发源(例如 https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio)即可覆盖,此时无需 --workspace-id。", + }, + ], + exampleArgs: [ + { "en-US": "--workspace-id ws-xxx", "zh-CN": "--workspace-id ws-xxx" }, + { + "en-US": "--risk-level high --page-size 50", + "zh-CN": "--risk-level high --page-size 50", + }, + { + "en-US": '--asset-type agent --app-name "demo app" --output json', + "zh-CN": '--asset-type agent --app-name "测试应用0" --output json', + }, + { + "en-US": "--status-list unhandled --status-list handling", + "zh-CN": "--status-list unhandled --status-list handling", + }, + ], + async run(ctx) { + const { settings, flags } = ctx; + const format = detectOutputFormat(settings.output); + const host = resolveSecurityHost(ctx); + + const params = new URLSearchParams(); + setSecurityParam(params, "current_page", flags.page); + setSecurityParam(params, "page_size", flags.pageSize); + setSecurityParam(params, "risk_level", flags.riskLevel); + setSecurityParam(params, "risk_name", flags.riskName); + setSecurityParam(params, "status", flags.status); + setSecurityParam(params, "status_list", flags.statusList); + setSecurityParam(params, "app_name", flags.appName); + setSecurityParam(params, "asset_type", flags.assetType); + setSecurityParam(params, "vendor", flags.vendor); + setSecurityParam(params, "order_by", flags.orderBy); + setSecurityParam(params, "order", flags.order); + setSecurityParam(params, "lang", flags.lang); + + const query = params.toString(); + const base = securityAgentLogsEndpoint(host); + const endpoint = query ? `${base}?${query}` : base; + + if (settings.dryRun) { + emitResult({ endpoint, method: "GET" }, format); + return; + } + + const data = await securityGet(ctx.client, endpoint); + const alerts = data?.data ?? []; + + if (format === "json") { + emitResult(data ?? { stats: null, data: [], next_page: null }, format); + return; + } + + if (settings.quiet) { + for (const alert of alerts) emitBare(alert.alert_id); + return; + } + + const stats = data?.stats; + if (stats) { + emitBare( + `Total: ${stats.total ?? "-"} high: ${stats.high ?? "-"} ` + + `medium: ${stats.medium ?? "-"} low: ${stats.low ?? "-"}\n`, + ); + } + + if (alerts.length === 0) { + emitBare("No alerts found."); + return; + } + + for (const alert of alerts) renderAlert(alert); + + if (data?.next_page) { + emitBare(`Next page cursor: ${data.next_page}`); + } + }, +}); diff --git a/packages/commands/src/commands/security/overview.ts b/packages/commands/src/commands/security/overview.ts new file mode 100644 index 00000000..a87c3e04 --- /dev/null +++ b/packages/commands/src/commands/security/overview.ts @@ -0,0 +1,107 @@ +import { + defineCommand, + detectOutputFormat, + securityGet, + securityOverviewEndpoint, + type FlagsDef, + type SecurityOverview, + type SecurityScanStat, +} from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { + CAPABILITY_LABELS, + PROTECTION_LABELS, + SCAN_CARDS, + WORKSPACE_FLAG, + renderToggles, + resolveSecurityHost, +} from "./shared.ts"; + +const OVERVIEW_FLAGS = { + ...WORKSPACE_FLAG, +} satisfies FlagsDef; + +export default defineCommand({ + description: { + "en-US": "Show the Agent security protection overview (last 24 hours)", + "zh-CN": "查看 Agent 安全防护总览(最近 24 小时)", + }, + auth: "apiKey", + usageArgs: "[flags]", + flags: OVERVIEW_FLAGS, + notes: [ + { + "en-US": "Auth: uses DashScope API Key (Bearer token).", + "zh-CN": "鉴权:使用 DashScope API Key(Bearer Token)。", + }, + { + "en-US": "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id.", + "zh-CN": "`--workspace-id` 可通过 BAILIAN_WORKSPACE_ID 环境变量或配置项 workspace_id 设置。", + }, + { + "en-US": + "Fixed to the last 24 hours; the AgentStudio region is always cn-beijing and is not configurable.", + "zh-CN": "固定统计最近 24 小时;AgentStudio 地域固定为 cn-beijing,不可配置。", + }, + { + "en-US": + "AgentStudio host: derived from --workspace-id by default; point --base-url / DASHSCOPE_BASE_URL (or `auth login --base-url`) at a workspace or pre-release origin such as https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio to override it, and --workspace-id is then not required.", + "zh-CN": + "AgentStudio 域名:默认由 --workspace-id 推导;将 --base-url / DASHSCOPE_BASE_URL(或 `auth login --base-url`)指向某个 workspace 或预发源(例如 https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio)即可覆盖,此时无需 --workspace-id。", + }, + ], + exampleArgs: [ + { "en-US": "--workspace-id ws-xxx", "zh-CN": "--workspace-id ws-xxx" }, + { + "en-US": "--workspace-id ws-xxx --output json", + "zh-CN": "--workspace-id ws-xxx --output json", + }, + ], + async run(ctx) { + const { settings } = ctx; + const format = detectOutputFormat(settings.output); + const endpoint = securityOverviewEndpoint(resolveSecurityHost(ctx)); + + if (settings.dryRun) { + emitResult({ endpoint, method: "GET" }, format); + return; + } + + const data = await securityGet(ctx.client, endpoint); + if (!data) { + if (format === "json") emitResult({}, format); + else emitBare("Overview unavailable."); + return; + } + + if (format === "json") { + emitResult(data, format); + return; + } + + // Banner totals are client-side sums across the detection cards. Each card + // accepts the snake_case (REST) or camelCase (console-gateway) field. + const cards = SCAN_CARDS.map(({ label, keys }) => { + const stat = keys + .map((key) => data[key] as SecurityScanStat | null | undefined) + .find((value) => value !== undefined); + return { label, stat: stat ?? null }; + }); + const sum = (pick: (stat: SecurityScanStat) => number | null): number => + cards.reduce((total, card) => total + (card.stat ? (pick(card.stat) ?? 0) : 0), 0); + + emitBare(`Scanned: ${sum((stat) => stat.scanned)} Risks: ${sum((stat) => stat.hit)}`); + + renderToggles("Capabilities", data.capabilities, CAPABILITY_LABELS); + renderToggles("Protection", data.protection, PROTECTION_LABELS); + + emitBare("\nDetections"); + for (const { label, stat } of cards) { + if (!stat) { + emitBare(` ${label} (unavailable)`); + } else { + emitBare(` ${label} hit ${stat.hit ?? "-"} / scanned ${stat.scanned ?? "-"}`); + } + } + }, +}); diff --git a/packages/commands/src/commands/security/shared.ts b/packages/commands/src/commands/security/shared.ts new file mode 100644 index 00000000..b4068748 --- /dev/null +++ b/packages/commands/src/commands/security/shared.ts @@ -0,0 +1,117 @@ +// Shared building blocks for the Agent security commands (bl security *). +// +// AgentStudio uses the same per-workspace host scheme as the knowledge admin +// plane, so the --workspace-id flag and its three-level resolver are reused +// from there rather than duplicated — keeping the "workspace required" error +// identical across both command groups. +import { + agentStudioHost, + isDashScopeGateway, + type SecurityAlert, + type SecurityOverview, + type SecurityToggle, +} from "bailian-cli-core"; +import { emitBare } from "bailian-cli-runtime"; +import { resolveWorkspaceId, WORKSPACE_FLAG } from "../knowledge/shared.ts"; + +export { resolveWorkspaceId, WORKSPACE_FLAG }; + +/** + * Resolve the AgentStudio host these commands talk to. + * + * Default: derive the per-workspace production host from --workspace-id (or + * BAILIAN_WORKSPACE_ID / config). But when the model base URL has been pointed + * at a non-DashScope origin — an explicit --base-url / DASHSCOPE_BASE_URL for a + * pre-release, private, or mock deployment — honor it as the host override and + * skip the workspace requirement (that origin is already fully qualified). + */ +export function resolveSecurityHost(ctx: { + client: { baseUrl: string }; + flags: { workspaceId?: string }; + settings: { workspaceId?: string }; + identity: { binName: string }; +}): string { + const origin = ctx.client.baseUrl.replace(/\/+$/, ""); + if (origin && !isDashScopeGateway(origin)) return origin; + return agentStudioHost(resolveWorkspaceId(ctx)); +} + +// The API returns codes only — display names live here to match the console. +export const CAPABILITY_LABELS: Record = { + agent_identity: "Agent 身份签发", + content_safety: "内容安全", + supply_chain_scan: "供应链静态扫描", + credential_isolation: "凭证隔离", + session_lifecycle: "session 生命周期治理", +}; + +// Protection entries are keyed by asset type; the doc lists only the codes, so +// the display names are maintained here. +export const PROTECTION_LABELS: Record = { + flow_agent: "Flow Agent", + managed_agent: "Managed Agents", + knowledge_base: "RAG", + memory: "Memory", + mcp: "Store", + external_agent: "BYOA 托管", +}; + +/** Detection cards summed into the overview banner: label + snake_case (REST) / camelCase (gateway) keys. */ +export const SCAN_CARDS: Array<{ label: string; keys: Array }> = [ + { label: "内容安全", keys: ["content_safety", "contentSafety"] }, + { label: "文件扫描", keys: ["file_scan", "fileScan"] }, + { label: "技能扫描", keys: ["skill_scan", "skillScan"] }, +]; + +/** Append a query param only when present; arrays append each item (repeatable). */ +export function setSecurityParam( + params: URLSearchParams, + key: string, + value: string | number | string[] | null | undefined, +): void { + if (value === undefined || value === null || value === "") return; + if (Array.isArray(value)) { + for (const entry of value) params.append(key, entry); + return; + } + params.set(key, String(value)); +} + +/** Render a capability / protection toggle group; codes map to display names. */ +export function renderToggles( + title: string, + toggles: SecurityToggle[] | null | undefined, + labels: Record, +): void { + emitBare(`\n${title}`); + if (!toggles || toggles.length === 0) { + emitBare(" (unavailable)"); + return; + } + for (const toggle of toggles) { + const count = typeof toggle.count === "number" ? ` (${toggle.count})` : ""; + emitBare(` ${toggle.enabled ? "on " : "off"} ${labels[toggle.key] ?? toggle.key}${count}`); + } +} + +/** check_time / handle_time are millisecond timestamp strings, sometimes ISO 8601. */ +export function formatSecurityTime(value: string | null | undefined): string { + if (!value) return "-"; + const millis = /^\d+$/.test(value) ? Number(value) : Date.parse(value); + if (Number.isNaN(millis)) return value; + return new Date(millis).toISOString().replace("T", " ").slice(0, 19); +} + +/** Render one alert row in text mode. */ +export function renderAlert(alert: SecurityAlert): void { + const level = (alert.risk_level ?? "unknown").toUpperCase(); + emitBare(`[${level}] ${alert.risk_name ?? "-"} (${alert.alert_id})`); + emitBare( + ` app: ${alert.app_name ?? "-"} asset: ${alert.asset_name ?? "-"} (${alert.asset_type ?? "-"})`, + ); + emitBare( + ` status: ${alert.status ?? "-"} source: ${alert.source ?? "-"} checked: ${formatSecurityTime(alert.check_time)}`, + ); + if (alert.risk_desc) emitBare(` ${alert.risk_desc}`); + emitBare(""); +} diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 028f0ccb..fdcc025c 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -75,6 +75,8 @@ export { default as speechSynthesize } from "./commands/speech/synthesize.ts"; export { default as speechRecognize } from "./commands/speech/recognize.ts"; export { default as fileUpload } from "./commands/file/upload.ts"; export { default as consoleCall } from "./commands/console/call.ts"; +export { default as securityOverview } from "./commands/security/overview.ts"; +export { default as securityAlerts } from "./commands/security/alerts.ts"; export { default as usageFree } from "./commands/usage/free.ts"; export { default as usageFreetier } from "./commands/usage/freetier.ts"; export { default as usageStats } from "./commands/usage/stats.ts"; diff --git a/packages/commands/tests/e2e/security.e2e.test.ts b/packages/commands/tests/e2e/security.e2e.test.ts new file mode 100644 index 00000000..7334e1a6 --- /dev/null +++ b/packages/commands/tests/e2e/security.e2e.test.ts @@ -0,0 +1,212 @@ +import { describe, expect, test } from "vite-plus/test"; +import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts"; +import { SECURITY_ROUTES } from "./topic-routes.ts"; + +interface DryRunBody { + endpoint?: string; + method?: string; +} + +describe("e2e: security overview", () => { + test("--help 展示 --workspace-id", async () => { + const { stderr, exitCode } = await runCommandHelp(SECURITY_ROUTES, [ + "security", + "overview", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--workspace-id/i); + }); + + test("缺 workspace 时报 USAGE (2)", async () => { + const { stderr, exitCode } = await runCommandE2e( + SECURITY_ROUTES, + ["security", "overview", "--api-key", "sk-fake", "--output", "json"], + { BAILIAN_WORKSPACE_ID: "", BAILIAN_CONFIG_DIR: "/tmp" }, + ); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/workspace.*required/i); + }); + + test("--dry-run 由 workspace 推导 AgentStudio 域名,不发请求", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e( + SECURITY_ROUTES, + ["security", "overview", "--dry-run", "--workspace-id", "ws_test", "--output", "json"], + { BAILIAN_CONFIG_DIR: "/tmp", DASHSCOPE_BASE_URL: "" }, + ); // isolate dev config base_url + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.method).toBe("GET"); + expect(data.endpoint).toMatch(/ws_test\.cn-beijing\.maas\.aliyuncs\.com/); + expect(data.endpoint).toMatch(/api\/v1\/agentstudio\/security\/overview/); + }); + + test("--dry-run --base-url 覆盖为自定义 origin,无需 workspace", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "overview", + "--dry-run", + "--base-url", + "https://security.pre.example.com", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.method).toBe("GET"); + expect(data.endpoint).toBe( + "https://security.pre.example.com/api/v1/agentstudio/security/overview", + ); + }); +}); + +describe("e2e: security alerts", () => { + test("--help 展示筛选/分页/排序 flags", async () => { + const { stderr, exitCode } = await runCommandHelp(SECURITY_ROUTES, [ + "security", + "alerts", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--workspace-id/i); + expect(stderr).toMatch(/--risk-level/i); + expect(stderr).toMatch(/--page-size/i); + expect(stderr).toMatch(/--asset-type/i); + expect(stderr).toMatch(/--status-list/i); + expect(stderr).toMatch(/--order-by/i); + expect(stderr).toMatch(/--lang/i); + }); + + test("--dry-run 把筛选/分页参数落到 query string", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "alerts", + "--dry-run", + "--workspace-id", + "ws_test", + "--risk-level", + "high", + "--page-size", + "50", + "--page", + "2", + "--order", + "asc", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.method).toBe("GET"); + expect(data.endpoint).toMatch(/api\/v1\/agentstudio\/security\/agent_logs/); + expect(data.endpoint).toMatch(/risk_level=high/); + expect(data.endpoint).toMatch(/page_size=50/); + expect(data.endpoint).toMatch(/current_page=2/); + expect(data.endpoint).toMatch(/order=asc/); + }); + + test("--status-list 可重复传参(数组展开为多个 query 值)", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "alerts", + "--dry-run", + "--workspace-id", + "ws_test", + "--status-list", + "unhandled", + "--status-list", + "handling", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const { endpoint } = parseStdoutJson(stdout); + expect(endpoint).toMatch(/status_list=unhandled/); + expect(endpoint).toMatch(/status_list=handling/); + }); + + test("中文过滤值以 UTF-8 编码进入 query", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "alerts", + "--dry-run", + "--workspace-id", + "ws_test", + "--app-name", + "测试应用0", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const { endpoint } = parseStdoutJson(stdout); + expect(endpoint).toMatch(/app_name=/); + expect(decodeURIComponent(endpoint ?? "")).toContain("测试应用0"); + }); + + test("非法 --risk-level 报 USAGE (2) 且零请求", async () => { + const { exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "alerts", + "--dry-run", + "--workspace-id", + "ws_test", + "--risk-level", + "NOT_A_LEVEL", + ]); + expect(exitCode).toBe(2); + }); + + test("非法 --asset-type 报 USAGE (2)", async () => { + const { exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "alerts", + "--dry-run", + "--workspace-id", + "ws_test", + "--asset-type", + "NOT_A_TYPE", + ]); + expect(exitCode).toBe(2); + }); +}); + +// Live: exercises the real envelope unwrap + Bearer injection. AgentStudio may +// require a service-linked role the test account lacks (errorCode 12000092 → +// AUTH exit 3), which is a correct server-driven outcome, so both 0 and 3 pass. +describe.skipIf(!isKbAdminE2EReady())("e2e: security (live)", () => { + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + + test("overview 返回解包后的 JSON 或权限错误", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "overview", + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + expect([0, 3], stderr).toContain(exitCode); + if (exitCode === 0) { + const data = parseStdoutJson>(stdout); + expect(data).toBeTypeOf("object"); + } + }); + + test("alerts 返回列表 JSON 或权限错误", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SECURITY_ROUTES, [ + "security", + "alerts", + "--workspace-id", + workspaceId, + "--page-size", + "5", + "--output", + "json", + ]); + expect([0, 3], stderr).toContain(exitCode); + if (exitCode === 0) { + const data = parseStdoutJson<{ data?: unknown[] }>(stdout); + expect(Array.isArray(data.data)).toBe(true); + } + }); +}); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 957a0b8f..5e3e88c7 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -82,6 +82,11 @@ export const MCP_ROUTES: E2eRouteExports = { export const SEARCH_WEB_ROUTES: E2eRouteExports = { "search web": "searchWeb" }; +export const SECURITY_ROUTES: E2eRouteExports = { + "security overview": "securityOverview", + "security alerts": "securityAlerts", +}; + export const PIPELINE_ROUTES: E2eRouteExports = { "pipeline run": "pipelineRun", "pipeline validate": "pipelineValidate", diff --git a/packages/core/package.json b/packages/core/package.json index d15b40e8..488ab335 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-core", - "version": "1.22.0", + "version": "1.23.0", "description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index 8717c942..764d0f4a 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -115,6 +115,33 @@ export function knowledgeChatEndpoint(workspaceId: string): string { return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat`; } +// ---- Agent Security Center (AgentStudio, workspace-based host) ---- +// AgentStudio shares the per-workspace host scheme with the RAG admin plane +// below, not the shared DashScope gateway. The region is fixed to cn-beijing +// per the backend — a full cloud region, a different namespace from the CLI's +// region (cn / us / intl), so it is never derived from it. + +/** Per-workspace AgentStudio host (same shape as {@link ragEndpoint}'s host). */ +export function agentStudioHost(workspaceId: string): string { + return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com`; +} + +const SECURITY_PREFIX = "/api/v1/agentstudio/security"; + +// Security endpoints take an already-resolved AgentStudio host (origin) — +// normally agentStudioHost(workspaceId), or a --base-url override for +// pre-release / private / mock. See resolveSecurityHost in client/security.ts. + +/** GET protection overview — fixed to the last 24 hours, no request params. */ +export function securityOverviewEndpoint(host: string): string { + return `${host}${SECURITY_PREFIX}/overview`; +} + +/** GET alert list (agent_logs); filters / pagination go in the query string. */ +export function securityAgentLogsEndpoint(host: string): string { + return `${host}${SECURITY_PREFIX}/agent_logs`; +} + // ---- MCP Services (Streamable HTTP) ---- export function mcpWebSearchPath(): string { return "/api/v1/mcps/WebSearch/mcp"; diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 0a387f1a..c12d242d 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -19,6 +19,9 @@ export { ragEndpoint, RAG_PATHS, responsesPath, + agentStudioHost, + securityOverviewEndpoint, + securityAgentLogsEndpoint, speechRecognizePath, speechSynthesizePath, taskPath, @@ -61,6 +64,7 @@ export { } from "./headers.ts"; export type { HttpDeps, RequestOpts } from "./http.ts"; export { request, requestJson } from "./http.ts"; +export { securityGet, parseSecurityBody, isDashScopeGateway } from "./security.ts"; export { createInstrumentedFetch, type FetchImplementation } from "./instrumented-fetch.ts"; export { Client, diff --git a/packages/core/src/client/security.ts b/packages/core/src/client/security.ts new file mode 100644 index 00000000..039a21d2 --- /dev/null +++ b/packages/core/src/client/security.ts @@ -0,0 +1,150 @@ +import { REGIONS } from "../config/schema.ts"; +import { BailianError } from "../errors/base.ts"; +import { ExitCode } from "../errors/codes.ts"; +import type { Client } from "./client.ts"; + +/** Documented AgentStudio error codes → actionable hint. */ +const SECURITY_ERROR_HINTS: Record = { + "12000090": "STS credentials unavailable — retry in a moment", + "12000091": "Service-linked role creation failed — retry", + "12000092": "Use the primary account to create the service-linked role", + "12000093": "Cloud security service is unavailable — retry later", + "12000094": "Alert query failed — treat as no data", +}; + +/** The public DashScope gateway origins — the default model base URLs. */ +const DASHSCOPE_GATEWAY_ORIGINS = new Set(Object.values(REGIONS)); + +/** + * Whether an origin is a public DashScope gateway. The security commands derive + * their per-workspace AgentStudio host by default, so a base URL that is just + * the DashScope gateway is the default and is ignored; any other origin + * (pre-release / private / mock) is treated as an explicit host override. + */ +export function isDashScopeGateway(origin: string): boolean { + return DASHSCOPE_GATEWAY_ORIGINS.has(origin.replace(/\/+$/, "")); +} + +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +/** First non-empty string among the candidates, else undefined (treats "" as absent). */ +function firstNonEmptyString(...candidates: unknown[]): string | undefined { + for (const candidate of candidates) { + if (typeof candidate === "string" && candidate.length > 0) return candidate; + } + return undefined; +} + +function throwSecurityFailure( + errorCode: string | undefined, + errorMsg: string | undefined, + rawResponse: string, +): never { + const code = errorCode ?? "unknown"; + // When neither a code nor a message is present the envelope shape is + // unrecognised (e.g. a backend contract change) — surface a body snippet so + // the failure is diagnosable instead of an opaque "unknown - no message". + const bodySnippet = + errorCode === undefined && errorMsg === undefined + ? `\nUnexpected response body (truncated): ${rawResponse.slice(0, 800)}` + : ""; + throw new BailianError( + `Security API failed: ${code} - ${errorMsg ?? "no message"}${bodySnippet}`, + // 12000092 (no permission to create the service-linked role) is an auth + // problem the caller can act on; everything else is a generic failure. + code === "12000092" ? ExitCode.AUTH : ExitCode.GENERAL, + SECURITY_ERROR_HINTS[code], + { rawResponse: rawResponse.slice(0, 500) }, + ); +} + +/** + * Parse an Agent Security Center response body and return the business payload. + * + * The backend serves these through the Zelda "DataV2" double-envelope — the same + * shape the console gateway returns (see console/models.ts unwrapResponse): + * + * { code, successResponse, requestId, + * data: { success, errorCode, errorMsg, + * DataV2: { ret: ["SUCCESS::…"], + * data: { success, failed, data: } } } } + * + * The payload lives at `data.DataV2.data.data`. The legacy flat envelope + * `{ success, data, errorCode, errorMsg }` is still accepted so any endpoint + * that has not migrated keeps working. A failed or unrecognized shape throws + * with the raw body attached (surfaced by --output json) for diagnosis. + */ +export function parseSecurityBody(raw: string, contentType?: string | null): T | null { + let body: unknown; + try { + body = JSON.parse(raw); + } catch { + throw new BailianError( + `Security API returned non-JSON response (${contentType ?? "unknown type"}).`, + ExitCode.GENERAL, + undefined, + { rawResponse: raw.slice(0, 500) }, + ); + } + + const root = asRecord(body); + const data = root ? asRecord(root.data) : undefined; + const dataV2 = data ? asRecord(data.DataV2) : undefined; + + // Zelda / DataV2 double-envelope (current backend contract). + if (dataV2) { + const inner = asRecord(dataV2.data); + const ret = Array.isArray(dataV2.ret) ? dataV2.ret.map((entry) => String(entry)) : []; + const retOk = ret.length === 0 || ret.some((line) => line.startsWith("SUCCESS")); + const errorCode = firstNonEmptyString(data?.errorCode, root?.errorCode); + const errorMsg = + firstNonEmptyString(data?.errorMsg, root?.errorMsg) ?? (retOk ? undefined : ret.join("; ")); + const failed = + root?.successResponse === false || + data?.success === false || + inner?.success === false || + inner?.failed === true || + !retOk || + errorCode !== undefined; + if (failed) throwSecurityFailure(errorCode, errorMsg, raw); + return ((inner ? inner.data : undefined) ?? null) as T | null; + } + + // Legacy flat envelope: { success, data, errorCode, errorMsg }. + if (root && "success" in root) { + if (!root.success) { + throwSecurityFailure( + firstNonEmptyString(root.errorCode), + firstNonEmptyString(root.errorMsg), + raw, + ); + } + return (root.data ?? null) as T | null; + } + + // Bare payload: the REST endpoint currently returns the business object + // directly, with no envelope. Treat the root object as the payload. + if (root) return root as T; + + // Not an object at all — surface the raw body so the contract change is visible. + throwSecurityFailure(undefined, undefined, raw); +} + +/** + * GET a Security Center endpoint and unwrap its envelope. + * + * `url` is an absolute AgentStudio URL (see securityOverviewEndpoint / + * securityAgentLogsEndpoint, or a --base-url override); the Client uses it + * verbatim and injects the Bearer token. Genuine HTTP errors (non-2xx) surface + * through the Client transport; only HTTP 200 bodies reach parseSecurityBody. + * Returns null when the server reports success with no payload. + */ +export async function securityGet(client: Client, url: string): Promise { + const response = await client.request({ path: url, method: "GET" }); + const raw = await response.text(); + return parseSecurityBody(raw, response.headers.get("content-type")); +} diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index cf240e3f..0b5e3509 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -84,3 +84,4 @@ export type { UserProfileResponse, } from "./api.ts"; export type * from "./knowledge-admin.ts"; +export type * from "./security.ts"; diff --git a/packages/core/src/types/security.ts b/packages/core/src/types/security.ts new file mode 100644 index 00000000..23cff26c --- /dev/null +++ b/packages/core/src/types/security.ts @@ -0,0 +1,65 @@ +// Types for the Agent Security Center (AgentStudio). The server returns codes +// only — display names are maintained by the client (see commands/security). +// Kept separate from api.ts, mirroring knowledge-admin.ts. + +/** A capability / protection switch keyed by a stable code. */ +export interface SecurityToggle { + key: string; + enabled: boolean; + /** Protection entries carry an asset count; absent for some (e.g. external_agent). */ + count?: number | null; +} + +/** Detection card: `hit` is the headline number, `scanned` the total. */ +export interface SecurityScanStat { + hit: number | null; + scanned: number | null; +} + +/** Protection overview — fixed to the last 24 hours, no request params. */ +export interface SecurityOverview { + capabilities?: SecurityToggle[] | null; + protection?: SecurityToggle[] | null; + // Detection cards: the REST endpoint returns snake_case, the console-gateway + // (DataV2) shape returns camelCase. Accept both; the renderer picks whichever + // is present. + content_safety?: SecurityScanStat | null; + file_scan?: SecurityScanStat | null; + skill_scan?: SecurityScanStat | null; + contentSafety?: SecurityScanStat | null; + fileScan?: SecurityScanStat | null; + skillScan?: SecurityScanStat | null; +} + +export type SecurityRiskLevel = "high" | "medium" | "low"; + +/** A single alert row from agent_logs. */ +export interface SecurityAlert { + alert_id: string; + risk_level?: SecurityRiskLevel | null; + risk_name?: string | null; + risk_desc?: string | null; + asset_type?: string | null; + asset_name?: string | null; + app_id?: string | null; + app_name?: string | null; + agent_name?: string | null; + status?: string | null; + source?: string | null; + /** Millisecond timestamp string; some environments return ISO 8601 instead. */ + check_time?: string | null; + handle_time?: string | null; + vendor?: string | null; +} + +/** agent_logs list — cursor paginated; `next_page` is null on the last page. */ +export interface SecurityAlertList { + stats?: { + total?: number | null; + high?: number | null; + medium?: number | null; + low?: number | null; + } | null; + data?: SecurityAlert[] | null; + next_page?: string | number | null; +} diff --git a/packages/core/tests/security-envelope.test.ts b/packages/core/tests/security-envelope.test.ts new file mode 100644 index 00000000..147dd025 --- /dev/null +++ b/packages/core/tests/security-envelope.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from "vite-plus/test"; +import { parseSecurityBody } from "../src/client/security.ts"; +import { BailianError } from "../src/errors/base.ts"; +import { ExitCode } from "../src/errors/codes.ts"; + +// The backend serves Agent Security Center through the Zelda "DataV2" double +// envelope; the business payload lives at data.DataV2.data.data and the +// detection cards are camelCase. parseSecurityBody must unwrap it, still accept +// the legacy flat envelope, and turn failures into BailianError with the right +// exit code. + +function dataV2Envelope(payload: unknown): string { + return JSON.stringify({ + code: "200", + successResponse: true, + requestId: "req-1", + data: { + success: true, + errorCode: "", + errorMsg: "", + DataV2: { + ret: ["SUCCESS::接口调用成功"], + data: { success: true, failed: false, data: payload }, + }, + }, + }); +} + +function catchError(run: () => unknown): BailianError { + try { + run(); + } catch (error) { + return error as BailianError; + } + throw new Error("expected parseSecurityBody to throw"); +} + +test("unwraps the DataV2 double-envelope to the camelCase payload", () => { + const payload = { + contentSafety: { hit: 4, scanned: 253055 }, + fileScan: { hit: 52, scanned: 579 }, + skillScan: { hit: 2, scanned: 37 }, + capabilities: [{ key: "agent_identity", enabled: true }], + protection: [ + { key: "flow_agent", enabled: true, count: 1656 }, + { key: "external_agent", enabled: false }, + ], + }; + expect(parseSecurityBody(dataV2Envelope(payload))).toEqual(payload); +}); + +test("returns a bare payload (no envelope) as the REST endpoint sends", () => { + const bare = JSON.stringify({ + capabilities: [{ key: "agent_identity", enabled: true, count: null }], + protection: [{ key: "flow_agent", enabled: true, count: 1656 }], + content_safety: { hit: 4, scanned: 253561 }, + file_scan: { hit: 52, scanned: 579 }, + skill_scan: { hit: 2, scanned: 37 }, + }); + const result = parseSecurityBody>(bare); + expect(result?.content_safety).toEqual({ hit: 4, scanned: 253561 }); + expect(result?.protection).toEqual([{ key: "flow_agent", enabled: true, count: 1656 }]); +}); + +test("still accepts the legacy flat envelope", () => { + expect(parseSecurityBody<{ a: number }>('{"success":true,"data":{"a":1}}')).toEqual({ a: 1 }); +}); + +test("success with a null payload returns null, not an error", () => { + expect(parseSecurityBody(dataV2Envelope(null))).toBeNull(); +}); + +test("maps legacy 12000092 to the AUTH exit code", () => { + const error = catchError(() => + parseSecurityBody('{"success":false,"errorCode":"12000092","errorMsg":"no permission"}'), + ); + expect(error.exitCode).toBe(ExitCode.AUTH); + expect(error.message).toContain("12000092"); +}); + +test("surfaces a DataV2 failure errorCode as a GENERAL error", () => { + const body = JSON.stringify({ + code: "500", + successResponse: false, + data: { + success: false, + errorCode: "12000093", + errorMsg: "service down", + DataV2: { ret: ["FAIL::boom"], data: { success: false, failed: true } }, + }, + }); + const error = catchError(() => parseSecurityBody(body)); + expect(error.exitCode).toBe(ExitCode.GENERAL); + expect(error.message).toContain("12000093"); +}); + +test("rejects a non-JSON body with the content type", () => { + const error = catchError(() => parseSecurityBody("gateway", "text/html")); + expect(error.message).toContain("non-JSON"); +}); diff --git a/packages/kscli/package.json b/packages/kscli/package.json index 325c9ab0..ab728b96 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-studio-cli", - "version": "1.22.0", + "version": "1.23.0", "description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.", "keywords": [ "alibaba-cloud", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 5b7a6ca1..c566bc46 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-runtime", - "version": "1.22.0", + "version": "1.23.0", "description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.", "homepage": "https://bailian.console.aliyun.com/cli", "bugs": { diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index 6b905c20..b6974324 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-cli metadata: - version: "1.22.0" + version: "1.23.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 1a0c94b4..7fa42852 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -86,6 +86,8 @@ Use this index for the skill-scoped quick index and global flags. | `bl quota list` | API Key | View model rate limits (QPM/TPM, account and workspace level) | [quota.md](quota.md) | | `bl quota update` | API Key | Update model rate limits (QPM/TPM) | [quota.md](quota.md) | | `bl search web` | API Key | Search the web using DashScope MCP WebSearch service | [search.md](search.md) | +| `bl security alerts` | API Key | List Agent security alerts | [security.md](security.md) | +| `bl security overview` | API Key | Show the Agent security protection overview (last 24 hours) | [security.md](security.md) | | `bl skill add` | No Auth | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) | | `bl skill init` | No Auth | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) | | `bl skill list` | No Auth | List registry skills and diff against local installs | [skill.md](skill.md) | @@ -125,6 +127,7 @@ Use this index for the skill-scoped quick index and global flags. | `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) | | `quota` | `check`, `delete`, `history`, `list`, `update` | [quota.md](quota.md) | | `search` | `web` | [search.md](search.md) | +| `security` | `alerts`, `overview` | [security.md](security.md) | | `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) | | `text` | `chat` | [text.md](text.md) | | `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) | diff --git a/skills/bailian-cli/reference/security.md b/skills/bailian-cli/reference/security.md new file mode 100644 index 00000000..07b0f13e --- /dev/null +++ b/skills/bailian-cli/reference/security.md @@ -0,0 +1,103 @@ +# `bl security` commands + +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. +> Regenerate: `pnpm --filter bailian-cli run generate:reference`. + +Index: [index.md](index.md) + +## Commands in this group + +| Command | Authentication | Description | +| ---------------------- | -------------- | ----------------------------------------------------------- | +| `bl security alerts` | API Key | List Agent security alerts | +| `bl security overview` | API Key | Show the Agent security protection overview (last 24 hours) | + +## Command details + +### `bl security alerts` + +| Field | Value | +| ------------------ | ---------------------------- | +| **Name** | `security alerts` | +| **Description** | List Agent security alerts | +| **Authentication** | API Key | +| **Usage** | `bl security alerts [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------------------------------------------------------- | ------ | -------- | ------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--page ` | number | no | Page number (default: 1) | +| `--page-size ` | number | no | Results per page (default: 20) | +| `--risk-level ` | string | no | Filter by risk level: high, medium, low | +| `--risk-name ` | string | no | Filter by risk name | +| `--status ` | string | no | Filter by handling status | +| `--status-list ` | array | no | Filter by multiple statuses (repeatable) | +| `--app-name ` | string | no | Filter by application name | +| `--asset-type ` | string | no | Filter by asset type: agent, tool, skill, knowledge_base, memory, channel | +| `--vendor ` | string | no | Filter by vendor | +| `--order-by ` | string | no | Sort field (default: check_time) | +| `--order ` | string | no | Sort direction: asc, desc (default: desc) | +| `--lang ` | string | no | Response language: zh, en | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- Auth: uses DashScope API Key (Bearer token). +- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id. +- Filters, pagination and sorting go in the query string; enum flags are validated before any request is sent. +- AgentStudio host: derived from --workspace-id by default; point --base-url / DASHSCOPE_BASE_URL (or `auth login --base-url`) at a workspace or pre-release origin such as https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio to override it, and --workspace-id is then not required. + +#### Examples + +```bash +bl security alerts --workspace-id ws-xxx +``` + +```bash +bl security alerts --risk-level high --page-size 50 +``` + +```bash +bl security alerts --asset-type agent --app-name "demo app" --output json +``` + +```bash +bl security alerts --status-list unhandled --status-list handling +``` + +### `bl security overview` + +| Field | Value | +| ------------------ | ----------------------------------------------------------- | +| **Name** | `security overview` | +| **Description** | Show the Agent security protection overview (last 24 hours) | +| **Authentication** | API Key | +| **Usage** | `bl security overview [flags]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | --------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | + +#### Notes + +- Auth: uses DashScope API Key (Bearer token). +- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or config workspace_id. +- Fixed to the last 24 hours; the AgentStudio region is always cn-beijing and is not configurable. +- AgentStudio host: derived from --workspace-id by default; point --base-url / DASHSCOPE_BASE_URL (or `auth login --base-url`) at a workspace or pre-release origin such as https://.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio to override it, and --workspace-id is then not required. + +#### Examples + +```bash +bl security overview --workspace-id ws-xxx +``` + +```bash +bl security overview --workspace-id ws-xxx --output json +``` diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index a3340ec4..58d98ae9 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-finetune metadata: - version: "1.22.0" + version: "1.23.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index c0d47a35..580da612 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-gen metadata: - version: "1.22.0" + version: "1.23.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 97035e1e..907a2614 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-managed-agent metadata: - version: "1.22.0" + version: "1.23.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index c7a2780b..06340bbf 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-protocol metadata: - version: "1.22.0" + version: "1.23.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index 6e3923a8..aafb68ac 100644 --- a/skills/bailian-web-search/SKILL.md +++ b/skills/bailian-web-search/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-web-search metadata: - version: "1.22.0" + version: "1.23.0" requires: bins: ["bl"] description: >-