Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vite-hooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ git add \
skills/bailian-finetune/reference \
skills/bailian-managed-agent/SKILL.md \
skills/bailian-managed-agent/reference \
skills/bailian-sandbox/SKILL.md \
skills/bailian-sandbox/reference \
skills/bailian-web-search/SKILL.md

vp staged
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ packages/core/src/auth/ # apiKey / console credential 解析与落盘
packages/core/src/client/ # HTTP client / endpoints / console gateway
```

Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-sandbox` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。

约定:

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and

[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)

## [Unreleased]

### Added

- **Sandbox control-plane commands** — added `bl sandbox` instance and template lifecycle commands for all 13 Bailian Sandbox REST APIs, including default template-build polling and explicit `--async` submission mode.

### Security

- Sandbox calls use the Bailian API Key as a Bearer token without an E2B SDK or E2B API key. Returned connection credentials and dry-run environment values are redacted by default; use `--show-credentials` only when the raw connection tokens are required.

## [1.22.0] - 2026-09-08

### Changed
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)

## [未发布]

### 新增

- **Sandbox 管控面命令** —— 新增 `bl sandbox` 实例与模版生命周期命令,覆盖百炼 Sandbox 的 13 个 REST API;模版构建默认等待完成,也可通过 `--async` 仅提交并立即返回。

### 安全

- Sandbox 调用使用百炼 API Key 作为 Bearer Token,不依赖 E2B SDK,也不发送 E2B API Key。连接凭据和 dry-run 中的环境变量默认脱敏;仅在确实需要原始连接 Token 时使用 `--show-credentials`。

## [1.22.0] - 2026-09-08

### 变更
Expand Down
1 change: 1 addition & 0 deletions docs/agents/skill-change.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
### C. 归属与生成

- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表;hub 最多加一行 hand-off
- [ ] 新增领域目录时:补齐 `SKILL.md`(含 `metadata.version`),同步 `packages/cli/package.json` 的 reference 格式化路径、`.vite-hooks/pre-commit` 的生成物暂存清单、`tools/release/check.mjs` 的生成物校验清单;同步 hub 与共享协议的领域路由
- [ ] 跑 `pnpm run sync:skill-assets`(或 commit 走 pre-commit),提交生成的 `reference/` 与 version 同步结果
- [ ] 高风险命令生成的 reference 必须包含 `Risk` / `Risk message` 和简短 Agent safety 提示;带 `--yes` 的示例必须标注只能在确认后执行,不要手改生成物
- [ ] 默认模型若写在领域路由表(如 `bailian-gen`):与命令 default / [model-add-remove.md](model-add-remove.md) 一并核对
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference'",
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference skills/bailian-sandbox/reference'",
"sync:skill-version": "tsx ../../tools/sync-skill-metadata.ts",
"build": "vp pack",
"dev": "tsx src/main.ts",
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,21 @@ import {
managedAgentFileSearch,
managedAgentFileDownload,
managedAgentFileDelete,
sandboxCreate,
sandboxOfficialImages,
sandboxFileUpload,
sandboxList,
sandboxGet,
sandboxConnect,
sandboxPause,
sandboxResume,
sandboxDelete,
sandboxTemplateCreate,
sandboxTemplateList,
sandboxTemplateGet,
sandboxTemplateUpdate,
sandboxTemplateBuildStatus,
sandboxTemplateDelete,
} from "bailian-cli-commands";

// Full bailian-cli product: every command, exposed under the `bl` binary.
Expand Down Expand Up @@ -423,6 +438,21 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent file search": managedAgentFileSearch,
"managed-agent file download": managedAgentFileDownload,
"managed-agent file delete": managedAgentFileDelete,
"sandbox create": sandboxCreate,
"sandbox official-images": sandboxOfficialImages,
"sandbox file upload": sandboxFileUpload,
"sandbox list": sandboxList,
"sandbox get": sandboxGet,
"sandbox connect": sandboxConnect,
"sandbox pause": sandboxPause,
"sandbox resume": sandboxResume,
"sandbox delete": sandboxDelete,
"sandbox template create": sandboxTemplateCreate,
"sandbox template list": sandboxTemplateList,
"sandbox template get": sandboxTemplateGet,
"sandbox template update": sandboxTemplateUpdate,
"sandbox template build-status": sandboxTemplateBuildStatus,
"sandbox template delete": sandboxTemplateDelete,
};

