feat(sandbox): add Bailian Sandbox lifecycle commands

This commit is contained in:
chenanran555
2026-09-07 15:11:51 +08:00
parent 8e5dd127a9
commit 332efb05b8
26 changed files with 2910 additions and 8 deletions
+2
View File
@@ -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
+1 -1
View File
@@ -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)。
约定:
+10
View File
@@ -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.20.0] - 2026-09-03
> Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations.
+10
View File
@@ -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.20.0] - 2026-09-03
> Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。
+1
View File
@@ -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) 一并核对
+1 -1
View File
@@ -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",
+26
View File
@@ -194,6 +194,19 @@ import {
managedAgentFileSearch,
managedAgentFileDownload,
managedAgentFileDelete,
sandboxCreate,
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.
@@ -399,6 +412,19 @@ 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 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,
};
/**
@@ -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 ");
});
@@ -0,0 +1,423 @@
import {
BailianError,
detectOutputFormat,
defineCommand,
ExitCode,
sandboxEndpoint,
SANDBOX_PATHS,
sandboxInstanceActionPath,
sandboxInstancePath,
type FlagsDef,
type ParsedFlags,
} from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import {
BODY_FLAG,
displayValue,
INSTANCE_TIMEOUT_FLAG,
mergeObjectField,
parseKeyValueEntries,
readRequestBody,
redactConnectionCredentials,
redactRequestSecrets,
resolveWorkspaceId,
SANDBOX_NOTES,
setDefined,
SHOW_CREDENTIALS_FLAG,
validateIntegerRange,
WORKSPACE_FLAG,
type JsonObject,
} from "./shared.ts";
interface SandboxInfo extends JsonObject {
sandboxID?: string;
templateID?: string;
state?: string;
cpuCount?: number;
memoryMB?: number;
startedAt?: string;
endAt?: string;
}
const SANDBOX_ID_FLAG = {
sandboxId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Sandbox instance ID", "zh-CN": "Sandbox 实例 ID" },
},
} satisfies FlagsDef;
const CREATE_FLAGS = {
...WORKSPACE_FLAG,
...BODY_FLAG,
templateId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Ready template ID; may alternatively be supplied as templateID in --body",
"zh-CN": "已就绪的模版 ID也可通过 --body 中的 templateID 提供",
},
},
...INSTANCE_TIMEOUT_FLAG,
allowInternetAccess: {
type: "boolean",
valueHint: "<bool>",
description: { "en-US": "Allow public internet access", "zh-CN": "允许访问公网" },
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Instance metadata entry; repeat for multiple values",
"zh-CN": "实例 Metadata 键值;可重复传入",
},
},
env: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Instance environment variable; repeat for multiple values",
"zh-CN": "实例环境变量;可重复传入",
},
},
autoPause: {
type: "boolean",
valueHint: "<bool>",
description: {
"en-US": "Pause the instance when its timeout expires",
"zh-CN": "实例超时后自动暂停",
},
},
autoResume: {
type: "boolean",
valueHint: "<bool>",
description: {
"en-US": "Automatically resume a paused instance when connecting",
"zh-CN": "连接已暂停实例时自动恢复",
},
},
allowOut: {
type: "array",
valueHint: "<address>",
description: {
"en-US": "Outbound allow-list entry; repeat for multiple values",
"zh-CN": "出站白名单条目;可重复传入",
},
},
denyOut: {
type: "array",
valueHint: "<address>",
description: {
"en-US": "Outbound deny-list entry; repeat for multiple values",
"zh-CN": "出站黑名单条目;可重复传入",
},
},
maskRequestHost: {
type: "string",
valueHint: "<host>",
description: {
"en-US": "Override the outbound request Host header",
"zh-CN": "覆盖出站请求的 Host Header",
},
},
...SHOW_CREDENTIALS_FLAG,
} satisfies FlagsDef;
type CreateFlags = ParsedFlags<typeof CREATE_FLAGS>;
const LIST_FLAGS = {
...WORKSPACE_FLAG,
templateId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Filter by template ID", "zh-CN": "按模版 ID 筛选" },
},
sandboxId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Filter by sandbox ID", "zh-CN": "按 Sandbox ID 筛选" },
},
state: {
type: "string",
valueHint: "<state>",
description: {
"en-US": "Filter by state, for example running or paused",
"zh-CN": "按状态筛选,例如 running 或 paused",
},
},
limit: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Maximum results (1-50)", "zh-CN": "最大返回数量150" },
},
} satisfies FlagsDef;
const GET_FLAGS = {
...WORKSPACE_FLAG,
...SANDBOX_ID_FLAG,
...SHOW_CREDENTIALS_FLAG,
} satisfies FlagsDef;
const ACTION_FLAGS = { ...WORKSPACE_FLAG, ...SANDBOX_ID_FLAG } satisfies FlagsDef;
const CONNECTION_ACTION_FLAGS = {
...WORKSPACE_FLAG,
...SANDBOX_ID_FLAG,
...BODY_FLAG,
...INSTANCE_TIMEOUT_FLAG,
...SHOW_CREDENTIALS_FLAG,
} satisfies FlagsDef;
export async function buildSandboxCreateBody(flags: CreateFlags): Promise<JsonObject> {
const body = await readRequestBody(flags.body);
setDefined(body, "templateID", flags.templateId);
setDefined(body, "timeout", flags.instanceTimeout);
setDefined(body, "allow_internet_access", flags.allowInternetAccess);
setDefined(body, "autoPause", flags.autoPause);
setDefined(body, "autoResume", flags.autoResume);
mergeObjectField(body, "metadata", parseKeyValueEntries(flags.metadata, "--metadata"));
mergeObjectField(body, "envVars", parseKeyValueEntries(flags.env, "--env"));
const networkOverrides: JsonObject = {};
setDefined(networkOverrides, "allowOut", flags.allowOut);
setDefined(networkOverrides, "denyOut", flags.denyOut);
setDefined(networkOverrides, "maskRequestHost", flags.maskRequestHost);
mergeObjectField(body, "network", networkOverrides);
if (typeof body.templateID !== "string" || body.templateID.trim().length === 0) {
throw new BailianError(
"Sandbox create requires --template-id or templateID in --body.",
ExitCode.USAGE,
);
}
validateIntegerRange(body.timeout, "Sandbox timeout", 300, 604800);
return body;
}
async function buildConnectionBody(flags: {
body?: string;
instanceTimeout?: number;
}): Promise<JsonObject> {
const body = await readRequestBody(flags.body);
setDefined(body, "timeout", flags.instanceTimeout);
validateIntegerRange(body.timeout, "Sandbox timeout", 300, 604800);
return body;
}
function emitSandboxObject(
sandbox: SandboxInfo,
options: { format: "text" | "json"; quiet: boolean; showCredentials: boolean },
): void {
const output = options.showCredentials ? sandbox : redactConnectionCredentials(sandbox);
if (options.quiet) {
emitBare(displayValue(sandbox.sandboxID));
return;
}
emitResult(output, options.format);
}
export const sandboxCreate = defineCommand({
description: { "en-US": "Create a Sandbox instance", "zh-CN": "创建 Sandbox 实例" },
auth: "apiKey",
usageArgs: "(--template-id <id> | --body <json|@path>) [flags]",
flags: CREATE_FLAGS,
exampleArgs: [
"--template-id tpl-xxx --instance-timeout 3600",
"--body @sandbox.json --dry-run --output json",
"--template-id tpl-xxx --show-credentials --output json",
],
notes: SANDBOX_NOTES,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, SANDBOX_PATHS.sandboxes);
const body = await buildSandboxCreateBody(ctx.flags);
if (ctx.settings.dryRun) {
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
return;
}
const response = await ctx.client.requestJson<SandboxInfo>({
path: endpoint,
method: "POST",
body,
});
emitSandboxObject(response, {
format,
quiet: ctx.settings.quiet,
showCredentials: ctx.flags.showCredentials,
});
},
});
export const sandboxList = defineCommand({
description: { "en-US": "List Sandbox instances", "zh-CN": "列出 Sandbox 实例" },
auth: "apiKey",
usageArgs: "[--template-id <id>] [--sandbox-id <id>] [--state <state>] [--limit <n>]",
flags: LIST_FLAGS,
exampleArgs: ["", "--state running --limit 20", "--template-id tpl-xxx --output json"],
notes: SANDBOX_NOTES,
validate(flags) {
if (
flags.limit !== undefined &&
(!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 50)
) {
return "--limit must be an integer between 1 and 50.";
}
return undefined;
},
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const url = new URL(sandboxEndpoint(workspaceId, SANDBOX_PATHS.sandboxList));
if (ctx.flags.templateId) url.searchParams.set("templateID", ctx.flags.templateId);
if (ctx.flags.sandboxId) url.searchParams.set("sandboxID", ctx.flags.sandboxId);
if (ctx.flags.state) url.searchParams.set("state", ctx.flags.state);
if (ctx.flags.limit !== undefined) url.searchParams.set("limit", String(ctx.flags.limit));
const response = await ctx.client.requestJson<SandboxInfo[]>({
path: url.toString(),
method: "GET",
});
if (ctx.settings.quiet) {
for (const sandbox of response) emitBare(displayValue(sandbox.sandboxID));
return;
}
const format = detectOutputFormat(ctx.settings.output);
if (format === "json") {
emitResult(response, format);
return;
}
if (response.length === 0) {
emitBare("No Sandbox instances found.");
return;
}
const rows = response.map((sandbox) => [
displayValue(sandbox.sandboxID),
displayValue(sandbox.templateID),
displayValue(sandbox.state),
displayValue(sandbox.cpuCount),
displayValue(sandbox.memoryMB),
displayValue(sandbox.startedAt),
displayValue(sandbox.endAt),
]);
for (const line of formatTable(
["ID", "TEMPLATE", "STATE", "CPU", "MEMORY_MB", "STARTED_AT", "END_AT"],
rows,
)) {
emitBare(line);
}
},
});
export const sandboxGet = defineCommand({
description: { "en-US": "Get Sandbox instance details", "zh-CN": "获取 Sandbox 实例详情" },
auth: "apiKey",
usageArgs: "--sandbox-id <id> [--show-credentials]",
flags: GET_FLAGS,
exampleArgs: ["--sandbox-id sbx-xxx", "--sandbox-id sbx-xxx --show-credentials --output json"],
notes: SANDBOX_NOTES,
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxInstancePath(ctx.flags.sandboxId));
const response = await ctx.client.requestJson<SandboxInfo>({ path: endpoint, method: "GET" });
emitSandboxObject(response, {
format: detectOutputFormat(ctx.settings.output),
quiet: ctx.settings.quiet,
showCredentials: ctx.flags.showCredentials,
});
},
});
function connectionCommand(action: "connect" | "resume") {
const actionText = action === "connect" ? "Connect to" : "Resume";
const actionTextZh = action === "connect" ? "连接" : "恢复";
return defineCommand({
description: {
"en-US": `${actionText} a Sandbox instance and return connection information`,
"zh-CN": `${actionTextZh} Sandbox 实例并返回连接信息`,
},
auth: "apiKey",
usageArgs: "--sandbox-id <id> [--instance-timeout <seconds>] [--show-credentials]",
flags: CONNECTION_ACTION_FLAGS,
exampleArgs: [
`--sandbox-id sbx-xxx --instance-timeout 3600`,
`--sandbox-id sbx-xxx --show-credentials --output json`,
],
notes: SANDBOX_NOTES,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(
workspaceId,
sandboxInstanceActionPath(ctx.flags.sandboxId, action),
);
const body = await buildConnectionBody(ctx.flags);
if (ctx.settings.dryRun) {
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
return;
}
const response = await ctx.client.requestJson<SandboxInfo>({
path: endpoint,
method: "POST",
body: Object.keys(body).length > 0 ? body : undefined,
});
emitSandboxObject(response, {
format,
quiet: ctx.settings.quiet,
showCredentials: ctx.flags.showCredentials,
});
},
});
}
export const sandboxConnect = connectionCommand("connect");
export const sandboxResume = connectionCommand("resume");
export const sandboxPause = defineCommand({
description: { "en-US": "Pause a Sandbox instance", "zh-CN": "暂停 Sandbox 实例" },
auth: "apiKey",
usageArgs: "--sandbox-id <id>",
flags: ACTION_FLAGS,
exampleArgs: ["--sandbox-id sbx-xxx", "--sandbox-id sbx-xxx --dry-run --output json"],
notes: SANDBOX_NOTES,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(
workspaceId,
sandboxInstanceActionPath(ctx.flags.sandboxId, "pause"),
);
if (ctx.settings.dryRun) {
emitResult({ method: "POST", endpoint, request: null }, format);
return;
}
await ctx.client.request({ path: endpoint, method: "POST" });
if (ctx.settings.quiet) emitBare(ctx.flags.sandboxId);
else emitResult({ sandboxID: ctx.flags.sandboxId, paused: true }, format);
},
});
export const sandboxDelete = defineCommand({
description: { "en-US": "Release a Sandbox instance", "zh-CN": "释放 Sandbox 实例" },
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This permanently releases the Sandbox instance and cannot be undone.",
"zh-CN": "该操作会永久释放 Sandbox 实例,且无法撤销。",
},
},
usageArgs: "--sandbox-id <id>",
flags: ACTION_FLAGS,
exampleArgs: ["--sandbox-id sbx-xxx --dry-run", "--sandbox-id sbx-xxx --yes"],
notes: SANDBOX_NOTES,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxInstancePath(ctx.flags.sandboxId));
if (ctx.settings.dryRun) {
emitResult({ method: "DELETE", endpoint, request: null }, format);
return;
}
await ctx.client.request({ path: endpoint, method: "DELETE" });
if (ctx.settings.quiet) emitBare(ctx.flags.sandboxId);
else emitResult({ sandboxID: ctx.flags.sandboxId, deleted: true }, format);
},
});
@@ -0,0 +1,231 @@
import { readFile } from "node:fs/promises";
import { BailianError, ExitCode, type FlagsDef, type LocalizedText } from "bailian-cli-core";
export const WORKSPACE_FLAG = {
workspaceId: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID)",
"zh-CN": "Sandbox Endpoint 使用的 Workspace ID也可设置 BAILIAN_WORKSPACE_ID",
},
},
} satisfies FlagsDef;
export const BODY_FLAG = {
body: {
type: "string",
valueHint: "<json|@path>",
description: {
"en-US": "JSON request body, inline or loaded from an @file path; explicit flags override it",
"zh-CN": "JSON 请求体,可内联或从 @文件路径读取;显式 Flag 优先",
},
},
} satisfies FlagsDef;
export const INSTANCE_TIMEOUT_FLAG = {
instanceTimeout: {
type: "number",
valueHint: "<seconds>",
description: {
"en-US": "Sandbox lifetime after this operation (300-604800 seconds)",
"zh-CN": "本次操作后的 Sandbox 存活时间300604800 秒)",
},
},
} satisfies FlagsDef;
export const SHOW_CREDENTIALS_FLAG = {
showCredentials: {
type: "switch",
description: {
"en-US": "Print envd and traffic access tokens instead of redacting them",
"zh-CN": "输出 envd 与流量访问 Token 原值,不进行脱敏",
},
},
} satisfies FlagsDef;
export const POLL_INTERVAL_FLAG = {
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: {
"en-US": "Template build polling interval (default: 5 seconds)",
"zh-CN": "模版构建轮询间隔默认5 秒)",
},
},
} satisfies FlagsDef;
export const SANDBOX_NOTES: LocalizedText[] = [
{
"en-US": "Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.",
"zh-CN": "鉴权:使用百炼 API Key 作为 Authorization Bearer Token不会发送 E2B Key。",
},
{
"en-US":
"The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.",
"zh-CN": "Workspace 依次从 --workspace-id、BAILIAN_WORKSPACE_ID、配置项 workspace_id 解析。",
},
{
"en-US":
"Sandbox is currently available in cn-beijing only and requires prior SLR authorization.",
"zh-CN": "Sandbox 当前仅支持 cn-beijing首次使用前需完成 SLR 授权。",
},
{
"en-US":
"Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.",
"zh-CN":
"全局 --timeout 限制 HTTP 请求和模版构建总轮询时间;--instance-timeout 映射到 Sandbox API 的实例存活时间字段。",
},
];
export type JsonObject = Record<string, unknown>;
export function resolveWorkspaceId(ctx: {
flags: { workspaceId?: string };
settings: { workspaceId?: string };
identity: { binName: string };
}): string {
const workspaceId = ctx.flags.workspaceId || ctx.settings.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
ExitCode.USAGE,
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
);
}
return workspaceId;
}
function asJsonObject(value: unknown, source: string): JsonObject {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
throw new BailianError(`${source} must contain a JSON object.`, ExitCode.USAGE);
}
return value as JsonObject;
}
export async function readRequestBody(argument?: string): Promise<JsonObject> {
if (!argument) return {};
const source = argument.startsWith("@") ? argument.slice(1) : undefined;
if (source !== undefined && source.length === 0) {
throw new BailianError("--body @path must include a file path.", ExitCode.USAGE);
}
const raw = source === undefined ? argument : await readFile(source, "utf8");
try {
return asJsonObject(JSON.parse(raw) as unknown, source ? `Body file ${source}` : "--body");
} catch (error) {
if (error instanceof BailianError) throw error;
if (error instanceof SyntaxError) {
throw new BailianError(
source ? `Body file ${source} contains invalid JSON.` : "--body contains invalid JSON.",
ExitCode.USAGE,
);
}
throw error;
}
}
export function setDefined(body: JsonObject, key: string, value: unknown): void {
if (value !== undefined) body[key] = value;
}
export function parseKeyValueEntries(entries: string[] | undefined, flag: string): JsonObject {
const values: JsonObject = {};
for (const entry of entries ?? []) {
const separator = entry.indexOf("=");
if (separator <= 0) {
throw new BailianError(`${flag} values must use KEY=VALUE format.`, ExitCode.USAGE);
}
const key = entry.slice(0, separator).trim();
if (!key) {
throw new BailianError(`${flag} values must include a non-empty key.`, ExitCode.USAGE);
}
values[key] = entry.slice(separator + 1);
}
return values;
}
export function mergeObjectField(body: JsonObject, key: string, overrides: JsonObject): void {
if (Object.keys(overrides).length === 0) return;
const current = body[key];
const base =
current !== null && typeof current === "object" && !Array.isArray(current)
? (current as JsonObject)
: {};
body[key] = { ...base, ...overrides };
}
export function validateIntegerRange(
value: unknown,
label: string,
minimum: number,
maximum: number,
): void {
if (
value !== undefined &&
(typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum)
) {
throw new BailianError(
`${label} must be an integer between ${minimum} and ${maximum}.`,
ExitCode.USAGE,
);
}
}
export function validatePositiveInteger(value: unknown, label: string): void {
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
throw new BailianError(`${label} must be a positive integer.`, ExitCode.USAGE);
}
}
const CONNECTION_CREDENTIAL_KEYS = new Set(["envdAccessToken", "trafficAccessToken"]);
const SENSITIVE_KEY = /(?:token|secret|password|credential|api[_-]?key)/i;
function redactValue(value: unknown, redactEnvironmentMaps: boolean): unknown {
if (Array.isArray(value)) return value.map((entry) => redactValue(entry, redactEnvironmentMaps));
if (value === null || typeof value !== "object") return value;
const output: JsonObject = {};
for (const [key, entry] of Object.entries(value as JsonObject)) {
if (
CONNECTION_CREDENTIAL_KEYS.has(key) ||
SENSITIVE_KEY.test(key) ||
(redactEnvironmentMaps && (key === "envVars" || key === "envConfig"))
) {
if (entry !== undefined && entry !== null) {
output[key] =
(key === "envVars" || key === "envConfig") &&
typeof entry === "object" &&
!Array.isArray(entry)
? Object.fromEntries(
Object.keys(entry as JsonObject).map((environmentKey) => [
environmentKey,
"[REDACTED]",
]),
)
: "[REDACTED]";
} else {
output[key] = entry;
}
continue;
}
output[key] = redactValue(entry, redactEnvironmentMaps);
}
return output;
}
export function redactConnectionCredentials<T>(value: T): T {
return redactValue(value, false) as T;
}
export function redactRequestSecrets<T>(value: T): T {
return redactValue(value, true) as T;
}
export function displayValue(value: unknown): string {
if (value === undefined || value === null || value === "") return "-";
if (Array.isArray(value)) return value.join(",");
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return `${value}`;
}
return JSON.stringify(value) ?? "-";
}
@@ -0,0 +1,586 @@
import {
BailianError,
detectOutputFormat,
defineCommand,
ExitCode,
sandboxEndpoint,
SANDBOX_PATHS,
sandboxTemplateBuildStatusPath,
sandboxTemplatePath,
type Client,
type FlagsDef,
type ParsedFlags,
type Settings,
} from "bailian-cli-core";
import { createSpinner, emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import {
BODY_FLAG,
displayValue,
mergeObjectField,
parseKeyValueEntries,
POLL_INTERVAL_FLAG,
readRequestBody,
redactRequestSecrets,
resolveWorkspaceId,
SANDBOX_NOTES,
setDefined,
validateIntegerRange,
validatePositiveInteger,
WORKSPACE_FLAG,
type JsonObject,
} from "./shared.ts";
const TEMPLATE_ASYNC_FLAG = {
async: {
type: "switch",
description: {
"en-US": "Return the submitted templateID/buildID immediately without polling",
"zh-CN": "提交后立即返回 templateID/buildID不轮询构建状态",
},
},
} satisfies FlagsDef;
interface TemplateInfo extends JsonObject {
templateID?: string;
templateName?: string;
cpuCount?: number;
memoryMB?: number;
spawnCount?: number;
buildCount?: number;
updatedAt?: string;
buildID?: string;
buildStatus?: string;
}
interface TemplateBuildStatus extends JsonObject {
templateID?: string;
buildID?: string;
status?: "building" | "ready" | "error";
reason?: { code?: number | string; message?: string };
}
const TEMPLATE_ID_FLAG = {
templateId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Sandbox template ID", "zh-CN": "Sandbox 模版 ID" },
},
} satisfies FlagsDef;
const TEMPLATE_MUTATION_FIELDS = {
name: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Template name", "zh-CN": "模版名称" },
},
cpuCount: {
type: "number",
valueHint: "<cores>",
description: { "en-US": "vCPU count", "zh-CN": "vCPU 核数" },
},
memoryMb: {
type: "number",
valueHint: "<mb>",
description: { "en-US": "Memory in MB", "zh-CN": "内存大小MB" },
},
fromImage: {
type: "string",
valueHint: "<image>",
description: { "en-US": "Base image identifier", "zh-CN": "基础镜像标识" },
},
imageName: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Base image display name", "zh-CN": "基础镜像展示名称" },
},
env: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Template environment variable; repeat for multiple values",
"zh-CN": "模版环境变量;可重复传入",
},
},
allowOut: {
type: "array",
valueHint: "<address>",
description: {
"en-US": "Outbound allow-list entry; repeat for multiple values",
"zh-CN": "出站白名单条目;可重复传入",
},
},
denyOut: {
type: "array",
valueHint: "<address>",
description: {
"en-US": "Outbound deny-list entry; repeat for multiple values",
"zh-CN": "出站黑名单条目;可重复传入",
},
},
autoPauseTime: {
type: "number",
valueHint: "<seconds>",
description: {
"en-US": "Automatically pause after 300-604800 seconds",
"zh-CN": "在 300604800 秒后自动暂停",
},
},
maxRunningTime: {
type: "number",
valueHint: "<seconds>",
description: {
"en-US": "Maximum running lifetime in seconds (300-604800)",
"zh-CN": "最大运行时间300604800",
},
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Template description", "zh-CN": "模版描述" },
},
} satisfies FlagsDef;
const CREATE_FLAGS = {
...WORKSPACE_FLAG,
...BODY_FLAG,
...TEMPLATE_MUTATION_FIELDS,
tag: {
type: "array",
valueHint: "<tag>",
description: { "en-US": "E2B tag; repeat for multiple values", "zh-CN": "E2B Tag可重复传入" },
},
alias: {
type: "string",
valueHint: "<alias>",
description: { "en-US": "E2B template alias", "zh-CN": "E2B 模版别名" },
},
...TEMPLATE_ASYNC_FLAG,
...POLL_INTERVAL_FLAG,
} satisfies FlagsDef;
const UPDATE_FLAGS = {
...WORKSPACE_FLAG,
...TEMPLATE_ID_FLAG,
...BODY_FLAG,
...TEMPLATE_MUTATION_FIELDS,
...TEMPLATE_ASYNC_FLAG,
...POLL_INTERVAL_FLAG,
} satisfies FlagsDef;
type CreateFlags = ParsedFlags<typeof CREATE_FLAGS>;
type UpdateFlags = ParsedFlags<typeof UPDATE_FLAGS>;
const LIST_FLAGS = {
...WORKSPACE_FLAG,
limit: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Maximum results (1-100)", "zh-CN": "最大返回数量1100" },
},
cursor: {
type: "string",
valueHint: "<cursor>",
description: { "en-US": "Server-side pagination cursor", "zh-CN": "服务端分页 Cursor" },
},
} satisfies FlagsDef;
const GET_FLAGS = { ...WORKSPACE_FLAG, ...TEMPLATE_ID_FLAG } satisfies FlagsDef;
const BUILD_STATUS_FLAGS = {
...WORKSPACE_FLAG,
...TEMPLATE_ID_FLAG,
buildId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Template build ID", "zh-CN": "模版构建 ID" },
},
} satisfies FlagsDef;
function applyTemplateMutationFlags(body: JsonObject, flags: CreateFlags | UpdateFlags): void {
setDefined(body, "name", flags.name);
setDefined(body, "cpuCount", flags.cpuCount);
setDefined(body, "memoryMB", flags.memoryMb);
setDefined(body, "fromImage", flags.fromImage);
setDefined(body, "imageName", flags.imageName);
setDefined(body, "autoPauseTime", flags.autoPauseTime);
setDefined(body, "maxRunningTimeout", flags.maxRunningTime);
setDefined(body, "description", flags.description);
mergeObjectField(body, "envConfig", parseKeyValueEntries(flags.env, "--env"));
const networkOverrides: JsonObject = {};
setDefined(networkOverrides, "allowOut", flags.allowOut);
setDefined(networkOverrides, "denyOut", flags.denyOut);
mergeObjectField(body, "networkConfig", networkOverrides);
}
function validateTemplateMutationBody(body: JsonObject, create: boolean): void {
if (create && (typeof body.name !== "string" || body.name.trim().length === 0)) {
throw new BailianError("Template create requires --name or name in --body.", ExitCode.USAGE);
}
const hasCpu = body.cpuCount !== undefined;
const hasMemory = body.memoryMB !== undefined;
if (create && (!hasCpu || !hasMemory)) {
throw new BailianError(
"Template create requires both --cpu-count and --memory-mb, or their --body fields.",
ExitCode.USAGE,
);
}
if (hasCpu !== hasMemory) {
throw new BailianError("cpuCount and memoryMB must be provided together.", ExitCode.USAGE);
}
if (hasCpu) validatePositiveInteger(body.cpuCount, "cpuCount");
if (hasMemory) validatePositiveInteger(body.memoryMB, "memoryMB");
validateIntegerRange(body.autoPauseTime, "autoPauseTime", 300, 604800);
validateIntegerRange(body.maxRunningTimeout, "maxRunningTimeout", 300, 604800);
if (!create && Object.keys(body).length === 0) {
throw new BailianError("Template update requires at least one field.", ExitCode.USAGE);
}
}
export async function buildTemplateCreateBody(flags: CreateFlags): Promise<JsonObject> {
const body = await readRequestBody(flags.body);
applyTemplateMutationFlags(body, flags);
setDefined(body, "tags", flags.tag);
setDefined(body, "alias", flags.alias);
validateTemplateMutationBody(body, true);
return body;
}
export async function buildTemplateUpdateBody(flags: UpdateFlags): Promise<JsonObject> {
const body = await readRequestBody(flags.body);
applyTemplateMutationFlags(body, flags);
validateTemplateMutationBody(body, false);
return body;
}
export async function waitForTemplateBuild(
client: Client,
settings: Settings,
endpoint: string,
intervalSec: number,
): Promise<TemplateBuildStatus> {
const deadline = Date.now() + settings.timeout * 1000;
const spinner = createSpinner("Waiting for template build...");
if (!settings.quiet) spinner.start();
try {
while (true) {
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) break;
const status = await client.requestJson<TemplateBuildStatus>({
path: endpoint,
method: "GET",
timeout: remainingMs / 1000,
});
if (!settings.quiet) spinner.update(`Build status: ${status.status ?? "unknown"}`);
if (status.status === "ready") {
spinner.stop("Template build ready.");
return status;
}
if (status.status === "error") {
spinner.stop("Template build failed.");
const reason =
status.reason?.message ??
(status.reason?.code !== undefined ? `${status.reason.code}` : "Template build failed.");
throw new BailianError(reason, ExitCode.GENERAL);
}
const sleepMs = Math.min(intervalSec * 1000, deadline - Date.now());
if (sleepMs > 0) {
await new Promise<void>((resolve) => setTimeout(resolve, sleepMs));
}
}
} finally {
spinner.stop();
}
throw new BailianError(
"Template build polling timed out.",
ExitCode.TIMEOUT,
"Increase --timeout or query the build status command with the returned IDs.",
);
}
function validatePollInterval(flags: { pollInterval?: number }): string | undefined {
if (
flags.pollInterval !== undefined &&
(!Number.isInteger(flags.pollInterval) || flags.pollInterval <= 0)
) {
return "--poll-interval must be a positive integer.";
}
return undefined;
}
async function emitTemplateMutationResult(options: {
response: TemplateInfo;
workspaceId: string;
client: Client;
settings: Settings;
async: boolean;
pollInterval?: number;
}): Promise<void> {
const format = detectOutputFormat(options.settings.output);
if (!options.response.templateID || !options.response.buildID) {
throw new BailianError(
"Template mutation response is missing templateID or buildID.",
ExitCode.GENERAL,
);
}
if (options.async) {
if (options.settings.quiet) {
emitBare(`${options.response.templateID}\t${options.response.buildID}`);
} else {
emitResult(options.response, format);
}
return;
}
const buildEndpoint = sandboxEndpoint(
options.workspaceId,
sandboxTemplateBuildStatusPath(options.response.templateID, options.response.buildID),
);
const build = await waitForTemplateBuild(
options.client,
options.settings,
buildEndpoint,
options.pollInterval ?? 5,
);
if (options.settings.quiet) emitBare(displayValue(options.response.templateID));
else emitResult({ template: options.response, build }, format);
}
export const sandboxTemplateCreate = defineCommand({
description: { "en-US": "Create a Sandbox template", "zh-CN": "创建 Sandbox 模版" },
auth: "apiKey",
usageArgs: "(--name <name> --cpu-count <cores> --memory-mb <mb> | --body <json|@path>) [flags]",
flags: CREATE_FLAGS,
exampleArgs: [
"--name python --cpu-count 1 --memory-mb 2048",
"--body @template.json --async --output json",
"--name browser --cpu-count 4 --memory-mb 8192 --dry-run --output json",
],
notes: [
...SANDBOX_NOTES,
{
"en-US": "File mounts and other complete nested structures can be supplied through --body.",
"zh-CN": "文件挂载等完整嵌套结构可通过 --body 提供。",
},
{
"en-US":
"By default the command waits for build status ready; --async returns the submitted build immediately.",
"zh-CN": "默认等待构建状态变为 ready--async 会立即返回已提交的构建信息。",
},
],
validate: validatePollInterval,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, SANDBOX_PATHS.templateCreate);
const body = await buildTemplateCreateBody(ctx.flags);
if (ctx.settings.dryRun) {
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
return;
}
const response = await ctx.client.requestJson<TemplateInfo>({
path: endpoint,
method: "POST",
body,
});
await emitTemplateMutationResult({
response,
workspaceId,
client: ctx.client,
settings: ctx.settings,
async: ctx.flags.async,
pollInterval: ctx.flags.pollInterval,
});
},
});
export const sandboxTemplateList = defineCommand({
description: { "en-US": "List Sandbox templates", "zh-CN": "列出 Sandbox 模版" },
auth: "apiKey",
usageArgs: "[--limit <n>] [--cursor <cursor>]",
flags: LIST_FLAGS,
exampleArgs: ["", "--limit 100 --output json"],
notes: [
...SANDBOX_NOTES,
{
"en-US":
"The API response does not expose a next cursor, so automatic --all pagination is unavailable.",
"zh-CN": "API 响应未提供下一页 Cursor因此不支持自动 --all 翻页。",
},
],
validate(flags) {
if (
flags.limit !== undefined &&
(!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 100)
) {
return "--limit must be an integer between 1 and 100.";
}
return undefined;
},
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const url = new URL(sandboxEndpoint(workspaceId, SANDBOX_PATHS.templateList));
if (ctx.flags.limit !== undefined) url.searchParams.set("limit", String(ctx.flags.limit));
if (ctx.flags.cursor) url.searchParams.set("cursor", ctx.flags.cursor);
const response = await ctx.client.requestJson<TemplateInfo[]>({
path: url.toString(),
method: "GET",
});
if (ctx.settings.quiet) {
for (const template of response) emitBare(displayValue(template.templateID));
return;
}
const format = detectOutputFormat(ctx.settings.output);
if (format === "json") {
emitResult(response, format);
return;
}
if (response.length === 0) {
emitBare("No Sandbox templates found.");
return;
}
const rows = response.map((template) => [
displayValue(template.templateID),
displayValue(template.templateName),
displayValue(template.cpuCount),
displayValue(template.memoryMB),
displayValue(template.spawnCount),
displayValue(template.buildCount),
displayValue(template.updatedAt),
]);
for (const line of formatTable(
["ID", "NAME", "CPU", "MEMORY_MB", "INSTANCES", "BUILDS", "UPDATED_AT"],
rows,
)) {
emitBare(line);
}
},
});
export const sandboxTemplateGet = defineCommand({
description: { "en-US": "Get Sandbox template details", "zh-CN": "获取 Sandbox 模版详情" },
auth: "apiKey",
usageArgs: "--template-id <id>",
flags: GET_FLAGS,
exampleArgs: ["--template-id tpl-xxx", "--template-id tpl-xxx --output json"],
notes: SANDBOX_NOTES,
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxTemplatePath(ctx.flags.templateId));
const response = await ctx.client.requestJson<TemplateInfo>({ path: endpoint, method: "GET" });
if (ctx.settings.quiet) emitBare(displayValue(response.templateID));
else emitResult(response, detectOutputFormat(ctx.settings.output));
},
});
export const sandboxTemplateUpdate = defineCommand({
description: { "en-US": "Update a Sandbox template", "zh-CN": "更新 Sandbox 模版" },
auth: "apiKey",
usageArgs: "--template-id <id> (--body <json|@path> | [fields])",
flags: UPDATE_FLAGS,
exampleArgs: [
"--template-id tpl-xxx --cpu-count 4 --memory-mb 8192",
"--template-id tpl-xxx --body @template-update.json --async --output json",
"--template-id tpl-xxx --description updated --dry-run --output json",
],
notes: [
...SANDBOX_NOTES,
{
"en-US": "Supplying envConfig or --env replaces the template's complete environment map.",
"zh-CN": "传入 envConfig 或 --env 会整体替换模版的环境变量 Map。",
},
{
"en-US":
"By default the command waits for build status ready; --async returns the submitted build immediately.",
"zh-CN": "默认等待构建状态变为 ready--async 会立即返回已提交的构建信息。",
},
],
validate: validatePollInterval,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxTemplatePath(ctx.flags.templateId));
const body = await buildTemplateUpdateBody(ctx.flags);
if (ctx.settings.dryRun) {
emitResult({ method: "PUT", endpoint, request: redactRequestSecrets(body) }, format);
return;
}
const response = await ctx.client.requestJson<TemplateInfo>({
path: endpoint,
method: "PUT",
body,
});
await emitTemplateMutationResult({
response,
workspaceId,
client: ctx.client,
settings: ctx.settings,
async: ctx.flags.async,
pollInterval: ctx.flags.pollInterval,
});
},
});
export const sandboxTemplateBuildStatus = defineCommand({
description: {
"en-US": "Get Sandbox template build status",
"zh-CN": "获取 Sandbox 模版构建状态",
},
auth: "apiKey",
usageArgs: "--template-id <id> --build-id <id>",
flags: BUILD_STATUS_FLAGS,
exampleArgs: [
"--template-id tpl-xxx --build-id build-xxx",
"--template-id tpl-xxx --build-id build-xxx --output json",
],
notes: SANDBOX_NOTES,
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(
workspaceId,
sandboxTemplateBuildStatusPath(ctx.flags.templateId, ctx.flags.buildId),
);
const response = await ctx.client.requestJson<TemplateBuildStatus>({
path: endpoint,
method: "GET",
});
if (ctx.settings.quiet) emitBare(displayValue(response.status));
else emitResult(response, detectOutputFormat(ctx.settings.output));
},
});
export const sandboxTemplateDelete = defineCommand({
description: { "en-US": "Delete a Sandbox template", "zh-CN": "删除 Sandbox 模版" },
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the Sandbox template and cannot be undone.",
"zh-CN": "该操作会永久删除 Sandbox 模版,且无法撤销。",
},
},
usageArgs: "--template-id <id>",
flags: GET_FLAGS,
exampleArgs: ["--template-id tpl-xxx --dry-run", "--template-id tpl-xxx --yes"],
notes: [
...SANDBOX_NOTES,
{
"en-US":
"The server rejects deletion while running or paused instances still use the template.",
"zh-CN": "仍有运行中或已暂停实例使用该模版时,服务端会拒绝删除。",
},
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxTemplatePath(ctx.flags.templateId));
if (ctx.settings.dryRun) {
emitResult({ method: "DELETE", endpoint, request: null }, format);
return;
}
await ctx.client.request({ path: endpoint, method: "DELETE" });
if (ctx.settings.quiet) emitBare(ctx.flags.templateId);
else emitResult({ templateID: ctx.flags.templateId, deleted: true }, format);
},
});
+17
View File
@@ -209,3 +209,20 @@ export { default as skillUpdate } from "./commands/skill/update.ts";
export { default as skillRemove } from "./commands/skill/remove.ts";
export { default as skillList } from "./commands/skill/list.ts";
export { default as skillInit } from "./commands/skill/init.ts";
export {
sandboxConnect,
sandboxCreate,
sandboxDelete,
sandboxGet,
sandboxList,
sandboxPause,
sandboxResume,
} from "./commands/sandbox/instance.ts";
export {
sandboxTemplateBuildStatus,
sandboxTemplateCreate,
sandboxTemplateDelete,
sandboxTemplateGet,
sandboxTemplateList,
sandboxTemplateUpdate,
} from "./commands/sandbox/template.ts";
@@ -0,0 +1,195 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "./helpers.ts";
import { SANDBOX_ROUTES } from "./topic-routes.ts";
const AUTH_ARGS = ["--api-key", "sk-sandbox-e2e", "--workspace-id", "ws-e2e"];
describe("e2e: Sandbox command discovery", () => {
test.each([
["sandbox", "create"],
["sandbox", "list"],
["sandbox", "get"],
["sandbox", "connect"],
["sandbox", "pause"],
["sandbox", "resume"],
["sandbox", "delete"],
["sandbox", "template", "create"],
["sandbox", "template", "list"],
["sandbox", "template", "get"],
["sandbox", "template", "update"],
["sandbox", "template", "build-status"],
["sandbox", "template", "delete"],
])("%s help resolves", async (...commandPath) => {
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [...commandPath, "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain(`Usage: bl ${commandPath.join(" ")}`);
expect(stderr).toContain("Authentication: API Key");
});
test("instance timeout is separate from the global request/poll timeout", async () => {
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [
"sandbox",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--instance-timeout <seconds>/);
expect(stderr).toMatch(/--timeout <seconds>/);
expect(stderr).toMatch(/no E2B key is sent/i);
});
test("template create exposes max-running-time and default polling controls", async () => {
const { stderr, exitCode } = await runCommandHelp(SANDBOX_ROUTES, [
"sandbox",
"template",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--max-running-time <seconds>/);
expect(stderr).toMatch(/--async/);
expect(stderr).toMatch(/--poll-interval <seconds>/);
expect(stderr).toMatch(/waits for build status ready/i);
});
});
describe("e2e: Sandbox offline validation and dry-run", () => {
test("create requires a template in flags or body", async () => {
const { stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
"sandbox",
"create",
...AUTH_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode).toBe(2);
expect(JSON.parse(stderr)).toMatchObject({
error: { code: 2, message: expect.stringMatching(/template-id|templateID/) },
});
});
test("create body flags win and request environment values are redacted", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
"sandbox",
"create",
"--body",
'{"templateID":"template-body","timeout":600,"envVars":{"BODY":"secret"}}',
"--template-id",
"template-flag",
"--instance-timeout",
"900",
"--env",
"FLAG=secret",
...AUTH_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
expect(parseStdoutJson(stdout)).toMatchObject({
method: "POST",
endpoint: "https://ws-e2e.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes",
request: {
templateID: "template-flag",
timeout: 900,
envVars: { BODY: "[REDACTED]", FLAG: "[REDACTED]" },
},
});
});
test.each([
{
name: "connect instance",
args: ["sandbox", "connect", "--sandbox-id", "sandbox-test", "--instance-timeout", "900"],
method: "POST",
suffix: "/sandboxes/sandbox-test/connect",
},
{
name: "pause instance",
args: ["sandbox", "pause", "--sandbox-id", "sandbox-test"],
method: "POST",
suffix: "/sandboxes/sandbox-test/pause",
},
{
name: "resume instance",
args: ["sandbox", "resume", "--sandbox-id", "sandbox-test"],
method: "POST",
suffix: "/sandboxes/sandbox-test/resume",
},
{
name: "delete instance",
args: ["sandbox", "delete", "--sandbox-id", "sandbox-test"],
method: "DELETE",
suffix: "/sandboxes/sandbox-test",
},
{
name: "create template",
args: [
"sandbox",
"template",
"create",
"--name",
"python",
"--cpu-count",
"1",
"--memory-mb",
"2048",
],
method: "POST",
suffix: "/v3/templates",
},
{
name: "update template",
args: [
"sandbox",
"template",
"update",
"--template-id",
"template-test",
"--description",
"updated",
],
method: "PUT",
suffix: "/templates/template-test",
},
{
name: "delete template",
args: ["sandbox", "template", "delete", "--template-id", "template-test"],
method: "DELETE",
suffix: "/templates/template-test",
},
])("$name dry-run stays offline", async ({ args, method, suffix }) => {
const { stdout, stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
...args,
...AUTH_ARGS,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const result = parseStdoutJson<{ method: string; endpoint: string }>(stdout);
expect(result.method).toBe(method);
expect(result.endpoint).toBe(
`https://ws-e2e.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox${suffix}`,
);
});
});
describe("e2e: Sandbox high-risk confirmation", () => {
test.each([
["sandbox", "delete", "--sandbox-id", "sandbox-test"],
["sandbox", "template", "delete", "--template-id", "template-test"],
])("%s requires --yes before a remote delete", async (...commandArgs) => {
const { stderr, exitCode } = await runCommandE2e(SANDBOX_ROUTES, [
...commandArgs,
...AUTH_ARGS,
"--output",
"json",
]);
expect(exitCode).toBe(7);
expect(JSON.parse(stderr)).toMatchObject({
error: { code: 7, type: "requires_confirmation" },
});
});
});
@@ -182,6 +182,22 @@ export const SKILL_ROUTES: E2eRouteExports = {
"skill init": "skillInit",
};
export const SANDBOX_ROUTES: E2eRouteExports = {
"sandbox create": "sandboxCreate",
"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",
};
export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"managed-agent init": "managedAgentInit",
"managed-agent validate": "managedAgentValidate",
+500
View File
@@ -0,0 +1,500 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { BailianError, ExitCode, type AnyCommand, type Settings } from "bailian-cli-core";
import { afterEach, describe, expect, test, vi } from "vite-plus/test";
import {
buildSandboxCreateBody,
sandboxConnect,
sandboxCreate,
sandboxDelete,
sandboxGet,
sandboxList,
sandboxPause,
sandboxResume,
} from "../src/commands/sandbox/instance.ts";
import {
buildTemplateCreateBody,
buildTemplateUpdateBody,
sandboxTemplateBuildStatus,
sandboxTemplateCreate,
sandboxTemplateDelete,
sandboxTemplateGet,
sandboxTemplateList,
sandboxTemplateUpdate,
waitForTemplateBuild,
} from "../src/commands/sandbox/template.ts";
import {
readRequestBody,
redactConnectionCredentials,
redactRequestSecrets,
} from "../src/commands/sandbox/shared.ts";
afterEach(() => {
vi.restoreAllMocks();
});
const SETTINGS: Settings = {
output: "json",
outputExplicit: true,
timeout: 30,
verbose: false,
quiet: true,
dryRun: false,
telemetry: false,
};
interface RecordedRequest {
path: string;
method?: string;
body?: unknown;
timeout?: number;
}
async function runCommand(
command: AnyCommand,
flags: Record<string, unknown>,
response: unknown,
): Promise<RecordedRequest> {
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const requestJson = vi.fn(async (_request: RecordedRequest) => response);
const request = vi.fn(async (requestOptions: RecordedRequest) => {
void requestOptions;
return new Response(null, { status: 204 });
});
await command.run({
identity: {
binName: "bl",
version: "test",
npmPackage: "bailian-cli",
clientName: "bailian-cli",
},
settings: SETTINGS,
flags,
client: { requestJson, request },
} as never);
const recorded = requestJson.mock.calls[0]?.[0] ?? request.mock.calls[0]?.[0];
if (!recorded) throw new Error("Expected the command to issue one request.");
expect(requestJson.mock.calls.length + request.mock.calls.length).toBe(1);
return recorded;
}
describe("Sandbox request input", () => {
test("--body accepts inline and @file JSON objects", async () => {
expect(await readRequestBody('{"name":"inline"}')).toEqual({ name: "inline" });
const directory = mkdtempSync(join(tmpdir(), "bl-sandbox-body-"));
const bodyPath = join(directory, "body.json");
writeFileSync(bodyPath, '{"name":"file"}', "utf8");
try {
expect(await readRequestBody(`@${bodyPath}`)).toEqual({ name: "file" });
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("instance flags override body scalars and merge key-value maps", async () => {
const body = await buildSandboxCreateBody({
body: JSON.stringify({
templateID: "template-body",
timeout: 600,
metadata: { body: "kept", override: "body" },
envVars: { BODY: "kept", OVERRIDE: "body" },
network: { allowOut: ["body.example"], maskRequestHost: "body.example" },
}),
templateId: "template-flag",
instanceTimeout: 900,
metadata: ["override=flag"],
env: ["OVERRIDE=flag"],
allowOut: ["flag.example"],
maskRequestHost: "flag.example",
showCredentials: false,
});
expect(body).toEqual({
templateID: "template-flag",
timeout: 900,
metadata: { body: "kept", override: "flag" },
envVars: { BODY: "kept", OVERRIDE: "flag" },
network: { allowOut: ["flag.example"], maskRequestHost: "flag.example" },
});
});
test("validates instance timeout and paired template CPU/memory fields", async () => {
await expect(
buildSandboxCreateBody({
templateId: "template-test",
instanceTimeout: 299,
showCredentials: false,
}),
).rejects.toMatchObject({ exitCode: ExitCode.USAGE });
await expect(
buildTemplateUpdateBody({
templateId: "template-test",
cpuCount: 2,
async: false,
}),
).rejects.toMatchObject({ exitCode: ExitCode.USAGE });
});
test("template create flags map max-running-time and nested overrides", async () => {
const body = await buildTemplateCreateBody({
body: JSON.stringify({ networkConfig: { allowOut: ["body.example"] } }),
name: "python",
cpuCount: 2,
memoryMb: 4096,
maxRunningTime: 3600,
allowOut: ["flag.example"],
tag: ["latest"],
async: false,
});
expect(body).toEqual({
name: "python",
cpuCount: 2,
memoryMB: 4096,
maxRunningTimeout: 3600,
tags: ["latest"],
networkConfig: { allowOut: ["flag.example"] },
});
});
test("redacts returned connection credentials and request environment secrets recursively", () => {
expect(
redactConnectionCredentials({
sandboxID: "sandbox-test",
envdAccessToken: "envd-secret",
connection: { trafficAccessToken: "traffic-secret", hostname: "example.test" },
}),
).toEqual({
sandboxID: "sandbox-test",
envdAccessToken: "[REDACTED]",
connection: { trafficAccessToken: "[REDACTED]", hostname: "example.test" },
});
expect(
redactRequestSecrets({ envVars: { PUBLIC_NAME: "secret" }, nested: { apiKey: "secret" } }),
).toEqual({
envVars: { PUBLIC_NAME: "[REDACTED]" },
nested: { apiKey: "[REDACTED]" },
});
});
});
describe("Sandbox command transport", () => {
test.each([
{
name: "create instance",
command: sandboxCreate,
flags: { workspaceId: "ws-test", templateId: "template-test", showCredentials: false },
response: { sandboxID: "sandbox-test" },
request: {
method: "POST",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes",
body: { templateID: "template-test" },
},
},
{
name: "list instances",
command: sandboxList,
flags: { workspaceId: "ws-test", templateId: "template-test", state: "running", limit: 10 },
response: [],
request: {
method: "GET",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/v2/sandboxes?templateID=template-test&state=running&limit=10",
},
},
{
name: "get instance",
command: sandboxGet,
flags: { workspaceId: "ws-test", sandboxId: "sandbox/a", showCredentials: false },
response: { sandboxID: "sandbox/a" },
request: {
method: "GET",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes/sandbox%2Fa",
},
},
{
name: "connect instance",
command: sandboxConnect,
flags: {
workspaceId: "ws-test",
sandboxId: "sandbox-test",
instanceTimeout: 900,
showCredentials: false,
},
response: { sandboxID: "sandbox-test" },
request: {
method: "POST",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes/sandbox-test/connect",
body: { timeout: 900 },
},
},
{
name: "pause instance",
command: sandboxPause,
flags: { workspaceId: "ws-test", sandboxId: "sandbox-test" },
response: undefined,
request: {
method: "POST",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes/sandbox-test/pause",
},
},
{
name: "resume instance",
command: sandboxResume,
flags: { workspaceId: "ws-test", sandboxId: "sandbox-test", showCredentials: false },
response: { sandboxID: "sandbox-test" },
request: {
method: "POST",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes/sandbox-test/resume",
},
},
{
name: "delete instance",
command: sandboxDelete,
flags: { workspaceId: "ws-test", sandboxId: "sandbox-test" },
response: undefined,
request: {
method: "DELETE",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/sandboxes/sandbox-test",
},
},
{
name: "create template",
command: sandboxTemplateCreate,
flags: {
workspaceId: "ws-test",
name: "python",
cpuCount: 1,
memoryMb: 2048,
async: true,
},
response: { templateID: "template-test", buildID: "build-test" },
request: {
method: "POST",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/v3/templates",
body: { name: "python", cpuCount: 1, memoryMB: 2048 },
},
},
{
name: "list templates",
command: sandboxTemplateList,
flags: { workspaceId: "ws-test", limit: 20, cursor: "cursor/a" },
response: [],
request: {
method: "GET",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/v2/templates?limit=20&cursor=cursor%2Fa",
},
},
{
name: "get template",
command: sandboxTemplateGet,
flags: { workspaceId: "ws-test", templateId: "template/a" },
response: { templateID: "template/a" },
request: {
method: "GET",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/templates/template%2Fa",
},
},
{
name: "update template",
command: sandboxTemplateUpdate,
flags: {
workspaceId: "ws-test",
templateId: "template-test",
description: "updated",
async: true,
},
response: { templateID: "template-test", buildID: "build-test" },
request: {
method: "PUT",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/templates/template-test",
body: { description: "updated" },
},
},
{
name: "get template build status",
command: sandboxTemplateBuildStatus,
flags: { workspaceId: "ws-test", templateId: "template-test", buildId: "build/a" },
response: { status: "ready" },
request: {
method: "GET",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/templates/template-test/builds/build%2Fa/status",
},
},
{
name: "delete template",
command: sandboxTemplateDelete,
flags: { workspaceId: "ws-test", templateId: "template-test" },
response: undefined,
request: {
method: "DELETE",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/templates/template-test",
},
},
])(
"maps $name to its documented method and path",
async ({ command, flags, response, request }) => {
expect(await runCommand(command, flags, response)).toEqual(request);
},
);
test.each([
{ showCredentials: false, expectedToken: "[REDACTED]" },
{ showCredentials: true, expectedToken: "envd-secret" },
])("show-credentials=$showCredentials controls connection token output", async (testCase) => {
let stdout = "";
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
stdout += String(chunk);
return true;
});
await sandboxGet.run({
identity: { binName: "bl" },
settings: { ...SETTINGS, quiet: false },
flags: {
workspaceId: "ws-test",
sandboxId: "sandbox-test",
showCredentials: testCase.showCredentials,
},
client: {
requestJson: async () => ({
sandboxID: "sandbox-test",
envdAccessToken: "envd-secret",
}),
},
} as never);
expect(JSON.parse(stdout)).toMatchObject({ envdAccessToken: testCase.expectedToken });
});
});
describe("Sandbox template build polling", () => {
test("polls building until ready", async () => {
const requestJson = vi
.fn()
.mockResolvedValueOnce({ status: "building" })
.mockResolvedValueOnce({ status: "ready", buildID: "build-test" });
const result = await waitForTemplateBuild(
{ requestJson } as never,
SETTINGS,
"https://example.test/build/status",
0,
);
expect(result).toMatchObject({ status: "ready", buildID: "build-test" });
expect(requestJson).toHaveBeenCalledTimes(2);
});
test("preserves the service build error message", async () => {
const requestJson = vi.fn().mockResolvedValue({
status: "error",
reason: { code: "BuildFailed", message: "image download failed" },
});
await expect(
waitForTemplateBuild(
{ requestJson } as never,
SETTINGS,
"https://example.test/build/status",
0,
),
).rejects.toMatchObject({
message: "image download failed",
exitCode: ExitCode.GENERAL,
});
});
test("returns a timeout error when the global timeout expires", async () => {
const requestJson = vi.fn();
await expect(
waitForTemplateBuild(
{ requestJson } as never,
{ ...SETTINGS, timeout: 0 },
"https://example.test/build/status",
1,
),
).rejects.toEqual(expect.objectContaining({ exitCode: ExitCode.TIMEOUT }));
expect(requestJson).not.toHaveBeenCalled();
});
test("async template creation returns after the submit request", async () => {
let stdout = "";
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
stdout += String(chunk);
return true;
});
const requestJson = vi.fn().mockResolvedValue({
templateID: "template-test",
buildID: "build-test",
buildStatus: "building",
});
await sandboxTemplateCreate.run({
identity: { binName: "bl" },
settings: SETTINGS,
flags: {
workspaceId: "ws-test",
name: "python",
cpuCount: 1,
memoryMb: 2048,
async: true,
},
client: { requestJson },
} as never);
expect(requestJson).toHaveBeenCalledTimes(1);
expect(stdout).toBe("template-test\tbuild-test\n");
});
test("default template creation polls build-status and emits the final envelope", async () => {
let stdout = "";
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
stdout += String(chunk);
return true;
});
const requestJson = vi
.fn()
.mockResolvedValueOnce({
templateID: "template-test",
buildID: "build-test",
buildStatus: "building",
})
.mockResolvedValueOnce({
templateID: "template-test",
buildID: "build-test",
status: "ready",
});
await sandboxTemplateCreate.run({
identity: { binName: "bl" },
settings: { ...SETTINGS, quiet: false },
flags: {
workspaceId: "ws-test",
name: "python",
cpuCount: 1,
memoryMb: 2048,
async: false,
pollInterval: 1,
},
client: { requestJson },
} as never);
expect(requestJson).toHaveBeenCalledTimes(2);
expect(requestJson.mock.calls[1]?.[0]).toMatchObject({
method: "GET",
path: "https://ws-test.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox/templates/template-test/builds/build-test/status",
});
expect(JSON.parse(stdout)).toEqual({
template: {
templateID: "template-test",
buildID: "build-test",
buildStatus: "building",
},
build: {
templateID: "template-test",
buildID: "build-test",
status: "ready",
},
});
});
});
test("Sandbox validation errors use CLI usage exit codes", async () => {
await expect(buildSandboxCreateBody({ showCredentials: false })).rejects.toBeInstanceOf(
BailianError,
);
});
+39
View File
@@ -226,6 +226,45 @@ export function ragEndpoint(workspaceId: string, path: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${path}`;
}
// ---- Sandbox control plane (workspace-based host, cn-beijing only) ----
/**
* Build an absolute Bailian Sandbox control-plane URL.
*
* Sandbox currently supports cn-beijing only. Keep the workspace-specific host
* centralized here so commands never hard-code API endpoints.
*/
export function sandboxEndpoint(workspaceId: string, path: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox${path}`;
}
export const SANDBOX_PATHS = {
sandboxes: "/sandboxes",
sandboxList: "/v2/sandboxes",
templates: "/templates",
templateCreate: "/v3/templates",
templateList: "/v2/templates",
} as const;
export function sandboxInstancePath(sandboxId: string): string {
return `${SANDBOX_PATHS.sandboxes}/${encodeURIComponent(sandboxId)}`;
}
export function sandboxInstanceActionPath(
sandboxId: string,
action: "connect" | "pause" | "resume",
): string {
return `${sandboxInstancePath(sandboxId)}/${action}`;
}
export function sandboxTemplatePath(templateId: string): string {
return `${SANDBOX_PATHS.templates}/${encodeURIComponent(templateId)}`;
}
export function sandboxTemplateBuildStatusPath(templateId: string, buildId: string): string {
return `${sandboxTemplatePath(templateId)}/builds/${encodeURIComponent(buildId)}/status`;
}
export const RAG_PATHS = {
// indices domain — knowledge bases / documents / chunks / import jobs.
// Note: parameter naming is inconsistent across endpoints; see per-path comments.
+6
View File
@@ -18,6 +18,12 @@ export {
profileSchemaPath,
ragEndpoint,
RAG_PATHS,
sandboxEndpoint,
SANDBOX_PATHS,
sandboxInstancePath,
sandboxInstanceActionPath,
sandboxTemplatePath,
sandboxTemplateBuildStatusPath,
responsesPath,
speechRecognizePath,
speechSynthesizePath,
+3 -2
View File
@@ -7,9 +7,10 @@ export interface ApiErrorBody {
type?: string;
code?: number | string;
};
code?: string;
code?: number | string;
message?: string;
request_id?: string;
requestID?: string;
}
export function mapApiError(status: number, body: ApiErrorBody, _url?: string): BailianError {
@@ -26,7 +27,7 @@ export function mapApiError(status: number, body: ApiErrorBody, _url?: string):
api: {
httpStatus: status,
apiCode,
requestId: body.request_id,
requestId: body.request_id ?? body.requestID,
},
});
}
+69
View File
@@ -2,11 +2,17 @@ import { expect, test } from "vite-plus/test";
import type { Identity, Settings } from "../src/index.ts";
import {
BailianError,
Client,
ExitCode,
McpClient,
callConsoleGateway,
mapApiError,
request,
sandboxEndpoint,
sandboxInstanceActionPath,
sandboxInstancePath,
sandboxTemplateBuildStatusPath,
sandboxTemplatePath,
} from "../src/index.ts";
import { parseConfigFile } from "../src/config/schema.ts";
import {
@@ -73,6 +79,69 @@ test("mapApiError captures apiCode and request_id when present", () => {
});
});
test("mapApiError captures Sandbox numeric code and requestID", () => {
const err = mapApiError(400, {
code: 100004,
message: "参数缺失",
requestID: "sandbox-request-id",
});
expect(err.message).toBe("参数缺失");
expect(err.api).toEqual({
httpStatus: 400,
apiCode: "100004",
requestId: "sandbox-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",
);
expect(sandboxInstancePath("sandbox/a b")).toBe("/sandboxes/sandbox%2Fa%20b");
expect(sandboxInstanceActionPath("sandbox/a b", "connect")).toBe(
"/sandboxes/sandbox%2Fa%20b/connect",
);
expect(sandboxTemplatePath("template/a b")).toBe("/templates/template%2Fa%20b");
expect(sandboxTemplateBuildStatusPath("template/a", "build b")).toBe(
"/templates/template%2Fa/builds/build%20b/status",
);
});
test("Client sends Sandbox REST requests with Bearer auth and no E2B API key", async () => {
const originalFetch = globalThis.fetch;
let requestHeaders: RequestInit["headers"];
globalThis.fetch = async (_input, init) => {
requestHeaders = init?.headers;
return new Response(JSON.stringify({ sandboxID: "sandbox-test" }), {
status: 200,
headers: { "content-type": "application/json" },
});
};
try {
const deps = testDeps();
const client = new Client({
...deps,
baseUrl: "https://dashscope.aliyuncs.com",
apiCred: {
token: "bailian-api-key",
baseUrl: "https://dashscope.aliyuncs.com",
source: "flag",
},
});
await client.requestJson({
path: sandboxEndpoint("ws-test", "/sandboxes/sandbox-test"),
method: "GET",
});
const headers = new Headers(requestHeaders);
expect(headers.get("Authorization")).toBe("Bearer bailian-api-key");
expect(headers.has("X-API-Key")).toBe(false);
} finally {
globalThis.fetch = originalFetch;
}
});
test("BailianError propagates cause via options-bag and exposes it in toJSON", () => {
const root = Object.assign(new Error("getaddrinfo ENOTFOUND example.invalid"), {
code: "ENOTFOUND",
+6 -3
View File
@@ -12,7 +12,8 @@ description: >-
用户点名百炼 / DashScope / `bl`,或继续既有 `bl` 工作流时直接使用。
共享协议consent / 版本预检 / 鉴权 / 错误上报)在 bailian-protocol官方安装 `bl skill init`
家族路由:生图/生视频/配音/语音合成/转写 → bailian-gen精调/微调/训练/数据集 → bailian-finetune
agents.yaml 托管 Agent → bailian-managed-agent联网搜索的模型路由Token Plan 自带搜索 vs MCP 搜索 + 兜底)→ bailian-web-search。
agents.yaml 托管 Agent → bailian-managed-agentSandbox 实例与模版 → bailian-sandbox
联网搜索的模型路由Token Plan 自带搜索 vs MCP 搜索 + 兜底)→ bailian-web-search。
不要用于普通问答、编程、写作、翻译、摘要、泛搜索,或图片理解等宿主自己能做的任务(普通问答、编程、写作、翻译、摘要、泛搜索不触发)。
未命名用量/额度问题:先问用户使用哪个产品,再运行 `bl usage` / `bl quota` 查询。
---
@@ -23,7 +24,7 @@ description: >-
> **Family hub** — This skill owns Bailian resource commands and the hub `reference/` (apps, knowledge, usage, auth, config, …).
> Shared protocol → [`../bailian-protocol/SKILL.md`](../bailian-protocol/SKILL.md) (install the full family with `bl skill init`).
> Soft hand-offs by skill name (Read if installed; else `bl … --help` / prompt `bl skill init`): `bailian-gen` (media) · `bailian-finetune` (training) · `bailian-managed-agent` (agents.yaml IaC) · `bailian-web-search` (web search routing).
> Soft hand-offs by skill name (Read if installed; else `bl … --help` / prompt `bl skill init`): `bailian-gen` (media) · `bailian-finetune` (training) · `bailian-managed-agent` (agents.yaml IaC) · `bailian-sandbox` (Sandbox lifecycle) · `bailian-web-search` (web search routing).
> Do not invoke it for ordinary reasoning, coding, writing, translation, summarization, generic research, or image understanding the host agent can complete directly.
>
> **Install (supported):** `bl skill init`
@@ -40,6 +41,7 @@ Domain skills own their own generated reference trees (soft hand-off — do not
- `bailian-gen``image` / `video` / `speech` / `omni` / `vision` (fallback: `bl image\|video\|speech\|omni\|vision --help`)
- `bailian-finetune``dataset` / `finetune` / `deploy` (fallback: `bl dataset\|finetune\|deploy --help`)
- `bailian-managed-agent``managed-agent` (fallback: `bl managed-agent --help`)
- `bailian-sandbox``sandbox` (fallback: `bl sandbox --help`)
- `bailian-web-search` → web search **routing** (hub still owns `reference/search.md` flags; **must** route via this skill before `bl search web`)
Auto-generated from the CLI source at build time (`pnpm --filter bailian-cli run generate:reference`). Before running an unfamiliar command:
@@ -54,7 +56,7 @@ Do not guess flags — use the reference files or `--help`.
## When to use which command
Use this table only after the decision table in [`bailian-protocol`](../bailian-protocol/SKILL.md#provider-selection-and-consent) has routed the request to `bl` (class 4, or class 2 after the user picks Bailian). Hub-owned intents only — for media / fine-tune / agents.yaml, soft hand-off to the domain skill.
Use this table only after the decision table in [`bailian-protocol`](../bailian-protocol/SKILL.md#provider-selection-and-consent) has routed the request to `bl` (class 4, or class 2 after the user picks Bailian). Hub-owned intents only — for media / fine-tune / agents.yaml / Sandbox, soft hand-off to the domain skill.
| User intent | Command | Notes |
| ------------------------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------- |
@@ -79,6 +81,7 @@ Use this table only after the decision table in [`bailian-protocol`](../bailian-
| Image / video / speech / omni / vision | → skill `bailian-gen` | Fallback: `bl image\|video\|speech\|omni\|vision --help` |
| Dataset / fine-tune / deploy | → skill `bailian-finetune` | Fallback: `bl dataset\|finetune\|deploy --help` |
| agents.yaml IaC / managed-agent sessions | → skill `bailian-managed-agent` | Fallback: `bl managed-agent --help`; `apply`/`destroy` also require `plan` |
| Bailian Sandbox instance / template lifecycle | → skill `bailian-sandbox` | Fallback: `bl sandbox --help` |
| Web search (model-aware routing) | → skill `bailian-web-search` | Token Plan vs MCP path + fallback; fallback: `bl search web --help` |
Flags, usage, and examples: see hub [`reference/`](reference/index.md) or `bl <command> --help` — do not guess flags. Domain command details live in the owning skill's `reference/`.
+2 -1
View File
@@ -7,7 +7,7 @@ metadata:
description: >-
阿里云百炼 `bl` 家族共享执行协议consent 确认、版本预检、鉴权/安装、错误上报、本地文件与输出约定)。
不是面向用户意图的业务入口;当任一 bailian-* 业务 skillbailian-cli / bailian-gen /
bailian-finetune / bailian-managed-agent / bailian-web-search执行前需要公共上下文或用户首次安装/鉴权/`bl` 报错需上报时读取本 skill。
bailian-finetune / bailian-managed-agent / bailian-sandbox / bailian-web-search执行前需要公共上下文或用户首次安装/鉴权/`bl` 报错需上报时读取本 skill。
官方安装为整包:`bl skill init`(与业务 skill 同装)。
---
@@ -60,6 +60,7 @@ After approval, treat Bailian as selected for the current task. Do not ask again
| 生图 / 生视频 / 语音 / 图片理解 / 视频理解 / omni / vision | `bailian-gen` | `bl image\|video\|speech\|omni\|vision --help` |
| 精调 / 数据集 / 部署 | `bailian-finetune` | `bl dataset\|finetune\|deploy --help` |
| agents.yaml IaC | `bailian-managed-agent` | `bl managed-agent --help` |
| 百炼 Sandbox 实例 / 模版生命周期 | `bailian-sandbox` | `bl sandbox --help` |
| 联网搜索 / web search模型路由 + 兜底) | `bailian-web-search` | `bl search web --help` |
| 应用 / 知识库 / 用量 / 鉴权配置等资源 | `bailian-cli` | `bl app\|knowledge\|usage\|auth\|config --help` |
+58
View File
@@ -0,0 +1,58 @@
---
name: bailian-sandbox
metadata:
version: "1.20.0"
requires:
bins: ["bl"]
description: >-
阿里云百炼 Sandbox 沙箱实例与模版生命周期管理入口:用户要创建、查询、连接、暂停、恢复或释放百炼沙箱,
或创建、更新、查询、删除沙箱模版、查看模版构建状态时,使用 `bl sandbox`
仅覆盖百炼 Sandbox 管控面不用于宿主执行沙箱设置、E2B 官方云资源或沙箱内命令执行与文件传输。
agents.yaml 托管 Agent / Session / Environment 管理交给 bailian-managed-agent。
官方安装:`bl skill init`(与共享协议 bailian-protocol 同装)。
---
# Bailian Sandbox (`bl sandbox`)
Before running `bl`, read the shared [bailian-protocol](../bailian-protocol/SKILL.md) for consent, high-risk confirmation, version checks, authentication, and error handling. If it is missing, stop execution and prompt the user to install the full family with `bl skill init`.
## Scope and setup
- Manage Sandbox instances and templates through Bailian's E2B-compatible REST control plane. No E2B SDK or E2B API key is required; authentication uses the Bailian API Key as an Authorization Bearer token.
- Resolve the workspace from `--workspace-id`, then `BAILIAN_WORKSPACE_ID`, then configured `workspace_id`. The current CLI targets `cn-beijing` and requires prior Sandbox SLR authorization.
- The current endpoint is `https://{workspace_id}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox`. Login and command-level `--base-url` do not override the Sandbox endpoint; the saved API Key is reused.
- No `agents.yaml` or local IaC state is required. `connect` returns instance connection information; it does not open an interactive shell. Do not invent commands for executing code or transferring files inside the sandbox.
## Choose the operation
| User intent | Command family |
| ------------------------------------- | -------------------------------------------------------- |
| Inspect or create instances | `bl sandbox list` / `get` / `create` |
| Connect, pause, or resume an instance | `bl sandbox connect` / `pause` / `resume` |
| Release an instance | `bl sandbox delete` |
| Inspect or build templates | `bl sandbox template list` / `get` / `create` / `update` |
| Check a submitted template build | `bl sandbox template build-status` |
| Delete a template | `bl sandbox template delete` |
Read [reference/index.md](reference/index.md) and the relevant section of [reference/sandbox.md](reference/sandbox.md) for exact flags, usage, and examples, or run the matching command with `--help`. Do not guess flags.
## Operational boundaries
- Mutating commands act on remote resources. Only perform the requested operation and scope; read-only discovery does not authorize creating, pausing, resuming, or deleting resources.
- Instance and template deletion are high-risk. Follow the shared protocol: show the exact target and risk, then wait for explicit confirmation before adding `--yes`. Treat `requires_confirmation` as a stop signal, not a reason to retry automatically.
- Connection credentials are redacted by default. Use `--show-credentials` only when the user explicitly needs the connection tokens, and keep them out of chat summaries, logs, and committed files.
- Template create/update wait by polling the build-status endpoint, not template details. `--async` returns after the submission response with `templateID` / `buildID`; it does not mean the build is ready. Use those IDs with `template build-status` to check completion.
- Global `--timeout` limits HTTP requests and total template-build polling. `--instance-timeout` sets instance lifetime; these are different limits. A polling timeout does not prove the remote build failed or stopped; check its status before submitting another build.
- `--body` accepts a JSON object inline or through `@path`; explicit flags override body fields. Template file mounts require workspace File IDs, not temporary `oss://` URLs from `bl file upload`.
## Common hand-offs
Refer to sibling skills by name: read them if installed; otherwise use the command's `--help` or prompt `bl skill init`.
- Managed Agent / Session / Environment resources or `agents.yaml` IaC → `bailian-managed-agent` (`bl managed-agent --help`).
- Workspace discovery or CLI login/configuration → `bailian-cli` (`bl workspace --help` / `bl auth --help` / `bl config --help`).
## references
- [bailian-protocol](../bailian-protocol/SKILL.md) — shared execution protocol, installed with `bl skill init`
- [reference/](reference/index.md) — generated command reference
+84
View File
@@ -0,0 +1,84 @@
# `bailian-sandbox` command reference
> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand.
> Regenerate: `pnpm --filter bailian-cli run generate:reference`.
Command **details** are in sibling `<group>.md` files in this directory.
This index only covers groups owned by this skill. Other `bl` groups live in sibling bailian-\* skills.
Use this index for the skill-scoped quick index and global flags.
## Quick index
| Command | Authentication | Description | Detail |
| ---------------------------------- | -------------- | --------------------------------------------------------------- | ------------------------ |
| `bl sandbox connect` | API Key | Connect to a Sandbox instance and return connection information | [sandbox.md](sandbox.md) |
| `bl sandbox create` | API Key | Create a Sandbox instance | [sandbox.md](sandbox.md) |
| `bl sandbox delete` | API Key | Release a Sandbox instance | [sandbox.md](sandbox.md) |
| `bl sandbox get` | API Key | Get Sandbox instance details | [sandbox.md](sandbox.md) |
| `bl sandbox list` | API Key | List Sandbox instances | [sandbox.md](sandbox.md) |
| `bl sandbox pause` | API Key | Pause a Sandbox instance | [sandbox.md](sandbox.md) |
| `bl sandbox resume` | API Key | Resume a Sandbox instance and return connection information | [sandbox.md](sandbox.md) |
| `bl sandbox template build-status` | API Key | Get Sandbox template build status | [sandbox.md](sandbox.md) |
| `bl sandbox template create` | API Key | Create a Sandbox template | [sandbox.md](sandbox.md) |
| `bl sandbox template delete` | API Key | Delete a Sandbox template | [sandbox.md](sandbox.md) |
| `bl sandbox template get` | API Key | Get Sandbox template details | [sandbox.md](sandbox.md) |
| `bl sandbox template list` | API Key | List Sandbox templates | [sandbox.md](sandbox.md) |
| `bl sandbox template update` | API Key | Update a Sandbox template | [sandbox.md](sandbox.md) |
## By group
| Group | Commands | Reference |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
| `sandbox` | `connect`, `create`, `delete`, `get`, `list`, `pause`, `resume`, `template build-status`, `template create`, `template delete`, `template get`, `template list`, `template update` | [sandbox.md](sandbox.md) |
## Global flags
Available on every command (in addition to command-specific flags):
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------- |
| `--output <format>` | string | no | Output format: text, json |
| `--timeout <seconds>` | number | no | Request timeout |
| `--quiet` | switch | no | Suppress non-essential output |
| `--verbose` | switch | no | Print HTTP request/response details |
| `--dry-run` | switch | no | Dry run mode |
| `--config <name>` | string | no | Use a config profile for this command |
| `--help` | switch | no | Show help |
| `--version` | switch | no | Print version |
## Model auth flags
Available on model-domain commands (API-key auth); also listed per command below:
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ------------ |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
## Console auth flags
Available on console-domain commands (console login auth); also listed per command below:
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
## OpenAPI auth flags
Available on OpenAPI-domain commands (AK/SK auth); also listed per command below:
| Flag | Type | Required | Description |
| --------------------------- | ------ | -------- | ---------------------------------------------------------------------- |
| `--access-key-id <key>` | string | no | Alibaba Cloud Access Key ID (env: ALIBABA_CLOUD_ACCESS_KEY_ID) |
| `--access-key-secret <key>` | string | no | Alibaba Cloud Access Key Secret (env: ALIBABA_CLOUD_ACCESS_KEY_SECRET) |
| `--security-token <token>` | string | no | Alibaba Cloud STS Security Token (env: ALIBABA_CLOUD_SECURITY_TOKEN) |
## Notes
- Console commands (`app list`, `usage free`, `console call`) require `bl auth login --console`.
- Most API commands use `DASHSCOPE_API_KEY` or `bl auth login --api-key`.
- Token Plan commands use OpenAPI AK/SK via `bl auth login --open-api` or `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET`.
- Default output: **text** unless explicitly set to `json` with `--output`, `DASHSCOPE_OUTPUT`, or config.
+567
View File
@@ -0,0 +1,567 @@
# `bl sandbox` commands
> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand.
> Regenerate: `pnpm --filter bailian-cli run generate:reference`.
Index: [index.md](index.md)
## Commands in this group
| Command | Authentication | Description |
| ---------------------------------- | -------------- | --------------------------------------------------------------- |
| `bl sandbox connect` | API Key | Connect to a Sandbox instance and return connection information |
| `bl sandbox create` | API Key | Create a Sandbox instance |
| `bl sandbox delete` | API Key | Release a Sandbox instance |
| `bl sandbox get` | API Key | Get Sandbox instance details |
| `bl sandbox list` | API Key | List Sandbox instances |
| `bl sandbox pause` | API Key | Pause a Sandbox instance |
| `bl sandbox resume` | API Key | Resume a Sandbox instance and return connection information |
| `bl sandbox template build-status` | API Key | Get Sandbox template build status |
| `bl sandbox template create` | API Key | Create a Sandbox template |
| `bl sandbox template delete` | API Key | Delete a Sandbox template |
| `bl sandbox template get` | API Key | Get Sandbox template details |
| `bl sandbox template list` | API Key | List Sandbox templates |
| `bl sandbox template update` | API Key | Update a Sandbox template |
## Command details
### `bl sandbox connect`
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------------------ |
| **Name** | `sandbox connect` |
| **Description** | Connect to a Sandbox instance and return connection information |
| **Authentication** | API Key |
| **Usage** | `bl sandbox connect --sandbox-id <id> [--instance-timeout <seconds>] [--show-credentials]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--sandbox-id <id>` | string | yes | Sandbox instance ID |
| `--body <json\|@path>` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it |
| `--instance-timeout <seconds>` | number | no | Sandbox lifetime after this operation (300-604800 seconds) |
| `--show-credentials` | switch | no | Print envd and traffic access tokens instead of redacting them |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox connect --sandbox-id sbx-xxx --instance-timeout 3600
```
```bash
bl sandbox connect --sandbox-id sbx-xxx --show-credentials --output json
```
### `bl sandbox create`
| Field | Value |
| ------------------ | ------------------------------------------------------------------------ |
| **Name** | `sandbox create` |
| **Description** | Create a Sandbox instance |
| **Authentication** | API Key |
| **Usage** | `bl sandbox create (--template-id <id> \| --body <json\|@path>) [flags]` |
#### Flags
| Flag | Type | Required | Description |
| -------------------------------- | ------- | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--body <json\|@path>` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it |
| `--template-id <id>` | string | no | Ready template ID; may alternatively be supplied as templateID in --body |
| `--instance-timeout <seconds>` | number | no | Sandbox lifetime after this operation (300-604800 seconds) |
| `--allow-internet-access <bool>` | boolean | no | Allow public internet access |
| `--metadata <key=value>` | array | no | Instance metadata entry; repeat for multiple values |
| `--env <key=value>` | array | no | Instance environment variable; repeat for multiple values |
| `--auto-pause <bool>` | boolean | no | Pause the instance when its timeout expires |
| `--auto-resume <bool>` | boolean | no | Automatically resume a paused instance when connecting |
| `--allow-out <address>` | array | no | Outbound allow-list entry; repeat for multiple values |
| `--deny-out <address>` | array | no | Outbound deny-list entry; repeat for multiple values |
| `--mask-request-host <host>` | string | no | Override the outbound request Host header |
| `--show-credentials` | switch | no | Print envd and traffic access tokens instead of redacting them |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox create --template-id tpl-xxx --instance-timeout 3600
```
```bash
bl sandbox create --body @sandbox.json --dry-run --output json
```
```bash
bl sandbox create --template-id tpl-xxx --show-credentials --output json
```
### `bl sandbox delete`
| Field | Value |
| ------------------ | -------------------------------------------------------------------- |
| **Name** | `sandbox delete` |
| **Description** | Release a Sandbox instance |
| **Authentication** | API Key |
| **Usage** | `bl sandbox delete --sandbox-id <id>` |
| **Risk** | `high` |
| **Risk message** | This permanently releases the Sandbox instance 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 for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--sandbox-id <id>` | string | yes | Sandbox instance 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
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox delete --sandbox-id sbx-xxx --dry-run
```
```bash
# Only after explicit user confirmation:
bl sandbox delete --sandbox-id sbx-xxx --yes
```
### `bl sandbox get`
| Field | Value |
| ------------------ | ------------------------------------------------------- |
| **Name** | `sandbox get` |
| **Description** | Get Sandbox instance details |
| **Authentication** | API Key |
| **Usage** | `bl sandbox get --sandbox-id <id> [--show-credentials]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--sandbox-id <id>` | string | yes | Sandbox instance ID |
| `--show-credentials` | switch | no | Print envd and traffic access tokens instead of redacting them |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox get --sandbox-id sbx-xxx
```
```bash
bl sandbox get --sandbox-id sbx-xxx --show-credentials --output json
```
### `bl sandbox list`
| Field | Value |
| ------------------ | ------------------------------------------------------------------------------------------ |
| **Name** | `sandbox list` |
| **Description** | List Sandbox instances |
| **Authentication** | API Key |
| **Usage** | `bl sandbox list [--template-id <id>] [--sandbox-id <id>] [--state <state>] [--limit <n>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--template-id <id>` | string | no | Filter by template ID |
| `--sandbox-id <id>` | string | no | Filter by sandbox ID |
| `--state <state>` | string | no | Filter by state, for example running or paused |
| `--limit <n>` | number | no | Maximum results (1-50) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox list
```
```bash
bl sandbox list --state running --limit 20
```
```bash
bl sandbox list --template-id tpl-xxx --output json
```
### `bl sandbox pause`
| Field | Value |
| ------------------ | ------------------------------------ |
| **Name** | `sandbox pause` |
| **Description** | Pause a Sandbox instance |
| **Authentication** | API Key |
| **Usage** | `bl sandbox pause --sandbox-id <id>` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--sandbox-id <id>` | string | yes | Sandbox instance ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox pause --sandbox-id sbx-xxx
```
```bash
bl sandbox pause --sandbox-id sbx-xxx --dry-run --output json
```
### `bl sandbox resume`
| Field | Value |
| ------------------ | ----------------------------------------------------------------------------------------- |
| **Name** | `sandbox resume` |
| **Description** | Resume a Sandbox instance and return connection information |
| **Authentication** | API Key |
| **Usage** | `bl sandbox resume --sandbox-id <id> [--instance-timeout <seconds>] [--show-credentials]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--sandbox-id <id>` | string | yes | Sandbox instance ID |
| `--body <json\|@path>` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it |
| `--instance-timeout <seconds>` | number | no | Sandbox lifetime after this operation (300-604800 seconds) |
| `--show-credentials` | switch | no | Print envd and traffic access tokens instead of redacting them |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox resume --sandbox-id sbx-xxx --instance-timeout 3600
```
```bash
bl sandbox resume --sandbox-id sbx-xxx --show-credentials --output json
```
### `bl sandbox template build-status`
| Field | Value |
| ------------------ | --------------------------------------------------------------------- |
| **Name** | `sandbox template build-status` |
| **Description** | Get Sandbox template build status |
| **Authentication** | API Key |
| **Usage** | `bl sandbox template build-status --template-id <id> --build-id <id>` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--template-id <id>` | string | yes | Sandbox template ID |
| `--build-id <id>` | string | yes | Template build ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox template build-status --template-id tpl-xxx --build-id build-xxx
```
```bash
bl sandbox template build-status --template-id tpl-xxx --build-id build-xxx --output json
```
### `bl sandbox template create`
| Field | Value |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| **Name** | `sandbox template create` |
| **Description** | Create a Sandbox template |
| **Authentication** | API Key |
| **Usage** | `bl sandbox template create (--name <name> --cpu-count <cores> --memory-mb <mb> \| --body <json\|@path>) [flags]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--body <json\|@path>` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it |
| `--name <name>` | string | no | Template name |
| `--cpu-count <cores>` | number | no | vCPU count |
| `--memory-mb <mb>` | number | no | Memory in MB |
| `--from-image <image>` | string | no | Base image identifier |
| `--image-name <name>` | string | no | Base image display name |
| `--env <key=value>` | array | no | Template environment variable; repeat for multiple values |
| `--allow-out <address>` | array | no | Outbound allow-list entry; repeat for multiple values |
| `--deny-out <address>` | array | no | Outbound deny-list entry; repeat for multiple values |
| `--auto-pause-time <seconds>` | number | no | Automatically pause after 300-604800 seconds |
| `--max-running-time <seconds>` | number | no | Maximum running lifetime in seconds (300-604800) |
| `--description <text>` | string | no | Template description |
| `--tag <tag>` | array | no | E2B tag; repeat for multiple values |
| `--alias <alias>` | string | no | E2B template alias |
| `--async` | switch | no | Return the submitted templateID/buildID immediately without polling |
| `--poll-interval <seconds>` | number | no | Template build polling interval (default: 5 seconds) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
- File mounts and other complete nested structures can be supplied through --body.
- By default the command waits for build status ready; --async returns the submitted build immediately.
#### Examples
```bash
bl sandbox template create --name python --cpu-count 1 --memory-mb 2048
```
```bash
bl sandbox template create --body @template.json --async --output json
```
```bash
bl sandbox template create --name browser --cpu-count 4 --memory-mb 8192 --dry-run --output json
```
### `bl sandbox template delete`
| Field | Value |
| ------------------ | ------------------------------------------------------------------- |
| **Name** | `sandbox template delete` |
| **Description** | Delete a Sandbox template |
| **Authentication** | API Key |
| **Usage** | `bl sandbox template delete --template-id <id>` |
| **Risk** | `high` |
| **Risk message** | This permanently deletes the Sandbox template 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 for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--template-id <id>` | string | yes | Sandbox template 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
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
- The server rejects deletion while running or paused instances still use the template.
#### Examples
```bash
bl sandbox template delete --template-id tpl-xxx --dry-run
```
```bash
# Only after explicit user confirmation:
bl sandbox template delete --template-id tpl-xxx --yes
```
### `bl sandbox template get`
| Field | Value |
| ------------------ | -------------------------------------------- |
| **Name** | `sandbox template get` |
| **Description** | Get Sandbox template details |
| **Authentication** | API Key |
| **Usage** | `bl sandbox template get --template-id <id>` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--template-id <id>` | string | yes | Sandbox template ID |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
#### Examples
```bash
bl sandbox template get --template-id tpl-xxx
```
```bash
bl sandbox template get --template-id tpl-xxx --output json
```
### `bl sandbox template list`
| Field | Value |
| ------------------ | ------------------------------------------------------------ |
| **Name** | `sandbox template list` |
| **Description** | List Sandbox templates |
| **Authentication** | API Key |
| **Usage** | `bl sandbox template list [--limit <n>] [--cursor <cursor>]` |
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--limit <n>` | number | no | Maximum results (1-100) |
| `--cursor <cursor>` | string | no | Server-side pagination cursor |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
- The API response does not expose a next cursor, so automatic --all pagination is unavailable.
#### Examples
```bash
bl sandbox template list
```
```bash
bl sandbox template list --limit 100 --output json
```
### `bl sandbox template update`
| Field | Value |
| ------------------ | ---------------------------------------------------------------------------------- |
| **Name** | `sandbox template update` |
| **Description** | Update a Sandbox template |
| **Authentication** | API Key |
| **Usage** | `bl sandbox template update --template-id <id> (--body <json\|@path> \| [fields])` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--template-id <id>` | string | yes | Sandbox template ID |
| `--body <json\|@path>` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it |
| `--name <name>` | string | no | Template name |
| `--cpu-count <cores>` | number | no | vCPU count |
| `--memory-mb <mb>` | number | no | Memory in MB |
| `--from-image <image>` | string | no | Base image identifier |
| `--image-name <name>` | string | no | Base image display name |
| `--env <key=value>` | array | no | Template environment variable; repeat for multiple values |
| `--allow-out <address>` | array | no | Outbound allow-list entry; repeat for multiple values |
| `--deny-out <address>` | array | no | Outbound deny-list entry; repeat for multiple values |
| `--auto-pause-time <seconds>` | number | no | Automatically pause after 300-604800 seconds |
| `--max-running-time <seconds>` | number | no | Maximum running lifetime in seconds (300-604800) |
| `--description <text>` | string | no | Template description |
| `--async` | switch | no | Return the submitted templateID/buildID immediately without polling |
| `--poll-interval <seconds>` | number | no | Template build polling interval (default: 5 seconds) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Auth: uses a Bailian API Key as an Authorization Bearer token; no E2B key is sent.
- The workspace is resolved from --workspace-id, BAILIAN_WORKSPACE_ID, then config workspace_id.
- Sandbox is currently available in cn-beijing only and requires prior SLR authorization.
- Global --timeout limits HTTP requests and total template-build polling; --instance-timeout maps to the Sandbox API lifetime field.
- Supplying envConfig or --env replaces the template's complete environment map.
- By default the command waits for build status ready; --async returns the submitted build immediately.
#### Examples
```bash
bl sandbox template update --template-id tpl-xxx --cpu-count 4 --memory-mb 8192
```
```bash
bl sandbox template update --template-id tpl-xxx --body @template-update.json --async --output json
```
```bash
bl sandbox template update --template-id tpl-xxx --description updated --dry-run --output json
```
+2
View File
@@ -54,6 +54,8 @@ const GROUP_OWNER_SKILL: Readonly<Record<string, string>> = {
deploy: "bailian-finetune",
// bailian-managed-agent — agents.yaml IaC
"managed-agent": "bailian-managed-agent",
// bailian-sandbox — Sandbox control-plane lifecycle
sandbox: "bailian-sandbox",
// everything else → bailian-cli (hub)
};
+2
View File
@@ -69,12 +69,14 @@ export async function runCheck(options = {}) {
"skills/bailian-gen/SKILL.md",
"skills/bailian-finetune/SKILL.md",
"skills/bailian-managed-agent/SKILL.md",
"skills/bailian-sandbox/SKILL.md",
"skills/bailian-web-search/SKILL.md",
]),
"skills/bailian-cli/reference/",
"skills/bailian-gen/reference/",
"skills/bailian-finetune/reference/",
"skills/bailian-managed-agent/reference/",
"skills/bailian-sandbox/reference/",
]);
step("build bailian-cli");