diff --git a/CHANGELOG.md b/CHANGELOG.md index e7902a2ba..19eb0b1e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and [中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md) +## [2.4.0] - 2026-10-19 + +### Added + +- **Managed Agent memory** — Manage Memory Stores, memory contents and historical versions, and mount memory into Sessions. +- **Workspace Webhooks** — Manage and test Webhooks, rotate signing secrets, and inspect delivery events. +- **Agent resource bindings** — Agent creation supports default Environment and Vault bindings and MCP market servers through `--environment`, `--vault` and `--mcp`. +- **Runtime environment variables** — Session creation, Session runs and Deployment creation accept repeatable `--environment_variables KEY=VALUE`. + ## [2.3.3] - 2026-10-19 ### Fixed diff --git a/CHANGELOG.zh.md b/CHANGELOG.zh.md index c42e31e32..3eaa5871a 100644 --- a/CHANGELOG.zh.md +++ b/CHANGELOG.zh.md @@ -6,6 +6,15 @@ [English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md) +## [2.4.0] - 2026-10-19 + +### 新增 + +- **Managed Agent 记忆管理** —— 支持管理 Memory Store、记忆内容和历史版本,并将记忆挂载到 Session。 +- **工作空间 Webhook** —— 支持管理和测试 Webhook、重置签名密钥及查询投递事件。 +- **Agent 资源绑定** —— 创建 Agent 时可通过 `--environment`、`--vault` 和 `--mcp` 绑定默认 Environment、Vault 及 MCP 市场服务。 +- **运行时环境变量** —— 创建 Session、运行 Session 和创建 Deployment 时可重复传入 `--environment_variables KEY=VALUE`。 + ## [2.3.3] - 2026-10-19 ### 修复 diff --git a/docs/agents/auth-change.md b/docs/agents/auth-change.md index 190a606ed..26d1a79a5 100644 --- a/docs/agents/auth-change.md +++ b/docs/agents/auth-change.md @@ -77,6 +77,14 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx 禁止命令层直接 `readConfigFile` 裸读凭证;Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫。 +### 例外:工作空间 Webhook 的轻量 SDK context + +`managed-agent webhook` 是工作空间 API,使用 `auth: "apiKey"`,不加载 `agents.yaml`、项目 `.env` 或 State,也不走上述项目凭证 bootstrap。命令通过 `managed-agent/_engine/**` 的轻量 context,将 `ctx.client.exportApiCredential()`、解析后的 Base URL 与工作空间作用域仅在内存中交给 SDK,固定 `provider: "bailian"`,并按 runtime 的 `settings.timeout` 构造请求 `AbortSignal`。不得用 YAML 临时文件拼接 Provider,也不得另建一条 HTTP 或凭证解析链。 + +- `--workspace-id` 由命令自声明,读取 `ctx.flags.workspaceId` 后回退 `ctx.settings.workspaceId`;Base URL 和 API Key 复用 CLI 的 flags → env → Profile 优先级。 +- 所有 Webhook 写操作声明 `risk: "high"`;`--dry-run` 在创建 SDK context、读取项目文件或请求远端前返回,不要求 API Key。 +- `create` / `reset-secret` 成功响应中的 `signing_secret` 保留在用户请求的结果中;verbose 诊断和错误输出必须脱敏,不得把成功响应的密钥复制到日志。服务端错误的普通 message 仍原样透传。 + ## 必查清单 ### A. core 层(类型 + 解析) diff --git a/docs/agents/command-flag-change.md b/docs/agents/command-flag-change.md index 8746d86f8..21b3de60a 100644 --- a/docs/agents/command-flag-change.md +++ b/docs/agents/command-flag-change.md @@ -53,6 +53,8 @@ pnpm -F bailian-cli exec tsx src/main.ts --new-flag x # 实测一遍 ## 常见漏点 +- SDK 的通用参数解析器可能接受其他 Provider 的资源类型;产品命令应在鉴权和 dry-run 输出前限制支持的类型,错误文案不得回显资源凭证或任意输入值。 +- 定向创建仅解析新增资源声明;已有项目中的 instructions / Memory 正文已完成文件读取,不能再把它们当作原始文件路径解析。跨资源引用继续交给完整项目的定向 plan 校验。 - 修改请求超时时,检查底层 HTTP 限制:core 的公共 HTTP 请求层仅为已设置超时计时器的请求关闭 `headersTimeout`,由请求 `AbortSignal` 控制响应头等待;保留 `bodyTimeout` 空闲保护,不全局关闭其他请求的超时。仅设置 Agent 默认值会被 Node `fetch` 的逐请求默认值覆盖。流式读取的总时长另由调用方管理,不能据此宣称 `--timeout` 覆盖整个流。 - ✗ 加了 array 型 flag 但没考虑用户可能传多次 - ✗ 改默认值忘记更新 description 里的 "(default: xxx)" 文案 diff --git a/docs/agents/lint-toolchain.md b/docs/agents/lint-toolchain.md index e4dfb5c50..bb68448f5 100644 --- a/docs/agents/lint-toolchain.md +++ b/docs/agents/lint-toolchain.md @@ -37,6 +37,7 @@ - [ ] 检查 workspace 内部依赖在 `dependencies` 里仍是 `"workspace:*"`(不要手改成实际版本号;发布时由 pack/publish 流程解析) - [ ] 升级后跑 `vp check && vp test` - [ ] 升级 `@types/node` 时注意 Node API 变化(如 fs.existsSync 行为) +- [ ] 使用 pnpm dependency patch 时提交补丁、注册配置和 lockfile,并用 frozen install 验证;如果依赖在构建中是 external,workspace 补丁不会随 npm 包传递,发布前必须更新到包含修复的上游版本 ### E. git hooks / pre-commit @@ -66,3 +67,4 @@ node tools/release/check.mjs - ✗ 改 cli/kscli 的 vite config 把 core/runtime/commands 不小心打成 inline,bundle 体积暴涨 - ✗ Oxlint 配置改了但 IDE 缓存还是旧的(IDE 可能要重启 ts server) - ✗ 升级依赖一并升 lockfile,改动量大但没拆 commit +- ✗ 本地源码启动器未设置 `NO_UPDATE_NOTIFIER=1`,旧分支运行成功后触发 npm 全局自动升级;验证时应覆盖普通命令路径,不能只测 `--help` diff --git a/docs/agents/telemetry-change.md b/docs/agents/telemetry-change.md index 600c8302b..8dc0e68fb 100644 --- a/docs/agents/telemetry-change.md +++ b/docs/agents/telemetry-change.md @@ -100,6 +100,8 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网 参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。 +`page` 仅记录有限数值页码;Webhook 等命令使用的不透明字符串游标不得进入本地日志或远端 AEM,即使字符串内容看起来是数字。 + 事件同时写入两处: 1. 本地 `~/.bailian/telemetry.jsonl`:权限 `0600`,超过 5 MB 后重建 diff --git a/packages/cli/package.json b/packages/cli/package.json index 8a09f053c..0ebc7917c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli", - "version": "2.3.3", + "version": "2.4.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 25c26bd24..2edc26490 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -270,6 +270,27 @@ import { managedAgentFileSearch, managedAgentFileDownload, managedAgentFileDelete, + managedAgentMemoryStoreCreate, + managedAgentMemoryStoreList, + managedAgentMemoryStoreGet, + managedAgentMemoryCreate, + managedAgentMemoryList, + managedAgentMemoryGet, + managedAgentMemoryUpdate, + managedAgentMemoryDelete, + managedAgentMemoryVersionList, + managedAgentMemoryVersionGet, + managedAgentMemoryVersionRedact, + managedAgentWebhookCreate, + managedAgentWebhookList, + managedAgentWebhookGet, + managedAgentWebhookUpdate, + managedAgentWebhookDelete, + managedAgentWebhookEnable, + managedAgentWebhookDisable, + managedAgentWebhookTest, + managedAgentWebhookResetSecret, + managedAgentWebhookEvents, sandboxCreate, sandboxOfficialImages, sandboxFileUpload, @@ -566,6 +587,27 @@ export const commands: Record = { "managed-agent file search": managedAgentFileSearch, "managed-agent file download": managedAgentFileDownload, "managed-agent file delete": managedAgentFileDelete, + "managed-agent memory-store create": managedAgentMemoryStoreCreate, + "managed-agent memory-store list": managedAgentMemoryStoreList, + "managed-agent memory-store get": managedAgentMemoryStoreGet, + "managed-agent memory-store memories create": managedAgentMemoryCreate, + "managed-agent memory-store memories list": managedAgentMemoryList, + "managed-agent memory-store memories get": managedAgentMemoryGet, + "managed-agent memory-store memories update": managedAgentMemoryUpdate, + "managed-agent memory-store memories delete": managedAgentMemoryDelete, + "managed-agent memory-store versions list": managedAgentMemoryVersionList, + "managed-agent memory-store versions get": managedAgentMemoryVersionGet, + "managed-agent memory-store versions redact": managedAgentMemoryVersionRedact, + "managed-agent webhook create": managedAgentWebhookCreate, + "managed-agent webhook list": managedAgentWebhookList, + "managed-agent webhook get": managedAgentWebhookGet, + "managed-agent webhook update": managedAgentWebhookUpdate, + "managed-agent webhook delete": managedAgentWebhookDelete, + "managed-agent webhook enable": managedAgentWebhookEnable, + "managed-agent webhook disable": managedAgentWebhookDisable, + "managed-agent webhook test": managedAgentWebhookTest, + "managed-agent webhook reset-secret": managedAgentWebhookResetSecret, + "managed-agent webhook events": managedAgentWebhookEvents, "sandbox create": sandboxCreate, "sandbox official-images": sandboxOfficialImages, "sandbox file upload": sandboxFileUpload, diff --git a/packages/cli/tests/skill-risk-confirmation.test.ts b/packages/cli/tests/skill-risk-confirmation.test.ts index 8843f7864..11f995782 100644 --- a/packages/cli/tests/skill-risk-confirmation.test.ts +++ b/packages/cli/tests/skill-risk-confirmation.test.ts @@ -9,6 +9,7 @@ const scopedCreateCommands = new Set([ "bl managed-agent agent create", "bl managed-agent deployment create", "bl managed-agent environment create", + "bl managed-agent memory-store create", "bl managed-agent skill create", "bl managed-agent vault create", "bl managed-agent vault credential create", diff --git a/packages/commands/package.json b/packages/commands/package.json index 29f5fff4c..785ad393b 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-commands", - "version": "2.3.3", + "version": "2.4.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": { @@ -40,7 +40,7 @@ "check": "vp check" }, "dependencies": { - "@openagentpack/sdk": "0.7.1", + "@openagentpack/sdk": "0.9.0", "bailian-cli-core": "workspace:*", "bailian-cli-runtime": "workspace:*", "boxen": "catalog:", diff --git a/packages/commands/src/commands/managed-agent/_engine/environment-variables.ts b/packages/commands/src/commands/managed-agent/_engine/environment-variables.ts new file mode 100644 index 000000000..042d3aa30 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/environment-variables.ts @@ -0,0 +1,46 @@ +import { BailianError, ExitCode, type FlagsDef } from "bailian-cli-core"; + +export const ENVIRONMENT_VARIABLE_FLAGS = { + // Preserve the API-aligned spelling requested for Session and Deployment commands. + environment_variables: { + type: "array", + valueHint: "", + description: { + "en-US": "Runtime environment variable; repeat for multiple values (last value wins)", + "zh-CN": "运行时环境变量;可重复传入,同名变量以后一次为准", + }, + }, +} satisfies FlagsDef; + +export function parseEnvironmentVariables( + entries: string[] | undefined, +): Record | undefined { + if (!entries?.length) return undefined; + return Object.fromEntries( + entries.map((entry) => { + const separator = entry.indexOf("="); + if (separator < 0) { + throw new BailianError( + "--environment_variables values must use KEY=VALUE format. / --environment_variables 必须使用 KEY=VALUE 格式。", + ExitCode.USAGE, + ); + } + const key = entry.slice(0, separator).trim(); + if (!key) { + throw new BailianError( + "--environment_variables keys must not be empty. / --environment_variables 的键不能为空。", + ExitCode.USAGE, + ); + } + return [key, entry.slice(separator + 1)]; + }), + ); +} + +export function redactEnvironmentVariables( + variables: Record | undefined, +): Record | undefined { + return variables === undefined + ? undefined + : Object.fromEntries(Object.keys(variables).map((key) => [key, "[REDACTED]"])); +} diff --git a/packages/commands/src/commands/managed-agent/_engine/managed-api-context.ts b/packages/commands/src/commands/managed-agent/_engine/managed-api-context.ts new file mode 100644 index 000000000..a5a168ab7 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/managed-api-context.ts @@ -0,0 +1,58 @@ +import { createManagedApiContext, resolveBailianApiBaseUrl, UserError } from "@openagentpack/sdk"; +import { BailianError, type Client, ExitCode } from "bailian-cli-core"; +import { type HostContext, installSdkTransport } from "./transport.ts"; + +export interface ManagedApiHost extends HostContext { + client: Client; + flags: { workspaceId?: string }; +} + +/** Resolve the CLI connection without loading project files or credentials. */ +export function resolveManagedApiBaseUrl(host: ManagedApiHost): string { + const configuredBaseUrl = host.client.url("", () => "") || undefined; + const workspaceId = host.flags.workspaceId ?? host.settings.workspaceId; + if (!configuredBaseUrl && !workspaceId?.trim()) { + throw new BailianError( + "Workspace ID is required when no base URL is configured. / 未配置 Base URL 时必须提供 Workspace ID。", + ExitCode.USAGE, + "Pass --workspace-id, set BAILIAN_WORKSPACE_ID, configure workspace_id, or set --base-url. / 请传入 --workspace-id、设置 BAILIAN_WORKSPACE_ID 或 workspace_id 配置,或通过 --base-url 指定地址。", + ); + } + try { + return resolveBailianApiBaseUrl({ baseUrl: configuredBaseUrl, workspaceId }); + } catch (error) { + if (!(error instanceof UserError)) throw error; + throw new BailianError( + configuredBaseUrl + ? "The configured API base URL is invalid. / 配置的 API Base URL 无效。" + : "Workspace ID must be a valid hostname label. / Workspace ID 必须是有效的主机名标签。", + ExitCode.USAGE, + configuredBaseUrl + ? "Configure a valid HTTP or HTTPS API base URL. / 请配置有效的 HTTP 或 HTTPS API Base URL。" + : "Use a workspace ID such as workspace-example, or set --base-url. / 请使用 workspace-example 这样的有效 Workspace ID,或设置 --base-url。", + ); + } +} + +/** Delegate the authStage-resolved credential to the SDK in memory only. */ +export function buildManagedApiContext(host: ManagedApiHost) { + const credential = host.client.exportApiCredential(); + if (!credential?.token.trim()) { + throw new BailianError( + "A Bailian API key is required. / 必须提供百炼 API Key。", + ExitCode.AUTH, + "Pass --api-key, set DASHSCOPE_API_KEY, or log in with auth login --api-key . / 请传入 --api-key、设置 DASHSCOPE_API_KEY,或使用 auth login --api-key 登录。", + ); + } + const baseUrl = resolveManagedApiBaseUrl(host); + installSdkTransport(host); + return createManagedApiContext({ + providers: { + bailian: { + api_key: credential.token, + base_url: baseUrl, + workspace_id: host.flags.workspaceId ?? host.settings.workspaceId, + }, + }, + }); +} diff --git a/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts b/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts index 21d877eda..692d5ab47 100644 --- a/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts +++ b/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts @@ -24,7 +24,13 @@ import { createFileStateScope } from "./file-state-manager.ts"; import { installSdkTransport } from "./transport.ts"; export type ScopedCreateHost = Pick; -export type ScopedCreateGroup = "agents" | "environments" | "skills" | "vaults" | "deployments"; +export type ScopedCreateGroup = + | "agents" + | "environments" + | "skills" + | "vaults" + | "deployments" + | "memory_stores"; export interface LoadedScopedCreateProject { configPath: string; @@ -54,7 +60,7 @@ export interface ScopedTopLevelCreateInput { host: ScopedCreateHost; project: LoadedScopedCreateProject; group: Exclude; - resourceType: "environment" | "skill" | "vault" | "deployment"; + resourceType: "environment" | "skill" | "vault" | "deployment" | "memory_store"; displayName: string; rawDeclaration: Record; resolvedDeclaration: Record; @@ -104,7 +110,12 @@ export function selectResourceKey(options: { .filter( (resource) => resource.address.provider === options.provider && - options.resourceTypes.includes(resource.address.type), + options.resourceTypes.includes(resource.address.type) && + !( + resource.address.type === "memory_store" && + resource.remote_id && + resource.memory_initialization + ), ) .map((resource) => resource.address.name), ); @@ -161,14 +172,19 @@ export async function loadScopedCreateProject( } export async function resolveCandidateDeclaration(options: { - project: LoadedScopedCreateProject; + project: Pick; group: ScopedCreateGroup; rawDeclaration: Record; }): Promise> { const temporaryKey = "__bailian_cli_create_candidate__"; - const rawConfig = structuredClone(options.project.config) as unknown as Record; - const declarations = (rawConfig[options.group] ?? {}) as Record; - rawConfig[options.group] = { ...declarations, [temporaryKey]: options.rawDeclaration }; + // Existing resources already have resolved file contents. Resolve only the new + // declaration here; scoped planning checks its references against the full project. + const rawConfig = { + version: options.project.config.version, + providers: options.project.config.providers, + defaults: options.project.config.defaults, + [options.group]: { [temporaryKey]: options.rawDeclaration }, + }; const resolved = await withAgentErrors(() => resolveProjectConfigFromObject(rawConfig, { projectName: options.project.projectName, diff --git a/packages/commands/src/commands/managed-agent/_engine/session-resources.ts b/packages/commands/src/commands/managed-agent/_engine/session-resources.ts new file mode 100644 index 000000000..9eed04c9b --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/session-resources.ts @@ -0,0 +1,36 @@ +import { parseSessionResources } from "@openagentpack/sdk"; +import { BailianError, ExitCode, type FlagsDef } from "bailian-cli-core"; +import { withAgentErrors } from "./errors.ts"; +import { parseJsonInputs } from "./scoped-create.ts"; + +export const SESSION_RESOURCE_FLAGS = { + resource: { + type: "array", + valueHint: "", + description: { + "en-US": + "Session resource JSON (repeatable); memory_store uses a YAML key, access and instructions", + "zh-CN": "Session 资源 JSON(可重复);memory_store 使用 YAML key、access 和 instructions", + }, + }, +} satisfies FlagsDef; + +export async function readSessionResources(values?: string[]) { + if (values === undefined) return undefined; + const decoded = await parseJsonInputs(values, "resource"); + if ( + decoded.some( + (resource) => + resource === null || + typeof resource !== "object" || + !("type" in resource) || + resource.type !== "memory_store", + ) + ) { + throw new BailianError( + "Bailian Session resources only support memory_store. / 百炼 Session 资源仅支持 memory_store。", + ExitCode.USAGE, + ); + } + return withAgentErrors(async () => parseSessionResources(decoded)); +} diff --git a/packages/commands/src/commands/managed-agent/agent/create.ts b/packages/commands/src/commands/managed-agent/agent/create.ts index a38dd8697..479269794 100644 --- a/packages/commands/src/commands/managed-agent/agent/create.ts +++ b/packages/commands/src/commands/managed-agent/agent/create.ts @@ -1,6 +1,7 @@ import { stat } from "node:fs/promises"; import { dirname, extname, relative, sep } from "node:path"; import { + type AgentMcpBuildInput, type BackendRuntimeInput, buildAgentDecl, type IStateManager, @@ -100,6 +101,31 @@ const CREATE_FLAGS = { "zh-CN": "内置工具名称(可重复)", }, }, + environment: { + type: "string", + valueHint: "", + description: { + "en-US": + "Existing Environment key from agents.yaml, used as the default Session/Deployment environment", + "zh-CN": "agents.yaml 中已有的 Environment key,作为 Session/Deployment 的默认环境", + }, + }, + vault: { + type: "string", + valueHint: "", + description: { + "en-US": "Existing Vault key from agents.yaml, used as the default Session/Deployment vault", + "zh-CN": "agents.yaml 中已有的 Vault key,作为 Session/Deployment 的默认 Vault", + }, + }, + mcp: { + type: "array", + valueHint: "", + description: { + "en-US": "MCP market server code; all of its tools are enabled (repeatable)", + "zh-CN": "MCP 市场服务 code,启用其全部工具(可重复)", + }, + }, file: { type: "string", valueHint: "", @@ -188,6 +214,35 @@ export function buildAgentSkillRefs( return refs; } +export function buildAgentMcpBindings(serverCodes: string[] | undefined): AgentMcpBuildInput[] { + const seen = new Set(); + const bindings: AgentMcpBuildInput[] = []; + for (const rawCode of serverCodes ?? []) { + const serverCode = rawCode.trim(); + if (!serverCode) { + throw new BailianError("--mcp values must not be empty.", ExitCode.USAGE); + } + if (seen.has(serverCode)) continue; + seen.add(serverCode); + bindings.push({ serverName: serverCode, serverType: "official", defaultEnabled: true }); + } + return bindings; +} + +function assertDeclaredReference( + label: "Environment" | "Vault", + key: string | undefined, + declarations: Record | undefined, + configPath: string, +): void { + if (key === undefined || declarations?.[key] !== undefined) return; + throw new BailianError( + `${label} '${key}' is not declared in ${configPath}.`, + ExitCode.USAGE, + `Create the ${label} first with the managed-agent ${label.toLowerCase()} create command, then pass its YAML key.`, + ); +} + function canonicalJson(value: unknown): string { const normalize = (candidate: unknown): unknown => { if (Array.isArray(candidate)) return candidate.map(normalize); @@ -336,13 +391,14 @@ export default defineCommand({ }, auth: "apiKey", usageArgs: - "--name --model --instructions [--description ] [--skill ...] [--type custom|official] [--skill-dir ...] [--tool ...] [--file ] [--yes]", + "--name --model --instructions [--description ] [--skill ...] [--type custom|official] [--skill-dir ...] [--tool ...] [--environment ] [--vault ] [--mcp ...] [--file ] [--yes]", flags: CREATE_FLAGS, exampleArgs: [ '--name assistant --model qwen3.8-max --instructions "You are helpful."', "--name assistant --model qwen3.8-max --instructions ./prompts/assistant.md --skill skill_abc --yes", "--name slides --model qwen3.8-max --instructions ./prompts/slides.md --skill skill_pptx --type official --yes", "--name reviewer --model qwen3.8-max --instructions ./prompts/reviewer.md --skill-dir ./skills/code-review --yes", + "--name researcher --model qwen3.8-max --instructions ./prompts/researcher.md --environment dev --vault secrets --mcp market-cmgjmcp00074952 --yes", ], notes: [ ...CREDENTIALS_NOTE, @@ -364,6 +420,18 @@ export default defineCommand({ "zh-CN": "--skill-dir 接收本地 Skill 目录或 ZIP,将其写为顶层 custom Skill 声明,并把生成的 YAML key 写入 Agent 的 skills 列表;Skill 与 Agent 按依赖顺序一并创建。--type 只作用于 --skill ID。", }, + { + "en-US": + "--environment and --vault write existing YAML keys into the Agent declaration as default runtime bindings; they are not sent to the Agent API. session create|run and deployment create use them unless those commands pass their own. The referenced Environment and Vault must already be created.", + "zh-CN": + "--environment 与 --vault 把已有的 YAML key 写入 Agent 声明,作为默认运行时绑定,不会发送给 Agent API;session create|run 与 deployment create 未显式传入时使用它们。引用的 Environment 与 Vault 必须已创建。", + }, + { + "en-US": + "--mcp accepts MCP market server codes (see mcp list) and enables all of their tools. To toggle individual tools, edit tools.mcp in YAML and run a full apply.", + "zh-CN": + "--mcp 接收 MCP 市场服务 code(见 mcp list),并启用其全部工具;如需按工具开关,修改 YAML 中的 tools.mcp 后执行全量 apply。", + }, ], validate: (flags) => { if (!flags.name.trim()) return "--name must not be empty."; @@ -376,6 +444,13 @@ export default defineCommand({ if (flags.skillDir?.some((source) => !source.trim())) { return "--skill-dir values must not be empty."; } + if (flags.environment !== undefined && !flags.environment.trim()) { + return "--environment must not be empty."; + } + if (flags.vault !== undefined && !flags.vault.trim()) return "--vault must not be empty."; + if (flags.mcp?.some((serverCode) => !serverCode.trim())) { + return "--mcp values must not be empty."; + } return undefined; }, async run(ctx) { @@ -384,6 +459,21 @@ export default defineCommand({ const file = flags.file ?? "agents.yaml"; const project = await loadScopedCreateProject(ctx, file); const provider = project.provider; + const environmentKey = flags.environment?.trim(); + const vaultKey = flags.vault?.trim(); + assertDeclaredReference( + "Environment", + environmentKey, + project.config.environments, + project.configPath, + ); + assertDeclaredReference("Vault", vaultKey, project.config.vaults, project.configPath); + const mcpBindings = buildAgentMcpBindings(flags.mcp); + const runtimeBindings = { + environment: environmentKey, + vault: vaultKey, + mcp: mcpBindings.map((binding) => binding.serverName), + }; const externalSkillRefs = buildAgentSkillRefs(flags.skill, agentSkillType(flags.type)); const localSkills = await prepareLocalSkillSources(project, flags.skillDir); const skillRefs: AgentSkillDecl[] = [ @@ -396,8 +486,11 @@ export default defineCommand({ description: flags.description, model: flags.model, instructions: flags.instructions, + environment: environmentKey, + vault: vaultKey, provider, builtinTools: flags.tool, + mcp: mcpBindings, }).agent; rawAgent.skills = skillRefs; const candidateConfig = structuredClone(project.config); @@ -468,7 +561,13 @@ export default defineCommand({ (diagnostic) => diagnostic.severity === "error", ); const result = { - agent: { key: agentKey, name: flags.name.trim(), provider, skills: skillRefs }, + agent: { + key: agentKey, + name: flags.name.trim(), + provider, + skills: skillRefs, + ...runtimeBindings, + }, local_skills: localSkills.map((skill) => ({ key: skill.key, name: skill.name, @@ -548,6 +647,7 @@ export default defineCommand({ provider, remote_id: remoteResources.agentId, skills: skillRefs, + ...runtimeBindings, }, local_skills: localSkills.map((skill) => ({ key: skill.key, diff --git a/packages/commands/src/commands/managed-agent/deployment/create.ts b/packages/commands/src/commands/managed-agent/deployment/create.ts index e25b847a2..b6c5a6923 100644 --- a/packages/commands/src/commands/managed-agent/deployment/create.ts +++ b/packages/commands/src/commands/managed-agent/deployment/create.ts @@ -1,6 +1,10 @@ import type { DeploymentDecl } from "@openagentpack/sdk"; import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; +import { + ENVIRONMENT_VARIABLE_FLAGS, + parseEnvironmentVariables, +} from "../_engine/environment-variables.ts"; import { loadScopedCreateProject, parseJsonInputs, @@ -11,6 +15,7 @@ import { } from "../_engine/scoped-create.ts"; const FLAGS = { + ...ENVIRONMENT_VARIABLE_FLAGS, name: { type: "string", valueHint: "", @@ -121,12 +126,14 @@ export default defineCommand({ }, auth: "apiKey", usageArgs: - "--name --agent (--message ... | --event ...) [--agent-version ] [--environment ] [--vault ...] [--resource ...] [--schedule --timezone ] [--description ] [--metadata ...] [--file ] [--yes]", + "--name --agent (--message ... | --event ...) [--agent-version ] [--environment ] [--environment_variables ...] [--vault ...] [--resource ...] [--schedule --timezone ] [--description ] [--metadata ...] [--file ] [--yes]", flags: FLAGS, exampleArgs: [ { - "en-US": '--name Daily --agent assistant --message "Generate the report"', - "zh-CN": '--name Daily --agent assistant --message "生成报告"', + "en-US": + '--name Daily --agent assistant --message "Generate the report" --environment_variables LOG_LEVEL=info', + "zh-CN": + '--name Daily --agent assistant --message "生成报告" --environment_variables LOG_LEVEL=info', }, { "en-US": @@ -138,6 +145,12 @@ export default defineCommand({ notes: [ ...CREDENTIALS_NOTE, ...SCOPED_CREATE_NOTE, + { + "en-US": + "--environment_variables writes string values to the Deployment YAML declaration when --yes is used. Repeat the flag for multiple variables; empty values and values containing = are allowed. Later apply runs manage these values; removing the field or using {} clears them. YAML stores these values; the key `__proto__` is reserved.", + "zh-CN": + "使用 --yes 时,--environment_variables 将字符串值写入 Deployment 的 YAML 声明。可重复传入,允许空值及包含 = 的值。后续 apply 会管理这些值;删除字段或设置为 {} 会清空变量。YAML 会保存这些值;不支持保留键 `__proto__`。", + }, { "en-US": "Initial Events must contain 1-50 user.message/system.message entries. --resource accepts only File Resources in this release.", @@ -151,6 +164,7 @@ export default defineCommand({ }, ], validate: (flags) => { + parseDeploymentEnvironmentVariables(flags.environment_variables); if (!flags.name.trim()) return "--name must not be empty."; if (!flags.agent.trim()) return "--agent must not be empty."; if (flags.message?.length && flags.event?.length) { @@ -189,6 +203,7 @@ export default defineCommand({ agent: ctx.flags.agent, agent_version: ctx.flags.agentVersion, environment: ctx.flags.environment, + environment_variables: parseDeploymentEnvironmentVariables(ctx.flags.environment_variables), vaults: ctx.flags.vault, resources, initial_events: initialEvents, @@ -225,6 +240,19 @@ export default defineCommand({ }, }); +function parseDeploymentEnvironmentVariables( + entries: string[] | undefined, +): Record | undefined { + const variables = parseEnvironmentVariables(entries); + if (variables && Object.hasOwn(variables, "__proto__")) { + throw new BailianError( + "Deployment environment_variables cannot use the reserved key __proto__. / Deployment 环境变量不能使用保留键 __proto__。", + ExitCode.USAGE, + ); + } + return variables; +} + function validateEvents( values: unknown[], ): Array<{ type: "user.message" | "system.message"; content: string }> { diff --git a/packages/commands/src/commands/managed-agent/memory-store/api.ts b/packages/commands/src/commands/managed-agent/memory-store/api.ts new file mode 100644 index 000000000..b5a204769 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/memory-store/api.ts @@ -0,0 +1,496 @@ +import { readFile } from "node:fs/promises"; +import { + createMemory, + deleteMemory, + getMemory, + getMemoryStore, + getMemoryVersion, + listMemories, + listMemoryStores, + listMemoryVersions, + redactMemoryVersion, + updateMemory, +} from "@openagentpack/sdk"; +import { + defineCommand, + detectOutputFormat, + type CommandRisk, + type FlagsDef, + type LocalizedText, +} from "bailian-cli-core"; +import { emitBare, emitResult } from "bailian-cli-runtime"; +import { withStdoutProtected } from "../_engine/console-capture.ts"; +import { withAgentErrors } from "../_engine/errors.ts"; +import { + buildManagedApiContext, + resolveManagedApiBaseUrl, + type ManagedApiHost, +} from "../_engine/managed-api-context.ts"; + +const WORKSPACE_FLAGS = { + workspaceId: { + type: "string", + valueHint: "", + description: { + "en-US": "Workspace ID; optional with a configured Base URL", + "zh-CN": "Workspace ID;已配置 Base URL 时可省略", + }, + }, +} as const satisfies FlagsDef; +const STORE_FLAGS = { + ...WORKSPACE_FLAGS, + storeId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Remote Memory Store ID", "zh-CN": "远端记忆库 ID" }, + }, +} as const satisfies FlagsDef; +const MEMORY_FLAGS = { + ...STORE_FLAGS, + memoryId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Remote Memory ID", "zh-CN": "远端记忆条目 ID" }, + }, +} as const satisfies FlagsDef; +const VERSION_FLAGS = { + ...STORE_FLAGS, + versionId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Remote Memory Version ID", "zh-CN": "远端记忆版本 ID" }, + }, +} as const satisfies FlagsDef; +const PAGE_FLAGS = { + limit: { + type: "number", + valueHint: "", + description: { "en-US": "Page size (1–100)", "zh-CN": "单页数量(1–100)" }, + }, + page: { + type: "string", + valueHint: "", + description: { + "en-US": "Opaque next_page cursor from the previous response", + "zh-CN": "上次响应中的不透明 next_page 游标", + }, + }, +} as const satisfies FlagsDef; +const VIEW_FLAGS = { + view: { + type: "string", + valueHint: "", + choices: ["basic", "full"], + description: { "en-US": "Response view (basic or full)", "zh-CN": "响应视图(basic 或 full)" }, + }, +} as const satisfies FlagsDef; +const CONTENT_FLAGS = { + content: { + type: "string", + valueHint: "", + description: { + "en-US": "Memory content; an empty string is allowed", + "zh-CN": "记忆正文;允许空字符串", + }, + }, + contentFile: { + type: "string", + valueHint: "", + description: { + "en-US": "Read UTF-8 content from a local file; exclusive with --content", + "zh-CN": "从本地文件读取 UTF-8 正文;与 --content 互斥", + }, + }, +} as const satisfies FlagsDef; +const PATH_FLAG = { + path: { + type: "string", + valueHint: "", + description: { "en-US": "Memory file path", "zh-CN": "记忆文件路径" }, + }, +} as const satisfies FlagsDef; +const SHA_FLAGS = { + expectedContentSha256: { + type: "string", + valueHint: "", + description: { + "en-US": "Expected current SHA-256; reject a concurrent content change", + "zh-CN": "预期当前 SHA-256;并发内容变化时拒绝更新", + }, + }, +} as const satisfies FlagsDef; +const API_NOTES: LocalizedText[] = [ + { + "en-US": + "Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents.", + "zh-CN": + "使用 CLI API Key 和工作空间配置调用 SDK;查询记忆库和管理正文无需 agents.yaml 或 State。", + }, + { + "en-US": + "Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged.", + "zh-CN": + "记忆库生命周期仍由 agents.yaml 管理。这些 API 命令使用远端 Store ID;next_page 游标保持原样传回。", + }, +]; +const WRITE_RISK: CommandRisk = { + level: "high", + message: { + "en-US": + "This changes persistent memory content. Review the dry-run request before confirming.", + "zh-CN": "该操作会修改持久化记忆正文。请先核对 dry-run 请求,再确认执行。", + }, +}; + +interface MemoryFlags { + workspaceId?: string; + storeId?: string; + memoryId?: string; + versionId?: string; + limit?: number; + page?: string; + depth?: number; + path?: string; + content?: string; + contentFile?: string; + expectedContentSha256?: string; + createdAtGte?: string; + createdAtLte?: string; +} +function validateFlags(flags: MemoryFlags, maxLimit = 100): string | undefined { + for (const [key, value] of Object.entries({ + "workspace-id": flags.workspaceId, + "store-id": flags.storeId, + "memory-id": flags.memoryId, + "version-id": flags.versionId, + page: flags.page, + path: flags.path, + "content-file": flags.contentFile, + })) { + if (value !== undefined && !value.trim()) + return `--${key} must not be empty. / --${key} 不能为空。`; + } + if ( + flags.limit !== undefined && + (!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > maxLimit) + ) + return `--limit must be an integer from 1 to ${maxLimit}. / --limit 必须为 1–${maxLimit} 的整数。`; + if (flags.depth !== undefined && (!Number.isInteger(flags.depth) || flags.depth < 0)) + return "--depth must be a non-negative integer. / --depth 必须为非负整数。"; + if (flags.content !== undefined && flags.contentFile !== undefined) + return "--content and --content-file are mutually exclusive. / --content 与 --content-file 互斥。"; + if ( + flags.expectedContentSha256 !== undefined && + !/^[a-fA-F0-9]{64}$/.test(flags.expectedContentSha256) + ) + return "--expected-content-sha256 requires 64 hexadecimal characters. / --expected-content-sha256 必须为 64 位十六进制字符串。"; + for (const value of [flags.createdAtGte, flags.createdAtLte]) { + if (value !== undefined && (!value.trim() || !Number.isFinite(Date.parse(value)))) + return "Created-at filters must be valid timestamps. / created-at 筛选条件必须为有效时间戳。"; + } + if ( + flags.createdAtGte && + flags.createdAtLte && + Date.parse(flags.createdAtGte) > Date.parse(flags.createdAtLte) + ) + return "--created-at-gte must not be after --created-at-lte. / --created-at-gte 不能晚于 --created-at-lte。"; + return undefined; +} +async function readContent(flags: MemoryFlags): Promise { + return flags.contentFile !== undefined ? readFile(flags.contentFile, "utf8") : flags.content; +} +type ApiContext = ReturnType; +async function runOperation( + host: ManagedApiHost, + operation: string, + request: Record, + invoke: (context: ApiContext) => Promise, +): Promise { + const format = detectOutputFormat(host.settings.output); + const result = host.settings.dryRun + ? { operation, provider: "bailian", base_url: resolveManagedApiBaseUrl(host), ...request } + : await withAgentErrors(() => withStdoutProtected(() => invoke(buildManagedApiContext(host)))); + if (format === "json") emitResult(result, format); + else emitBare(JSON.stringify(result, null, 2)); +} + +export const managedAgentMemoryStoreList = defineCommand({ + description: { "en-US": "List Memory Stores", "zh-CN": "列出记忆库" }, + auth: "apiKey", + flags: { + ...WORKSPACE_FLAGS, + ...PAGE_FLAGS, + includeArchived: { + type: "switch", + description: { "en-US": "Include archived stores", "zh-CN": "包含已归档记忆库" }, + }, + }, + usageArgs: "[--workspace-id ] [--limit ] [--page ] [--include-archived]", + exampleArgs: ["--workspace-id ws-example --output json"], + notes: API_NOTES, + validate: validateFlags, + async run(ctx) { + const options = { + limit: ctx.flags.limit, + page: ctx.flags.page, + include_archived: ctx.flags.includeArchived, + }; + await runOperation(ctx, "memory-store.list", { options }, (context) => + listMemoryStores(context.providers, "bailian", options), + ); + }, +}); +export const managedAgentMemoryStoreGet = defineCommand({ + description: { "en-US": "Get a Memory Store", "zh-CN": "获取记忆库" }, + auth: "apiKey", + flags: STORE_FLAGS, + usageArgs: "--store-id [--workspace-id ]", + exampleArgs: ["--workspace-id ws-example --store-id memstore_example"], + notes: API_NOTES, + validate: validateFlags, + async run(ctx) { + await runOperation(ctx, "memory-store.get", { store_id: ctx.flags.storeId }, (context) => + getMemoryStore(context.providers, "bailian", ctx.flags.storeId), + ); + }, +}); +export const managedAgentMemoryCreate = defineCommand({ + description: { "en-US": "Create a memory file", "zh-CN": "创建记忆文件" }, + auth: "apiKey", + risk: WRITE_RISK, + flags: { ...STORE_FLAGS, ...CONTENT_FLAGS, path: { ...PATH_FLAG.path, required: true } }, + usageArgs: "--store-id --path (--content | --content-file )", + exampleArgs: ["--store-id memstore_example --path notes.md --content-file ./notes.md --dry-run"], + notes: API_NOTES, + validate: (flags) => + validateFlags(flags) ?? + (flags.content === undefined && flags.contentFile === undefined + ? "Pass --content or --content-file. / 请提供 --content 或 --content-file。" + : undefined), + async run(ctx) { + const request = { path: ctx.flags.path, content: (await readContent(ctx.flags))! }; + await runOperation( + ctx, + "memory-store.memories.create", + { store_id: ctx.flags.storeId, request }, + (context) => createMemory(context.providers, "bailian", ctx.flags.storeId, request), + ); + }, +}); +export const managedAgentMemoryList = defineCommand({ + description: { "en-US": "List memory files and prefixes", "zh-CN": "列出记忆文件和目录前缀" }, + auth: "apiKey", + flags: { + ...STORE_FLAGS, + ...PAGE_FLAGS, + limit: { + ...PAGE_FLAGS.limit, + description: { "en-US": "Page size (1–20)", "zh-CN": "单页数量(1–20)" }, + }, + ...VIEW_FLAGS, + pathPrefix: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by memory path prefix", "zh-CN": "按记忆路径前缀筛选" }, + }, + depth: { + type: "number", + valueHint: "", + description: { "en-US": "Directory listing depth", "zh-CN": "目录列举深度" }, + }, + }, + usageArgs: + "--store-id [--page ] [--limit ] [--path-prefix ] [--depth ] [--view ]", + exampleArgs: ["--store-id memstore_example --path-prefix /notes/ --view full"], + notes: API_NOTES, + validate: (flags) => validateFlags(flags, 20), + async run(ctx) { + const options = { + limit: ctx.flags.limit, + page: ctx.flags.page, + path_prefix: ctx.flags.pathPrefix, + depth: ctx.flags.depth, + view: ctx.flags.view, + }; + await runOperation( + ctx, + "memory-store.memories.list", + { store_id: ctx.flags.storeId, options }, + (context) => listMemories(context.providers, "bailian", ctx.flags.storeId, options), + ); + }, +}); +export const managedAgentMemoryGet = defineCommand({ + description: { "en-US": "Get a memory file", "zh-CN": "获取记忆文件" }, + auth: "apiKey", + flags: MEMORY_FLAGS, + usageArgs: "--store-id --memory-id ", + exampleArgs: ["--store-id memstore_example --memory-id mem_example"], + notes: API_NOTES, + validate: validateFlags, + async run(ctx) { + await runOperation( + ctx, + "memory-store.memories.get", + { store_id: ctx.flags.storeId, memory_id: ctx.flags.memoryId }, + (context) => getMemory(context.providers, "bailian", ctx.flags.storeId, ctx.flags.memoryId), + ); + }, +}); +export const managedAgentMemoryUpdate = defineCommand({ + description: { + "en-US": "Update memory content with an optional SHA-256 precondition", + "zh-CN": "更新记忆正文,可使用 SHA-256 并发前置条件", + }, + auth: "apiKey", + risk: WRITE_RISK, + flags: { ...MEMORY_FLAGS, ...CONTENT_FLAGS, ...SHA_FLAGS }, + usageArgs: + "--store-id --memory-id (--content | --content-file ) [--expected-content-sha256 ]", + exampleArgs: [ + "--store-id memstore_example --memory-id mem_example --content-file ./notes.md --dry-run", + ], + notes: API_NOTES, + validate: (flags) => + validateFlags(flags) ?? + (flags.content === undefined && flags.contentFile === undefined + ? "Pass --content or --content-file. / 请提供 --content 或 --content-file。" + : undefined), + async run(ctx) { + const request = { + content: await readContent(ctx.flags), + ...(ctx.flags.expectedContentSha256 !== undefined + ? { expected_content_sha256: ctx.flags.expectedContentSha256 } + : {}), + }; + await runOperation( + ctx, + "memory-store.memories.update", + { store_id: ctx.flags.storeId, memory_id: ctx.flags.memoryId, request }, + (context) => + updateMemory(context.providers, "bailian", ctx.flags.storeId, ctx.flags.memoryId, request), + ); + }, +}); +export const managedAgentMemoryDelete = defineCommand({ + description: { "en-US": "Delete a memory file", "zh-CN": "删除记忆文件" }, + auth: "apiKey", + risk: WRITE_RISK, + flags: MEMORY_FLAGS, + usageArgs: "--store-id --memory-id ", + exampleArgs: ["--store-id memstore_example --memory-id mem_example --dry-run"], + notes: API_NOTES, + validate: validateFlags, + async run(ctx) { + await runOperation( + ctx, + "memory-store.memories.delete", + { store_id: ctx.flags.storeId, memory_id: ctx.flags.memoryId }, + (context) => + deleteMemory(context.providers, "bailian", ctx.flags.storeId, ctx.flags.memoryId), + ); + }, +}); +export const managedAgentMemoryVersionList = defineCommand({ + description: { "en-US": "List Memory Store versions", "zh-CN": "列出记忆库版本" }, + auth: "apiKey", + flags: { + ...STORE_FLAGS, + ...PAGE_FLAGS, + ...VIEW_FLAGS, + memoryId: { ...MEMORY_FLAGS.memoryId, required: false }, + operation: { + type: "string", + valueHint: "", + choices: ["created", "modified", "deleted"] as const, + description: { "en-US": "Filter by version operation", "zh-CN": "按版本操作筛选" }, + }, + createdAtGte: { + type: "string", + valueHint: "", + description: { + "en-US": "Inclusive creation timestamp lower bound", + "zh-CN": "创建时间下限(含)", + }, + }, + createdAtLte: { + type: "string", + valueHint: "", + description: { + "en-US": "Inclusive creation timestamp upper bound", + "zh-CN": "创建时间上限(含)", + }, + }, + }, + usageArgs: + "--store-id [--memory-id ] [--operation ] [--created-at-gte ] [--created-at-lte ] [--page ] [--limit ] [--view ]", + exampleArgs: ["--store-id memstore_example --operation modified --view full"], + notes: API_NOTES, + validate: validateFlags, + async run(ctx) { + const options = { + limit: ctx.flags.limit, + page: ctx.flags.page, + view: ctx.flags.view, + memory_id: ctx.flags.memoryId, + operation: ctx.flags.operation, + "created_at.gte": ctx.flags.createdAtGte, + "created_at.lte": ctx.flags.createdAtLte, + }; + await runOperation( + ctx, + "memory-store.versions.list", + { store_id: ctx.flags.storeId, options }, + (context) => listMemoryVersions(context.providers, "bailian", ctx.flags.storeId, options), + ); + }, +}); +export const managedAgentMemoryVersionGet = defineCommand({ + description: { "en-US": "Get a memory version", "zh-CN": "获取记忆版本" }, + auth: "apiKey", + flags: VERSION_FLAGS, + usageArgs: "--store-id --version-id ", + exampleArgs: ["--store-id memstore_example --version-id memver_example"], + notes: API_NOTES, + validate: validateFlags, + async run(ctx) { + await runOperation( + ctx, + "memory-store.versions.get", + { store_id: ctx.flags.storeId, version_id: ctx.flags.versionId }, + (context) => + getMemoryVersion(context.providers, "bailian", ctx.flags.storeId, ctx.flags.versionId), + ); + }, +}); +export const managedAgentMemoryVersionRedact = defineCommand({ + description: { + "en-US": "Permanently redact a memory version's content", + "zh-CN": "永久擦除记忆版本正文", + }, + auth: "apiKey", + risk: { + level: "high", + message: { + "en-US": "This permanently redacts the selected version's content and cannot be undone.", + "zh-CN": "该操作将永久擦除指定版本正文,无法撤销。", + }, + }, + flags: VERSION_FLAGS, + usageArgs: "--store-id --version-id ", + exampleArgs: ["--store-id memstore_example --version-id memver_example --dry-run"], + notes: API_NOTES, + validate: validateFlags, + async run(ctx) { + await runOperation( + ctx, + "memory-store.versions.redact", + { store_id: ctx.flags.storeId, version_id: ctx.flags.versionId }, + (context) => + redactMemoryVersion(context.providers, "bailian", ctx.flags.storeId, ctx.flags.versionId), + ); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/memory-store/create.ts b/packages/commands/src/commands/managed-agent/memory-store/create.ts new file mode 100644 index 000000000..892f56d72 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/memory-store/create.ts @@ -0,0 +1,136 @@ +import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core"; +import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; +import { + loadScopedCreateProject, + parseJsonInputs, + parseMetadata, + resolveCandidateDeclaration, + runScopedTopLevelCreate, + SCOPED_CREATE_NOTE, +} from "../_engine/scoped-create.ts"; + +const FLAGS = { + name: { + type: "string", + valueHint: "", + required: true, + description: { + "en-US": "Memory Store display name; generates a YAML key", + "zh-CN": "Memory Store 显示名称;自动生成 YAML key", + }, + }, + description: { + type: "string", + valueHint: "", + description: { + "en-US": "Store description (default: empty)", + "zh-CN": "记忆库描述(默认:空)", + }, + }, + metadata: { + type: "array", + valueHint: "", + description: { "en-US": "Metadata entry (repeatable)", "zh-CN": "Metadata 条目(可重复)" }, + }, + entry: { + type: "array", + valueHint: "", + description: { + "en-US": "Initial entry JSON with key and content (repeatable)", + "zh-CN": "含 key、content 的初始化条目 JSON(可重复)", + }, + }, + file: { + type: "string", + valueHint: "", + description: { + "en-US": "Config file path (default: agents.yaml)", + "zh-CN": "配置文件路径(默认:agents.yaml)", + }, + }, + yes: { + type: "switch", + description: { + "en-US": "Write YAML and run the scoped remote create", + "zh-CN": "写入 YAML 并执行定向远端创建", + }, + }, +} satisfies FlagsDef; + +export default defineCommand({ + description: { + "en-US": "Declare and create a Memory Store through scoped YAML apply", + "zh-CN": "通过定向 YAML Apply 声明并创建记忆库", + }, + auth: "apiKey", + flags: FLAGS, + usageArgs: + "--name [--description ] [--metadata ...] [--entry ...] [--file ] [--yes]", + exampleArgs: [ + "--name Notes", + { + "en-US": '--name Notes --entry \'{"key":"notes.md","content":"Initial notes"}\' --dry-run', + "zh-CN": '--name Notes --entry \'{"key":"notes.md","content":"初始笔记"}\' --dry-run', + }, + ], + notes: [ + ...CREDENTIALS_NOTE, + ...SCOPED_CREATE_NOTE, + { + "en-US": + "Store lifecycle is managed by agents.yaml. Entries initialize new stores; existing content is preserved unless entries_mode: sync is explicitly configured.", + "zh-CN": + "记忆库生命周期由 agents.yaml 管理。entries 仅初始化新库;只有显式配置 entries_mode: sync 才持续同步已有正文。", + }, + ], + validate: (flags) => + !flags.name.trim() ? "--name must not be empty. / --name 不能为空。" : undefined, + async run(ctx) { + const entries = (await parseJsonInputs(ctx.flags.entry, "entry")).map((value) => { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new BailianError( + "Each --entry must be a JSON object. / 每个 --entry 必须为 JSON 对象。", + ExitCode.USAGE, + ); + } + const entry = value as Record; + if (typeof entry.key !== "string" || !entry.key.trim() || typeof entry.content !== "string") { + throw new BailianError( + "Each --entry requires a non-empty key and string content. / 每个 --entry 必须包含非空 key 和字符串 content。", + ExitCode.USAGE, + ); + } + return { key: entry.key, content: entry.content }; + }); + const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml"); + const rawDeclaration: Record = { + name: ctx.flags.name.trim(), + description: ctx.flags.description ?? "", + provider: project.provider, + metadata: parseMetadata(ctx.flags.metadata), + ...(entries.length ? { entries } : {}), + }; + const resolvedDeclaration = await resolveCandidateDeclaration({ + project, + group: "memory_stores", + rawDeclaration, + }); + await runScopedTopLevelCreate({ + host: ctx, + project, + group: "memory_stores", + resourceType: "memory_store", + displayName: ctx.flags.name.trim(), + rawDeclaration, + resolvedDeclaration, + existingDeclarations: (project.config.memory_stores ?? {}) as unknown as Record< + string, + Record + >, + effectiveName: (key, declaration) => + typeof declaration.name === "string" ? declaration.name : key, + fallbackKey: "memory-store", + yes: ctx.flags.yes, + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/session-create.ts b/packages/commands/src/commands/managed-agent/session-create.ts index ce921cd80..111a9fba3 100644 --- a/packages/commands/src/commands/managed-agent/session-create.ts +++ b/packages/commands/src/commands/managed-agent/session-create.ts @@ -4,9 +4,17 @@ import { createSessionForAgent } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; +import { + parseEnvironmentVariables, + redactEnvironmentVariables, + ENVIRONMENT_VARIABLE_FLAGS, +} from "./_engine/environment-variables.ts"; +import { readSessionResources, SESSION_RESOURCE_FLAGS } from "./_engine/session-resources.ts"; import { parseMemoryStores } from "./_engine/session-render.ts"; const SESSION_CREATE_FLAGS = { + ...ENVIRONMENT_VARIABLE_FLAGS, + ...SESSION_RESOURCE_FLAGS, file: { type: "string", valueHint: "", @@ -40,8 +48,8 @@ const SESSION_CREATE_FLAGS = { type: "string", valueHint: "", description: { - "en-US": "Override agent's memory stores (comma-separated)", - "zh-CN": "覆盖 Agent 的 Memory Store(以逗号分隔)", + "en-US": "Override memory stores with comma-separated YAML keys", + "zh-CN": "使用逗号分隔的 YAML key 覆盖 Memory Store", }, }, title: { @@ -54,9 +62,12 @@ const SESSION_CREATE_FLAGS = { export default defineCommand({ description: { "en-US": "Create a new session for an agent", "zh-CN": "为 Agent 创建新 Session" }, auth: "apiKey", - usageArgs: "[--agent ] [--environment ] [--title ] [--file <path>]", + usageArgs: + "[--agent <name>] [--environment <name>] [--title <title>] [--environment_variables <KEY=VALUE>...] [--memory-stores <keys>] [--resource <json|@path>...] [--file <path>]", flags: SESSION_CREATE_FLAGS, exampleArgs: [ + "--agent assistant --environment_variables LOG_LEVEL=info --environment_variables FEATURE_FLAG=on", + '--agent assistant --resource \'{"type":"memory_store","memory_store":"notes","access":"read_only"}\'', "", "--agent assistant", { @@ -65,10 +76,16 @@ export default defineCommand({ }, ], notes: CREDENTIALS_NOTE, + async validate(flags) { + parseEnvironmentVariables(flags.environment_variables); + await readSessionResources(flags.resource); + }, async run(ctx) { const { settings, flags } = ctx; const format = detectOutputFormat(settings.output); const file = flags.file ?? "agents.yaml"; + const environmentVariables = parseEnvironmentVariables(flags.environment_variables); + const resources = await readSessionResources(flags.resource); if (settings.dryRun) { emitResult( @@ -77,8 +94,10 @@ export default defineCommand({ agent: flags.agent ?? "auto", provider: "bailian", environment: flags.environment, + environment_variables: redactEnvironmentVariables(environmentVariables), vault: flags.vault, memory_stores: parseMemoryStores(flags.memoryStores), + resources, title: flags.title, }, config_file: file, @@ -95,8 +114,10 @@ export default defineCommand({ agent: flags.agent, provider: "bailian", environment: flags.environment, + environmentVariables, vault: flags.vault, memoryStores: parseMemoryStores(flags.memoryStores), + resources, title: flags.title, }); }), diff --git a/packages/commands/src/commands/managed-agent/session-debug.ts b/packages/commands/src/commands/managed-agent/session-debug.ts index c41af4d13..8d96dd332 100644 --- a/packages/commands/src/commands/managed-agent/session-debug.ts +++ b/packages/commands/src/commands/managed-agent/session-debug.ts @@ -17,6 +17,7 @@ import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; import { writeOutputFile } from "./_engine/output-file.ts"; import { fetchAllPages } from "./_engine/pagination.ts"; +import { redactEnvironmentVariables } from "./_engine/environment-variables.ts"; const SESSION_ID_FLAG = { sessionId: { @@ -145,10 +146,17 @@ export function redactSensitiveValues(value: unknown): unknown { if (typeof value === "string") return redactSensitiveText(value); if (!value || typeof value !== "object") return value; return Object.fromEntries( - Object.entries(value).map(([key, entry]) => [ - key, - SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactSensitiveValues(entry), - ]), + Object.entries(value).map(([key, entry]) => { + if ( + key === "environment_variables" && + entry !== null && + typeof entry === "object" && + !Array.isArray(entry) + ) { + return [key, redactEnvironmentVariables(entry as Record<string, unknown>)]; + } + return [key, SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactSensitiveValues(entry)]; + }), ); } diff --git a/packages/commands/src/commands/managed-agent/session-run.ts b/packages/commands/src/commands/managed-agent/session-run.ts index 95bded48c..089a8682f 100644 --- a/packages/commands/src/commands/managed-agent/session-run.ts +++ b/packages/commands/src/commands/managed-agent/session-run.ts @@ -4,6 +4,12 @@ import { startSessionRun, startSessionRunPolling } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; import { withStdoutProtected } from "./_engine/console-capture.ts"; import { withAgentErrors } from "./_engine/errors.ts"; +import { + parseEnvironmentVariables, + redactEnvironmentVariables, + ENVIRONMENT_VARIABLE_FLAGS, +} from "./_engine/environment-variables.ts"; +import { readSessionResources, SESSION_RESOURCE_FLAGS } from "./_engine/session-resources.ts"; import { parseMemoryStores, renderCollectedEvents, @@ -11,6 +17,8 @@ import { } from "./_engine/session-render.ts"; const SESSION_RUN_FLAGS = { + ...ENVIRONMENT_VARIABLE_FLAGS, + ...SESSION_RESOURCE_FLAGS, prompt: { type: "string", valueHint: "<text>", @@ -50,8 +58,8 @@ const SESSION_RUN_FLAGS = { type: "string", valueHint: "<names>", description: { - "en-US": "Override agent's memory stores (comma-separated)", - "zh-CN": "覆盖 Agent 的 Memory Store(以逗号分隔)", + "en-US": "Override memory stores with comma-separated YAML keys", + "zh-CN": "使用逗号分隔的 YAML key 覆盖 Memory Store", }, }, title: { @@ -74,9 +82,20 @@ export default defineCommand({ "zh-CN": "创建 Session、发送消息并流式输出响应", }, auth: "apiKey", - usageArgs: "--prompt <text> [--agent <name>] [--no-stream] [--file <path>]", + usageArgs: + "--prompt <text> [--agent <name>] [--no-stream] [--environment_variables <KEY=VALUE>...] [--memory-stores <keys>] [--resource <json|@path>...] [--file <path>]", flags: SESSION_RUN_FLAGS, exampleArgs: [ + { + "en-US": '--agent assistant --environment_variables LOG_LEVEL=info --prompt "hello"', + "zh-CN": '--agent assistant --environment_variables LOG_LEVEL=info --prompt "你好"', + }, + { + "en-US": + '--agent assistant --resource \'{"type":"memory_store","memory_store":"notes","access":"read_only"}\' --prompt "hello"', + "zh-CN": + '--agent assistant --resource \'{"type":"memory_store","memory_store":"notes","access":"read_only"}\' --prompt "你好"', + }, { "en-US": '--prompt "hello"', "zh-CN": '--prompt "你好"', @@ -95,18 +114,26 @@ export default defineCommand({ "--output json 输出一个 Envelope:{ session_id, provider, agent, events }。读取 session_id 后可串联调用 `session send/get/events/delete`。", }, ], + async validate(flags) { + parseEnvironmentVariables(flags.environment_variables); + await readSessionResources(flags.resource); + }, async run(ctx) { const { settings, flags } = ctx; const format = detectOutputFormat(settings.output); const file = flags.file ?? "agents.yaml"; + const environmentVariables = parseEnvironmentVariables(flags.environment_variables); + const resources = await readSessionResources(flags.resource); const asJson = format === "json"; const runOptions = { agent: flags.agent, provider: "bailian", environment: flags.environment, + environmentVariables, vault: flags.vault, memoryStores: parseMemoryStores(flags.memoryStores), + resources, title: flags.title, }; @@ -118,8 +145,10 @@ export default defineCommand({ agent: flags.agent ?? "auto", provider: "bailian", environment: flags.environment, + environment_variables: redactEnvironmentVariables(environmentVariables), vault: flags.vault, memory_stores: runOptions.memoryStores, + resources, title: flags.title, mode: flags.noStream ? "polling" : "streaming", }, diff --git a/packages/commands/src/commands/managed-agent/webhook.ts b/packages/commands/src/commands/managed-agent/webhook.ts new file mode 100644 index 000000000..20c91e336 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/webhook.ts @@ -0,0 +1,513 @@ +import { + createRemoteWebhookEndpoint, + deleteRemoteWebhookEndpoint, + disableRemoteWebhookEndpoint, + enableRemoteWebhookEndpoint, + getRemoteWebhookEndpoint, + listRemoteWebhookEndpoints, + listRemoteWebhookEvents, + resetRemoteWebhookSecret, + testRemoteWebhookEndpoint, + updateRemoteWebhookEndpoint, + type WebhookRequestOptions, +} from "@openagentpack/sdk"; +import { + BailianError, + defineCommand, + detectOutputFormat, + ExitCode, + type CommandRisk, + type FlagsDef, + type LocalizedText, +} from "bailian-cli-core"; +import { emitBare, emitResult } from "bailian-cli-runtime"; +import { withStdoutProtected } from "./_engine/console-capture.ts"; +import { withAgentErrors } from "./_engine/errors.ts"; +import { + buildManagedApiContext, + type ManagedApiHost, + resolveManagedApiBaseUrl, +} from "./_engine/managed-api-context.ts"; + +const WORKSPACE_FLAG = { + workspaceId: { + type: "string", + valueHint: "<id>", + description: { + "en-US": "Workspace ID; optional when a base URL is configured", + "zh-CN": "Workspace ID;已配置 Base URL 时可省略", + }, + }, +} satisfies FlagsDef; + +const WEBHOOK_ID_FLAGS = { + ...WORKSPACE_FLAG, + webhookId: { + type: "string", + valueHint: "<wep_id>", + description: { "en-US": "Webhook endpoint ID", "zh-CN": "Webhook 端点 ID" }, + required: true, + }, +} satisfies FlagsDef; + +const WEBHOOK_INPUT_FLAGS = { + url: { + type: "string", + valueHint: "<url>", + description: { + "en-US": "Public HTTP or HTTPS callback URL", + "zh-CN": "公网 HTTP 或 HTTPS 回调地址", + }, + }, + event: { + type: "array", + valueHint: "<type>", + description: { + "en-US": "Named event subscription; repeat for multiple events, wildcards are unsupported", + "zh-CN": "具名订阅事件;可重复传入多个事件,不支持通配符", + }, + }, + description: { + type: "string", + valueHint: "<text>", + description: { + "en-US": "Description, up to 256 characters; an empty string clears it on update", + "zh-CN": "描述,最多 256 个字符;更新时传空字符串可清空", + }, + }, +} satisfies FlagsDef; + +const WEBHOOK_NOTES: LocalizedText[] = [ + { + "en-US": + "Manages workspace-level Webhooks directly; no agents.yaml or local state is required.", + "zh-CN": "直接管理工作空间级 Webhook;无需 agents.yaml 或本地状态文件。", + }, + { + "en-US": + "Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only.", + "zh-CN": + "鉴权:--api-key > DASHSCOPE_API_KEY > 当前 Profile 的 API Key。凭证仅在内存中注入 SDK。", + }, + { + "en-US": + "Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint.", + "zh-CN": + "Base URL:--base-url > DASHSCOPE_BASE_URL > Profile 的 base_url。未配置地址时,按 --workspace-id > BAILIAN_WORKSPACE_ID > 配置项 workspace_id 选择 cn-beijing 端点。", + }, +]; + +const SECRET_NOTE: LocalizedText = { + "en-US": + "Signing Secret is returned only on create/reset. Save it from this successful response; the CLI does not persist it.", + "zh-CN": "Signing Secret 仅在创建或重置时返回。请从本次成功响应中保存;CLI 不会自动持久化。", +}; + +interface WebhookFlags { + webhookId?: string; + workspaceId?: string; + url?: string; + event?: string[]; + description?: string; + limit?: number; + page?: string; +} + +function validateWebhookFlags(flags: WebhookFlags): string | undefined { + if (flags.webhookId !== undefined && !flags.webhookId.trim()) { + return "--webhook-id must not be empty. / --webhook-id 不能为空。"; + } + if (flags.workspaceId !== undefined && !flags.workspaceId.trim()) { + return "--workspace-id must not be empty. / --workspace-id 不能为空。"; + } + if (flags.url !== undefined) { + try { + const callbackUrl = new URL(flags.url); + if (callbackUrl.protocol !== "http:" && callbackUrl.protocol !== "https:") { + return "--url must use HTTP or HTTPS. / --url 必须使用 HTTP 或 HTTPS。"; + } + } catch { + return "--url must be a valid HTTP or HTTPS URL. / --url 必须是有效的 HTTP 或 HTTPS 地址。"; + } + } + if ( + flags.event !== undefined && + (flags.event.length === 0 || flags.event.some((event) => !event.trim() || event.includes("*"))) + ) { + return "--event requires non-empty named events without wildcards. / --event 必须是非空具名事件,不能使用通配符。"; + } + if (flags.description !== undefined && Array.from(flags.description).length > 256) { + return "--description must contain at most 256 characters. / --description 最多包含 256 个字符。"; + } + if ( + flags.limit !== undefined && + (!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 100) + ) { + return "--limit must be an integer between 1 and 100. / --limit 必须是 1 到 100 之间的整数。"; + } + if (flags.page !== undefined && !flags.page.trim()) { + return "--page must be a non-empty pagination cursor. / --page 必须是非空分页游标。"; + } + return undefined; +} + +type ManagedApiContext = ReturnType<typeof buildManagedApiContext>; + +interface WebhookOperation { + operation: string; + webhookId?: string; + request?: { url?: string; events?: string[]; description?: string }; + limit?: number; + page?: string; + returnsSecret?: boolean; + invoke: (context: ManagedApiContext, options: WebhookRequestOptions) => Promise<unknown>; +} + +function emitWebhookResult(result: unknown, host: ManagedApiHost): void { + const format = detectOutputFormat(host.settings.output); + if (format === "json") emitResult(result, format); + else emitBare(JSON.stringify(result, null, 2)); +} + +async function runWebhookOperation( + host: ManagedApiHost, + operation: WebhookOperation, +): Promise<void> { + const baseUrl = resolveManagedApiBaseUrl(host); + if (host.settings.dryRun) { + emitWebhookResult( + { + operation: `webhook.${operation.operation}`, + provider: "bailian", + base_url: baseUrl, + ...(operation.webhookId !== undefined ? { webhook_id: operation.webhookId } : {}), + ...(operation.request !== undefined ? { request: operation.request } : {}), + ...(operation.limit !== undefined ? { limit: operation.limit } : {}), + ...(operation.page !== undefined ? { page: operation.page } : {}), + }, + host, + ); + return; + } + + const timeoutSeconds = host.settings.timeout; + if ( + !Number.isFinite(timeoutSeconds) || + timeoutSeconds <= 0 || + timeoutSeconds * 1000 > 2_147_483_647 + ) { + throw new BailianError( + "--timeout must be greater than 0 and at most 2147483.647 seconds. / --timeout 必须大于 0 且不超过 2147483.647 秒。", + ExitCode.USAGE, + ); + } + const response = await withAgentErrors(() => + withStdoutProtected(async () => { + const context = buildManagedApiContext(host); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), Math.ceil(timeoutSeconds * 1000)); + try { + return await operation.invoke(context, { provider: "bailian", signal: controller.signal }); + } catch (error) { + if (controller.signal.aborted) { + throw new BailianError( + `Webhook request timed out after ${timeoutSeconds} seconds. / Webhook 请求在 ${timeoutSeconds} 秒后超时。`, + ExitCode.TIMEOUT, + "Check the endpoint state before retrying a mutation; increase --timeout if needed. / 重试变更操作前请先检查端点状态;可按需增大 --timeout。", + ); + } + throw error; + } finally { + clearTimeout(timer); + } + }), + ); + emitWebhookResult(response, host); + if (operation.returnsSecret) { + process.stderr.write( + "Signing Secret is shown only in this successful create/reset response; save it now. / Signing Secret 仅在本次创建或重置成功响应中返回,请立即保存。\n", + ); + } +} + +export const managedAgentWebhookCreate = defineCommand({ + description: { "en-US": "Create a Webhook endpoint", "zh-CN": "创建 Webhook 端点" }, + auth: "apiKey", + risk: { + level: "high", + message: { + "en-US": + "This creates a Webhook and starts delivering subscribed events to its callback URL.", + "zh-CN": "该操作会创建 Webhook,并开始向回调地址投递订阅事件。", + }, + }, + flags: { + ...WORKSPACE_FLAG, + ...WEBHOOK_INPUT_FLAGS, + url: { ...WEBHOOK_INPUT_FLAGS.url, required: true }, + event: { ...WEBHOOK_INPUT_FLAGS.event, required: true }, + }, + usageArgs: + "--url <url> --event <type> [--event <type>] [--description <text>] [--workspace-id <id>]", + exampleArgs: [ + "--workspace-id workspace-example --url https://example.com/webhooks --event session.updated --yes", + ], + notes: [...WEBHOOK_NOTES, SECRET_NOTE], + validate: validateWebhookFlags, + async run(ctx) { + const request = { + url: ctx.flags.url, + events: ctx.flags.event, + ...(ctx.flags.description !== undefined ? { description: ctx.flags.description } : {}), + }; + await runWebhookOperation(ctx, { + operation: "create", + request, + returnsSecret: true, + invoke: (context, options) => createRemoteWebhookEndpoint(context, request, options), + }); + }, +}); + +export const managedAgentWebhookList = defineCommand({ + description: { "en-US": "List all Webhook endpoints", "zh-CN": "列出全部 Webhook 端点" }, + auth: "apiKey", + flags: WORKSPACE_FLAG, + usageArgs: "[--workspace-id <id>]", + exampleArgs: ["--workspace-id workspace-example --output json"], + notes: [ + ...WEBHOOK_NOTES, + { + "en-US": "Returns every undeleted Webhook without pagination.", + "zh-CN": "返回所有未删除的 Webhook,不分页。", + }, + ], + validate: validateWebhookFlags, + async run(ctx) { + await runWebhookOperation(ctx, { + operation: "list", + invoke: (context, options) => listRemoteWebhookEndpoints(context, options), + }); + }, +}); + +interface WebhookIdCommand { + operation: string; + description: LocalizedText; + risk?: CommandRisk; + notes?: LocalizedText[]; + returnsSecret?: boolean; + invoke: ( + context: ManagedApiContext, + id: string, + options: WebhookRequestOptions, + ) => Promise<unknown>; +} + +function defineWebhookIdCommand(command: WebhookIdCommand) { + return defineCommand({ + description: command.description, + auth: "apiKey", + risk: command.risk, + flags: WEBHOOK_ID_FLAGS, + usageArgs: "--webhook-id <wep_id> [--workspace-id <id>]", + exampleArgs: [ + `--workspace-id workspace-example --webhook-id wep_example${command.risk ? " --yes" : ""}`, + ], + notes: [...WEBHOOK_NOTES, ...(command.notes ?? [])], + validate: validateWebhookFlags, + async run(ctx) { + await runWebhookOperation(ctx, { + operation: command.operation, + webhookId: ctx.flags.webhookId, + returnsSecret: command.returnsSecret, + invoke: (context, options) => command.invoke(context, ctx.flags.webhookId, options), + }); + }, + }); +} + +export const managedAgentWebhookGet = defineWebhookIdCommand({ + operation: "get", + description: { "en-US": "Get a Webhook endpoint", "zh-CN": "查询 Webhook 端点详情" }, + invoke: getRemoteWebhookEndpoint, +}); + +export const managedAgentWebhookUpdate = defineCommand({ + description: { "en-US": "Update a Webhook endpoint", "zh-CN": "更新 Webhook 端点" }, + auth: "apiKey", + risk: { + level: "high", + message: { + "en-US": "This updates the callback URL, description, or event subscriptions of the Webhook.", + "zh-CN": "该操作会更新 Webhook 的回调地址、描述或订阅事件。", + }, + }, + flags: { ...WEBHOOK_ID_FLAGS, ...WEBHOOK_INPUT_FLAGS }, + usageArgs: + "--webhook-id <wep_id> [--url <url>] [--event <type>] [--description <text>] [--workspace-id <id>]", + exampleArgs: ['--workspace-id workspace-example --webhook-id wep_example --description "" --yes'], + notes: [ + ...WEBHOOK_NOTES, + { + "en-US": "Omitted fields stay unchanged; --event replaces the complete subscription list.", + "zh-CN": "未传字段保持不变;--event 会替换整个订阅列表。", + }, + ], + validate(flags) { + const error = validateWebhookFlags(flags); + if (error) return error; + if (flags.url === undefined && flags.event === undefined && flags.description === undefined) { + return "Provide --url, --event, or --description to update. / 更新时请至少提供 --url、--event 或 --description。"; + } + }, + async run(ctx) { + const request = { + ...(ctx.flags.url !== undefined ? { url: ctx.flags.url } : {}), + ...(ctx.flags.event !== undefined ? { events: ctx.flags.event } : {}), + ...(ctx.flags.description !== undefined ? { description: ctx.flags.description } : {}), + }; + await runWebhookOperation(ctx, { + operation: "update", + webhookId: ctx.flags.webhookId, + request, + invoke: (context, options) => + updateRemoteWebhookEndpoint(context, ctx.flags.webhookId, request, options), + }); + }, +}); + +export const managedAgentWebhookDelete = defineWebhookIdCommand({ + operation: "delete", + description: { "en-US": "Delete a Webhook endpoint", "zh-CN": "删除 Webhook 端点" }, + risk: { + level: "high", + message: { + "en-US": "This deletes the Webhook and cancels its pending deliveries.", + "zh-CN": "该操作会删除 Webhook,并取消尚未投递的事件。", + }, + }, + invoke: deleteRemoteWebhookEndpoint, +}); + +export const managedAgentWebhookEnable = defineWebhookIdCommand({ + operation: "enable", + description: { "en-US": "Enable a Webhook endpoint", "zh-CN": "启用 Webhook 端点" }, + risk: { + level: "high", + message: { + "en-US": + "This resumes new event deliveries. Events from the disabled period are not replayed.", + "zh-CN": "该操作会恢复新事件投递,不会补发禁用期间的事件。", + }, + }, + invoke: enableRemoteWebhookEndpoint, +}); + +export const managedAgentWebhookDisable = defineWebhookIdCommand({ + operation: "disable", + description: { "en-US": "Disable a Webhook endpoint", "zh-CN": "禁用 Webhook 端点" }, + risk: { + level: "high", + message: { + "en-US": "This stops new event deliveries and cancels pending deliveries.", + "zh-CN": "该操作会停止新事件投递,并取消尚未投递的事件。", + }, + }, + invoke: disableRemoteWebhookEndpoint, +}); + +export const managedAgentWebhookTest = defineWebhookIdCommand({ + operation: "test", + description: { + "en-US": "Send a real Webhook test event", + "zh-CN": "实际发送一次 Webhook 测试事件", + }, + risk: { + level: "high", + message: { + "en-US": + "This sends a real callback request; redirects or security validation failures may disable the Webhook.", + "zh-CN": "该操作会实际发送回调请求;重定向或安全校验失败可能禁用 Webhook。", + }, + }, + notes: [ + { + "en-US": + "Tests are synchronous, are not retried or recorded, and do not affect consecutive failure counts.", + "zh-CN": "测试同步执行,不重试、不保存投递记录,也不影响连续失败计数。", + }, + ], + invoke: testRemoteWebhookEndpoint, +}); + +export const managedAgentWebhookResetSecret = defineWebhookIdCommand({ + operation: "reset-secret", + description: { "en-US": "Reset a Webhook signing secret", "zh-CN": "重置 Webhook 验签密钥" }, + risk: { + level: "high", + message: { + "en-US": + "This immediately invalidates the previous signing secret. Save the new secret and update your receiver.", + "zh-CN": "该操作会立即使旧验签密钥失效。请保存新密钥并更新接收端。", + }, + }, + notes: [SECRET_NOTE], + returnsSecret: true, + invoke: resetRemoteWebhookSecret, +}); + +export const managedAgentWebhookEvents = defineCommand({ + description: { + "en-US": "List recent Webhook delivery events", + "zh-CN": "查询最近的 Webhook 投递事件", + }, + auth: "apiKey", + flags: { + ...WEBHOOK_ID_FLAGS, + limit: { + type: "number", + valueHint: "<count>", + description: { + "en-US": "Page size, 1–100 (default: 20)", + "zh-CN": "每页数量,1–100(默认:20)", + }, + }, + page: { + type: "string", + valueHint: "<cursor>", + description: { + "en-US": "Opaque next_page cursor from the previous response", + "zh-CN": "上一页响应中的不透明 next_page 游标", + }, + }, + }, + usageArgs: "--webhook-id <wep_id> [--limit <count>] [--page <cursor>] [--workspace-id <id>]", + exampleArgs: [ + "--workspace-id workspace-example --webhook-id wep_example --limit 20 --output json", + ], + notes: [ + ...WEBHOOK_NOTES, + { + "en-US": + "Returns one page of delivery events retained for 7 days. Continue with next_page; deleted Webhooks' retained events remain queryable.", + "zh-CN": + "返回一页保留 7 天的投递事件。可用 next_page 继续查询;Webhook 删除后仍可查询保留期内的事件。", + }, + ], + validate: validateWebhookFlags, + async run(ctx) { + await runWebhookOperation(ctx, { + operation: "events", + webhookId: ctx.flags.webhookId, + limit: ctx.flags.limit, + page: ctx.flags.page, + invoke: (context, options) => + listRemoteWebhookEvents(context, ctx.flags.webhookId, { + ...options, + limit: ctx.flags.limit, + page: ctx.flags.page, + }), + }); + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 21de894ff..c2c1a873d 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -279,6 +279,18 @@ export { managedAgentFileSearch, managedAgentFileUpload, } from "./commands/managed-agent/file.ts"; +export { + managedAgentWebhookCreate, + managedAgentWebhookList, + managedAgentWebhookGet, + managedAgentWebhookUpdate, + managedAgentWebhookDelete, + managedAgentWebhookEnable, + managedAgentWebhookDisable, + managedAgentWebhookTest, + managedAgentWebhookResetSecret, + managedAgentWebhookEvents, +} from "./commands/managed-agent/webhook.ts"; export { default as workspaceInit } from "./commands/workspace/init.ts"; export { default as pluginInstall } from "./commands/plugin/install.ts"; export { default as pluginLink } from "./commands/plugin/link.ts"; @@ -308,3 +320,17 @@ export { sandboxTemplateList, sandboxTemplateUpdate, } from "./commands/sandbox/template.ts"; + +export { default as managedAgentMemoryStoreCreate } from "./commands/managed-agent/memory-store/create.ts"; +export { + managedAgentMemoryStoreList, + managedAgentMemoryStoreGet, + managedAgentMemoryCreate, + managedAgentMemoryList, + managedAgentMemoryGet, + managedAgentMemoryUpdate, + managedAgentMemoryDelete, + managedAgentMemoryVersionList, + managedAgentMemoryVersionGet, + managedAgentMemoryVersionRedact, +} from "./commands/managed-agent/memory-store/api.ts"; diff --git a/packages/commands/tests/e2e/managed-agent-deployment-environment.e2e.test.ts b/packages/commands/tests/e2e/managed-agent-deployment-environment.e2e.test.ts new file mode 100644 index 000000000..f98614979 --- /dev/null +++ b/packages/commands/tests/e2e/managed-agent-deployment-environment.e2e.test.ts @@ -0,0 +1,514 @@ +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type Server } from "node:http"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { + LocalFileStateBackend, + planProjectWithStateBackend, + resolveProjectConfig, +} from "@openagentpack/sdk"; +import { runNodeMain } from "e2e/runner"; +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { parse, parseDocument } from "yaml"; +import { commandsPackageRoot, parseStdoutJson, runCommandHelp } from "./helpers.ts"; +import type { E2eRouteExports } from "./topic-routes.ts"; + +const ROUTES: E2eRouteExports = { + "managed-agent deployment create": "managedAgentDeploymentCreate", + "managed-agent apply": "managedAgentApply", + "managed-agent plan": "managedAgentPlan", +}; +const directories: string[] = []; +const servers: Server[] = []; +const API_PATH = "/api/v1/agentstudio"; +const DEPLOYMENT_KEY = "daily-report"; +const DEPLOYMENT_ID = "dep_environment_e2e"; +const DEPLOYMENT_PATH = `${API_PATH}/deployments/${DEPLOYMENT_ID}`; +const AGENT_ID = "agent_environment_e2e"; +const ENVIRONMENT_ID = "env_environment_e2e"; +const CREATE_ARGS = [ + "managed-agent", + "deployment", + "create", + "--name", + "Daily Report", + "--agent", + "assistant", + "--message", + "Generate the report", +]; +const ENVIRONMENT_ENTRIES = [ + "DUPLICATE=fake-old-deployment-value", + " TRIMMED_KEY = fake-spaced-deployment-value ", + "EMPTY=", + "EQUALS=fake=deployment=value", + "DUPLICATE=fake-new-deployment-value", +]; +const ENVIRONMENT_VARIABLES = { + DUPLICATE: "fake-new-deployment-value", + TRIMMED_KEY: " fake-spaced-deployment-value ", + EMPTY: "", + EQUALS: "fake=deployment=value", +}; +const ENVIRONMENT_ARGS = ENVIRONMENT_ENTRIES.flatMap((entry) => ["--environment_variables", entry]); +const UPDATE_CASES: Array<{ label: string; variables?: Record<string, string> }> = [ + { label: "change", variables: { CHANGED: "fake-updated-deployment-value", EMPTY: "" } }, + { label: "empty object", variables: {} }, + { label: "remove field", variables: undefined }, +]; + +afterEach(async () => { + await Promise.all( + servers.splice(0).map( + (server) => + new Promise<void>((resolve, reject) => { + server.closeAllConnections(); + server.close((error) => (error ? reject(error) : resolve())); + }), + ), + ); + for (const directory of directories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +function setup(config: Record<string, unknown> = {}) { + const directory = mkdtempSync(join(tmpdir(), "bl-deployment-environment-e2e-")); + directories.push(directory); + writeFileSync(join(directory, "config.json"), JSON.stringify(config)); + writeFileSync(join(directory, "provider-config.json"), "{}"); + return { + directory, + env: { + BAILIAN_CONFIG_DIR: directory, + AGENTS_CONFIG_PATH: join(directory, "provider-config.json"), + DASHSCOPE_API_KEY: "", + DASHSCOPE_BASE_URL: "", + BAILIAN_API_KEY: "", + BAILIAN_BASE_URL: "", + BAILIAN_WORKSPACE_ID: "", + BAILIAN_E2E: "0", + HTTP_PROXY: "", + HTTPS_PROXY: "", + ALL_PROXY: "", + http_proxy: "", + https_proxy: "", + all_proxy: "", + NO_PROXY: "127.0.0.1,localhost", + }, + }; +} + +function run( + args: string[], + environment: ReturnType<typeof setup>, + format: "json" | "text" = "json", +) { + return runNodeMain( + join(commandsPackageRoot, "tests/e2e/harness/main.ts"), + [...args, "--output", format], + { + cwd: environment.directory, + env: { + ...environment.env, + BAILIAN_E2E_ROUTES: JSON.stringify( + Object.entries(ROUTES).map(([path, exportName]) => ({ path, export: exportName })), + ), + }, + }, + ); +} + +function snapshot(directory: string) { + return Object.fromEntries( + readdirSync(directory) + .sort() + .map((filename) => [filename, readFileSync(join(directory, filename), "utf8")]), + ); +} + +function expectNoVariableValues(output: string, variables = ENVIRONMENT_VARIABLES) { + for (const value of Object.values(variables).filter(Boolean)) { + expect(output).not.toContain(value); + } + expect(output).not.toContain("fake-old-deployment-value"); +} + +async function seedProject( + environment: ReturnType<typeof setup>, + origin: string, + options: { deploymentVariables?: Record<string, string>; unrelatedStore?: boolean } = {}, +) { + const configPath = join(environment.directory, "agents.yaml"); + writeFileSync( + configPath, + [ + 'version: "1"', + "# Preserve this existing project comment.", + "providers:", + " bailian:", + " api_key: test-deployment-key", + ` base_url: ${origin}${API_PATH}`, + "defaults:", + " provider: bailian", + "environments:", + " sandbox:", + ` environment_id: ${ENVIRONMENT_ID}`, + " config:", + " type: cloud", + "agents:", + " assistant:", + " name: Assistant", + " model: qwen3.8-max", + " instructions: Complete the test prompt.", + " environment: sandbox", + " provider: bailian", + ...(options.unrelatedStore + ? [ + "memory_stores:", + " other:", + " name: Keep these unrelated notes", + ' description: ""', + " provider: bailian", + ] + : []), + ...(options.deploymentVariables === undefined + ? [] + : [ + "deployments:", + ` ${DEPLOYMENT_KEY}:`, + " name: Daily Report", + " agent: assistant", + " provider: bailian", + " initial_events:", + " - type: user.message", + " content: Generate the report", + ` environment_variables: ${JSON.stringify(options.deploymentVariables)}`, + ]), + "", + ].join("\n"), + ); + const loaded = await resolveProjectConfig(configPath, { resolveEnv: false }); + const stateBackend = new LocalFileStateBackend({ configPath }); + const stateScope = { projectId: loaded.projectName }; + const planned = await planProjectWithStateBackend( + { + projectName: loaded.projectName, + config: loaded.config, + configPath, + providers: loaded.config.providers, + stateBackend, + stateScope, + }, + { provider: "bailian", refresh: false, quiet: true }, + ); + await stateBackend.write(stateScope, (state) => { + for (const resource of [ + { type: "agent" as const, name: "assistant", remoteId: AGENT_ID }, + { type: "environment" as const, name: "sandbox", remoteId: ENVIRONMENT_ID }, + ]) { + const address = { type: resource.type, name: resource.name, provider: "bailian" }; + const action = planned.plan.actions.find( + (entry) => entry.address.type === resource.type && entry.address.name === resource.name, + ); + const contentHash = (action?.after as { content_hash?: string } | undefined)?.content_hash; + if (!contentHash) + throw new Error(`Missing desired hash for ${resource.type}.${resource.name}.`); + state.setResource({ + address, + remote_id: resource.remoteId, + content_hash: contentHash, + desired_hash: contentHash, + ...(resource.type === "environment" ? { externally_managed: true } : {}), + }); + } + }); + return configPath; +} + +async function gateway() { + const received: { method?: string; path: string; body: string; authorization?: string }[] = []; + let deployment: Record<string, unknown> | undefined; + const server = createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + const path = request.url ?? ""; + const body = Buffer.concat(chunks).toString("utf8"); + received.push({ + method: request.method, + path, + body, + authorization: request.headers.authorization, + }); + let responseBody: unknown; + if (request.method === "GET" && path === `${API_PATH}/agents/${AGENT_ID}`) { + responseBody = { + id: AGENT_ID, + name: "Assistant", + model: { id: "qwen3.8-max" }, + system: "Complete the test prompt.", + tools: [{ type: "builtin_toolkit", default_config: { enabled: true }, configs: [] }], + skills: [], + }; + } else if ( + request.method === "GET" && + path === `${API_PATH}/environments/${ENVIRONMENT_ID}` + ) { + responseBody = { + id: ENVIRONMENT_ID, + config: { type: "cloud", networking: { type: "unrestricted" } }, + }; + } else if (request.method === "POST" && path === `${API_PATH}/deployments`) { + deployment = { + ...JSON.parse(body), + id: DEPLOYMENT_ID, + type: "deployment", + status: "ready", + }; + responseBody = deployment; + } else if (request.method === "GET" && path === DEPLOYMENT_PATH && deployment) { + responseBody = deployment; + } else if (request.method === "POST" && path === DEPLOYMENT_PATH && deployment) { + deployment = { ...deployment, ...JSON.parse(body) }; + responseBody = deployment; + } else { + response.writeHead(404, { "content-type": "application/json" }); + response.end( + JSON.stringify({ message: `Unexpected test request: ${request.method} ${path}` }), + ); + return; + } + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify(responseBody)); + }); + }); + await new Promise<void>((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + servers.push(server); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a local server address."); + return { origin: `http://127.0.0.1:${address.port}`, received }; +} + +function expectedCreateBody(directory: string, variables?: Record<string, string>) { + return { + name: "Daily Report", + agent: { id: AGENT_ID }, + environment_id: ENVIRONMENT_ID, + initial_events: [ + { role: "user", type: "message", content: [{ type: "text", text: "Generate the report" }] }, + ], + metadata: { "agents.project": basename(directory), "agents.resource": DEPLOYMENT_KEY }, + ...(variables === undefined ? {} : { environment_variables: variables }), + }; +} + +describe("e2e: managed-agent Deployment environment variable help and validation", () => { + test("Deployment create help documents repeatable variables", async () => { + const result = await runCommandHelp(ROUTES, [ + "managed-agent", + "deployment", + "create", + "--help", + ]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("--environment_variables"); + expect(result.stderr).toContain("<KEY=VALUE>"); + expect(result.stderr).toContain("last value wins"); + }); + + test.each([ + { entry: "fake-secret-without-separator", reason: "KEY=VALUE" }, + { entry: " =fake-secret-without-key", reason: "keys must not be empty" }, + { entry: "__proto__=fake-proto-value", reason: "__proto__" }, + ])("rejects $reason before authentication, HTTP, or YAML writes", async ({ entry, reason }) => { + const { origin, received } = await gateway(); + for (const config of [ + { base_url: origin }, + { base_url: origin, api_key: "test-deployment-key" }, + ]) { + const environment = setup(config); + writeFileSync(join(environment.directory, "agents.yaml"), 'version: "1"\n'); + const before = snapshot(environment.directory); + const result = await run( + [...CREATE_ARGS, "--environment_variables", entry, "--yes"], + environment, + ); + expect(result.exitCode, result.stderr).toBe(2); + expect(result.stderr).toContain("environment_variables"); + expect(result.stderr).toContain(reason); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(entry); + expect(`${result.stdout}\n${result.stderr}`).not.toContain("fake-secret"); + expect(`${result.stdout}\n${result.stderr}`).not.toContain("fake-proto-value"); + expect(snapshot(environment.directory)).toEqual(before); + } + expect(received).toEqual([]); + }); +}); + +describe("e2e: managed-agent Deployment environment variable previews", () => { + test.each([true, false])( + "dry-run performs no writes or requests with variables (%s)", + async (provided) => { + const { origin, received } = await gateway(); + const environment = setup({ base_url: origin }); + await seedProject(environment, origin, { unrelatedStore: true }); + const before = snapshot(environment.directory); + const result = await run( + [...CREATE_ARGS, ...(provided ? ENVIRONMENT_ARGS : []), "--dry-run"], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + resource: { type: "deployment", key: DEPLOYMENT_KEY, provider: "bailian" }, + yaml_written: false, + ready_to_create: true, + requires_confirmation: false, + actions: expect.arrayContaining([ + expect.objectContaining({ + action: "create", + address: expect.objectContaining({ type: "deployment", name: DEPLOYMENT_KEY }), + }), + ]), + }); + expectNoVariableValues(`${result.stdout}\n${result.stderr}`); + expect(received).toEqual([]); + expect(snapshot(environment.directory)).toEqual(before); + }, + ); + + test("default preview checks dependencies and keeps YAML and State unchanged", async () => { + const { origin, received } = await gateway(); + const environment = setup({ base_url: origin, api_key: "test-deployment-key" }); + await seedProject(environment, origin, { unrelatedStore: true }); + const before = snapshot(environment.directory); + const result = await run([...CREATE_ARGS, ...ENVIRONMENT_ARGS], environment); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + yaml_written: false, + requires_confirmation: true, + ready_to_create: true, + }); + expectNoVariableValues(`${result.stdout}\n${result.stderr}`); + expect(received.length).toBeGreaterThan(0); + expect(received.every((request) => request.method === "GET")).toBe(true); + expect(received.some((request) => request.path.includes("memory_stores"))).toBe(false); + expect(snapshot(environment.directory)).toEqual(before); + }); +}); + +describe("e2e: managed-agent Deployment environment variable SDK HTTP protocol", () => { + test.each([true, false])( + "scoped create persists YAML and sends variables only when provided (%s)", + async (provided) => { + const { origin, received } = await gateway(); + const environment = setup({ base_url: origin, api_key: "test-deployment-key" }); + const configPath = await seedProject(environment, origin, { unrelatedStore: true }); + const before = parse(readFileSync(configPath, "utf8")); + const result = await run( + [...CREATE_ARGS, ...(provided ? ENVIRONMENT_ARGS : []), "--yes"], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + resource: { type: "deployment", key: DEPLOYMENT_KEY, remote_id: DEPLOYMENT_ID }, + yaml_written: true, + }); + const written = readFileSync(configPath, "utf8"); + const config = parse(written); + expect(config.deployments[DEPLOYMENT_KEY]).toMatchObject({ + name: "Daily Report", + agent: "assistant", + initial_events: [{ type: "user.message", content: "Generate the report" }], + }); + if (provided) + expect(config.deployments[DEPLOYMENT_KEY].environment_variables).toEqual( + ENVIRONMENT_VARIABLES, + ); + else expect(config.deployments[DEPLOYMENT_KEY]).not.toHaveProperty("environment_variables"); + for (const group of ["providers", "defaults", "environments", "agents", "memory_stores"]) + expect(config[group]).toEqual(before[group]); + expect(written).toContain("# Preserve this existing project comment."); + const mutations = received.filter((request) => request.method !== "GET"); + expect(mutations).toHaveLength(1); + expect(mutations[0]).toMatchObject({ + method: "POST", + path: `${API_PATH}/deployments`, + authorization: "Bearer test-deployment-key", + }); + expect(JSON.parse(mutations[0]!.body)).toEqual( + expectedCreateBody(environment.directory, provided ? ENVIRONMENT_VARIABLES : undefined), + ); + expect(received.some((request) => request.path.includes("memory_stores"))).toBe(false); + }, + ); + + test.each(UPDATE_CASES)("YAML apply manages variables: $label", async ({ variables }) => { + const { origin, received } = await gateway(); + const environment = setup({ base_url: origin, api_key: "test-deployment-key" }); + const configPath = await seedProject(environment, origin, { + deploymentVariables: ENVIRONMENT_VARIABLES, + }); + const created = await run(["managed-agent", "apply", "--no-refresh", "--yes"], environment); + expect(created.exitCode, created.stderr).toBe(0); + const createRequests = received.filter((request) => request.method === "POST"); + expect(createRequests).toHaveLength(1); + expect(JSON.parse(createRequests[0]!.body)).toEqual( + expectedCreateBody(environment.directory, ENVIRONMENT_VARIABLES), + ); + received.splice(0); + + const document = parseDocument(readFileSync(configPath, "utf8")); + if (variables === undefined) + document.deleteIn(["deployments", DEPLOYMENT_KEY, "environment_variables"]); + else document.setIn(["deployments", DEPLOYMENT_KEY, "environment_variables"], variables); + writeFileSync(configPath, document.toString()); + const beforePreview = snapshot(environment.directory); + for (const format of ["json", "text"] as const) { + const preview = await run(["managed-agent", "plan", "--dry-run"], environment, format); + expect(preview.exitCode, preview.stderr).toBe(0); + expectNoVariableValues(`${preview.stdout}\n${preview.stderr}`); + if (variables) { + for (const value of Object.values(variables).filter(Boolean)) + expect(`${preview.stdout}\n${preview.stderr}`).not.toContain(value); + } + if (format === "json") { + expect(parseStdoutJson(preview.stdout)).toMatchObject({ + actions: expect.arrayContaining([ + expect.objectContaining({ + action: "update", + address: expect.objectContaining({ type: "deployment", name: DEPLOYMENT_KEY }), + }), + ]), + }); + } + expect(received).toEqual([]); + expect(snapshot(environment.directory)).toEqual(beforePreview); + } + + const applied = await run(["managed-agent", "apply", "--no-refresh", "--yes"], environment); + expect(applied.exitCode, applied.stderr).toBe(0); + expect(parseStdoutJson(applied.stdout)).toMatchObject({ succeeded: 1, failed: 0 }); + const mutations = received.filter((request) => request.method !== "GET"); + expect(mutations).toHaveLength(1); + expect(mutations[0]).toMatchObject({ + method: "POST", + path: DEPLOYMENT_PATH, + authorization: "Bearer test-deployment-key", + }); + expect(JSON.parse(mutations[0]!.body)).toEqual({ + ...expectedCreateBody(environment.directory, variables ?? {}), + description: "", + vault_ids: [], + resources: [], + schedule: null, + }); + expect(received).toContainEqual( + expect.objectContaining({ method: "GET", path: DEPLOYMENT_PATH }), + ); + expect(readFileSync(configPath, "utf8")).toBe(beforePreview["agents.yaml"]); + }); +}); diff --git a/packages/commands/tests/e2e/managed-agent-mcp-tools.e2e.test.ts b/packages/commands/tests/e2e/managed-agent-mcp-tools.e2e.test.ts new file mode 100644 index 000000000..025dd1de9 --- /dev/null +++ b/packages/commands/tests/e2e/managed-agent-mcp-tools.e2e.test.ts @@ -0,0 +1,260 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type Server } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { runNodeMain } from "e2e/runner"; +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { stringify } from "yaml"; +import { commandsPackageRoot, parseStdoutJson } from "./helpers.ts"; +import type { E2eRouteExports } from "./topic-routes.ts"; + +const ROUTES: E2eRouteExports = { + "managed-agent validate": "managedAgentValidate", + "managed-agent apply": "managedAgentApply", + "managed-agent agent create": "managedAgentAgentCreate", +}; +const API_PATH = "/api/v1/agentstudio"; +const MCP_SERVER = "market-cmgjmcp00074952"; +const AGENT_ID = "agent-mcp-tools-e2e"; +const directories: string[] = []; +const servers: Server[] = []; +const OVERRIDES = [ + { name: "query_financial_data", enabled: true }, + { name: "export_financial_data", enabled: false }, +]; +const CONFIG_CASES = [ + { label: "omitted configs", configs: undefined }, + { label: "empty configs", configs: [] }, + { label: "explicit enabled and disabled overrides", configs: OVERRIDES }, +]; + +afterEach(async () => { + await Promise.all( + servers.splice(0).map( + (server) => + new Promise<void>((resolve, reject) => { + server.closeAllConnections(); + server.close((error) => (error ? reject(error) : resolve())); + }), + ), + ); + for (const directory of directories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +function setup(origin: string) { + const directory = mkdtempSync(join(tmpdir(), "bl-mcp-tools-e2e-")); + directories.push(directory); + writeFileSync( + join(directory, "config.json"), + JSON.stringify({ api_key: "test-mcp-tools-key", base_url: `${origin}${API_PATH}` }), + ); + writeFileSync(join(directory, ".env"), "\n"); + return { + directory, + baseUrl: `${origin}${API_PATH}`, + configPath: join(directory, "agents.yaml"), + env: { + BAILIAN_CONFIG_DIR: directory, + AGENTS_CONFIG_PATH: join(directory, "provider-config.json"), + AGENTS_PROVIDER: "bailian", + DASHSCOPE_API_KEY: "", + DASHSCOPE_BASE_URL: "", + BAILIAN_API_KEY: "", + BAILIAN_BASE_URL: "", + BAILIAN_WORKSPACE_ID: "", + BAILIAN_E2E: "0", + HTTP_PROXY: "", + HTTPS_PROXY: "", + ALL_PROXY: "", + http_proxy: "", + https_proxy: "", + all_proxy: "", + NO_PROXY: "127.0.0.1,localhost", + }, + }; +} + +function writeProject( + environment: ReturnType<typeof setup>, + configs?: Array<{ name: string; enabled: boolean }>, +) { + writeFileSync( + environment.configPath, + stringify({ + version: "1", + providers: { bailian: { base_url: environment.baseUrl } }, + defaults: { provider: "bailian" }, + agents: { + assistant: { + model: "qwen3.8-max", + instructions: "Use the financial data MCP tools.", + tools: { + builtin: ["bash"], + mcp: [ + { + type: "mcp_toolkit", + mcp_server_name: MCP_SERVER, + default_config: { enabled: true }, + ...(configs === undefined ? {} : { configs }), + }, + ], + }, + mcp_servers: [{ type: "official", name: MCP_SERVER }], + }, + }, + }), + ); +} + +function run(args: string[], environment: ReturnType<typeof setup>) { + return runNodeMain( + join(commandsPackageRoot, "tests/e2e/harness/main.ts"), + ["managed-agent", ...args, "--output", "json"], + { + cwd: environment.directory, + env: { + ...environment.env, + BAILIAN_E2E_ROUTES: JSON.stringify( + Object.entries(ROUTES).map(([path, exportName]) => ({ path, export: exportName })), + ), + }, + }, + ); +} + +async function gateway() { + const received: Array<{ method?: string; path: string; body: Record<string, unknown> }> = []; + let agent: Record<string, unknown> | undefined; + const server = createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + const path = request.url ?? ""; + const text = Buffer.concat(chunks).toString("utf8"); + const body = text ? JSON.parse(text) : {}; + received.push({ method: request.method, path, body }); + if ( + request.method === "POST" && + (path === `${API_PATH}/agents` || path === `${API_PATH}/agents/${AGENT_ID}`) + ) { + agent = { ...body, id: AGENT_ID, version: "1" }; + } else if ( + !(request.method === "GET" && path === `${API_PATH}/agents/${AGENT_ID}` && agent) + ) { + response.writeHead(404, { "content-type": "application/json" }); + response.end( + JSON.stringify({ message: `Unexpected test request: ${request.method} ${path}` }), + ); + return; + } + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify(agent)); + }); + }); + await new Promise<void>((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + servers.push(server); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a local server address."); + return { origin: `http://127.0.0.1:${address.port}`, received }; +} + +function expectMcpToolkit(body: Record<string, unknown>, configs = OVERRIDES) { + expect(body.mcp_servers).toEqual([{ type: "official", name: MCP_SERVER }]); + const tools = body.tools as Array<Record<string, unknown>>; + expect(tools.filter((tool) => tool.type === "mcp_toolkit")).toEqual([ + { + type: "mcp_toolkit", + mcp_server_name: MCP_SERVER, + default_config: { enabled: true }, + configs, + }, + ]); +} + +describe("e2e: managed-agent MCP defaults without placeholder tool configs (local HTTP)", () => { + test.each(CONFIG_CASES)("validate and apply accept $label", async ({ configs }) => { + const { origin, received } = await gateway(); + const environment = setup(origin); + writeProject(environment, configs); + const yamlBefore = readFileSync(environment.configPath, "utf8"); + + const validated = await run(["validate"], environment); + expect(validated.exitCode, validated.stderr).toBe(0); + expect(parseStdoutJson(validated.stdout)).toMatchObject({ valid: true }); + expect(received).toEqual([]); + + const applied = await run(["apply", "--no-refresh", "--yes"], environment); + expect(applied.exitCode, applied.stderr).toBe(0); + expect(parseStdoutJson(applied.stdout)).toMatchObject({ succeeded: 1, failed: 0 }); + const mutations = received.filter((request) => request.method === "POST"); + expect(mutations).toHaveLength(1); + expect(mutations[0]).toMatchObject({ path: `${API_PATH}/agents` }); + expectMcpToolkit(mutations[0]!.body, configs ?? []); + expect(readFileSync(environment.configPath, "utf8")).toBe(yamlBefore); + }); + + test("apply removes explicit overrides and sends an empty config list on update", async () => { + const { origin, received } = await gateway(); + const environment = setup(origin); + writeProject(environment, OVERRIDES); + const created = await run(["apply", "--no-refresh", "--yes"], environment); + expect(created.exitCode, created.stderr).toBe(0); + expectMcpToolkit(received[0]!.body); + received.splice(0); + + writeProject(environment); + const applied = await run(["apply", "--no-refresh", "--yes"], environment); + expect(applied.exitCode, applied.stderr).toBe(0); + expect(parseStdoutJson(applied.stdout)).toMatchObject({ succeeded: 1, failed: 0 }); + const mutations = received.filter((request) => request.method === "POST"); + expect(mutations).toHaveLength(1); + expect(mutations[0]).toMatchObject({ path: `${API_PATH}/agents/${AGENT_ID}` }); + expectMcpToolkit(mutations[0]!.body, []); + }); + + test("agent create --mcp sends the market server with every tool enabled", async () => { + const { origin, received } = await gateway(); + const environment = setup(origin); + writeFileSync( + environment.configPath, + stringify({ + version: "1", + providers: { bailian: { base_url: environment.baseUrl } }, + defaults: { provider: "bailian" }, + }), + ); + + const created = await run( + [ + "agent", + "create", + "--name", + "Finance Assistant", + "--model", + "qwen3.8-max", + "--instructions", + "Use the financial data MCP tools.", + "--mcp", + MCP_SERVER, + "--mcp", + MCP_SERVER, + "--yes", + ], + environment, + ); + expect(created.exitCode, created.stderr).toBe(0); + expect(parseStdoutJson(created.stdout)).toMatchObject({ + status: "completed", + agent: { key: "finance-assistant", remote_id: AGENT_ID, mcp: [MCP_SERVER] }, + }); + const mutations = received.filter((request) => request.method === "POST"); + expect(mutations).toHaveLength(1); + expect(mutations[0]).toMatchObject({ path: `${API_PATH}/agents` }); + expectMcpToolkit(mutations[0]!.body, []); + }); +}); diff --git a/packages/commands/tests/e2e/managed-agent-memory.e2e.test.ts b/packages/commands/tests/e2e/managed-agent-memory.e2e.test.ts new file mode 100644 index 000000000..d7f2f9ee7 --- /dev/null +++ b/packages/commands/tests/e2e/managed-agent-memory.e2e.test.ts @@ -0,0 +1,782 @@ +import { createHash } from "node:crypto"; +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type Server } from "node:http"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { LocalFileStateBackend, StateManager } from "@openagentpack/sdk"; +import { runNodeMain } from "e2e/runner"; +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { parse } from "yaml"; +import { commandsPackageRoot, parseStdoutJson, runCommandHelp } from "./helpers.ts"; +import { MANAGED_AGENT_MEMORY_ROUTES as ROUTES } from "./topic-routes.ts"; + +const directories: string[] = []; +const servers: Server[] = []; +const STORE_ID = "memstore_a/b c"; +const MEMORY_ID = "memory_a/b c"; +const VERSION_ID = "memver_a/b c"; +const STORE_ARGS = ["--store-id", STORE_ID]; +const MEMORY_ARGS = [...STORE_ARGS, "--memory-id", MEMORY_ID]; +const VERSION_ARGS = [...STORE_ARGS, "--version-id", VERSION_ID]; +const BASE_PATH = "/api/v1/agentstudio/memory_stores"; +const STORE_PATH = `${BASE_PATH}/${encodeURIComponent(STORE_ID)}`; +const SHA = "a".repeat(64); +const OPERATIONS = [ + { + action: ["list"], + args: ["--limit", "100", "--page", "opaque+/=cursor"], + method: "GET", + path: BASE_PATH, + query: { limit: "100", page: "opaque+/=cursor", include_archived: "false" }, + response: { data: [], next_page: "next+/=cursor" }, + }, + { + action: ["get"], + args: STORE_ARGS, + method: "GET", + path: STORE_PATH, + response: { id: STORE_ID, name: "Notes", type: "memory_store" }, + }, + { + action: ["memories", "create"], + args: [...STORE_ARGS, "--path", "/notes.md", "--content", ""], + method: "POST", + path: `${STORE_PATH}/memories`, + write: true, + body: { path: "/notes.md", content: "" }, + response: { id: MEMORY_ID, type: "memory", path: "/notes.md", content: "" }, + }, + { + action: ["memories", "list"], + args: [ + ...STORE_ARGS, + "--path-prefix", + "/notes/", + "--depth", + "1", + "--view", + "full", + "--limit", + "20", + "--page", + "opaque+/=cursor", + ], + method: "GET", + path: `${STORE_PATH}/memories`, + query: { + path_prefix: "/notes/", + depth: "1", + view: "full", + limit: "20", + page: "opaque+/=cursor", + }, + response: { data: [], prefixes: ["/notes/"], next_page: "next+/=cursor" }, + }, + { + action: ["memories", "get"], + args: MEMORY_ARGS, + method: "GET", + path: `${STORE_PATH}/memories/${encodeURIComponent(MEMORY_ID)}`, + response: { id: MEMORY_ID, type: "memory", path: "/notes.md", content: "notes" }, + }, + { + action: ["memories", "update"], + args: [...MEMORY_ARGS, "--content", "", "--expected-content-sha256", SHA], + method: "POST", + path: `${STORE_PATH}/memories/${encodeURIComponent(MEMORY_ID)}`, + write: true, + body: { content: "", precondition: { type: "content_sha256", content_sha256: SHA } }, + response: { id: MEMORY_ID, type: "memory", content: "" }, + }, + { + action: ["memories", "delete"], + args: MEMORY_ARGS, + method: "DELETE", + path: `${STORE_PATH}/memories/${encodeURIComponent(MEMORY_ID)}`, + write: true, + response: { id: MEMORY_ID, type: "memory_deleted" }, + }, + { + action: ["versions", "list"], + args: [ + ...STORE_ARGS, + "--memory-id", + MEMORY_ID, + "--operation", + "modified", + "--created-at-gte", + "2026-10-01T00:00:00Z", + "--created-at-lte", + "2026-10-13T00:00:00Z", + "--view", + "full", + "--limit", + "100", + "--page", + "opaque+/=cursor", + ], + method: "GET", + path: `${STORE_PATH}/memory_versions`, + query: { + memory_id: MEMORY_ID, + operation: "modified", + "created_at.gte": "2026-10-01T00:00:00Z", + "created_at.lte": "2026-10-13T00:00:00Z", + view: "full", + limit: "100", + page: "opaque+/=cursor", + }, + response: { + data: [{ id: VERSION_ID, type: "memory_version", operation: "modified" }], + next_page: null, + }, + }, + { + action: ["versions", "get"], + args: VERSION_ARGS, + method: "GET", + path: `${STORE_PATH}/memory_versions/${encodeURIComponent(VERSION_ID)}`, + response: { id: VERSION_ID, type: "memory_version", content: "old notes" }, + }, + { + action: ["versions", "redact"], + args: VERSION_ARGS, + method: "POST", + path: `${STORE_PATH}/memory_versions/${encodeURIComponent(VERSION_ID)}/redact`, + write: true, + response: { id: VERSION_ID, type: "memory_version", redacted: true }, + }, +]; + +afterEach(async () => { + await Promise.all( + servers.splice(0).map( + (server) => + new Promise<void>((resolve, reject) => { + server.closeAllConnections(); + server.close((error) => (error ? reject(error) : resolve())); + }), + ), + ); + for (const directory of directories.splice(0)) + rmSync(directory, { recursive: true, force: true }); +}); +function setup(config: Record<string, unknown> = {}) { + const directory = mkdtempSync(join(tmpdir(), "bl-memory-e2e-")); + directories.push(directory); + writeFileSync(join(directory, "config.json"), JSON.stringify(config)); + return { + directory, + env: { + BAILIAN_CONFIG_DIR: directory, + DASHSCOPE_API_KEY: "", + DASHSCOPE_BASE_URL: "", + BAILIAN_API_KEY: "", + BAILIAN_BASE_URL: "", + BAILIAN_WORKSPACE_ID: "", + BAILIAN_E2E: "0", + HTTP_PROXY: "", + HTTPS_PROXY: "", + ALL_PROXY: "", + http_proxy: "", + https_proxy: "", + all_proxy: "", + NO_PROXY: "127.0.0.1,localhost", + }, + }; +} +function run(args: string[], environment: ReturnType<typeof setup>) { + return runNodeMain( + join(commandsPackageRoot, "tests/e2e/harness/main.ts"), + [...args, "--output", "json"], + { + cwd: environment.directory, + env: { + ...environment.env, + BAILIAN_E2E_ROUTES: JSON.stringify( + Object.entries(ROUTES).map(([path, exportName]) => ({ path, export: exportName })), + ), + }, + }, + ); +} +async function gateway(body: unknown, status = 200) { + const received: { method?: string; path?: string; body: string; authorization?: string }[] = []; + const server = createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + received.push({ + method: request.method, + path: request.url, + body: Buffer.concat(chunks).toString("utf8"), + authorization: request.headers.authorization, + }); + response.writeHead(status, { "content-type": "application/json" }); + response.end(JSON.stringify(body)); + }); + }); + await new Promise<void>((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + servers.push(server); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a local server address."); + return { origin: `http://127.0.0.1:${address.port}`, received }; +} + +describe("e2e: managed-agent memory help and validation", () => { + test.each(Object.keys(ROUTES))("%s has API Key help", async (path) => { + const result = await runCommandHelp(ROUTES, [...path.split(" "), "--help"]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain(`Usage: bl ${path}`); + expect(result.stderr).toContain("Authentication: API Key"); + if (path === "managed-agent memory-store memories list") { + expect(result.stderr).toContain("Page size (1–20)"); + } else if (path.endsWith(" list")) { + expect(result.stderr).toContain("Page size (1–100)"); + } + }); + test.each(["0", "1.5", "21", "100"])( + "memories list rejects --limit %s before making a request", + async (limit) => { + const server = await gateway({ data: [] }); + const result = await run( + ["managed-agent", "memory-store", "memories", "list", ...STORE_ARGS, "--limit", limit], + setup({ api_key: "test-key", base_url: `${server.origin}/api/v1/agentstudio` }), + ); + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain("--limit must be an integer from 1 to 20."); + expect(result.stderr).toContain("--limit 必须为 1–20 的整数。"); + expect(server.received).toEqual([]); + }, + ); + test.each([ + { action: ["create"], args: [], missing: "--name" }, + { action: ["get"], args: [], missing: "--store-id" }, + { action: ["memories", "create"], args: STORE_ARGS, missing: "--path" }, + { action: ["memories", "get"], args: STORE_ARGS, missing: "--memory-id" }, + { action: ["versions", "get"], args: STORE_ARGS, missing: "--version-id" }, + ])("$action rejects missing $missing", async ({ action, args, missing }) => { + const result = await run( + ["managed-agent", "memory-store", ...action, ...args, "--quiet"], + setup(), + ); + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain(missing); + }); + test.each( + [ + [ + "memories", + "create", + ...STORE_ARGS, + "--path", + "/notes", + "--content", + "x", + "--content-file", + "x", + ], + ["memories", "update", ...MEMORY_ARGS], + ["memories", "list", ...STORE_ARGS, "--depth", "-1"], + ["list", "--limit", "101"], + ["versions", "list", ...STORE_ARGS, "--created-at-gte", "invalid"], + [ + "versions", + "list", + ...STORE_ARGS, + "--created-at-gte", + "2026-10-13", + "--created-at-lte", + "2026-10-01", + ], + [ + "memories", + "update", + ...MEMORY_ARGS, + "--content", + "x", + "--expected-content-sha256", + "invalid", + ], + ].map((args) => ({ args })), + )("rejects invalid input $args before network", async ({ args }) => { + const result = await run( + ["managed-agent", "memory-store", ...args, "--dry-run", "--workspace-id", "ws-test"], + setup(), + ); + expect(result.exitCode).toBe(2); + }); +}); + +describe("e2e: managed-agent memory offline preview and confirmation", () => { + test.each(OPERATIONS)( + "$action dry-run performs no requests or writes", + async ({ action, args }) => { + const { origin, received } = await gateway({}); + const environment = setup({ base_url: origin }); + const before = readdirSync(environment.directory); + const result = await run( + ["managed-agent", "memory-store", ...action, ...args, "--dry-run"], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + operation: `memory-store.${action.join(".")}`, + provider: "bailian", + }); + expect(received).toEqual([]); + expect(readdirSync(environment.directory)).toEqual(before); + }, + ); + test.each(OPERATIONS.filter((operation) => operation.write))( + "$action requires confirmation without requests", + async ({ action, args }) => { + const { origin, received } = await gateway({}); + const result = await run( + ["managed-agent", "memory-store", ...action, ...args], + setup({ base_url: origin, api_key: "test-key" }), + ); + expect(result.exitCode).toBe(7); + expect(result.stderr).toContain("requires_confirmation"); + expect(received).toEqual([]); + }, + ); + test("Store create previews YAML and keeps original files intact", async () => { + const environment = setup(); + const source = + 'version: "1"\nproviders:\n bailian:\n api_key: ""\n workspace_id: ws-test\n'; + const path = join(environment.directory, "agents.yaml"); + writeFileSync(path, source); + const result = await run( + [ + "managed-agent", + "memory-store", + "create", + "--name", + "Project Notes", + "--description", + "", + "--metadata", + "owner=platform", + "--entry", + '{"key":"notes.md","content":""}', + "--dry-run", + ], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + resource: { type: "memory_store", key: "project-notes", name: "Project Notes" }, + yaml_written: false, + ready_to_create: true, + }); + expect(readFileSync(path, "utf8")).toBe(source); + expect(readdirSync(environment.directory).sort()).toEqual(["agents.yaml", "config.json"]); + }); + test("Store create does not reinterpret existing entry content as another file", async () => { + const environment = setup(); + const path = join(environment.directory, "agents.yaml"); + const source = [ + 'version: "1"', + "providers:", + " bailian:", + ' api_key: ""', + " workspace_id: ws-test", + "memory_stores:", + " existing:", + ' description: ""', + " entries:", + " - key: existing.md", + " content: ./existing.md", + "", + ].join("\n"); + writeFileSync(path, source); + writeFileSync(join(environment.directory, "existing.md"), "./missing-second-file.md"); + writeFileSync(join(environment.directory, "new.md"), "../literal-new-entry.md"); + + const result = await run( + [ + "managed-agent", + "memory-store", + "create", + "--name", + "Other", + "--entry", + JSON.stringify({ key: "new.md", content: "./new.md" }), + "--dry-run", + ], + environment, + ); + + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + resource: { type: "memory_store", key: "other" }, + yaml_written: false, + ready_to_create: true, + actions: [{ action: "create", address: { type: "memory_store", name: "other" } }], + }); + expect(readFileSync(path, "utf8")).toBe(source); + expect(readFileSync(join(environment.directory, "existing.md"), "utf8")).toBe( + "./missing-second-file.md", + ); + expect(readdirSync(environment.directory).sort()).toEqual([ + "agents.yaml", + "config.json", + "existing.md", + "new.md", + ]); + }); + test("Store create defaults to a preview, then --yes writes YAML and creates only that store", async () => { + const { origin, received } = await gateway({ + id: "memstore_created", + type: "memory_store", + name: "Notes", + description: "", + metadata: {}, + }); + const environment = setup({ base_url: origin, api_key: "test-key" }); + const source = [ + 'version: "1"', + "providers:", + " bailian:", + ' api_key: "test-key"', + ` base_url: ${origin}/api/v1/agentstudio`, + "# Keep this unrelated declaration.", + "memory_stores:", + " other:", + " name: Other notes", + " description: Keep me", + " provider: bailian", + "", + ].join("\n"); + const path = join(environment.directory, "agents.yaml"); + writeFileSync(path, source); + const args = ["managed-agent", "memory-store", "create", "--name", "Notes"]; + + const preview = await run(args, environment); + expect(preview.exitCode, preview.stderr).toBe(0); + expect(parseStdoutJson(preview.stdout)).toMatchObject({ + resource: { type: "memory_store", key: "notes" }, + yaml_written: false, + requires_confirmation: true, + ready_to_create: true, + }); + expect(received).toEqual([]); + expect(readFileSync(path, "utf8")).toBe(source); + expect(readdirSync(environment.directory).sort()).toEqual(["agents.yaml", "config.json"]); + + const applied = await run([...args, "--yes"], environment); + expect(applied.exitCode, applied.stderr).toBe(0); + expect(parseStdoutJson(applied.stdout)).toMatchObject({ + resource: { type: "memory_store", key: "notes", remote_id: "memstore_created" }, + yaml_written: true, + }); + expect(received.map(({ method, path }) => ({ method, path }))).toEqual([ + { method: "POST", path: BASE_PATH }, + { method: "GET", path: `${BASE_PATH}/memstore_created` }, + ]); + expect(JSON.parse(received[0]!.body)).toEqual({ name: "Notes", description: "" }); + const written = readFileSync(path, "utf8"); + expect(written).toContain("# Keep this unrelated declaration."); + expect(parse(written)).toMatchObject({ + memory_stores: { + other: { name: "Other notes", description: "Keep me", provider: "bailian" }, + notes: { name: "Notes", description: "", provider: "bailian" }, + }, + }); + }); + test("Store initialization retries the same Store with 20-item pages and preserves existing content", async () => { + const store = { + id: "memstore_initializing", + type: "memory_store", + name: "Notes", + description: "", + metadata: {}, + archived_at: null, + }; + const entries = [ + { key: "alpha.md", content: "Initial alpha" }, + { key: "beta.md", content: "Initial beta" }, + { key: "gamma.md", content: "Initial gamma" }, + ]; + const memories = new Map<string, { id: string; content: string }>(); + const received: { method: string; path: string; page: string | null; limit: string | null }[] = + []; + const createdPaths: string[] = []; + const nextPage = "opaque+/=cursor"; + let storeCreates = 0; + let failBeta = true; + const server = createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + const url = new URL(request.url!, "http://localhost"); + const method = request.method!; + const rawBody = Buffer.concat(chunks).toString("utf8"); + const body = rawBody ? (JSON.parse(rawBody) as Record<string, unknown>) : {}; + const reply = (status: number, value: unknown) => { + response.writeHead(status, { "content-type": "application/json" }); + response.end(JSON.stringify(value)); + }; + received.push({ + method, + path: url.pathname, + page: url.searchParams.get("page"), + limit: url.searchParams.get("limit"), + }); + if (method === "POST" && url.pathname === BASE_PATH) { + storeCreates += 1; + reply(200, store); + return; + } + if ( + (method === "GET" || method === "POST") && + url.pathname === `${BASE_PATH}/${store.id}` + ) { + reply(200, store); + return; + } + if (url.pathname === `${BASE_PATH}/${store.id}/memories`) { + if (method === "GET") { + if (url.searchParams.get("limit") !== "20") { + reply(400, { message: "limit must be between 1 and 20" }); + return; + } + const page = url.searchParams.get("page"); + if (page !== null && page !== nextPage) { + reply(400, { message: "Invalid opaque cursor" }); + return; + } + const offset = page === nextPage ? 20 : 0; + const all = [...memories].map(([path, memory]) => ({ + id: memory.id, + type: "memory", + path, + content_sha256: createHash("sha256").update(memory.content).digest("hex"), + })); + reply(200, { + data: all.slice(offset, offset + 20), + next_page: offset + 20 < all.length ? nextPage : null, + }); + return; + } + if (method === "POST") { + const path = body.path as string; + if (path === "/beta.md" && failBeta) { + reply(503, { message: "Injected beta initialization failure" }); + return; + } + createdPaths.push(path); + const memory = { id: `memory_${memories.size + 1}`, content: body.content as string }; + memories.set(path, memory); + reply(200, { id: memory.id, type: "memory", path }); + return; + } + } + reply(500, { message: `Unexpected request: ${method} ${url.pathname}` }); + }); + }); + servers.push(server); + await new Promise<void>((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("Expected a local server address."); + const origin = `http://127.0.0.1:${address.port}`; + const environment = setup({ base_url: origin, api_key: "test-key" }); + const configPath = join(environment.directory, "agents.yaml"); + writeFileSync( + configPath, + [ + 'version: "1"', + "providers:", + " bailian:", + ' api_key: "test-key"', + ` base_url: ${origin}/api/v1/agentstudio`, + "", + ].join("\n"), + ); + const args = [ + "managed-agent", + "memory-store", + "create", + "--name", + "Notes", + ...entries.flatMap((entry) => ["--entry", JSON.stringify(entry)]), + "--yes", + ]; + const first = await run(args, environment); + expect(first.exitCode).toBe(1); + expect(first.stderr).toContain("Injected beta initialization failure"); + expect(`${first.stdout}${first.stderr}`).toContain("503"); + expect(`${first.stdout}${first.stderr}`).not.toContain("limit must be between 1 and 20"); + expect(createdPaths).toEqual(["/alpha.md"]); + const backend = new LocalFileStateBackend({ configPath }); + const statePath = backend.getStatePath({ projectId: basename(environment.directory) }); + const resourceAddress = { provider: "bailian", type: "memory_store" as const, name: "notes" }; + const checkpoint = await StateManager.load(statePath); + expect(checkpoint.getResource(resourceAddress)).toMatchObject({ + remote_id: store.id, + memory_initialization: { + pending_entries: entries.slice(1).map((entry) => ({ + path: `/${entry.key}`, + content_sha256: createHash("sha256").update(entry.content).digest("hex"), + })), + }, + }); + + memories.get("/alpha.md")!.content = "Agent updated alpha"; + for (let index = 0; index < 20; index += 1) { + memories.set(`/filler-${index}.md`, { id: `filler_${index}`, content: "Keep filler" }); + } + memories.set("/beta.md", { id: "memory_agent_beta", content: "Agent supplied beta" }); + failBeta = false; + const resumed = await run(args, environment); + expect(resumed.exitCode, resumed.stderr).toBe(0); + expect(parseStdoutJson(resumed.stdout)).toMatchObject({ + resource: { type: "memory_store", key: "notes", remote_id: store.id }, + reused_pending: true, + }); + expect(storeCreates).toBe(1); + expect(createdPaths).toEqual(["/alpha.md", "/gamma.md"]); + expect(memories.get("/alpha.md")?.content).toBe("Agent updated alpha"); + expect(memories.get("/beta.md")?.content).toBe("Agent supplied beta"); + expect(memories.get("/gamma.md")?.content).toBe("Initial gamma"); + const listRequests = received.filter( + ({ method, path }) => method === "GET" && path.endsWith("/memories"), + ); + expect(listRequests.every(({ limit }) => limit === "20")).toBe(true); + expect(listRequests.some(({ page }) => page === nextPage)).toBe(true); + const completed = await StateManager.load(statePath); + expect(completed.getResource(resourceAddress)?.remote_id).toBe(store.id); + expect(completed.getResource(resourceAddress)?.memory_initialization).toBeUndefined(); + expect(Object.keys(parse(readFileSync(configPath, "utf8")).memory_stores)).toEqual(["notes"]); + }); + test("content-file preview reads an empty UTF-8 file", async () => { + const environment = setup({ base_url: "http://127.0.0.1:1" }); + const path = join(environment.directory, "empty.txt"); + writeFileSync(path, ""); + const result = await run( + [ + "managed-agent", + "memory-store", + "memories", + "update", + ...MEMORY_ARGS, + "--content-file", + path, + "--dry-run", + ], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ request: { content: "" } }); + }); + test.each(["create", "run"])( + "Session %s previews repeatable resources and legacy YAML keys", + async (action) => { + const environment = setup(); + const resource = { + type: "memory_store", + memory_store: "notes", + access: "read_only", + instructions: "Use project notes", + }; + const path = join(environment.directory, "mount.json"); + writeFileSync(path, JSON.stringify(resource)); + const result = await run( + [ + "managed-agent", + "session", + action, + ...(action === "run" ? ["--prompt", "hello"] : []), + "--memory-stores", + "notes,other", + "--resource", + `@${path}`, + "--dry-run", + ], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + [action === "create" ? "would_create_session" : "would_run"]: { + memory_stores: ["notes", "other"], + resources: [resource], + }, + }); + }, + ); + test.each(["create", "run"])( + "Session %s rejects invalid resource access before auth or project loading", + async (action) => { + const environment = setup(); + const result = await run( + [ + "managed-agent", + "session", + action, + ...(action === "run" ? ["--prompt", "hello"] : []), + "--resource", + '{"type":"memory_store","memory_store":"notes","access":"write"}', + "--quiet", + ], + environment, + ); + expect(result.exitCode, result.stderr).toBe(2); + expect(result.stderr).toContain("access"); + expect(readdirSync(environment.directory)).toEqual(["config.json"]); + }, + ); +}); + +describe("e2e: managed-agent memory SDK HTTP protocol", () => { + test.each(OPERATIONS)( + "$action sends SDK method, path and fields and preserves response", + async ({ action, args, method, path, query, body, response, write }) => { + const { origin, received } = await gateway(response); + const environment = setup({ base_url: origin, api_key: "test-key" }); + const result = await run( + ["managed-agent", "memory-store", ...action, ...args, ...(write ? ["--yes"] : [])], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toEqual(response); + expect(received).toHaveLength(1); + const request = received[0]!; + const url = new URL(request.path!, origin); + expect(request.method).toBe(method); + expect(url.pathname).toBe(path); + expect(Object.fromEntries(url.searchParams)).toEqual(query ?? {}); + expect(request.authorization).toBe("Bearer test-key"); + if (body) expect(JSON.parse(request.body)).toEqual(body); + expect(readdirSync(environment.directory)).toEqual(["config.json"]); + }, + ); + test("service errors remain authoritative", async () => { + const { origin } = await gateway( + { message: "Memory service exact failure", request_id: "memory-error" }, + 409, + ); + const result = await run( + [ + "managed-agent", + "memory-store", + "memories", + "update", + ...MEMORY_ARGS, + "--content", + "x", + "--yes", + ], + setup({ base_url: origin, api_key: "test-key" }), + ); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("Memory service exact failure"); + }); +}); diff --git a/packages/commands/tests/e2e/managed-agent-session-environment.e2e.test.ts b/packages/commands/tests/e2e/managed-agent-session-environment.e2e.test.ts new file mode 100644 index 000000000..a357ca17d --- /dev/null +++ b/packages/commands/tests/e2e/managed-agent-session-environment.e2e.test.ts @@ -0,0 +1,420 @@ +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type Server } from "node:http"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; +import { LocalFileStateBackend, StateManager } from "@openagentpack/sdk"; +import { runNodeMain } from "e2e/runner"; +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { commandsPackageRoot, parseStdoutJson, runCommandHelp } from "./helpers.ts"; +import type { E2eRouteExports } from "./topic-routes.ts"; + +const ROUTES: E2eRouteExports = { + "managed-agent session create": "managedAgentSessionCreate", + "managed-agent session run": "managedAgentSessionRun", +}; +const directories: string[] = []; +const servers: Server[] = []; +const BASE_PATH = "/api/v1/agentstudio/sessions"; +const SESSION_ID = "sess_environment_e2e"; +const SESSION_PATH = `${BASE_PATH}/${SESSION_ID}`; +const AGENT_ID = "agent_environment_e2e"; +const ENVIRONMENT_ID = "env_environment_e2e"; +const ENVIRONMENT_ENTRIES = [ + "DUPLICATE=fake-old-value", + " TRIMMED_KEY = fake-spaced-value ", + "EMPTY=", + "EQUALS=fake=value=with=equals", + "DUPLICATE=fake-new-value", +]; +const ENVIRONMENT_VARIABLES = { + DUPLICATE: "fake-new-value", + TRIMMED_KEY: " fake-spaced-value ", + EMPTY: "", + EQUALS: "fake=value=with=equals", +}; +const ENVIRONMENT_ARGS = ENVIRONMENT_ENTRIES.flatMap((entry) => ["--environment_variables", entry]); +const EVENTS = [ + { + id: "evt_assistant_environment_e2e", + type: "message", + role: "assistant", + content: [{ type: "text", text: "Environment run completed." }], + }, + { + id: "evt_status_environment_e2e", + type: "session_status", + content: [{ type: "data", data: { session_status: "completed" } }], + }, +]; + +afterEach(async () => { + await Promise.all( + servers.splice(0).map( + (server) => + new Promise<void>((resolve, reject) => { + server.closeAllConnections(); + server.close((error) => (error ? reject(error) : resolve())); + }), + ), + ); + for (const directory of directories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +function setup(config: Record<string, unknown> = {}) { + const directory = mkdtempSync(join(tmpdir(), "bl-session-environment-e2e-")); + directories.push(directory); + writeFileSync(join(directory, "config.json"), JSON.stringify(config)); + writeFileSync(join(directory, "provider-config.json"), "{}"); + return { + directory, + env: { + BAILIAN_CONFIG_DIR: directory, + AGENTS_CONFIG_PATH: join(directory, "provider-config.json"), + DASHSCOPE_API_KEY: "", + DASHSCOPE_BASE_URL: "", + BAILIAN_API_KEY: "", + BAILIAN_BASE_URL: "", + BAILIAN_WORKSPACE_ID: "", + BAILIAN_E2E: "0", + HTTP_PROXY: "", + HTTPS_PROXY: "", + ALL_PROXY: "", + http_proxy: "", + https_proxy: "", + all_proxy: "", + NO_PROXY: "127.0.0.1,localhost", + }, + }; +} + +function sessionArgs(action: string) { + return ["managed-agent", "session", action, ...(action === "run" ? ["--prompt", "hello"] : [])]; +} + +function run(args: string[], environment: ReturnType<typeof setup>) { + return runNodeMain( + join(commandsPackageRoot, "tests/e2e/harness/main.ts"), + [...args, "--output", "json"], + { + cwd: environment.directory, + env: { + ...environment.env, + BAILIAN_E2E_ROUTES: JSON.stringify( + Object.entries(ROUTES).map(([path, exportName]) => ({ path, export: exportName })), + ), + }, + }, + ); +} + +async function seedProject( + environment: ReturnType<typeof setup>, + origin: string, + agentEnvironmentVariables?: Record<string, string>, +) { + const configPath = join(environment.directory, "agents.yaml"); + writeFileSync( + configPath, + [ + 'version: "1"', + "providers:", + " bailian:", + ' api_key: ""', + ` base_url: ${origin}/api/v1/agentstudio`, + "environments:", + " sandbox:", + ` environment_id: ${ENVIRONMENT_ID}`, + " config:", + " type: cloud", + "agents:", + " assistant:", + " model: qwen3.8-max", + " instructions: Complete the test prompt.", + " environment: sandbox", + " provider: bailian", + ...(agentEnvironmentVariables === undefined + ? [] + : [` environment_variables: ${JSON.stringify(agentEnvironmentVariables)}`]), + "", + ].join("\n"), + ); + const backend = new LocalFileStateBackend({ configPath }); + const statePath = backend.getStatePath({ projectId: basename(environment.directory) }); + const state = StateManager.initialize(statePath); + state.setResource({ + address: { type: "agent", name: "assistant", provider: "bailian" }, + remote_id: AGENT_ID, + content_hash: "test-agent-hash", + desired_hash: "test-agent-hash", + }); + await state.save(); + return { configPath, statePath }; +} + +async function gateway(environmentVariables?: Record<string, string>) { + const received: { + method?: string; + path: string; + body: string; + authorization?: string; + }[] = []; + const sessionResponse = { + id: SESSION_ID, + agent: { id: AGENT_ID }, + environment_id: ENVIRONMENT_ID, + status: "completed", + vault_ids: [], + resources: [], + server_note: "Preserve the original Session response.", + ...(environmentVariables === undefined ? {} : { environment_variables: environmentVariables }), + }; + const server = createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + const path = request.url ?? ""; + received.push({ + method: request.method, + path, + body: Buffer.concat(chunks).toString("utf8"), + authorization: request.headers.authorization, + }); + if (request.method === "GET" && path === `${SESSION_PATH}/events/stream`) { + response.writeHead(200, { "content-type": "text/event-stream" }); + response.end(EVENTS.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("")); + return; + } + let body: unknown; + if ( + (request.method === "POST" && path === BASE_PATH) || + (request.method === "GET" && path === SESSION_PATH) + ) { + body = sessionResponse; + } else if (request.method === "POST" && path === `${SESSION_PATH}/events`) { + body = { id: "evt_user_environment_e2e" }; + } else if (request.method === "GET" && path.startsWith(`${SESSION_PATH}/events?`)) { + body = { data: EVENTS, has_more: false, next_page: null }; + } else { + response.writeHead(404, { "content-type": "application/json" }); + response.end( + JSON.stringify({ message: `Unexpected test request: ${request.method} ${path}` }), + ); + return; + } + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify(body)); + }); + }); + await new Promise<void>((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + servers.push(server); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a local server address."); + return { origin: `http://127.0.0.1:${address.port}`, received, sessionResponse }; +} + +describe("e2e: managed-agent Session environment variable help and validation", () => { + test.each(["create", "run"])("Session %s help documents repeatable variables", async (action) => { + const result = await runCommandHelp(ROUTES, ["managed-agent", "session", action, "--help"]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("--environment_variables"); + expect(result.stderr).toContain("<KEY=VALUE>"); + expect(result.stderr).toContain("last value wins"); + }); + + test.each( + ["create", "run"].flatMap((action) => + [ + { entry: "fake-secret-without-separator", reason: "KEY=VALUE" }, + { entry: " =fake-secret-without-key", reason: "keys must not be empty" }, + ].map((invalid) => ({ action, ...invalid })), + ), + )( + "Session $action rejects $reason before authentication or HTTP", + async ({ action, entry, reason }) => { + const { origin, received } = await gateway(); + for (const config of [ + { base_url: origin }, + { base_url: origin, api_key: "test-session-key" }, + ]) { + const environment = setup(config); + const before = readdirSync(environment.directory); + const result = await run( + [...sessionArgs(action), "--environment_variables", entry, "--quiet"], + environment, + ); + expect(result.exitCode, result.stderr).toBe(2); + expect(result.stderr).toContain("--environment_variables"); + expect(result.stderr).toContain(reason); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(entry); + expect(readdirSync(environment.directory)).toEqual(before); + } + expect(received).toEqual([]); + }, + ); +}); + +describe("e2e: managed-agent Session environment variable offline preview", () => { + test.each(["create", "run"])( + "Session %s dry-run keeps keys and redacts every value", + async (action) => { + const { origin, received } = await gateway(); + const environment = setup({ base_url: origin }); + const before = readdirSync(environment.directory); + const result = await run( + [...sessionArgs(action), ...ENVIRONMENT_ARGS, "--dry-run"], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + [action === "create" ? "would_create_session" : "would_run"]: { + environment_variables: Object.fromEntries( + Object.keys(ENVIRONMENT_VARIABLES).map((key) => [key, "[REDACTED]"]), + ), + }, + }); + for (const value of Object.values(ENVIRONMENT_VARIABLES).filter(Boolean)) { + expect(`${result.stdout}\n${result.stderr}`).not.toContain(value); + } + expect(`${result.stdout}\n${result.stderr}`).not.toContain("fake-old-value"); + expect(received).toEqual([]); + expect(readdirSync(environment.directory)).toEqual(before); + }, + ); + + test.each(["create", "run"])("Session %s dry-run omits unspecified variables", async (action) => { + const result = await run([...sessionArgs(action), "--dry-run"], setup()); + expect(result.exitCode, result.stderr).toBe(0); + const output = parseStdoutJson(result.stdout) as Record<string, Record<string, unknown>>; + expect(output[action === "create" ? "would_create_session" : "would_run"]).not.toHaveProperty( + "environment_variables", + ); + }); +}); + +describe("e2e: managed-agent Session environment variable SDK HTTP protocol", () => { + test.each(["create", "run"])( + "Session %s rejects inherited Agent YAML variables before HTTP", + async (action) => { + const { origin, received } = await gateway(); + const environment = setup({ base_url: origin, api_key: "test-session-key" }); + const inheritedValue = "fake-inherited-agent-secret"; + const { configPath, statePath } = await seedProject(environment, origin, { + YAML_ONLY: inheritedValue, + }); + const originalConfig = readFileSync(configPath, "utf8"); + const originalState = readFileSync(statePath, "utf8"); + const result = await run(sessionArgs(action), environment); + expect(result.exitCode, result.stderr).toBe(2); + expect(result.stderr).toContain("environment_variables"); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(inheritedValue); + expect(received).toEqual([]); + expect(readFileSync(configPath, "utf8")).toBe(originalConfig); + expect(readFileSync(statePath, "utf8")).toBe(originalState); + }, + ); + + test.each([true, false])( + "Session create sends variables only when provided (%s)", + async (provided) => { + const variables = provided ? ENVIRONMENT_VARIABLES : undefined; + const { origin, received, sessionResponse } = await gateway(variables); + const environment = setup({ base_url: origin, api_key: "test-session-key" }); + const { configPath, statePath } = await seedProject(environment, origin); + const originalConfig = readFileSync(configPath, "utf8"); + const originalState = readFileSync(statePath, "utf8"); + const result = await run( + [...sessionArgs("create"), ...(provided ? ENVIRONMENT_ARGS : [])], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + agent: "assistant", + session: { id: SESSION_ID, attributes: sessionResponse }, + }); + const output = parseStdoutJson(result.stdout) as { session: Record<string, unknown> }; + if (provided) expect(output.session.environment_variables).toEqual(ENVIRONMENT_VARIABLES); + else expect(output.session).not.toHaveProperty("environment_variables"); + expect(received).toHaveLength(1); + expect(received[0]).toMatchObject({ + method: "POST", + path: BASE_PATH, + authorization: "Bearer test-session-key", + }); + expect(JSON.parse(received[0]!.body)).toEqual({ + agent: AGENT_ID, + environment_id: ENVIRONMENT_ID, + ...(provided ? { environment_variables: ENVIRONMENT_VARIABLES } : {}), + }); + expect(readFileSync(configPath, "utf8")).toBe(originalConfig); + expect(readFileSync(statePath, "utf8")).toBe(originalState); + }, + ); + + test.each(["streaming", "polling"])("Session run forwards variables through %s", async (mode) => { + const { origin, received } = await gateway(ENVIRONMENT_VARIABLES); + const environment = setup({ base_url: origin, api_key: "test-session-key" }); + const { configPath, statePath } = await seedProject(environment, origin); + const originalConfig = readFileSync(configPath, "utf8"); + const originalState = readFileSync(statePath, "utf8"); + const result = await run( + [...sessionArgs("run"), ...ENVIRONMENT_ARGS, ...(mode === "polling" ? ["--no-stream"] : [])], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + session_id: SESSION_ID, + provider: "bailian", + agent: "assistant", + events: expect.arrayContaining([ + expect.objectContaining({ + type: "message", + role: "assistant", + content: [{ type: "text", text: "Environment run completed." }], + metadata: { display_bucket: "message" }, + }), + expect.objectContaining({ + type: "session_status", + metadata: { display_bucket: "status", status: "completed" }, + }), + ]), + }); + expect(received[0]).toMatchObject({ method: "POST", path: BASE_PATH }); + expect(JSON.parse(received[0]!.body)).toEqual({ + agent: AGENT_ID, + environment_id: ENVIRONMENT_ID, + environment_variables: ENVIRONMENT_VARIABLES, + }); + const sent = received.find( + (request) => request.method === "POST" && request.path === `${SESSION_PATH}/events`, + ); + expect(sent).toBeDefined(); + expect(JSON.parse(sent!.body)).toEqual({ + input: [{ role: "user", type: "message", content: [{ type: "text", text: "hello" }] }], + }); + const requestedPaths = received.map((request) => ({ + method: request.method, + path: request.path, + })); + expect(requestedPaths).toHaveLength(mode === "polling" ? 4 : 3); + if (mode === "polling") { + expect(requestedPaths).toContainEqual({ method: "GET", path: SESSION_PATH }); + expect(requestedPaths).toContainEqual({ + method: "GET", + path: `${SESSION_PATH}/events?limit=100`, + }); + } else { + expect(requestedPaths).toContainEqual({ + method: "GET", + path: `${SESSION_PATH}/events/stream`, + }); + } + for (const request of received) expect(request.authorization).toBe("Bearer test-session-key"); + expect(readFileSync(configPath, "utf8")).toBe(originalConfig); + expect(readFileSync(statePath, "utf8")).toBe(originalState); + }); +}); diff --git a/packages/commands/tests/e2e/managed-agent-session-resources.e2e.test.ts b/packages/commands/tests/e2e/managed-agent-session-resources.e2e.test.ts new file mode 100644 index 000000000..72beee5d4 --- /dev/null +++ b/packages/commands/tests/e2e/managed-agent-session-resources.e2e.test.ts @@ -0,0 +1,124 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import type { E2eRouteExports } from "./topic-routes.ts"; + +const ROUTES: E2eRouteExports = { + "managed-agent session create": "managedAgentSessionCreate", + "managed-agent session run": "managedAgentSessionRun", +}; +const directories: string[] = []; +const PRIVATE_TOKEN = "fake-session-resource-private-token"; +const GITHUB_RESOURCE = { + type: "github_repository", + url: "https://github.com/example/synthetic-review", + authorization_token: PRIVATE_TOKEN, +}; +const INPUT_FORMS = ["inline", "file"] as const; + +afterEach(() => { + for (const directory of directories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +function fixture(resource: Record<string, unknown>, inputForm: (typeof INPUT_FORMS)[number]) { + const directory = mkdtempSync(join(tmpdir(), "bl-session-resources-e2e-")); + directories.push(directory); + const json = JSON.stringify(resource); + const resourcePath = join(directory, "resource.json"); + if (inputForm === "file") writeFileSync(resourcePath, json); + return { + resourceArgument: inputForm === "file" ? `@${resourcePath}` : json, + configPath: join(directory, "missing-project.yaml"), + }; +} + +function runSession(action: string, resourceFixture: ReturnType<typeof fixture>, dryRun: boolean) { + return runCommandE2e( + ROUTES, + [ + "managed-agent", + "session", + action, + ...(action === "run" ? ["--prompt", "hello"] : []), + "--resource", + resourceFixture.resourceArgument, + "--file", + resourceFixture.configPath, + ...(dryRun ? ["--dry-run"] : []), + "--output", + "json", + "--quiet", + ], + { + DASHSCOPE_API_KEY: undefined, + BAILIAN_API_KEY: undefined, + BAILIAN_E2E: "0", + DO_NOT_TRACK: "1", + }, + ); +} + +describe("e2e: managed-agent Session resource provider validation", () => { + test.each( + ["create", "run"].flatMap((action) => + INPUT_FORMS.flatMap((inputForm) => + [true, false].map((dryRun) => ({ action, inputForm, dryRun })), + ), + ), + )( + "Session $action rejects $inputForm GitHub resources before auth or preview (dry-run: $dryRun)", + async ({ action, inputForm, dryRun }) => { + const result = await runSession(action, fixture(GITHUB_RESOURCE, inputForm), dryRun); + expect(result.exitCode, result.stderr).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("Bailian Session resources only support memory_store"); + expect(result.stderr).toContain("百炼 Session 资源仅支持 memory_store"); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(PRIVATE_TOKEN); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(GITHUB_RESOURCE.type); + }, + ); + + test.each(INPUT_FORMS)( + "does not echo an arbitrary resource type from %s input", + async (inputForm) => { + const privateType = "fake-private-resource-type"; + const result = await runSession( + "create", + fixture({ ...GITHUB_RESOURCE, type: privateType }, inputForm), + true, + ); + expect(result.exitCode, result.stderr).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("Bailian Session resources only support memory_store"); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(privateType); + expect(`${result.stdout}\n${result.stderr}`).not.toContain(PRIVATE_TOKEN); + }, + ); + + test.each([ + { action: "create", inputForm: "inline" }, + { action: "run", inputForm: "file" }, + ] as const)( + "Session $action still previews $inputForm Memory Store resources", + async ({ action, inputForm }) => { + const resource = { + type: "memory_store", + memory_store: "notes", + access: "read_only", + instructions: "Use the project notes", + }; + const result = await runSession(action, fixture(resource, inputForm), true); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + [action === "create" ? "would_create_session" : "would_run"]: { + provider: "bailian", + resources: [resource], + }, + }); + }, + ); +}); diff --git a/packages/commands/tests/e2e/managed-agent-webhook.e2e.test.ts b/packages/commands/tests/e2e/managed-agent-webhook.e2e.test.ts new file mode 100644 index 000000000..545ae4b90 --- /dev/null +++ b/packages/commands/tests/e2e/managed-agent-webhook.e2e.test.ts @@ -0,0 +1,559 @@ +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { createServer, type IncomingHttpHeaders, type Server } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { + WebhookEndpoint, + WebhookEventWithDelivery, + WebhookTestResult, +} from "@openagentpack/sdk"; +import { runNodeMain } from "e2e/runner"; +import { afterEach, describe, expect, test } from "vite-plus/test"; +import { commandsPackageRoot, parseStdoutJson, runCommandHelp } from "./helpers.ts"; +import { MANAGED_AGENT_WEBHOOK_ROUTES as ROUTES } from "./topic-routes.ts"; + +const API_ROOT = "/api/v1/agentstudio"; +const ENDPOINT_PATH = `${API_ROOT}/webhook_endpoints`; +const WEBHOOK_ID = "wep_a/b c"; +const ENCODED_WEBHOOK_ID = encodeURIComponent(WEBHOOK_ID); +const SIGNING_SECRET = "whsec-webhook-response-test-only"; +const API_KEY = "sk-webhook-cli-test-only"; +const CALLBACK_URL = "https://receiver.example.test/agent-events"; +const EVENT_TYPES = ["session.updated", "session.status_idled"]; +const ENDPOINT = { + id: WEBHOOK_ID, + description: "test endpoint", + url: CALLBACK_URL, + events: EVENT_TYPES, + status: "ACTIVE", + disabled_reason: null, + consecutive_fail: 0, + last_success_at: null, + last_failure_at: null, + created_at: "2026-09-14T00:00:00Z", + updated_at: "2026-09-14T00:00:00Z", + request_id: "request-webhook", +} satisfies WebhookEndpoint; +const EVENT = { + id: "whe_testevent", + type: "event", + created_at: "2026-09-14T00:00:00Z", + data: { id: "whe_testevent", type: "webhook.test", workspace_id: "ws-test" }, + request_id: "request-test", +} satisfies WebhookTestResult; +const DELIVERY_EVENT = { + type: EVENT.type, + id: EVENT.id, + created_at: EVENT.created_at, + data: { ...EVENT.data, type: "session.updated", session_thread_id: "thread-test" }, + delivery: { + webhook_id: WEBHOOK_ID, + status: "SUCCEEDED", + attempt_count: 1, + delivery_at: "2026-09-14T00:00:00Z", + finish_at: "2026-09-14T00:00:01Z", + failure_reason: null, + }, +} satisfies WebhookEventWithDelivery; +const CREATE_ARGS = [ + "--url", + CALLBACK_URL, + "--event", + EVENT_TYPES[0]!, + "--event", + EVENT_TYPES[1]!, + "--description", + "test endpoint", +]; +const SINGLE_ARGS = ["--webhook-id", WEBHOOK_ID]; +const OPERATIONS = [ + { + action: "create", + args: CREATE_ARGS, + method: "POST", + suffix: "", + write: true, + body: { url: CALLBACK_URL, events: EVENT_TYPES, description: "test endpoint" }, + response: { ...ENDPOINT, signing_secret: SIGNING_SECRET }, + status: 201, + }, + { + action: "list", + args: [], + method: "GET", + suffix: "", + write: false, + response: { data: [ENDPOINT], request_id: "request-list" }, + }, + { + action: "get", + args: SINGLE_ARGS, + method: "GET", + suffix: `/${ENCODED_WEBHOOK_ID}`, + write: false, + response: ENDPOINT, + }, + { + action: "update", + args: [...SINGLE_ARGS, "--description", ""], + method: "PUT", + suffix: `/${ENCODED_WEBHOOK_ID}`, + write: true, + body: { description: "" }, + response: ENDPOINT, + }, + { + action: "delete", + args: SINGLE_ARGS, + method: "DELETE", + suffix: `/${ENCODED_WEBHOOK_ID}`, + write: true, + response: { request_id: "request-delete" }, + }, + { + action: "enable", + args: SINGLE_ARGS, + method: "POST", + suffix: `/${ENCODED_WEBHOOK_ID}/enable`, + write: true, + response: ENDPOINT, + }, + { + action: "disable", + args: SINGLE_ARGS, + method: "POST", + suffix: `/${ENCODED_WEBHOOK_ID}/disable`, + write: true, + response: { ...ENDPOINT, status: "DISABLED" }, + }, + { + action: "test", + args: SINGLE_ARGS, + method: "POST", + suffix: `/${ENCODED_WEBHOOK_ID}/test`, + write: true, + response: EVENT, + }, + { + action: "reset-secret", + args: SINGLE_ARGS, + method: "POST", + suffix: `/${ENCODED_WEBHOOK_ID}/reset_secret`, + write: true, + response: { + id: WEBHOOK_ID, + signing_secret: SIGNING_SECRET, + updated_at: "2026-09-14T01:00:00Z", + request_id: "request-reset", + }, + }, + { + action: "events", + args: [...SINGLE_ARGS, "--limit", "2", "--page", "opaque+/=&cursor"], + method: "GET", + suffix: `/${ENCODED_WEBHOOK_ID}/events`, + write: false, + response: { data: [DELIVERY_EVENT], next_page: "next+/=&cursor", request_id: "request-events" }, + }, +]; +const directories: string[] = []; +const servers: Server[] = []; + +afterEach(async () => { + await Promise.all( + servers.splice(0).map( + (server) => + new Promise<void>((resolve, reject) => { + server.closeAllConnections(); + server.close((error) => (error ? reject(error) : resolve())); + }), + ), + ); + for (const directory of directories.splice(0)) + rmSync(directory, { recursive: true, force: true }); +}); + +function setup(config: Record<string, unknown> = {}) { + const directory = mkdtempSync(join(tmpdir(), "bl-webhook-e2e-")); + directories.push(directory); + writeFileSync(join(directory, "config.json"), JSON.stringify(config)); + const env: NodeJS.ProcessEnv = { + BAILIAN_CONFIG_DIR: directory, + DASHSCOPE_API_KEY: "", + DASHSCOPE_BASE_URL: "", + BAILIAN_API_KEY: "", + BAILIAN_BASE_URL: "", + BAILIAN_WORKSPACE_ID: "", + BAILIAN_E2E: "0", + HTTP_PROXY: "", + HTTPS_PROXY: "", + ALL_PROXY: "", + http_proxy: "", + https_proxy: "", + all_proxy: "", + NO_PROXY: "127.0.0.1,localhost", + }; + return { directory, env }; +} + +function runWebhook( + action: string, + args: string[], + environment: ReturnType<typeof setup>, + overrides: NodeJS.ProcessEnv = {}, + outputFormat = "json", +) { + return runNodeMain( + join(commandsPackageRoot, "tests", "e2e", "harness", "main.ts"), + ["managed-agent", "webhook", action, ...args, "--output", outputFormat], + { + cwd: environment.directory, + env: { + ...environment.env, + ...overrides, + BAILIAN_E2E_ROUTES: JSON.stringify( + Object.entries(ROUTES).map(([path, exportName]) => ({ path, export: exportName })), + ), + }, + }, + ); +} + +async function gateway(responseBody: unknown = ENDPOINT, status = 200, holdResponse = false) { + const received: { method?: string; path?: string; headers: IncomingHttpHeaders; body: string }[] = + []; + const server = createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + received.push({ + method: request.method, + path: request.url, + headers: request.headers, + body: Buffer.concat(chunks).toString("utf8"), + }); + if (holdResponse) return; + response.writeHead(status, { "content-type": "application/json" }); + response.end(JSON.stringify(responseBody)); + }); + }); + await new Promise<void>((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + servers.push(server); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Expected a local TCP server."); + return { origin: `http://127.0.0.1:${address.port}`, received }; +} + +describe("e2e: managed-agent Webhook discovery and validation", () => { + test.each(OPERATIONS)( + "$action help resolves with API Key authentication", + async ({ action, write }) => { + const result = await runCommandHelp(ROUTES, ["managed-agent", "webhook", action, "--help"]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain(`Usage: bl managed-agent webhook ${action}`); + expect(result.stderr).toContain("Authentication: API Key"); + expect(result.stderr).toContain("--workspace-id"); + expect(result.stderr).not.toContain("--console-site"); + expect(result.stderr).not.toContain("--provider"); + expect(result.stderr).not.toContain("--file"); + if (write) expect(result.stderr).toContain("--yes"); + else expect(result.stderr).not.toContain("--yes"); + }, + ); + + test.each([ + { action: "create", args: ["--event", "session.updated"], missing: "--url" }, + { action: "create", args: ["--url", CALLBACK_URL], missing: "--event" }, + ...OPERATIONS.filter(({ action }) => !["create", "list"].includes(action)).map( + ({ action }) => ({ action, args: [], missing: "--webhook-id" }), + ), + ])( + "$action rejects missing $missing before authentication", + async ({ action, args, missing }) => { + const result = await runWebhook( + action, + [...args, "--dry-run", "--workspace-id", "ws-test"], + setup(), + ); + expect(result.exitCode).toBe(2); + expect(JSON.parse(result.stderr).error.message).toContain(missing); + }, + ); + + test.each([ + { action: "update", args: SINGLE_ARGS }, + { action: "create", args: ["--url", "not-a-url", "--event", "session.updated"] }, + { + action: "create", + args: ["--url", "ftp://example.test/hook", "--event", "session.updated"], + }, + { action: "create", args: ["--url", CALLBACK_URL, "--event", " "] }, + { action: "create", args: ["--url", CALLBACK_URL, "--event", "session.*"] }, + { action: "update", args: [...SINGLE_ARGS, "--description", "字".repeat(257)] }, + { action: "get", args: ["--webhook-id", " "] }, + { action: "events", args: [...SINGLE_ARGS, "--page", " "] }, + ...["0", "101", "1.5"].map((limit) => ({ + action: "events", + args: [...SINGLE_ARGS, "--limit", limit], + })), + ])("$action rejects invalid request values offline", async ({ action, args }) => { + const result = await runWebhook( + action, + [...args, "--workspace-id", "ws-test", "--dry-run"], + setup(), + ); + expect(result.exitCode).toBe(2); + }); +}); + +describe("e2e: managed-agent Webhook confirmation and offline preview", () => { + test.each(OPERATIONS.filter(({ write }) => write))( + "$action requires --yes before any request", + async ({ action, args }) => { + const { origin, received } = await gateway(); + const environment = setup({ api_key: API_KEY, base_url: origin }); + const result = await runWebhook(action, args, environment); + expect(result.exitCode).toBe(7); + expect(JSON.parse(result.stderr)).toMatchObject({ + error: { code: 7, type: "requires_confirmation" }, + }); + expect(received).toEqual([]); + }, + ); + + test.each(OPERATIONS)( + "$action dry-run needs neither credentials nor project files and performs no request", + async ({ action, args, body }) => { + const { origin, received } = await gateway(); + const environment = setup({ base_url: origin }); + const before = readdirSync(environment.directory); + const result = await runWebhook(action, [...args, "--dry-run"], environment); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toMatchObject({ + operation: `webhook.${action}`, + provider: "bailian", + base_url: `${origin}${API_ROOT}`, + }); + if (body) expect(parseStdoutJson(result.stdout)).toMatchObject({ request: body }); + if (!["create", "list"].includes(action)) + expect(parseStdoutJson(result.stdout)).toMatchObject({ webhook_id: WEBHOOK_ID }); + expect(received).toEqual([]); + expect(readdirSync(environment.directory)).toEqual(before); + }, + ); + + test("SDK requests and previews ignore invalid YAML, State and project .env without modifying them", async () => { + const { origin, received } = await gateway(); + const environment = setup({ api_key: API_KEY, base_url: origin }); + const files = { + "agents.yaml": "invalid: [yaml", + "agents.state.json": "{invalid", + ".env": "BAILIAN_BASE_URL=https://unused.example.test\n", + }; + for (const [name, content] of Object.entries(files)) + writeFileSync(join(environment.directory, name), content); + const getResult = await runWebhook("get", SINGLE_ARGS, environment); + expect(getResult.exitCode, getResult.stderr).toBe(0); + expect(received).toHaveLength(1); + const result = await runWebhook("create", [...CREATE_ARGS, "--dry-run"], environment); + expect(result.exitCode, result.stderr).toBe(0); + expect(received).toHaveLength(1); + for (const [name, content] of Object.entries(files)) + expect(readFileSync(join(environment.directory, name), "utf8")).toBe(content); + }); +}); + +describe("e2e: managed-agent Webhook SDK HTTP protocol", () => { + test.each(OPERATIONS)( + "$action sends the exact SDK method/path/body and preserves the response", + async ({ action, args, method, suffix, body, response, status, write }) => { + const { origin, received } = await gateway(response, status); + const environment = setup({ api_key: API_KEY, base_url: origin }); + const result = await runWebhook( + action, + [...args, ...(write ? ["--yes"] : []), "--verbose"], + environment, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toEqual(response); + expect(received).toHaveLength(1); + const request = received[0]!; + expect(request.method).toBe(method); + const url = new URL(request.path!, origin); + expect(url.pathname).toBe(`${ENDPOINT_PATH}${suffix}`); + expect(request.headers.authorization).toBe(`Bearer ${API_KEY}`); + expect(request.headers["x-access-token"]).toBeUndefined(); + if (body) expect(JSON.parse(request.body)).toEqual(body); + else expect(request.body === "" || request.body === "{}").toBe(true); + if (action === "events") + expect(Object.fromEntries(url.searchParams)).toEqual({ + limit: "2", + page: "opaque+/=&cursor", + }); + else expect(url.search).toBe(""); + expect(result.stderr).not.toContain(API_KEY); + expect(result.stderr).not.toContain(SIGNING_SECRET); + expect(readdirSync(environment.directory)).toEqual(["config.json"]); + }, + ); + + test("update sends only supplied fields and preserves repeated events", async () => { + const { origin, received } = await gateway(); + const result = await runWebhook( + "update", + [...SINGLE_ARGS, ...CREATE_ARGS, "--yes"], + setup({ api_key: API_KEY, base_url: origin }), + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(JSON.parse(received[0]!.body)).toEqual({ + url: CALLBACK_URL, + events: EVENT_TYPES, + description: "test endpoint", + }); + }); + + test("passes a future named subscription to the service without a local enum", async () => { + const { origin, received } = await gateway(); + const result = await runWebhook( + "update", + [...SINGLE_ARGS, "--event", "future.event", "--yes"], + setup({ api_key: API_KEY, base_url: origin }), + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(JSON.parse(received[0]!.body)).toEqual({ events: ["future.event"] }); + }); + + test.each(["create", "reset-secret"])( + "%s keeps the signing secret in successful text output, including --quiet", + async (action) => { + const response = { ...ENDPOINT, signing_secret: SIGNING_SECRET }; + const { origin } = await gateway(response); + const result = await runWebhook( + action, + [...(action === "create" ? CREATE_ARGS : SINGLE_ARGS), "--yes", "--quiet", "--verbose"], + setup({ api_key: API_KEY, base_url: origin }), + {}, + "text", + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(JSON.parse(result.stdout)).toEqual(response); + expect(result.stderr).not.toContain(SIGNING_SECRET); + expect(result.stderr).not.toContain(API_KEY); + }, + ); + + test("missing API Key fails before opening the SDK connection", async () => { + const { origin, received } = await gateway(); + const result = await runWebhook("get", SINGLE_ARGS, setup({ base_url: origin })); + expect(result.exitCode).toBe(3); + expect(received).toEqual([]); + }); + + test("terminal event pages preserve next_page null without adding has_more", async () => { + const response = { data: [DELIVERY_EVENT], next_page: null, request_id: "request-last-page" }; + const { origin } = await gateway(response); + const result = await runWebhook( + "events", + SINGLE_ARGS, + setup({ api_key: API_KEY, base_url: origin }), + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseStdoutJson(result.stdout)).toEqual(response); + }); + + test("runtime timeout aborts the SDK request", async () => { + const { origin, received } = await gateway(undefined, 200, true); + const startedAt = Date.now(); + const result = await runWebhook( + "get", + [...SINGLE_ARGS, "--timeout", "1"], + setup({ api_key: API_KEY, base_url: origin }), + ); + expect(result.exitCode).toBe(5); + expect(received).toHaveLength(1); + expect(Date.now() - startedAt).toBeLessThan(15_000); + expect(result.stderr).not.toContain(API_KEY); + }); + + test.each([400, 401, 403, 429, 500])( + "HTTP %s preserves service error text without exposing response secrets", + async (status) => { + const message = `Original Webhook service failure ${status}:原始错误`; + const { origin, received } = await gateway( + { + code: "WebhookFailure", + message, + request_id: "request-failure", + signing_secret: SIGNING_SECRET, + }, + status, + ); + const result = await runWebhook( + "get", + [...SINGLE_ARGS, "--verbose"], + setup({ api_key: API_KEY, base_url: origin }), + ); + expect(result.exitCode).toBe(1); + const errorOutput = result.stderr.slice(result.stderr.lastIndexOf('{\n "error":')); + expect(JSON.parse(errorOutput).error.message).toBe(message); + expect(result.stdout).not.toContain(SIGNING_SECRET); + expect(result.stderr).not.toContain(SIGNING_SECRET); + expect(result.stderr).not.toContain(API_KEY); + expect(received.length).toBeGreaterThanOrEqual(1); + }, + ); +}); + +describe("e2e: managed-agent Webhook CLI configuration precedence", () => { + test.each(["flags", "environment", "profile"])( + "%s selects SDK credentials and gateway without agents.yaml", + async (source) => { + const chosen = await gateway(); + const fallback = await gateway(); + const environment = setup({ + active_config: "webhooks", + webhooks: { + api_key: source === "profile" ? API_KEY : "sk-profile-lower-priority", + base_url: source === "profile" ? chosen.origin : fallback.origin, + workspace_id: "ws-profile", + }, + }); + const args = + source === "flags" + ? ["--api-key", API_KEY, "--base-url", chosen.origin, "--workspace-id", "ws-flag"] + : []; + const overrides = + source === "profile" + ? {} + : { + DASHSCOPE_API_KEY: source === "environment" ? API_KEY : "sk-env-lower-priority", + DASHSCOPE_BASE_URL: source === "environment" ? chosen.origin : fallback.origin, + BAILIAN_WORKSPACE_ID: "ws-env", + }; + const result = await runWebhook("get", [...SINGLE_ARGS, ...args], environment, overrides); + expect(result.exitCode, result.stderr).toBe(0); + expect(chosen.received).toHaveLength(1); + expect(chosen.received[0]!.headers.authorization).toBe(`Bearer ${API_KEY}`); + expect(fallback.received).toEqual([]); + }, + ); + + test.each(["flags", "environment", "profile"])( + "%s workspace supplies the default AgentStudio URL in dry-run", + async (source) => { + const environment = setup({ workspace_id: "ws-profile" }); + const args = source === "flags" ? ["--workspace-id", "ws-flag"] : []; + const overrides = source === "profile" ? {} : { BAILIAN_WORKSPACE_ID: "ws-env" }; + const result = await runWebhook("list", [...args, "--dry-run"], environment, overrides); + expect(result.exitCode, result.stderr).toBe(0); + const workspace = + source === "flags" ? "ws-flag" : source === "environment" ? "ws-env" : "ws-profile"; + expect(parseStdoutJson(result.stdout)).toMatchObject({ + base_url: `https://${workspace}.cn-beijing.maas.aliyuncs.com${API_ROOT}`, + }); + }, + ); +}); diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index a6d1b400d..35e184f95 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -36,6 +36,28 @@ const DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES = [ "bailian.deployment.file.mount_path.duplicate", ]; const projectDirectories: string[] = []; +const RUNTIME_BINDING_YAML = `version: "1" +providers: + bailian: + api_key: \${DASHSCOPE_API_KEY} + base_url: \${BAILIAN_BASE_URL} +defaults: + provider: bailian +environments: + dev: + config: + type: cloud + networking: + type: unrestricted +vaults: + secrets: + display_name: Secrets + credentials: [] +`; +const RUNTIME_BINDING_ENV = { + DASHSCOPE_API_KEY: "sk-e2e-agent-runtime-binding", + BAILIAN_BASE_URL: "http://127.0.0.1:1/api/v1/agentstudio", +}; afterEach(async () => { for (const directory of projectDirectories.splice(0)) { @@ -43,6 +65,14 @@ afterEach(async () => { } }); +async function writeRuntimeBindingProject(): Promise<string> { + const directory = await mkdtemp(join(tmpdir(), "bl-agent-runtime-binding-e2e-")); + projectDirectories.push(directory); + const configPath = join(directory, "agents.yaml"); + await writeFile(configPath, RUNTIME_BINDING_YAML, "utf8"); + return configPath; +} + async function writeLocalSkillDirectory(parentDirectory: string): Promise<string> { const skillDirectory = join(parentDirectory, "local-report"); await mkdir(join(skillDirectory, "scripts"), { recursive: true }); @@ -546,7 +576,11 @@ agents: expect(stderr).toMatch(/--name|--model|--instructions|--skill|--skill-dir|--type|--yes/i); expect(stderr).toMatch(/remote Skill ID|远端 Skill ID/i); expect(stderr).toMatch(/local Skill directory|本地 Skill 目录/i); - expect(stderr).not.toMatch(/--key|--environment|--vault/i); + expect(stderr).toMatch(/--environment <yaml-key>/); + expect(stderr).toMatch(/--vault <yaml-key>/); + expect(stderr).toMatch(/--mcp <server-code>/); + expect(stderr).toMatch(/MCP market server code|MCP 市场服务 code/i); + expect(stderr).not.toMatch(/--key/i); }); test("managed-agent agent create 缺少 --name 时退出为用法错误 (2)", async () => { @@ -565,9 +599,37 @@ agents: }); test.each([ - ["--environment", "dev"], - ["--vault", "secrets"], - ])("managed-agent agent create 不接受运行时参数 %s", async (flag, value) => { + ["--environment", /Environment 'missing' is not declared/], + ["--vault", /Vault 'missing' is not declared/], + ])("managed-agent agent create 拒绝未声明的 %s key", async (flag, message) => { + const configPath = await writeRuntimeBindingProject(); + const { stderr, exitCode } = await runCommandE2e( + MANAGED_AGENT_ROUTES, + [ + "managed-agent", + "agent", + "create", + "--name", + "Runtime Binding Agent", + "--model", + "qwen3.8-max", + "--instructions", + "help", + flag, + "missing", + "--file", + configPath, + "--dry-run", + "--quiet", + ], + RUNTIME_BINDING_ENV, + ); + expect(exitCode).toBe(2); + expect(stderr).toMatch(message); + expect(await readFile(configPath, "utf8")).toBe(RUNTIME_BINDING_YAML); + }); + + test("managed-agent agent create 拒绝空的 --mcp", async () => { const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", "agent", @@ -578,12 +640,107 @@ agents: "qwen3.8-max", "--instructions", "help", - flag, - value, + "--mcp", + " ", "--quiet", ]); expect(exitCode).toBe(2); - expect(stderr).toMatch(new RegExp(`(?:Unknown flag|未知参数).*${flag}`, "i")); + expect(stderr).toMatch(/--mcp values must not be empty/); + }); + + test("managed-agent agent create --dry-run 回显运行时绑定且不改 YAML", async () => { + const configPath = await writeRuntimeBindingProject(); + const { stdout, stderr, exitCode } = await runCommandE2e( + MANAGED_AGENT_ROUTES, + [ + "managed-agent", + "agent", + "create", + "--name", + "Runtime Binding Agent", + "--model", + "qwen3.8-max", + "--instructions", + "help", + "--environment", + "dev", + "--vault", + "secrets", + "--mcp", + "market-a", + "--mcp", + "market-a", + "--mcp", + "market-b", + "--file", + configPath, + "--dry-run", + "--output", + "json", + ], + RUNTIME_BINDING_ENV, + ); + expect(exitCode, stderr).toBe(0); + expect(parseStdoutJson(stdout)).toMatchObject({ + agent: { + key: "runtime-binding-agent", + environment: "dev", + vault: "secrets", + mcp: ["market-a", "market-b"], + }, + yaml_written: false, + }); + expect(await readFile(configPath, "utf8")).toBe(RUNTIME_BINDING_YAML); + }); + + test("managed-agent agent create --yes 将运行时绑定和 MCP 写入 Agent 声明", async () => { + const configPath = await writeRuntimeBindingProject(); + const { stderr, exitCode } = await runCommandE2e( + MANAGED_AGENT_ROUTES, + [ + "managed-agent", + "agent", + "create", + "--name", + "Runtime Binding Agent", + "--model", + "qwen3.8-max", + "--instructions", + "help", + "--environment", + "dev", + "--vault", + "secrets", + "--mcp", + "market-a", + "--file", + configPath, + "--yes", + ], + RUNTIME_BINDING_ENV, + ); + // dev and secrets are declared but never created, so create-only blocks after the YAML write. + expect(exitCode).toBe(2); + expect(stderr).toMatch(/Reconcile first/i); + const config = parse(await readFile(configPath, "utf8")) as { + agents?: Record<string, Record<string, unknown>>; + }; + const agent = config.agents?.["runtime-binding-agent"]; + expect(agent).toMatchObject({ + environment: "dev", + vault: "secrets", + tools: { + mcp: [ + { + type: "mcp_toolkit", + mcp_server_name: "market-a", + default_config: { enabled: true }, + configs: [], + }, + ], + }, + }); + expect(agent?.mcp_servers).toEqual([{ name: "market-a", type: "official" }]); }); test("managed-agent agent create 将 official Skill ID 写为外部引用", async () => { diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 7ae05a84a..7effa96f4 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -281,6 +281,19 @@ export const SANDBOX_ROUTES: E2eRouteExports = { "sandbox template delete": "sandboxTemplateDelete", }; +export const MANAGED_AGENT_WEBHOOK_ROUTES: E2eRouteExports = { + "managed-agent webhook create": "managedAgentWebhookCreate", + "managed-agent webhook list": "managedAgentWebhookList", + "managed-agent webhook get": "managedAgentWebhookGet", + "managed-agent webhook update": "managedAgentWebhookUpdate", + "managed-agent webhook delete": "managedAgentWebhookDelete", + "managed-agent webhook enable": "managedAgentWebhookEnable", + "managed-agent webhook disable": "managedAgentWebhookDisable", + "managed-agent webhook test": "managedAgentWebhookTest", + "managed-agent webhook reset-secret": "managedAgentWebhookResetSecret", + "managed-agent webhook events": "managedAgentWebhookEvents", +}; + export const MANAGED_AGENT_ROUTES: E2eRouteExports = { "managed-agent init": "managedAgentInit", "managed-agent validate": "managedAgentValidate", @@ -508,3 +521,19 @@ export const JOURNEY_J5_ROUTES: E2eRouteExports = { "knowledge file delete": "knowledgeFileDelete", "knowledge doc upload": "knowledgeDocUpload", }; + +export const MANAGED_AGENT_MEMORY_ROUTES: E2eRouteExports = { + "managed-agent memory-store create": "managedAgentMemoryStoreCreate", + "managed-agent memory-store list": "managedAgentMemoryStoreList", + "managed-agent memory-store get": "managedAgentMemoryStoreGet", + "managed-agent memory-store memories create": "managedAgentMemoryCreate", + "managed-agent memory-store memories list": "managedAgentMemoryList", + "managed-agent memory-store memories get": "managedAgentMemoryGet", + "managed-agent memory-store memories update": "managedAgentMemoryUpdate", + "managed-agent memory-store memories delete": "managedAgentMemoryDelete", + "managed-agent memory-store versions list": "managedAgentMemoryVersionList", + "managed-agent memory-store versions get": "managedAgentMemoryVersionGet", + "managed-agent memory-store versions redact": "managedAgentMemoryVersionRedact", + "managed-agent session create": "managedAgentSessionCreate", + "managed-agent session run": "managedAgentSessionRun", +}; diff --git a/packages/commands/tests/managed-agent-api-helpers.test.ts b/packages/commands/tests/managed-agent-api-helpers.test.ts index 4d1eec9ac..abf1e3e0a 100644 --- a/packages/commands/tests/managed-agent-api-helpers.test.ts +++ b/packages/commands/tests/managed-agent-api-helpers.test.ts @@ -72,3 +72,26 @@ test("session export ZIP is valid-shaped and redacts credential-like values", () expect(decoded).toContain("events.json"); expect(Array.from(zip.slice(-22, -18))).toEqual([0x50, 0x4b, 0x05, 0x06]); }); + +test("session diagnostics redact every environment value while preserving names and input", () => { + const variables = { LOG_LEVEL: "debug", CUSTOM_VALUE: "private-value", EMPTY: "" }; + const session = { + environment_variables: variables, + attributes: { environment_variables: variables }, + }; + const masked = { LOG_LEVEL: "[REDACTED]", CUSTOM_VALUE: "[REDACTED]", EMPTY: "[REDACTED]" }; + const redacted = redactSensitiveValues(session); + + expect(redacted).toEqual({ + environment_variables: masked, + attributes: { environment_variables: masked }, + }); + expect(variables.CUSTOM_VALUE).toBe("private-value"); + const zip = createZip([ + { name: "session.json", content: new TextEncoder().encode(JSON.stringify(redacted)) }, + ]); + const decoded = new TextDecoder().decode(zip); + expect(decoded).toContain("CUSTOM_VALUE"); + expect(decoded).toContain("[REDACTED]"); + expect(decoded).not.toContain("private-value"); +}); diff --git a/packages/commands/tests/managed-agent-create.test.ts b/packages/commands/tests/managed-agent-create.test.ts index c2f81989b..247459f04 100644 --- a/packages/commands/tests/managed-agent-create.test.ts +++ b/packages/commands/tests/managed-agent-create.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { buildAgentDecl, StateManager } from "@openagentpack/sdk"; import { expect, test } from "vite-plus/test"; import { + buildAgentMcpBindings, buildAgentSkillRefs, normalizeAgentKey, replaceConfigAtomically, @@ -105,6 +106,15 @@ test("Agent Skill ID 可生成 official 外部引用", () => { expect(() => buildAgentSkillRefs([" "])).toThrow(/must not be empty/); }); +test("MCP server code 去重并默认启用全部工具", () => { + expect(buildAgentMcpBindings([" market-a ", "market-a", "market-b"])).toEqual([ + { serverName: "market-a", serverType: "official", defaultEnabled: true }, + { serverName: "market-b", serverType: "official", defaultEnabled: true }, + ]); + expect(buildAgentMcpBindings(undefined)).toEqual([]); + expect(() => buildAgentMcpBindings([" "])).toThrow(/--mcp values must not be empty/); +}); + test("本地 Skill 来源复用同声明的待创建或已跟踪 key", () => { const declaration = { name: "local-report", diff --git a/packages/commands/tests/managed-agent-memory-create.test.ts b/packages/commands/tests/managed-agent-memory-create.test.ts new file mode 100644 index 000000000..1dc7652dd --- /dev/null +++ b/packages/commands/tests/managed-agent-memory-create.test.ts @@ -0,0 +1,60 @@ +import { randomUUID } from "node:crypto"; +import { StateManager } from "@openagentpack/sdk"; +import { expect, test } from "vite-plus/test"; +import { selectResourceKey } from "../src/commands/managed-agent/_engine/scoped-create.ts"; + +const declaration = { + name: "Notes", + description: "", + metadata: {}, + provider: "bailian", + entries: [{ key: "notes.md", content: "Initial notes" }], +}; +function selection( + initializing: boolean, + pendingEntries: Array<{ path: string; content_sha256: string }>, + candidate = declaration, +) { + const state = StateManager.initialize(`/tmp/bl-memory-create-${randomUUID()}.json`); + state.setResource({ + address: { provider: "bailian", type: "memory_store", name: "notes" }, + remote_id: "memstore_tracked", + content_hash: "old", + ...(initializing ? { memory_initialization: { pending_entries: pendingEntries } } : {}), + }); + return selectResourceKey({ + displayName: "Notes", + provider: "bailian", + resourceTypes: ["memory_store"], + declarations: { notes: declaration }, + candidate, + effectiveName: (key, entry) => (typeof entry.name === "string" ? entry.name : key), + state, + }); +} +test.each([ + { pendingEntries: [] }, + { pendingEntries: [{ path: "/notes.md", content_sha256: "a".repeat(64) }] }, +])("reuses the same pending local Store declaration with marker %p", ({ pendingEntries }) => { + expect(selection(true, pendingEntries)).toEqual({ key: "notes", reusedPending: true }); +}); +test("does not adopt a completed Store solely because its display name matches", () => { + expect(selection(false, [])).toEqual({ key: "notes-2", reusedPending: false }); +}); +test("does not reuse a pending Store with a changed declaration", () => { + expect(selection(true, [], { ...declaration, description: "Changed notes" })).toEqual({ + key: "notes-2", + reusedPending: false, + }); +}); +test("reuses a pending Store when declaration object order differs", () => { + expect( + selection(true, [], { + entries: [{ content: "Initial notes", key: "notes.md" }], + provider: "bailian", + metadata: {}, + description: "", + name: "Notes", + }), + ).toEqual({ key: "notes", reusedPending: true }); +}); diff --git a/packages/commands/tests/managed-agent-scoped-create.test.ts b/packages/commands/tests/managed-agent-scoped-create.test.ts new file mode 100644 index 000000000..16d4a224a --- /dev/null +++ b/packages/commands/tests/managed-agent-scoped-create.test.ts @@ -0,0 +1,117 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { resolveProjectConfig } from "@openagentpack/sdk"; +import { afterEach, expect, test } from "vite-plus/test"; +import { stringify } from "yaml"; +import { resolveCandidateDeclaration } from "../src/commands/managed-agent/_engine/scoped-create.ts"; + +const directories: string[] = []; + +afterEach(async () => { + await Promise.all( + directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function loadProject(content: string, group: "memory_stores" | "agents") { + const directory = await mkdtemp(join(tmpdir(), "bl-scoped-create-")); + directories.push(directory); + const configPath = join(directory, "agents.yaml"); + await writeFile(join(directory, "existing.md"), content); + await writeFile( + configPath, + stringify({ + version: "1", + providers: { bailian: {} }, + defaults: { provider: "bailian" }, + [group]: { + existing: + group === "memory_stores" + ? { description: "", entries: [{ key: "existing.md", content: "./existing.md" }] } + : { model: "qwen3.8-max", instructions: "./existing.md" }, + }, + }), + ); + const project = await resolveProjectConfig(configPath, { resolveEnv: false }); + return { directory, configPath, project }; +} + +test.each(["./missing.md", "../missing.md", "/missing.md"])( + "scoped create preserves resolved Memory content %s and resolves only the new entry", + async (content) => { + const { directory, configPath, project } = await loadProject(content, "memory_stores"); + const originalConfig = structuredClone(project.config); + const originalSource = await readFile(configPath, "utf8"); + const newContent = "./literal-new-entry.md"; + await writeFile(join(directory, "new.md"), newContent); + + const declaration = await resolveCandidateDeclaration({ + project, + group: "memory_stores", + rawDeclaration: { + name: "New notes", + description: "", + entries: [{ key: "new.md", content: "./new.md" }], + }, + }); + + expect(declaration).toMatchObject({ + name: "New notes", + entries: [{ key: "new.md", content: newContent }], + }); + expect(project.config).toEqual(originalConfig); + expect(await readFile(configPath, "utf8")).toBe(originalSource); + }, +); + +test("scoped create preserves resolved Agent instructions and new resource references", async () => { + const { directory, project } = await loadProject("./missing-instructions.md", "agents"); + const instructions = "../literal-new-instructions.md"; + await writeFile(join(directory, "new.md"), instructions); + + const declaration = await resolveCandidateDeclaration({ + project, + group: "agents", + rawDeclaration: { + model: "qwen3.8-max", + instructions: "./new.md", + environment: "development", + vault: "secrets", + skills: ["report"], + }, + }); + + expect(declaration).toMatchObject({ + instructions, + environment: "development", + vault: "secrets", + skills: ["report"], + }); + expect(project.config.agents?.existing?.instructions).toBe("./missing-instructions.md"); +}); + +test("scoped create still rejects missing new entry files", async () => { + const { project } = await loadProject("Existing notes", "memory_stores"); + await expect( + resolveCandidateDeclaration({ + project, + group: "memory_stores", + rawDeclaration: { + description: "", + entries: [{ key: "new.md", content: "./missing-new.md" }], + }, + }), + ).rejects.toThrow(/Memory entry file not found: .*missing-new\.md/); +}); + +test("scoped create still validates the new declaration schema", async () => { + const { project } = await loadProject("Existing notes", "memory_stores"); + await expect( + resolveCandidateDeclaration({ + project, + group: "memory_stores", + rawDeclaration: { description: "", entries: [{ key: "new.md", content: 123 }] }, + }), + ).rejects.toThrow(/entries\.0\.content/); +}); diff --git a/packages/core/package.json b/packages/core/package.json index fae2ee38d..5b38a3288 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-core", - "version": "2.3.3", + "version": "2.4.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/errors/api.ts b/packages/core/src/errors/api.ts index 91fff6527..8523c4142 100644 --- a/packages/core/src/errors/api.ts +++ b/packages/core/src/errors/api.ts @@ -15,7 +15,7 @@ export interface ApiErrorBody { export function mapApiError(status: number, body: ApiErrorBody, _url?: string): BailianError { const apiMsg = body.error?.message || body.message || `HTTP ${status}`; - const rawCode = body.error?.type ?? body.code; + const rawCode = body.error?.type ?? body.error?.code ?? body.code; const apiCode = typeof rawCode === "string" ? rawCode diff --git a/packages/core/src/telemetry/tracker.ts b/packages/core/src/telemetry/tracker.ts index 0335d3a02..a65534835 100644 --- a/packages/core/src/telemetry/tracker.ts +++ b/packages/core/src/telemetry/tracker.ts @@ -77,6 +77,8 @@ function extractParams(flags: Record<string, unknown>): Record<string, unknown> if (GLOBAL_FLAG_KEYS.has(key)) continue; if (!PARAM_ALLOWLIST.has(key)) continue; if (value === undefined || value === false) continue; + // Some commands use page for an opaque cursor rather than a numeric page. + if (key === "page" && (typeof value !== "number" || !Number.isFinite(value))) continue; params[key] = value; } return params; diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index 620504532..8de2969df 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -94,6 +94,22 @@ test("mapApiError captures Sandbox numeric code and requestID", () => { }); }); +test("mapApiError preserves AgentStudio nested error codes and server messages", () => { + for (const apiCode of ["11800016", 11800016]) { + const error = mapApiError(400, { + error: { code: apiCode, message: "invalid webhook request" }, + request_id: "webhook-request-id", + }); + expect(error.exitCode).toBe(ExitCode.GENERAL); + expect(error.message).toBe("invalid webhook request"); + expect(error.api).toEqual({ + httpStatus: 400, + apiCode: "11800016", + requestId: "webhook-request-id", + }); + } +}); + test("Sandbox endpoint builders fix cn-beijing and encode path identifiers", () => { expect(sandboxEndpoint("ws-test", "/sandboxes")).toBe( "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes", diff --git a/packages/core/tests/telemetry-tracker.test.ts b/packages/core/tests/telemetry-tracker.test.ts index 68397111f..1870673ae 100644 --- a/packages/core/tests/telemetry-tracker.test.ts +++ b/packages/core/tests/telemetry-tracker.test.ts @@ -52,3 +52,33 @@ test("records BailianError exitCode in local events and AEM ext", async () => { ext: expect.objectContaining({ exitCode: 7 }), }); }); + +test.each([ + { label: "numeric page", page: 2, expectedParams: { page: 2, pageSize: 20 } }, + { label: "opaque cursor", page: "opaque+/=&cursor", expectedParams: { pageSize: 20 } }, + { label: "numeric-looking cursor", page: "2", expectedParams: { pageSize: 20 } }, + { label: "NaN page", page: Number.NaN, expectedParams: { pageSize: 20 } }, + { label: "positive infinity page", page: Infinity, expectedParams: { pageSize: 20 } }, + { label: "negative infinity page", page: -Infinity, expectedParams: { pageSize: 20 } }, +])( + "filters $label consistently in local and remote telemetry", + async ({ page, expectedParams }) => { + sinkMocks.localSink.mockClear(); + sinkMocks.remoteSink.mockClear(); + + await trackCommandExecution( + { identity, settings: { telemetry: true } as Settings, authMethod: "apiKey" }, + ["managed-agent", "webhook", "events"], + { page, pageSize: 20 }, + async () => {}, + ); + + expect(sinkMocks.localSink).toHaveBeenCalledOnce(); + expect(sinkMocks.remoteSink).toHaveBeenCalledOnce(); + const localEvent = sinkMocks.localSink.mock.calls[0]![0]; + const remoteEvent = sinkMocks.remoteSink.mock.calls[0]![0]; + expect(localEvent.params).toEqual(expectedParams); + expect(remoteEvent.params).toEqual(expectedParams); + expect(buildRemoteAemOptions(remoteEvent).c1).toEqual(expectedParams); + }, +); diff --git a/packages/kscli/package.json b/packages/kscli/package.json index 6592e6b91..aadcbeae4 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -1,6 +1,6 @@ { "name": "knowledge-studio-cli", - "version": "2.3.3", + "version": "2.4.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 9b08b5ff6..224b9d42d 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "bailian-cli-runtime", - "version": "2.3.3", + "version": "2.4.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/packages/runtime/src/middleware.ts b/packages/runtime/src/middleware.ts index 660916912..b44271b2a 100644 --- a/packages/runtime/src/middleware.ts +++ b/packages/runtime/src/middleware.ts @@ -189,6 +189,11 @@ export const telemetryStage: Middleware = (ctx, next) => { * if `next()` throws, the notice is skipped (no update nag on failure). */ export const versionCheckStage: Middleware = async (ctx, next) => { + // Local source launchers can disable background updates without quieting output. + if (process.env.NO_UPDATE_NOTIFIER === "1") { + await next(); + return; + } const pending = checkForUpdate( ctx.identity.version, ctx.identity.npmPackage, diff --git a/packages/runtime/tests/version-check-stage.test.ts b/packages/runtime/tests/version-check-stage.test.ts new file mode 100644 index 000000000..4a40e1610 --- /dev/null +++ b/packages/runtime/tests/version-check-stage.test.ts @@ -0,0 +1,90 @@ +import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test"; + +const updateMocks = vi.hoisted(() => ({ + checkForUpdate: vi.fn(), + getPendingUpdateNotification: vi.fn(), + performAutoUpdate: vi.fn(), + shouldAutoUpdate: vi.fn(), +})); + +vi.mock("../src/utils/update-checker.ts", () => updateMocks); + +import { versionCheckStage, type RunContext } from "../src/middleware.ts"; + +function makeContext(): RunContext { + return { + identity: { + binName: "bl", + version: "1.24.0", + clientName: "bailian-cli-test", + npmPackage: "bailian-cli", + }, + path: ["auth", "login"], + settings: { quiet: false }, + } as RunContext; +} + +beforeEach(() => { + vi.stubEnv("NO_UPDATE_NOTIFIER", undefined); + updateMocks.checkForUpdate.mockResolvedValue(undefined); + updateMocks.getPendingUpdateNotification.mockReturnValue("2.2.1"); + updateMocks.shouldAutoUpdate.mockReturnValue(true); + updateMocks.performAutoUpdate.mockResolvedValue(true); +}); + +afterEach(() => { + vi.unstubAllEnvs(); + vi.resetAllMocks(); +}); + +test("NO_UPDATE_NOTIFIER=1 skips update checks and installation while running the command", async () => { + vi.stubEnv("NO_UPDATE_NOTIFIER", "1"); + const next = vi.fn(async () => {}); + + await versionCheckStage(makeContext(), next); + + expect(next).toHaveBeenCalledOnce(); + expect(updateMocks.checkForUpdate).not.toHaveBeenCalled(); + expect(updateMocks.getPendingUpdateNotification).not.toHaveBeenCalled(); + expect(updateMocks.shouldAutoUpdate).not.toHaveBeenCalled(); + expect(updateMocks.performAutoUpdate).not.toHaveBeenCalled(); +}); + +test.each([undefined, "0"])( + "normal update behavior is preserved when the opt-out is %s", + async (envValue) => { + vi.stubEnv("NO_UPDATE_NOTIFIER", envValue); + const next = vi.fn(async () => {}); + + await versionCheckStage(makeContext(), next); + + expect(next).toHaveBeenCalledOnce(); + expect(updateMocks.checkForUpdate).toHaveBeenCalledWith( + "1.24.0", + "bailian-cli", + "bailian-cli-test", + ); + expect(updateMocks.performAutoUpdate).toHaveBeenCalledWith( + "1.24.0", + "2.2.1", + "bailian-cli", + "bailian-cli-test", + ); + }, +); + +test.each([undefined, "1"])( + "command errors propagate unchanged when the opt-out is %s", + async (envValue) => { + vi.stubEnv("NO_UPDATE_NOTIFIER", envValue); + const commandError = new Error("command failed"); + const next = vi.fn(async () => { + throw commandError; + }); + + await expect(versionCheckStage(makeContext(), next)).rejects.toBe(commandError); + + expect(next).toHaveBeenCalledOnce(); + expect(updateMocks.performAutoUpdate).not.toHaveBeenCalled(); + }, +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 727db099e..ee1700780 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,8 +190,8 @@ importers: packages/commands: dependencies: '@openagentpack/sdk': - specifier: 0.7.1 - version: 0.7.1 + specifier: 0.9.0 + version: 0.9.0 bailian-cli-core: specifier: workspace:* version: link:../core @@ -1113,8 +1113,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@openagentpack/sdk@0.7.1': - resolution: {integrity: sha512-OENks8UdfWFanvhJIVbqyUX1+XGavHnrxDiFx4SoJoGRXi45cOgyYtWWbG5VdDI2I5Dpo9eGNaw/JtDnkdxUcg==} + '@openagentpack/sdk@0.9.0': + resolution: {integrity: sha512-Y99LtN1gpXK8vSnvHx/4NspL0BJtFYwJNkpwhMzBMY8rRwkmP4QOSUX24ovboBzQTXAgEU3n3QFusjHDbxdeQg==} engines: {node: '>=18.17.0'} '@oxc-project/runtime@0.150.0': @@ -3580,7 +3580,7 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@openagentpack/sdk@0.7.1': + '@openagentpack/sdk@0.9.0': dependencies: jszip: 3.10.1 yaml: 2.9.0 diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index f79fc0235..7c18e894d 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-cli metadata: - version: "2.3.3" + version: "2.4.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-finetune/SKILL.md b/skills/bailian-finetune/SKILL.md index a3fcb1edb..e1162aa80 100644 --- a/skills/bailian-finetune/SKILL.md +++ b/skills/bailian-finetune/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-finetune metadata: - version: "2.3.3" + version: "2.4.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-gen/SKILL.md b/skills/bailian-gen/SKILL.md index 79f03c249..77f980168 100644 --- a/skills/bailian-gen/SKILL.md +++ b/skills/bailian-gen/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-gen metadata: - version: "2.3.3" + version: "2.4.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index a1e33670c..e5513f580 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -1,14 +1,14 @@ --- name: bailian-managed-agent metadata: - version: "2.3.3" + version: "2.4.0" requires: bins: ["bl"] description: >- 阿里云百炼托管 Agent 声明式基础设施与 API 命令入口:用户要创建agent、初始化 agents.yaml、校验或预览配置变更、 创建/更新/销毁托管 Agent 或 Deployment、在 Workbench 编辑和调试目录项目、管理本地快照版本,或查询 - Agent/Environment/Skill/Vault/Deployment、管理 Session/Event/File、运行/暂停 Deployment 时使用 - `bl managed-agent`。持久资源仍以 agents.yaml 为唯一事实源做 IaC;公开 API 能力按资源透出 + Agent/Environment/Skill/Vault/Deployment/Memory Store、管理 Session/Event/File/Webhook/记忆正文及版本、运行/暂停 Deployment 时使用 + `bl managed-agent`。Agent/Environment/Skill/Vault/Deployment/Memory Store 仍以 agents.yaml 为唯一事实源做 IaC;Webhook 使用无需项目文件的工作空间 API;公开 API 能力按资源透出 list/get/search/versions/download、数据面和运行时动作命令。apply / destroy 与破坏性 API 命令受统一高风险确认闸门保护; 务必先展示预览再让用户确认,禁止自动添加 `--yes`。 反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、 @@ -29,9 +29,10 @@ description: >- 3. Only after explicit user confirmation, retry `apply` / `destroy` / single-resource create with `--yes`. 4. Never add `--yes` on your own initiative before the user has confirmed. -API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault / Deployment 的 create 命令仍通过 +API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault / Deployment / Memory Store 的 create 命令仍通过 `agents.yaml → scoped plan → scoped apply` 管理;查询命令和 Session、Event、File、Deployment 运行时动作直接调用 API。 -`session archive|delete`、`file delete`、`deployment run|pause|unpause` 也需要先 `--dry-run`,确认后才传 `--yes`。 +`session archive|delete`、`file delete`、`deployment run|pause|unpause`、`memory-store memories create|update|delete` 和 `memory-store versions redact` 也需要先 `--dry-run`,确认后才传 `--yes`。 +Webhook 是工作空间级 API 资源,不写入 `agents.yaml` 或 State。`webhook create|update|delete|enable|disable|test|reset-secret` 都是高风险操作:先展示 `--dry-run` 的目标和请求,用户确认后才加 `--yes`;`test` 会向接收端投递事件。 `state rm`, `session archive|delete`, `file delete`, `deployment run|pause|unpause`, and future `risk: high` commands follow the shared protocol: show the risk message and exact scope, then wait @@ -85,11 +86,13 @@ for explicit confirmation before re-running with `--yes`. | Empty Vault | `bl managed-agent vault create` | | Credential in a Vault | `bl managed-agent vault credential create` | | Deployment | `bl managed-agent deployment create` | +| Memory Store | `bl managed-agent memory-store create` | - 用户只提供资源 `name`;CLI 自动生成稳定的 YAML 逻辑 key,同名资源用递增后缀并存。Credential 追加到指定 Vault,不单独生成 key。 - `agent create --skill <id>` 直接绑定已存在的远端 Skill,不要求顶层 `skills` 声明;默认写为 `type: custom`,平台 Skill 显式增加 `--type official`。同一次命令中的全部 `--skill` 共用该类型。 - `agent create --skill-dir <path>` 接收可重复的本地 Skill 目录或 ZIP:CLI 从其中的 `SKILL.md` 读取 name,自动生成顶层 `skills.<key>` 声明,并把该 key 写入 Agent 的 `skills` 列表;定向 Apply 会先上传 custom Skill,再创建 Agent。`--type` 只作用于 `--skill <id>`,不改变本地 Skill 的 custom 类型。 -- Environment 和 Vault 属于 Session/Deployment 运行时绑定,不是 Agent 创建参数;在 `session create|run` 或 `deployment create` 中传入。 +- `agent create --environment <key>` / `--vault <key>` 把已声明的顶层 Environment / Vault key 写入 Agent 声明,作为默认运行时绑定;百炼 Agent API 不接收这两个字段,`session create|run` 与 `deployment create` 未显式传入时才使用它们,显式传入则覆盖。引用的资源必须先用对应的 `create` 命令创建,未声明的 key 直接报用法错误。 +- `agent create --mcp <server-code>` 接收可重复的 MCP 市场服务 code(来自 `bl mcp list`),写入 `mcp_servers`(`type: official`)与对应的 `tools.mcp`,默认启用全部工具;如需按工具开关,修改 YAML 中的 `tools.mcp[].configs` 后执行全量 `apply`。 - 默认只预览自动 key 和定向计划;`--dry-run` 完全离线,只有显式 `--yes` 才写 YAML 并创建远端资源。 - 定向流程只刷新目标资源及其传递依赖;无关资源不检测 Drift、不产生 action,也不阻塞。 - 目标资源必须是 `create`,相关依赖必须已经处于 `no-op`;项目级 Drift 和删除仍由全量 `plan/apply` 处理。 @@ -156,6 +159,9 @@ Managed Agents 的子线程通过 Event 中的 `session_thread_id` 暴露;公 | Discover agents and versions | `agent list`, `agent get`, `agent search`, `agent versions` | | Discover environments | `environment list`, `environment get`, `environment search` | | Discover skills and download a version | `skill list`, `skill get`, `skill search`, `skill versions`, `skill download` | +| Declare and inspect Memory Stores | `memory-store create`, `memory-store list`, `memory-store get` | +| Manage persistent memory contents | `memory-store memories create`, `list`, `get`, `update`, `delete` | +| Inspect or redact memory versions | `memory-store versions list`, `get`, `redact` | | Inspect vault envelopes | `vault list`, `vault get`, `vault search` | | Inspect deployments and run history | `deployment list`, `deployment get`, `deployment search`, `deployment runs list`, `deployment runs get` | | Run or pause deployments | `deployment run`, `deployment pause`, `deployment unpause` | @@ -163,13 +169,56 @@ Managed Agents 的子线程通过 Event 中的 `session_thread_id` 暴露;公 | Work with raw events | `session event send`, `session event list`, `session event stream` | | Diagnose/export a session | `session debug`, `session export` | | Work with files | `file upload`, `file list`, `file get`, `file search`, `file download`, `file delete` | +| Inspect workspace Webhooks and events | `webhook list`, `webhook get`, `webhook events` | +| Manage Webhook delivery endpoints | `webhook create`, `webhook update`, `webhook delete`, `webhook enable`, `webhook disable` | +| Test delivery or rotate its secret | `webhook test`, `webhook reset-secret` | - 所有 Cursor 都是不透明字符串:只回传 `next_page`,不得转换为数字页码。 - 客户端搜索默认最多扫描 10 页;需要扩大范围时显式传 `--page-limit`。Deployment 搜索直接映射服务端 `keyword`。 - 下载必须给出 `--output-file`;默认不覆盖已有文件,只有用户确认后才可加 `--force`。 - `session export` 只导出诊断元数据,不含 File 正文,并会脱敏凭证类字段。 +- `session create|run --environment_variables KEY=VALUE` injects variables into this Session only. Repeat the flag for multiple variables; the last value for a key wins. Empty values and values containing `=` are allowed. Dry-run and diagnostic exports redact values. / 环境变量仅用于本次 Session,不写入 YAML 或 State;可重复传入,允许空值和包含 `=` 的值,预览和诊断导出会脱敏变量值。 +- `deployment create --environment_variables KEY=VALUE` declares runtime variables under `deployments.<key>.environment_variables` as a string map. With `--yes`, the scoped create writes YAML and creates the Deployment. YAML + `apply` also manages this field; changing values updates the Deployment, and removing the field or setting `{}` clears it. Create and plan previews omit values; YAML stores them. / Deployment 环境变量以字符串键值对象声明;创建命令使用 `--yes` 后写入 YAML 并定向创建,也可直接用 YAML + `apply` 管理。修改值会更新部署,删除字段或设置 `{}` 会清空变量。预览输出不展示变量值,YAML 会保存这些值。 - 公开 Managed Agents API 没有模型 Catalog,也没有 MCP OAuth Login;CLI 不注册对应命令。 +### Memory Stores and Session mounts + +- Store lifecycle stays in `agents.yaml`: create with `memory-store create`, and preview changes with `plan` before `apply`. There are no direct Store update, archive, or delete commands. / 记忆库生命周期统一走 IaC,变更先预览再确认。 +- Memory Store names are display labels and need not be unique. State records the remote ID; create a new store when no ID exists, and import a known ID explicitly to adopt an existing store. / 按 ID 识别记忆库,不按同名匹配。 +- `entries` initialize new stores only. Existing, imported, or adopted stores keep their runtime content. Set `entries_mode: sync` explicitly when declared paths must continue to follow YAML; removing a declared entry does not delete its remote file. / 默认只初始化新库,持续同步必须显式启用。 +- Store reads, memory content, and version APIs use the workspace API Key directly through the SDK. Their `--store-id` is a remote ID; they do not require YAML or State. `--content` and `--content-file` are exclusive and allow empty content. Updates can use `--expected-content-sha256` to detect concurrent edits. / 正文 API 使用远端 ID,空正文有效。 +- `session create|run --memory-stores` retains the comma-separated YAML key shorthand. Repeatable `--resource <json|@path>` supports `{ "type": "memory_store", "memory_store": "notes", "access": "read_only", "instructions": "..." }`. Explicit resources override the same shorthand mount while retaining other mounts. Duplicate explicit references are rejected; at most eight Memory Stores may be mounted. / resource 使用逻辑 key,可指定权限和说明。 +- Deployment currently supports File Resources only. An effective Memory mount, including one inherited from the Agent, is rejected. / Deployment 本期仅支持 File,不能继承或直接挂载记忆库。 +- Preview memory writes with `--dry-run`, then obtain confirmation before adding `--yes`. Version redaction permanently removes that version's content. / 版本擦除不可撤销,必须先预览并确认。 + +```bash +bl managed-agent memory-store create --name Notes --dry-run +bl managed-agent memory-store memories list --workspace-id ws-example --store-id memstore-example --view full +bl managed-agent session create --agent assistant \ + --resource '{"type":"memory_store","memory_store":"notes","access":"read_only"}' \ + --dry-run +``` + +Command fields and pagination are authoritative in `reference/managed-agent.md` and command help; return `next_page` unchanged. / 参数以生成手册和 help 为准,分页游标原样传回。 + +### Workspace Webhooks + +- Webhook commands use the CLI API Key, Base URL, and workspace settings directly through the SDK. They do not need `agents.yaml`, a project Build, or local State. / Webhook 命令直接使用 CLI 鉴权与工作空间配置,无需创建项目文件。 +- Select the workspace with `--workspace-id` or the configured environment/Profile. Explicit flags take precedence over environment variables, then the selected Profile. Check `webhook <command> --help` for the available request fields. +- `webhook events` uses `--limit` (1–100) and the opaque `--page` cursor; pass `next_page` back unchanged. / 事件分页游标保持原样传回,不转换成数字页码。 +- Successful `create` and `reset-secret` responses can contain `signing_secret`. Store that response securely for signature verification; do not paste it into logs, issue reports, or source files. Verbose diagnostics and errors must not expose secrets. / 创建或重置密钥的成功响应可能包含签名密钥;请安全保存,不写入日志或代码。 + +```bash +bl managed-agent webhook list --workspace-id ws-example --output json +bl managed-agent webhook create \ + --workspace-id ws-example \ + --url https://example.com/agent-events \ + --event session.updated \ + --dry-run +``` + +Review the preview and obtain confirmation before removing `--dry-run` and adding `--yes`. Event names are passed to the service as supplied; use the event types supported by your endpoint. / 预览并确认后再执行写操作;事件名以服务端支持的事件类型为准。 + ## Local state management | Intent | Command | diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index 7058a243b..817b1a9a4 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -9,86 +9,107 @@ Use this index for the skill-scoped quick index and global flags. ## Quick index -| Command | Authentication | Description | Detail | -| ------------------------------------------ | -------------- | ---------------------------------------------------------------------------- | ------------------------------------ | -| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | [managed-agent.md](managed-agent.md) | -| `bl managed-agent agent get` | API Key | Get a Managed Agent | [managed-agent.md](managed-agent.md) | -| `bl managed-agent agent list` | API Key | List Managed Agents | [managed-agent.md](managed-agent.md) | -| `bl managed-agent agent search` | API Key | Search Managed Agents | [managed-agent.md](managed-agent.md) | -| `bl managed-agent agent versions` | API Key | List Managed Agent versions | [managed-agent.md](managed-agent.md) | -| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment create` | API Key | Declare and create one Managed Agent Deployment through a scoped YAML apply | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | [managed-agent.md](managed-agent.md) | -| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | [managed-agent.md](managed-agent.md) | -| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) | -| `bl managed-agent environment create` | API Key | Declare and create one Managed Agent Environment through a scoped YAML apply | [managed-agent.md](managed-agent.md) | -| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | [managed-agent.md](managed-agent.md) | -| `bl managed-agent environment list` | API Key | List Managed Agent environments | [managed-agent.md](managed-agent.md) | -| `bl managed-agent environment search` | API Key | Search Managed Agent environments | [managed-agent.md](managed-agent.md) | -| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | [managed-agent.md](managed-agent.md) | -| `bl managed-agent file download` | API Key | Download Managed Agent file content | [managed-agent.md](managed-agent.md) | -| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | [managed-agent.md](managed-agent.md) | -| `bl managed-agent file list` | API Key | List Managed Agent files | [managed-agent.md](managed-agent.md) | -| `bl managed-agent file search` | API Key | Search Managed Agent files | [managed-agent.md](managed-agent.md) | -| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | [managed-agent.md](managed-agent.md) | -| `bl managed-agent init` | No Auth | Create an agents.yaml template | [managed-agent.md](managed-agent.md) | -| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) | -| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project build` | No Auth | Organize directory source and generate the immutable Publish Build | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project version disable` | No Auth | Disable directory project versions | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project version enable` | No Auth | Enable directory project versions | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project version list` | No Auth | List directory project versions | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project version preview` | No Auth | Preview a directory project version | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project version status` | No Auth | Show directory project version status | [managed-agent.md](managed-agent.md) | -| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session events` | API Key | List events for a Managed Agent session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session search` | API Key | Search Managed Agent sessions | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) | -| `bl managed-agent session update` | API Key | Update a Managed Agent session | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill create` | API Key | Declare and create one custom Managed Agent Skill from a local source | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill list` | API Key | List Managed Agent skills | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill search` | API Key | Search Managed Agent skills | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | [managed-agent.md](managed-agent.md) | -| `bl managed-agent skill-list` | API Key | List Managed Agent skills | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) | -| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) | -| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) | -| `bl managed-agent vault create` | API Key | Declare and create one empty Managed Agent Vault through a scoped YAML apply | [managed-agent.md](managed-agent.md) | -| `bl managed-agent vault credential create` | API Key | Append and create one environment-variable Credential in a tracked Vault | [managed-agent.md](managed-agent.md) | -| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | [managed-agent.md](managed-agent.md) | -| `bl managed-agent vault list` | API Key | List Managed Agent vaults | [managed-agent.md](managed-agent.md) | -| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | [managed-agent.md](managed-agent.md) | +| Command | Authentication | Description | Detail | +| ----------------------------------------------- | -------------- | ---------------------------------------------------------------------------- | ------------------------------------ | +| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | [managed-agent.md](managed-agent.md) | +| `bl managed-agent agent get` | API Key | Get a Managed Agent | [managed-agent.md](managed-agent.md) | +| `bl managed-agent agent list` | API Key | List Managed Agents | [managed-agent.md](managed-agent.md) | +| `bl managed-agent agent search` | API Key | Search Managed Agents | [managed-agent.md](managed-agent.md) | +| `bl managed-agent agent versions` | API Key | List Managed Agent versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment create` | API Key | Declare and create one Managed Agent Deployment through a scoped YAML apply | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | [managed-agent.md](managed-agent.md) | +| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | [managed-agent.md](managed-agent.md) | +| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) | +| `bl managed-agent environment create` | API Key | Declare and create one Managed Agent Environment through a scoped YAML apply | [managed-agent.md](managed-agent.md) | +| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | [managed-agent.md](managed-agent.md) | +| `bl managed-agent environment list` | API Key | List Managed Agent environments | [managed-agent.md](managed-agent.md) | +| `bl managed-agent environment search` | API Key | Search Managed Agent environments | [managed-agent.md](managed-agent.md) | +| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | [managed-agent.md](managed-agent.md) | +| `bl managed-agent file download` | API Key | Download Managed Agent file content | [managed-agent.md](managed-agent.md) | +| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | [managed-agent.md](managed-agent.md) | +| `bl managed-agent file list` | API Key | List Managed Agent files | [managed-agent.md](managed-agent.md) | +| `bl managed-agent file search` | API Key | Search Managed Agent files | [managed-agent.md](managed-agent.md) | +| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | [managed-agent.md](managed-agent.md) | +| `bl managed-agent init` | No Auth | Create an agents.yaml template | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store create` | API Key | Declare and create a Memory Store through scoped YAML apply | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store get` | API Key | Get a Memory Store | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store list` | API Key | List Memory Stores | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store memories create` | API Key | Create a memory file | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store memories delete` | API Key | Delete a memory file | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store memories get` | API Key | Get a memory file | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store memories list` | API Key | List memory files and prefixes | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store memories update` | API Key | Update memory content with an optional SHA-256 precondition | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store versions get` | API Key | Get a memory version | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store versions list` | API Key | List Memory Store versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent memory-store versions redact` | API Key | Permanently redact a memory version's content | [managed-agent.md](managed-agent.md) | +| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) | +| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project build` | No Auth | Organize directory source and generate the immutable Publish Build | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version disable` | No Auth | Disable directory project versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version enable` | No Auth | Enable directory project versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version list` | No Auth | List directory project versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version preview` | No Auth | Preview a directory project version | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project version status` | No Auth | Show directory project version status | [managed-agent.md](managed-agent.md) | +| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session events` | API Key | List events for a Managed Agent session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session search` | API Key | Search Managed Agent sessions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) | +| `bl managed-agent session update` | API Key | Update a Managed Agent session | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill create` | API Key | Declare and create one custom Managed Agent Skill from a local source | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill list` | API Key | List Managed Agent skills | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill search` | API Key | Search Managed Agent skills | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | [managed-agent.md](managed-agent.md) | +| `bl managed-agent skill-list` | API Key | List Managed Agent skills | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) | +| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) | +| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) | +| `bl managed-agent vault create` | API Key | Declare and create one empty Managed Agent Vault through a scoped YAML apply | [managed-agent.md](managed-agent.md) | +| `bl managed-agent vault credential create` | API Key | Append and create one environment-variable Credential in a tracked Vault | [managed-agent.md](managed-agent.md) | +| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | [managed-agent.md](managed-agent.md) | +| `bl managed-agent vault list` | API Key | List Managed Agent vaults | [managed-agent.md](managed-agent.md) | +| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook create` | API Key | Create a Webhook endpoint | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook delete` | API Key | Delete a Webhook endpoint | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook disable` | API Key | Disable a Webhook endpoint | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook enable` | API Key | Enable a Webhook endpoint | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook events` | API Key | List recent Webhook delivery events | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook get` | API Key | Get a Webhook endpoint | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook list` | API Key | List all Webhook endpoints | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook reset-secret` | API Key | Reset a Webhook signing secret | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook test` | API Key | Send a real Webhook test event | [managed-agent.md](managed-agent.md) | +| `bl managed-agent webhook update` | API Key | Update a Webhook endpoint | [managed-agent.md](managed-agent.md) | ## By group -| Group | Commands | Reference | -| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -| `managed-agent` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `deployment create`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment create`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `playground`, `project build`, `project init`, `project publish`, `project validate`, `project version disable`, `project version enable`, `project version list`, `project version preview`, `project version restore`, `project version status`, `project workbench`, `session archive`, `session create`, `session debug`, `session delete`, `session event list`, `session event send`, `session event stream`, `session events`, `session export`, `session get`, `session list`, `session run`, `session search`, `session send`, `session update`, `skill create`, `skill download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault create`, `vault credential create`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) | +| Group | Commands | Reference | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | +| `managed-agent` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `deployment create`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment create`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `memory-store create`, `memory-store get`, `memory-store list`, `memory-store memories create`, `memory-store memories delete`, `memory-store memories get`, `memory-store memories list`, `memory-store memories update`, `memory-store versions get`, `memory-store versions list`, `memory-store versions redact`, `plan`, `playground`, `project build`, `project init`, `project publish`, `project validate`, `project version disable`, `project version enable`, `project version list`, `project version preview`, `project version restore`, `project version status`, `project workbench`, `session archive`, `session create`, `session debug`, `session delete`, `session event list`, `session event send`, `session event stream`, `session events`, `session export`, `session get`, `session list`, `session run`, `session search`, `session send`, `session update`, `skill create`, `skill download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault create`, `vault credential create`, `vault get`, `vault list`, `vault search`, `webhook create`, `webhook delete`, `webhook disable`, `webhook enable`, `webhook events`, `webhook get`, `webhook list`, `webhook reset-secret`, `webhook test`, `webhook update` | [managed-agent.md](managed-agent.md) | ## Global flags diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index a830d3c95..b59d94b14 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -7,91 +7,112 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| ------------------------------------------ | -------------- | ---------------------------------------------------------------------------- | -| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | -| `bl managed-agent agent get` | API Key | Get a Managed Agent | -| `bl managed-agent agent list` | API Key | List Managed Agents | -| `bl managed-agent agent search` | API Key | Search Managed Agents | -| `bl managed-agent agent versions` | API Key | List Managed Agent versions | -| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | -| `bl managed-agent deployment create` | API Key | Declare and create one Managed Agent Deployment through a scoped YAML apply | -| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | -| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | -| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | -| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | -| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | -| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | -| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | -| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | -| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | -| `bl managed-agent environment create` | API Key | Declare and create one Managed Agent Environment through a scoped YAML apply | -| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | -| `bl managed-agent environment list` | API Key | List Managed Agent environments | -| `bl managed-agent environment search` | API Key | Search Managed Agent environments | -| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | -| `bl managed-agent file download` | API Key | Download Managed Agent file content | -| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | -| `bl managed-agent file list` | API Key | List Managed Agent files | -| `bl managed-agent file search` | API Key | Search Managed Agent files | -| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | -| `bl managed-agent init` | No Auth | Create an agents.yaml template | -| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | -| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | -| `bl managed-agent project build` | No Auth | Organize directory source and generate the immutable Publish Build | -| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | -| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | -| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | -| `bl managed-agent project version disable` | No Auth | Disable directory project versions | -| `bl managed-agent project version enable` | No Auth | Enable directory project versions | -| `bl managed-agent project version list` | No Auth | List directory project versions | -| `bl managed-agent project version preview` | No Auth | Preview a directory project version | -| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | -| `bl managed-agent project version status` | No Auth | Show directory project version status | -| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | -| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | -| `bl managed-agent session create` | API Key | Create a new session for an agent | -| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | -| `bl managed-agent session delete` | API Key | Delete a session | -| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | -| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | -| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | -| `bl managed-agent session events` | API Key | List events for a Managed Agent session | -| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | -| `bl managed-agent session get` | API Key | Get details of a session | -| `bl managed-agent session list` | API Key | List sessions from the provider | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | -| `bl managed-agent session search` | API Key | Search Managed Agent sessions | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | -| `bl managed-agent session update` | API Key | Update a Managed Agent session | -| `bl managed-agent skill create` | API Key | Declare and create one custom Managed Agent Skill from a local source | -| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | -| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | -| `bl managed-agent skill list` | API Key | List Managed Agent skills | -| `bl managed-agent skill search` | API Key | Search Managed Agent skills | -| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | -| `bl managed-agent skill-list` | API Key | List Managed Agent skills | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | -| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | -| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | -| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | -| `bl managed-agent vault create` | API Key | Declare and create one empty Managed Agent Vault through a scoped YAML apply | -| `bl managed-agent vault credential create` | API Key | Append and create one environment-variable Credential in a tracked Vault | -| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | -| `bl managed-agent vault list` | API Key | List Managed Agent vaults | -| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | +| Command | Authentication | Description | +| ----------------------------------------------- | -------------- | ---------------------------------------------------------------------------- | +| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | +| `bl managed-agent agent get` | API Key | Get a Managed Agent | +| `bl managed-agent agent list` | API Key | List Managed Agents | +| `bl managed-agent agent search` | API Key | Search Managed Agents | +| `bl managed-agent agent versions` | API Key | List Managed Agent versions | +| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | +| `bl managed-agent deployment create` | API Key | Declare and create one Managed Agent Deployment through a scoped YAML apply | +| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | +| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | +| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | +| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | +| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | +| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | +| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | +| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | +| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | +| `bl managed-agent environment create` | API Key | Declare and create one Managed Agent Environment through a scoped YAML apply | +| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | +| `bl managed-agent environment list` | API Key | List Managed Agent environments | +| `bl managed-agent environment search` | API Key | Search Managed Agent environments | +| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | +| `bl managed-agent file download` | API Key | Download Managed Agent file content | +| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | +| `bl managed-agent file list` | API Key | List Managed Agent files | +| `bl managed-agent file search` | API Key | Search Managed Agent files | +| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | +| `bl managed-agent init` | No Auth | Create an agents.yaml template | +| `bl managed-agent memory-store create` | API Key | Declare and create a Memory Store through scoped YAML apply | +| `bl managed-agent memory-store get` | API Key | Get a Memory Store | +| `bl managed-agent memory-store list` | API Key | List Memory Stores | +| `bl managed-agent memory-store memories create` | API Key | Create a memory file | +| `bl managed-agent memory-store memories delete` | API Key | Delete a memory file | +| `bl managed-agent memory-store memories get` | API Key | Get a memory file | +| `bl managed-agent memory-store memories list` | API Key | List memory files and prefixes | +| `bl managed-agent memory-store memories update` | API Key | Update memory content with an optional SHA-256 precondition | +| `bl managed-agent memory-store versions get` | API Key | Get a memory version | +| `bl managed-agent memory-store versions list` | API Key | List Memory Store versions | +| `bl managed-agent memory-store versions redact` | API Key | Permanently redact a memory version's content | +| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | +| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | +| `bl managed-agent project build` | No Auth | Organize directory source and generate the immutable Publish Build | +| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | +| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | +| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | +| `bl managed-agent project version disable` | No Auth | Disable directory project versions | +| `bl managed-agent project version enable` | No Auth | Enable directory project versions | +| `bl managed-agent project version list` | No Auth | List directory project versions | +| `bl managed-agent project version preview` | No Auth | Preview a directory project version | +| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | +| `bl managed-agent project version status` | No Auth | Show directory project version status | +| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | +| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | +| `bl managed-agent session create` | API Key | Create a new session for an agent | +| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | +| `bl managed-agent session delete` | API Key | Delete a session | +| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | +| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | +| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | +| `bl managed-agent session events` | API Key | List events for a Managed Agent session | +| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | +| `bl managed-agent session get` | API Key | Get details of a session | +| `bl managed-agent session list` | API Key | List sessions from the provider | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | +| `bl managed-agent session search` | API Key | Search Managed Agent sessions | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | +| `bl managed-agent session update` | API Key | Update a Managed Agent session | +| `bl managed-agent skill create` | API Key | Declare and create one custom Managed Agent Skill from a local source | +| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | +| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | +| `bl managed-agent skill list` | API Key | List Managed Agent skills | +| `bl managed-agent skill search` | API Key | Search Managed Agent skills | +| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | +| `bl managed-agent skill-list` | API Key | List Managed Agent skills | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | +| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | +| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | +| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | +| `bl managed-agent vault create` | API Key | Declare and create one empty Managed Agent Vault through a scoped YAML apply | +| `bl managed-agent vault credential create` | API Key | Append and create one environment-variable Credential in a tracked Vault | +| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | +| `bl managed-agent vault list` | API Key | List Managed Agent vaults | +| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | +| `bl managed-agent webhook create` | API Key | Create a Webhook endpoint | +| `bl managed-agent webhook delete` | API Key | Delete a Webhook endpoint | +| `bl managed-agent webhook disable` | API Key | Disable a Webhook endpoint | +| `bl managed-agent webhook enable` | API Key | Enable a Webhook endpoint | +| `bl managed-agent webhook events` | API Key | List recent Webhook delivery events | +| `bl managed-agent webhook get` | API Key | Get a Webhook endpoint | +| `bl managed-agent webhook list` | API Key | List all Webhook endpoints | +| `bl managed-agent webhook reset-secret` | API Key | Reset a Webhook signing secret | +| `bl managed-agent webhook test` | API Key | Send a real Webhook test event | +| `bl managed-agent webhook update` | API Key | Update a Webhook endpoint | ## Command details ### `bl managed-agent agent create` -| Field | Value | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent agent create` | -| **Description** | Declare and create one Managed Agent through an isolated YAML apply | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent agent create --name <name> --model <model> --instructions <text\|path> [--description <text>] [--skill <id>...] [--type custom\|official] [--skill-dir <path>...] [--tool <name>...] [--file <path>] [--yes]` | +| Field | Value | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent agent create` | +| **Description** | Declare and create one Managed Agent through an isolated YAML apply | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent agent create --name <name> --model <model> --instructions <text\|path> [--description <text>] [--skill <id>...] [--type custom\|official] [--skill-dir <path>...] [--tool <name>...] [--environment <yaml-key>] [--vault <yaml-key>] [--mcp <server-code>...] [--file <path>] [--yes]` | #### Flags @@ -105,6 +126,9 @@ Index: [index.md](index.md) | `--skill-dir <path>` | array | no | Local Skill directory or ZIP to declare, upload, and attach through the same scoped create (repeatable) | | `--type <custom\|official>` | string | no | Type applied to every --skill value (default: custom) | | `--tool <name>` | array | no | Builtin tool name (repeatable) | +| `--environment <yaml-key>` | string | no | Existing Environment key from agents.yaml, used as the default Session/Deployment environment | +| `--vault <yaml-key>` | string | no | Existing Vault key from agents.yaml, used as the default Session/Deployment vault | +| `--mcp <server-code>` | array | no | MCP market server code; all of its tools are enabled (repeatable) | | `--file <path>` | string | no | Config file path (default: agents.yaml) | | `--yes` | switch | no | Write YAML and run the scoped remote create | | `--api-key <key>` | string | no | API key | @@ -118,6 +142,8 @@ Index: [index.md](index.md) - Without --yes, previews the generated YAML key and scoped plan. --dry-run stays offline. Unrelated resources are not refreshed or drift-checked. - --skill writes an external Skill reference directly into the Agent declaration. --type defaults to custom; use --type official for platform Skills. These Skills are not managed through the top-level skills map. - --skill-dir accepts a local Skill directory or ZIP, writes it as a top-level custom Skill declaration, and writes its generated YAML key into the Agent skills list. Skill and Agent are created together in dependency order. --type applies only to --skill IDs. +- --environment and --vault write existing YAML keys into the Agent declaration as default runtime bindings; they are not sent to the Agent API. session create|run and deployment create use them unless those commands pass their own. The referenced Environment and Vault must already be created. +- --mcp accepts MCP market server codes (see mcp list) and enables all of their tools. To toggle individual tools, edit tools.mcp in YAML and run a full apply. #### Examples @@ -137,6 +163,10 @@ bl managed-agent agent create --name slides --model qwen3.8-max --instructions . bl managed-agent agent create --name reviewer --model qwen3.8-max --instructions ./prompts/reviewer.md --skill-dir ./skills/code-review --yes ``` +```bash +bl managed-agent agent create --name researcher --model qwen3.8-max --instructions ./prompts/researcher.md --environment dev --vault secrets --mcp market-cmgjmcp00074952 --yes +``` + ### `bl managed-agent agent get` | Field | Value | @@ -326,33 +356,34 @@ bl managed-agent apply --yes ### `bl managed-agent deployment create` -| Field | Value | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent deployment create` | -| **Description** | Declare and create one Managed Agent Deployment through a scoped YAML apply | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent deployment create --name <name> --agent <yaml-key> (--message <text>... \| --event <json\|@path>...) [--agent-version <number>] [--environment <yaml-key>] [--vault <yaml-key>...] [--resource <json\|@path>...] [--schedule <cron> --timezone <timezone>] [--description <text>] [--metadata <key=value>...] [--file <path>] [--yes]` | - -#### Flags - -| Flag | Type | Required | Description | -| -------------------------- | ------ | -------- | ----------------------------------------------------------------------- | -| `--name <name>` | string | yes | Remote Deployment display name; the YAML key is generated automatically | -| `--agent <yaml-key>` | string | yes | Existing Agent key from agents.yaml | -| `--agent-version <number>` | number | no | Agent version | -| `--environment <yaml-key>` | string | no | Existing Environment key from agents.yaml | -| `--vault <yaml-key>` | array | no | Existing Vault key from agents.yaml (repeatable) | -| `--message <text>` | array | no | Initial user message (repeatable) | -| `--event <json\|@path>` | array | no | Initial user.message or system.message JSON (repeatable) | -| `--resource <json\|@path>` | array | no | File Resource JSON with source or file_id (repeatable) | -| `--schedule <cron>` | string | no | Five-field cron expression | -| `--timezone <timezone>` | string | no | IANA schedule timezone | -| `--description <text>` | string | no | Deployment description | -| `--metadata <key=value>` | array | no | Metadata entry (repeatable) | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--yes` | switch | no | Write YAML and run the scoped remote create | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment create` | +| **Description** | Declare and create one Managed Agent Deployment through a scoped YAML apply | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment create --name <name> --agent <yaml-key> (--message <text>... \| --event <json\|@path>...) [--agent-version <number>] [--environment <yaml-key>] [--environment_variables <KEY=VALUE>...] [--vault <yaml-key>...] [--resource <json\|@path>...] [--schedule <cron> --timezone <timezone>] [--description <text>] [--metadata <key=value>...] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------------- | ------ | -------- | -------------------------------------------------------------------------- | +| `--environment_variables <KEY=VALUE>` | array | no | Runtime environment variable; repeat for multiple values (last value wins) | +| `--name <name>` | string | yes | Remote Deployment display name; the YAML key is generated automatically | +| `--agent <yaml-key>` | string | yes | Existing Agent key from agents.yaml | +| `--agent-version <number>` | number | no | Agent version | +| `--environment <yaml-key>` | string | no | Existing Environment key from agents.yaml | +| `--vault <yaml-key>` | array | no | Existing Vault key from agents.yaml (repeatable) | +| `--message <text>` | array | no | Initial user message (repeatable) | +| `--event <json\|@path>` | array | no | Initial user.message or system.message JSON (repeatable) | +| `--resource <json\|@path>` | array | no | File Resource JSON with source or file_id (repeatable) | +| `--schedule <cron>` | string | no | Five-field cron expression | +| `--timezone <timezone>` | string | no | IANA schedule timezone | +| `--description <text>` | string | no | Deployment description | +| `--metadata <key=value>` | array | no | Metadata entry (repeatable) | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and run the scoped remote create | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes @@ -360,13 +391,14 @@ bl managed-agent apply --yes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. - Without --yes, this command only previews. --dry-run is fully offline. The scoped flow checks only the target resource and its transitive dependencies; unrelated resources are not refreshed or drift-checked. +- --environment_variables writes string values to the Deployment YAML declaration when --yes is used. Repeat the flag for multiple variables; empty values and values containing = are allowed. Later apply runs manage these values; removing the field or using {} clears them. YAML stores these values; the key `__proto__` is reserved. - Initial Events must contain 1-50 user.message/system.message entries. --resource accepts only File Resources in this release. - Use either repeatable --message values or repeatable --event values; the two input forms cannot be mixed. #### Examples ```bash -bl managed-agent deployment create --name Daily --agent assistant --message "Generate the report" +bl managed-agent deployment create --name Daily --agent assistant --message "Generate the report" --environment_variables LOG_LEVEL=info ``` ```bash @@ -1102,6 +1134,381 @@ bl managed-agent init bl managed-agent init --agent-name assistant ``` +### `bl managed-agent memory-store create` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store create` | +| **Description** | Declare and create a Memory Store through scoped YAML apply | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store create --name <name> [--description <text>] [--metadata <key=value>...] [--entry <json\|@path>...] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------ | ------ | -------- | ---------------------------------------------------- | +| `--name <name>` | string | yes | Memory Store display name; generates a YAML key | +| `--description <text>` | string | no | Store description (default: empty) | +| `--metadata <key=value>` | array | no | Metadata entry (repeatable) | +| `--entry <json\|@path>` | array | no | Initial entry JSON with key and content (repeatable) | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and run the scoped remote create | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected. +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- Without --yes, this command only previews. --dry-run is fully offline. The scoped flow checks only the target resource and its transitive dependencies; unrelated resources are not refreshed or drift-checked. +- Store lifecycle is managed by agents.yaml. Entries initialize new stores; existing content is preserved unless entries_mode: sync is explicitly configured. + +#### Examples + +```bash +bl managed-agent memory-store create --name Notes +``` + +```bash +bl managed-agent memory-store create --name Notes --entry '{"key":"notes.md","content":"Initial notes"}' --dry-run +``` + +### `bl managed-agent memory-store get` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store get` | +| **Description** | Get a Memory Store | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store get --store-id <id> [--workspace-id <id>]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store get --workspace-id ws-example --store-id memstore_example +``` + +### `bl managed-agent memory-store list` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store list` | +| **Description** | List Memory Stores | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store list [--workspace-id <id>] [--limit <n>] [--page <cursor>] [--include-archived]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | -------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--limit <n>` | number | no | Page size (1–100) | +| `--page <cursor>` | string | no | Opaque next_page cursor from the previous response | +| `--include-archived` | switch | no | Include archived stores | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store list --workspace-id ws-example --output json +``` + +### `bl managed-agent memory-store memories create` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store memories create` | +| **Description** | Create a memory file | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store memories create --store-id <id> --path <path> (--content <text> \| --content-file <path>)` | +| **Risk** | `high` | +| **Risk message** | This changes persistent memory content. Review the dry-run request before confirming. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | -------------------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--content <text>` | string | no | Memory content; an empty string is allowed | +| `--content-file <path>` | string | no | Read UTF-8 content from a local file; exclusive with --content | +| `--path <path>` | string | yes | Memory file path | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store memories create --store-id memstore_example --path notes.md --content-file ./notes.md --dry-run +``` + +### `bl managed-agent memory-store memories delete` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store memories delete` | +| **Description** | Delete a memory file | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store memories delete --store-id <id> --memory-id <id>` | +| **Risk** | `high` | +| **Risk message** | This changes persistent memory content. Review the dry-run request before confirming. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--memory-id <id>` | string | yes | Remote Memory ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store memories delete --store-id memstore_example --memory-id mem_example --dry-run +``` + +### `bl managed-agent memory-store memories get` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store memories get` | +| **Description** | Get a memory file | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store memories get --store-id <id> --memory-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--memory-id <id>` | string | yes | Remote Memory ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store memories get --store-id memstore_example --memory-id mem_example +``` + +### `bl managed-agent memory-store memories list` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store memories list` | +| **Description** | List memory files and prefixes | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store memories list --store-id <id> [--page <cursor>] [--limit <n>] [--path-prefix <prefix>] [--depth <n>] [--view <basic\|full>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------ | ------ | -------- | -------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--limit <n>` | number | no | Page size (1–20) | +| `--page <cursor>` | string | no | Opaque next_page cursor from the previous response | +| `--view <basic\|full>` | string | no | Response view (basic or full) | +| `--path-prefix <prefix>` | string | no | Filter by memory path prefix | +| `--depth <n>` | number | no | Directory listing depth | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store memories list --store-id memstore_example --path-prefix /notes/ --view full +``` + +### `bl managed-agent memory-store memories update` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store memories update` | +| **Description** | Update memory content with an optional SHA-256 precondition | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store memories update --store-id <id> --memory-id <id> (--content <text> \| --content-file <path>) [--expected-content-sha256 <sha256>]` | +| **Risk** | `high` | +| **Risk message** | This changes persistent memory content. Review the dry-run request before confirming. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------------ | ------ | -------- | -------------------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--memory-id <id>` | string | yes | Remote Memory ID | +| `--content <text>` | string | no | Memory content; an empty string is allowed | +| `--content-file <path>` | string | no | Read UTF-8 content from a local file; exclusive with --content | +| `--expected-content-sha256 <sha256>` | string | no | Expected current SHA-256; reject a concurrent content change | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store memories update --store-id memstore_example --memory-id mem_example --content-file ./notes.md --dry-run +``` + +### `bl managed-agent memory-store versions get` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------ | +| **Name** | `managed-agent memory-store versions get` | +| **Description** | Get a memory version | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store versions get --store-id <id> --version-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--version-id <id>` | string | yes | Remote Memory Version ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store versions get --store-id memstore_example --version-id memver_example +``` + +### `bl managed-agent memory-store versions list` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent memory-store versions list` | +| **Description** | List Memory Store versions | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store versions list --store-id <id> [--memory-id <id>] [--operation <created\|modified\|deleted>] [--created-at-gte <timestamp>] [--created-at-lte <timestamp>] [--page <cursor>] [--limit <n>] [--view <basic\|full>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------------------ | ------ | -------- | -------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--limit <n>` | number | no | Page size (1–100) | +| `--page <cursor>` | string | no | Opaque next_page cursor from the previous response | +| `--view <basic\|full>` | string | no | Response view (basic or full) | +| `--memory-id <id>` | string | no | Remote Memory ID | +| `--operation <created\|modified\|deleted>` | string | no | Filter by version operation | +| `--created-at-gte <timestamp>` | string | no | Inclusive creation timestamp lower bound | +| `--created-at-lte <timestamp>` | string | no | Inclusive creation timestamp upper bound | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store versions list --store-id memstore_example --operation modified --view full +``` + +### `bl managed-agent memory-store versions redact` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------- | +| **Name** | `managed-agent memory-store versions redact` | +| **Description** | Permanently redact a memory version's content | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent memory-store versions redact --store-id <id> --version-id <id>` | +| **Risk** | `high` | +| **Risk message** | This permanently redacts the selected version's content and cannot be undone. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional with a configured Base URL | +| `--store-id <id>` | string | yes | Remote Memory Store ID | +| `--version-id <id>` | string | yes | Remote Memory Version ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Calls the SDK with the CLI API Key and workspace settings; no agents.yaml or State is required for reading stores or managing their contents. +- Store lifecycle remains in agents.yaml. Pass the remote Store ID to these API commands and pass next_page back unchanged. + +#### Examples + +```bash +bl managed-agent memory-store versions redact --store-id memstore_example --version-id memver_example --dry-run +``` + ### `bl managed-agent plan` | Field | Value | @@ -1546,25 +1953,27 @@ bl managed-agent session archive --session-id sess_abc --yes ### `bl managed-agent session create` -| Field | Value | -| ------------------ | ----------------------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent session create` | -| **Description** | Create a new session for an agent | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent session create [--agent <name>] [--environment <name>] [--title <title>] [--file <path>]` | +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent session create` | +| **Description** | Create a new session for an agent | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session create [--agent <name>] [--environment <name>] [--title <title>] [--environment_variables <KEY=VALUE>...] [--memory-stores <keys>] [--resource <json\|@path>...] [--file <path>]` | #### Flags -| Flag | Type | Required | Description | -| ------------------------- | ------ | -------- | ------------------------------------------------------------ | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--agent <name>` | string | no | Agent name (auto-detected when only one agent is configured) | -| `--environment <name>` | string | no | Override agent's declared environment | -| `--vault <name>` | string | no | Override agent's declared vault | -| `--memory-stores <names>` | string | no | Override agent's memory stores (comma-separated) | -| `--title <title>` | string | no | Session title | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------- | +| `--environment_variables <KEY=VALUE>` | array | no | Runtime environment variable; repeat for multiple values (last value wins) | +| `--resource <json\|@path>` | array | no | Session resource JSON (repeatable); memory_store uses a YAML key, access and instructions | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--agent <name>` | string | no | Agent name (auto-detected when only one agent is configured) | +| `--environment <name>` | string | no | Override agent's declared environment | +| `--vault <name>` | string | no | Override agent's declared vault | +| `--memory-stores <names>` | string | no | Override memory stores with comma-separated YAML keys | +| `--title <title>` | string | no | Session title | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes @@ -1574,6 +1983,14 @@ bl managed-agent session archive --session-id sess_abc --yes #### Examples +```bash +bl managed-agent session create --agent assistant --environment_variables LOG_LEVEL=info --environment_variables FEATURE_FLAG=on +``` + +```bash +bl managed-agent session create --agent assistant --resource '{"type":"memory_store","memory_store":"notes","access":"read_only"}' +``` + ```bash bl managed-agent session create ``` @@ -1920,27 +2337,29 @@ bl managed-agent session list --all ### `bl managed-agent session run` -| Field | Value | -| ------------------ | --------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent session run` | -| **Description** | Create a session, send a message, and stream the response | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent session run --prompt <text> [--agent <name>] [--no-stream] [--file <path>]` | +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent session run` | +| **Description** | Create a session, send a message, and stream the response | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session run --prompt <text> [--agent <name>] [--no-stream] [--environment_variables <KEY=VALUE>...] [--memory-stores <keys>] [--resource <json\|@path>...] [--file <path>]` | #### Flags -| Flag | Type | Required | Description | -| ------------------------- | ------ | -------- | ------------------------------------------------------------ | -| `--prompt <text>` | string | yes | Prompt to send (required) | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--agent <name>` | string | no | Agent name (auto-detected when only one agent is configured) | -| `--environment <name>` | string | no | Override agent's declared environment | -| `--vault <name>` | string | no | Override agent's declared vault | -| `--memory-stores <names>` | string | no | Override agent's memory stores (comma-separated) | -| `--title <title>` | string | no | Session title | -| `--no-stream` | switch | no | Use polling instead of SSE streaming | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------- | +| `--environment_variables <KEY=VALUE>` | array | no | Runtime environment variable; repeat for multiple values (last value wins) | +| `--resource <json\|@path>` | array | no | Session resource JSON (repeatable); memory_store uses a YAML key, access and instructions | +| `--prompt <text>` | string | yes | Prompt to send (required) | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--agent <name>` | string | no | Agent name (auto-detected when only one agent is configured) | +| `--environment <name>` | string | no | Override agent's declared environment | +| `--vault <name>` | string | no | Override agent's declared vault | +| `--memory-stores <names>` | string | no | Override memory stores with comma-separated YAML keys | +| `--title <title>` | string | no | Session title | +| `--no-stream` | switch | no | Use polling instead of SSE streaming | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes @@ -1951,6 +2370,14 @@ bl managed-agent session list --all #### Examples +```bash +bl managed-agent session run --agent assistant --environment_variables LOG_LEVEL=info --prompt "hello" +``` + +```bash +bl managed-agent session run --agent assistant --resource '{"type":"memory_store","memory_store":"notes","access":"read_only"}' --prompt "hello" +``` + ```bash bl managed-agent session run --prompt "hello" ``` @@ -2660,3 +3087,357 @@ bl managed-agent vault search --query github ```bash bl managed-agent vault search --query production --page-limit 20 --output json ``` + +### `bl managed-agent webhook create` + +| Field | Value | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent webhook create` | +| **Description** | Create a Webhook endpoint | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook create --url <url> --event <type> [--event <type>] [--description <text>] [--workspace-id <id>]` | +| **Risk** | `high` | +| **Risk message** | This creates a Webhook and starts delivering subscribed events to its callback URL. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | ------------------------------------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--url <url>` | string | yes | Public HTTP or HTTPS callback URL | +| `--event <type>` | array | yes | Named event subscription; repeat for multiple events, wildcards are unsupported | +| `--description <text>` | string | no | Description, up to 256 characters; an empty string clears it on update | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. +- Signing Secret is returned only on create/reset. Save it from this successful response; the CLI does not persist it. + +#### Examples + +```bash +# Only after explicit user confirmation: +bl managed-agent webhook create --workspace-id workspace-example --url https://example.com/webhooks --event session.updated --yes +``` + +### `bl managed-agent webhook delete` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------- | +| **Name** | `managed-agent webhook delete` | +| **Description** | Delete a Webhook endpoint | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook delete --webhook-id <wep_id> [--workspace-id <id>]` | +| **Risk** | `high` | +| **Risk message** | This deletes the Webhook and cancels its pending deliveries. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. + +#### Examples + +```bash +# Only after explicit user confirmation: +bl managed-agent webhook delete --workspace-id workspace-example --webhook-id wep_example --yes +``` + +### `bl managed-agent webhook disable` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------ | +| **Name** | `managed-agent webhook disable` | +| **Description** | Disable a Webhook endpoint | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook disable --webhook-id <wep_id> [--workspace-id <id>]` | +| **Risk** | `high` | +| **Risk message** | This stops new event deliveries and cancels pending deliveries. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. + +#### Examples + +```bash +# Only after explicit user confirmation: +bl managed-agent webhook disable --workspace-id workspace-example --webhook-id wep_example --yes +``` + +### `bl managed-agent webhook enable` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------ | +| **Name** | `managed-agent webhook enable` | +| **Description** | Enable a Webhook endpoint | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook enable --webhook-id <wep_id> [--workspace-id <id>]` | +| **Risk** | `high` | +| **Risk message** | This resumes new event deliveries. Events from the disabled period are not replayed. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. + +#### Examples + +```bash +# Only after explicit user confirmation: +bl managed-agent webhook enable --workspace-id workspace-example --webhook-id wep_example --yes +``` + +### `bl managed-agent webhook events` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent webhook events` | +| **Description** | List recent Webhook delivery events | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook events --webhook-id <wep_id> [--limit <count>] [--page <cursor>] [--workspace-id <id>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--limit <count>` | number | no | Page size, 1–100 (default: 20) | +| `--page <cursor>` | string | no | Opaque next_page cursor from the previous response | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. +- Returns one page of delivery events retained for 7 days. Continue with next_page; deleted Webhooks' retained events remain queryable. + +#### Examples + +```bash +bl managed-agent webhook events --workspace-id workspace-example --webhook-id wep_example --limit 20 --output json +``` + +### `bl managed-agent webhook get` + +| Field | Value | +| ------------------ | -------------------------------------------------------------------------- | +| **Name** | `managed-agent webhook get` | +| **Description** | Get a Webhook endpoint | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook get --webhook-id <wep_id> [--workspace-id <id>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. + +#### Examples + +```bash +bl managed-agent webhook get --workspace-id workspace-example --webhook-id wep_example +``` + +### `bl managed-agent webhook list` + +| Field | Value | +| ------------------ | ----------------------------------------------------- | +| **Name** | `managed-agent webhook list` | +| **Description** | List all Webhook endpoints | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook list [--workspace-id <id>]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. +- Returns every undeleted Webhook without pagination. + +#### Examples + +```bash +bl managed-agent webhook list --workspace-id workspace-example --output json +``` + +### `bl managed-agent webhook reset-secret` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent webhook reset-secret` | +| **Description** | Reset a Webhook signing secret | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook reset-secret --webhook-id <wep_id> [--workspace-id <id>]` | +| **Risk** | `high` | +| **Risk message** | This immediately invalidates the previous signing secret. Save the new secret and update your receiver. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. +- Signing Secret is returned only on create/reset. Save it from this successful response; the CLI does not persist it. + +#### Examples + +```bash +# Only after explicit user confirmation: +bl managed-agent webhook reset-secret --workspace-id workspace-example --webhook-id wep_example --yes +``` + +### `bl managed-agent webhook test` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent webhook test` | +| **Description** | Send a real Webhook test event | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook test --webhook-id <wep_id> [--workspace-id <id>]` | +| **Risk** | `high` | +| **Risk message** | This sends a real callback request; redirects or security validation failures may disable the Webhook. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ---------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. +- Tests are synchronous, are not retried or recorded, and do not affect consecutive failure counts. + +#### Examples + +```bash +# Only after explicit user confirmation: +bl managed-agent webhook test --workspace-id workspace-example --webhook-id wep_example --yes +``` + +### `bl managed-agent webhook update` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent webhook update` | +| **Description** | Update a Webhook endpoint | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent webhook update --webhook-id <wep_id> [--url <url>] [--event <type>] [--description <text>] [--workspace-id <id>]` | +| **Risk** | `high` | +| **Risk message** | This updates the callback URL, description, or event subscriptions of the Webhook. | + +> **Agent safety:** Never add `--yes` automatically. On `type="requires_confirmation"`, stop and ask for explicit user confirmation of the same action and scope. + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | ------------------------------------------------------------------------------- | +| `--workspace-id <id>` | string | no | Workspace ID; optional when a base URL is configured | +| `--webhook-id <wep_id>` | string | yes | Webhook endpoint ID | +| `--url <url>` | string | no | Public HTTP or HTTPS callback URL | +| `--event <type>` | array | no | Named event subscription; repeat for multiple events, wildcards are unsupported | +| `--description <text>` | string | no | Description, up to 256 characters; an empty string clears it on update | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Manages workspace-level Webhooks directly; no agents.yaml or local state is required. +- Auth: --api-key > DASHSCOPE_API_KEY > active profile API key. Credentials are injected into the SDK in memory only. +- Base URL: --base-url > DASHSCOPE_BASE_URL > profile base_url. Without a configured URL, use --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id for the cn-beijing endpoint. +- Omitted fields stay unchanged; --event replaces the complete subscription list. + +#### Examples + +```bash +# Only after explicit user confirmation: +bl managed-agent webhook update --workspace-id workspace-example --webhook-id wep_example --description "" --yes +``` diff --git a/skills/bailian-protocol/SKILL.md b/skills/bailian-protocol/SKILL.md index fd0bd702c..bf8dda0e8 100644 --- a/skills/bailian-protocol/SKILL.md +++ b/skills/bailian-protocol/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-protocol metadata: - version: "2.3.3" + version: "2.4.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index b7e430c66..30b4c2d1e 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -1,7 +1,7 @@ --- name: bailian-sandbox metadata: - version: "2.3.3" + version: "2.4.0" requires: bins: ["bl"] description: >- diff --git a/skills/bailian-web-search/SKILL.md b/skills/bailian-web-search/SKILL.md index 6e368370b..93a38a119 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: "2.3.3" + version: "2.4.0" requires: bins: ["bl"] description: >- diff --git a/tools/use-local-bl.sh b/tools/use-local-bl.sh new file mode 100644 index 000000000..2a4e84e85 --- /dev/null +++ b/tools/use-local-bl.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Enable this checkout's CLI in the current Bash/Zsh session: +# source /path/to/cli/tools/use-local-bl.sh +# Uses the installed workspace dependencies, including the linked local SDK. +# Disables background update checks and auto-install for the local CLI process. + +if [ -n "${ZSH_VERSION:-}" ]; then + case "$ZSH_EVAL_CONTEXT" in + *:file) ;; + *) + printf '%s\n' '请用 source 执行 / Run with: source tools/use-local-bl.sh' >&2 + exit 1 + ;; + esac +elif [ -n "${BASH_VERSION:-}" ]; then + if [ "${BASH_SOURCE[0]}" = "$0" ]; then + printf '%s\n' '请用 source 执行 / Run with: source tools/use-local-bl.sh' >&2 + exit 1 + fi +else + printf '%s\n' '请在 Bash 或 Zsh 中 source 此脚本 / Source this script in Bash or Zsh.' >&2 + return 1 2>/dev/null || exit 1 +fi + +_bailian_use_local_bl() { + local script_path="$1" + local repo_root node_bin loader_path entry_path bin_dir launcher_path help_output + + repo_root="$(cd -- "$(dirname -- "$script_path")/.." && pwd -P)" || return 1 + node_bin="$(command -v node)" || { + printf '%s\n' '未找到 Node.js / Node.js is required.' >&2 + return 1 + } + loader_path="$repo_root/node_modules/tsx/dist/loader.mjs" + entry_path="$repo_root/packages/cli/src/main.ts" + bin_dir="$repo_root/.scratch/local-bl/bin" + launcher_path="$bin_dir/bl" + + if [ ! -f "$loader_path" ] || [ ! -f "$entry_path" ]; then + printf '%s\n' '缺少源码或 tsx 依赖;请先在仓库安装依赖 / Missing source or tsx; install workspace dependencies first.' >&2 + return 1 + fi + + # Help loads the command map and SDK exports without a business API request. + if ! help_output="$(DO_NOT_TRACK=1 "$node_bin" --import "$loader_path" "$entry_path" \ + managed-agent memory-store memories update --help 2>&1)"; then + printf '%s\n' "$help_output" >&2 + printf '%s\n' '本地 Memory 命令加载失败,请检查依赖及本地 SDK 链接 / Cannot load Memory commands; check dependencies and the local SDK link.' >&2 + return 1 + fi + + mkdir -p -- "$bin_dir" || return 1 + { + printf '%s\n' '#!/usr/bin/env bash' '# Generated by tools/use-local-bl.sh; preserves the caller working directory.' + printf '%s\n' 'export NO_UPDATE_NOTIFIER=1' + printf 'exec %q --import %q %q "$@"\n' "$node_bin" "$loader_path" "$entry_path" + } > "$launcher_path" || return 1 + chmod +x "$launcher_path" || return 1 + + # Select local bl even when the shell previously defined an alias/function. + unalias bl 2>/dev/null || : + unset -f bl 2>/dev/null || : + case "$PATH" in + "$bin_dir"|"$bin_dir":*) ;; + *) export PATH="$bin_dir:$PATH" ;; + esac + hash -r 2>/dev/null || : + + printf '当前终端已启用本地 bl / Local bl enabled for this terminal:\n %s\n' "$launcher_path" + printf '%s\n' '可以执行 / Try: bl managed-agent memory-store --help' + printf '%s\n' '新终端请重新 source 此脚本 / Source this script again in each new terminal.' +} + +if [ -n "${ZSH_VERSION:-}" ]; then + _bailian_use_local_bl "${(%):-%x}" +else + _bailian_use_local_bl "${BASH_SOURCE[0]}" +fi +if [ "$?" -ne 0 ]; then + unset -f _bailian_use_local_bl + return 1 +fi +unset -f _bailian_use_local_bl