/**
Expand Down
53 changes: 53 additions & 0 deletions packages/cli/tests/skill-sandbox-reference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { validateSkillDir } from "bailian-cli-core";
import { expect, test } from "vite-plus/test";
import { parse } from "yaml";
import { commands } from "../src/commands.ts";

const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
const sandboxSkillDirectory = join(repositoryRoot, "skills/bailian-sandbox");

test("Sandbox is a valid standalone skill aligned with the CLI version", () => {
const metadata = validateSkillDir(sandboxSkillDirectory, "bailian-sandbox");
expect(metadata.name).toBe("bailian-sandbox");

const skillMarkdown = readFileSync(join(sandboxSkillDirectory, "SKILL.md"), "utf8");
const frontmatter = parse(skillMarkdown.split(/^---\s*$/m)[1]) as {
metadata: { version: string };
};
const cliPackage = JSON.parse(
readFileSync(join(repositoryRoot, "packages/cli/package.json"), "utf8"),
) as { version: string };
expect(frontmatter.metadata.version).toBe(cliPackage.version);

for (const link of skillMarkdown.matchAll(/\]\(([^)]+)\)/g)) {
expect(existsSync(join(sandboxSkillDirectory, link[1]))).toBe(true);
}
});

test("Sandbox reference contains every registered Sandbox command only in its owning skill", () => {
const expectedCommands = Object.keys(commands)
.filter((commandPath) => commandPath.startsWith("sandbox "))
.map((commandPath) => `bl ${commandPath}`)
.sort();
expect(expectedCommands.length).toBeGreaterThan(0);

const reference = readFileSync(join(sandboxSkillDirectory, "reference/sandbox.md"), "utf8");
const documentedCommands = [...reference.matchAll(/^### `([^`]+)`/gm)]
.map((match) => match[1])
.sort();
expect(documentedCommands).toEqual(expectedCommands);

const index = readFileSync(join(sandboxSkillDirectory, "reference/index.md"), "utf8");
for (const commandPath of expectedCommands) {
expect(index).toContain(`\`${commandPath}\``);
}
expect(existsSync(join(repositoryRoot, "skills/bailian-cli/reference/sandbox.md"))).toBe(false);
const hubIndex = readFileSync(
join(repositoryRoot, "skills/bailian-cli/reference/index.md"),
"utf8",
);
expect(hubIndex).not.toContain("`bl sandbox ");
});
113 changes: 113 additions & 0 deletions packages/commands/src/commands/sandbox/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
import {
agentStudioFilesPath,
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
sandboxBaseUrl,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { redactConnectionCredentials, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";

const UPLOAD_SOURCE = "sandbox_template";

export const sandboxFileUpload = defineCommand({
description: {
"en-US": "Upload a workspace file for Sandbox template mounts",
"zh-CN": "上传工作空间文件,供 Sandbox 模版挂载使用",
},
auth: "apiKey",
usageArgs: "--path <path> [--filename <name>] [--mime-type <type>]",
flags: {
...WORKSPACE_FLAG,
path: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Local file path", "zh-CN": "本地文件路径" },
},
filename: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" },
},
mimeType: {
type: "string",
valueHint: "<type>",
description: {
"en-US": "Multipart file MIME type (default: application/octet-stream)",
"zh-CN": "Multipart 文件部分的 MIME 类型(默认:application/octet-stream)",
},
},
},
exampleArgs: [
"--path ./config.json --output json",
"--path ./config.json --quiet",
"--path ./notes.txt --filename notes.txt --mime-type text/plain --dry-run --output json",
],
notes: [
{
"en-US":
"POST /api/v1/agentstudio/files with multipart fields file and source=sandbox_template. Uses a Bailian Bearer API Key, not Console authentication or an E2B key; no agents.yaml is needed.",
"zh-CN":
"向 /api/v1/agentstudio/files 发送 multipart 字段 file 和 source=sandbox_template。使用百炼 Bearer API Key,不使用 Console 鉴权或 E2B Key;无需 agents.yaml。",
},
{
"en-US":
"Base URL follows Sandbox: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. Without one, --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id selects the cn-beijing origin. The upload path has no /sandbox prefix.",
"zh-CN":
"Base URL 沿用 Sandbox:--base-url > DASHSCOPE_BASE_URL > 登录/Profile 的 base_url。未配置时,按 --workspace-id > BAILIAN_WORKSPACE_ID > 配置项 workspace_id 选择 cn-beijing 地址。上传路径不带 /sandbox 前缀。",
},
{
"en-US":
"Returns the upload response immediately; --quiet prints only its id. Upload does not wait for security review: status=checking is not ready to mount. Use an available file's id as mntConfig[].originFileId in template create/update --body, together with mountPath and optional originFileName, in the same workspace. This does not transfer files into a running instance.",
"zh-CN":
"上传响应返回后立即输出,--quiet 仅输出 id。不会等待安全审核:status=checking 不代表已可挂载。在同一工作空间的 template create/update --body 中,将可用文件的 id 填入 mntConfig[].originFileId,同时传入 mountPath 和可选的 originFileName。此命令不向运行中的实例传文件。",
},
{
"en-US":
"--dry-run previews the endpoint, source, and local path without reading or uploading the file. The service detects the MIME type and enforces upload limits.",
"zh-CN":
"--dry-run 仅预览 Endpoint、source 和本地路径,不读取或上传文件。MIME 类型检测和上传限制由服务端执行。",
},
],
async run(ctx) {
const endpoint = ctx.client.url(agentStudioFilesPath(), () =>
sandboxBaseUrl(resolveWorkspaceId(ctx)),
);
const filename = ctx.flags.filename ?? basename(ctx.flags.path);
const mimeType = ctx.flags.mimeType ?? "application/octet-stream";
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{
method: "POST",
endpoint,
request: { source: UPLOAD_SOURCE, file: { path: ctx.flags.path, filename, mimeType } },
},
format,
);
return;
}

const content = new Uint8Array(await readFile(ctx.flags.path));
const form = new FormData();
form.append("source", UPLOAD_SOURCE);
form.append("file", new Blob([content], { type: mimeType }), filename);
const file = await ctx.client.requestJson<Record<string, unknown>>({
method: "POST",
path: endpoint,
body: form,
});
if (!file || typeof file.id !== "string" || !file.id.trim()) {
throw new BailianError(
"Upload response did not contain a File ID. / 上传响应未包含 File ID。",
ExitCode.GENERAL,
);
}
if (ctx.settings.quiet) emitBare(file.id);
else emitResult(redactConnectionCredentials(file), format);
},
});
88 changes: 88 additions & 0 deletions packages/commands/src/commands/sandbox/images.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { defineCommand, detectOutputFormat, UsageError } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";

/** Pinned CLI presets, not a live catalog or a guarantee of regional availability. */
export const SANDBOX_IMAGES = [
{
id: "code-interpreter",
imageName: "代码解释器",
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/code-interpreter-v1:v0.0.44",
icon: "https://img.alicdn.com/imgextra/i2/O1CN01iX8RH9ckFvC093x2_!!6000000000905-2-tps-72-72.png",
description: {
"en-US": "Python / Node.js runtimes with common data-processing libraries",
"zh-CN": "Python / Node.js 运行时+常用数据处理库",
},
},
{
id: "browser",
imageName: "浏览器",
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44",
icon: "https://img.alicdn.com/imgextra/i4/O1CN01ShCbPEunrRI093x2_!!6000000000155-2-tps-72-72.png",
description: {
"en-US": "Chromium and a visual desktop for clicking, filling forms, and screenshots",
"zh-CN": "Chromium +可视化桌面,支持点击/ 填表/截图",
},
},
{
id: "all-in-one",
imageName: "全能型",
imageUrl: "fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/all-in-one:v0.0.44",
icon: "https://img.alicdn.com/imgextra/i1/O1CN01smhklUahsyE093x2_!!6000000006657-2-tps-72-72.png",
description: {
"en-US": "Code execution and browser capabilities together",
"zh-CN": "代码执行+浏览器双能力",
},
},
] as const;

