mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(sandbox): add Bailian Sandbox lifecycle commands
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)。
|
||||
|
||||
约定:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 操作。
|
||||
|
||||
@@ -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) 一并核对
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "最大返回数量(1–50)" },
|
||||
},
|
||||
} 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 存活时间(300–604800 秒)",
|
||||
},
|
||||
},
|
||||
} 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": "在 300–604800 秒后自动暂停",
|
||||
},
|
||||
},
|
||||
maxRunningTime: {
|
||||
type: "number",
|
||||
valueHint: "<seconds>",
|
||||
description: {
|
||||
"en-US": "Maximum running lifetime in seconds (300-604800)",
|
||||
"zh-CN": "最大运行时间(秒,300–604800)",
|
||||
},
|
||||
},
|
||||
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": "最大返回数量(1–100)" },
|
||||
},
|
||||
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);
|
||||
},
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
@@ -18,6 +18,12 @@ export {
|
||||
profileSchemaPath,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
sandboxEndpoint,
|
||||
SANDBOX_PATHS,
|
||||
sandboxInstancePath,
|
||||
sandboxInstanceActionPath,
|
||||
sandboxTemplatePath,
|
||||
sandboxTemplateBuildStatusPath,
|
||||
responsesPath,
|
||||
speechRecognizePath,
|
||||
speechSynthesizePath,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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-agent;Sandbox 实例与模版 → 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/`.
|
||||
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
description: >-
|
||||
阿里云百炼 `bl` 家族共享执行协议(consent 确认、版本预检、鉴权/安装、错误上报、本地文件与输出约定)。
|
||||
不是面向用户意图的业务入口;当任一 bailian-* 业务 skill(bailian-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` |
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user