export const SANDBOX_IMAGE_CHOICES = SANDBOX_IMAGES.flatMap((image) => [image.id, image.imageName]);

export const SANDBOX_IMAGE_NOTES = SANDBOX_IMAGES.map((image) => ({
"en-US": `${image.id} (${image.imageName}): ${image.description["en-US"]}. ${image.imageUrl}`,
"zh-CN": `${image.id}(${image.imageName}):${image.description["zh-CN"]}。${image.imageUrl}`,
}));

export function resolveSandboxImage(selector: string) {
const image = SANDBOX_IMAGES.find(
(candidate) => candidate.id === selector || candidate.imageName === selector,
);
if (!image) {
throw new UsageError(
`Unknown built-in image / 未知内置镜像: ${selector}. ${SANDBOX_IMAGE_CHOICES.join(", ")}`,
);
}
return image;
}

export const sandboxOfficialImages = defineCommand({
description: {
"en-US": "List the built-in Sandbox base images (offline)",
"zh-CN": "列出 CLI 内置的 Sandbox 基础镜像(离线)",
},
auth: "none",
exampleArgs: ["", "--output json", "--quiet"],
notes: [
{
"en-US":
"Use a preset ID or Chinese name with template create/update --image. These are pinned cn-beijing images; other environments may differ. Use --from-image for a custom image.",
"zh-CN":
"在 template create/update 中通过 --image 传入预设 ID 或中文名。这些是固定版本的 cn-beijing 镜像,其他环境可能不同;自定义镜像使用 --from-image。",
},
...SANDBOX_IMAGE_NOTES,
],
async run(ctx) {
if (ctx.settings.quiet) {
for (const image of SANDBOX_IMAGES) emitBare(image.id);
return;
}
if (detectOutputFormat(ctx.settings.output) === "json") {
emitResult(SANDBOX_IMAGES, "json");
return;
}
for (const image of SANDBOX_IMAGES) {
emitBare(`${image.id} (${image.imageName})`);
emitBare(` ${image.imageUrl}`);
emitBare(` ${image.description["en-US"]} / ${image.description["zh-CN"]}`);
}
},
});
Loading