mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(sandbox): add template file upload with sandbox_template source
This commit is contained in:
@@ -208,6 +208,7 @@ import {
|
||||
managedAgentFileDelete,
|
||||
sandboxCreate,
|
||||
sandboxOfficialImages,
|
||||
sandboxFileUpload,
|
||||
sandboxList,
|
||||
sandboxGet,
|
||||
sandboxConnect,
|
||||
@@ -439,6 +440,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"managed-agent file delete": managedAgentFileDelete,
|
||||
"sandbox create": sandboxCreate,
|
||||
"sandbox official-images": sandboxOfficialImages,
|
||||
"sandbox file upload": sandboxFileUpload,
|
||||
"sandbox list": sandboxList,
|
||||
"sandbox get": sandboxGet,
|
||||
"sandbox connect": sandboxConnect,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
import {
|
||||
agentStudioFilesPath,
|
||||
BailianError,
|
||||
defineCommand,
|
||||
detectOutputFormat,
|
||||
ExitCode,
|
||||
sandboxBaseUrl,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare, emitResult } from "bailian-cli-runtime";
|
||||
import { redactConnectionCredentials, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
|
||||
|
||||
const UPLOAD_SOURCE = "sandbox_template";
|
||||
|
||||
export const sandboxFileUpload = defineCommand({
|
||||
description: {
|
||||
"en-US": "Upload a workspace file for Sandbox template mounts",
|
||||
"zh-CN": "上传工作空间文件,供 Sandbox 模版挂载使用",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--path <path> [--filename <name>] [--mime-type <type>]",
|
||||
flags: {
|
||||
...WORKSPACE_FLAG,
|
||||
path: {
|
||||
type: "string",
|
||||
valueHint: "<path>",
|
||||
required: true,
|
||||
description: { "en-US": "Local file path", "zh-CN": "本地文件路径" },
|
||||
},
|
||||
filename: {
|
||||
type: "string",
|
||||
valueHint: "<name>",
|
||||
description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" },
|
||||
},
|
||||
mimeType: {
|
||||
type: "string",
|
||||
valueHint: "<type>",
|
||||
description: {
|
||||
"en-US": "Multipart file MIME type (default: application/octet-stream)",
|
||||
"zh-CN": "Multipart 文件部分的 MIME 类型(默认:application/octet-stream)",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--path ./config.json --output json",
|
||||
"--path ./config.json --quiet",
|
||||
"--path ./notes.txt --filename notes.txt --mime-type text/plain --dry-run --output json",
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
"POST /api/v1/agentstudio/files with multipart fields file and source=sandbox_template. Uses a Bailian Bearer API Key, not Console authentication or an E2B key; no agents.yaml is needed.",
|
||||
"zh-CN":
|
||||
"向 /api/v1/agentstudio/files 发送 multipart 字段 file 和 source=sandbox_template。使用百炼 Bearer API Key,不使用 Console 鉴权或 E2B Key;无需 agents.yaml。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Base URL follows Sandbox: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. Without one, --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id selects the cn-beijing origin. The upload path has no /sandbox prefix.",
|
||||
"zh-CN":
|
||||
"Base URL 沿用 Sandbox:--base-url > DASHSCOPE_BASE_URL > 登录/Profile 的 base_url。未配置时,按 --workspace-id > BAILIAN_WORKSPACE_ID > 配置项 workspace_id 选择 cn-beijing 地址。上传路径不带 /sandbox 前缀。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Returns the upload response immediately; --quiet prints only its id. Upload does not wait for security review: status=checking is not ready to mount. Use an available file's id as mntConfig[].originFileId in template create/update --body, together with mountPath and optional originFileName, in the same workspace. This does not transfer files into a running instance.",
|
||||
"zh-CN":
|
||||
"上传响应返回后立即输出,--quiet 仅输出 id。不会等待安全审核:status=checking 不代表已可挂载。在同一工作空间的 template create/update --body 中,将可用文件的 id 填入 mntConfig[].originFileId,同时传入 mountPath 和可选的 originFileName。此命令不向运行中的实例传文件。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"--dry-run previews the endpoint, source, and local path without reading or uploading the file. The service detects the MIME type and enforces upload limits.",
|
||||
"zh-CN":
|
||||
"--dry-run 仅预览 Endpoint、source 和本地路径,不读取或上传文件。MIME 类型检测和上传限制由服务端执行。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const endpoint = ctx.client.url(agentStudioFilesPath(), () =>
|
||||
sandboxBaseUrl(resolveWorkspaceId(ctx)),
|
||||
);
|
||||
const filename = ctx.flags.filename ?? basename(ctx.flags.path);
|
||||
const mimeType = ctx.flags.mimeType ?? "application/octet-stream";
|
||||
const format = detectOutputFormat(ctx.settings.output);
|
||||
if (ctx.settings.dryRun) {
|
||||
emitResult(
|
||||
{
|
||||
method: "POST",
|
||||
endpoint,
|
||||
request: { source: UPLOAD_SOURCE, file: { path: ctx.flags.path, filename, mimeType } },
|
||||
},
|
||||
format,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = new Uint8Array(await readFile(ctx.flags.path));
|
||||
const form = new FormData();
|
||||
form.append("source", UPLOAD_SOURCE);
|
||||
form.append("file", new Blob([content], { type: mimeType }), filename);
|
||||
const file = await ctx.client.requestJson<Record<string, unknown>>({
|
||||
method: "POST",
|
||||
path: endpoint,
|
||||
body: form,
|
||||
});
|
||||
if (!file || typeof file.id !== "string" || !file.id.trim()) {
|
||||
throw new BailianError(
|
||||
"Upload response did not contain a File ID. / 上传响应未包含 File ID。",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
if (ctx.settings.quiet) emitBare(file.id);
|
||||
else emitResult(redactConnectionCredentials(file), format);
|
||||
},
|
||||
});
|
||||
@@ -386,8 +386,10 @@ export const sandboxTemplateCreate = defineCommand({
|
||||
"--image 覆盖 body 中的 fromImage/imageName;显式 --from-image/--image-name 再覆盖对应预设字段。不传 --image 时保持原有镜像行为。",
|
||||
},
|
||||
{
|
||||
"en-US": "File mounts and other complete nested structures can be supplied through --body.",
|
||||
"zh-CN": "文件挂载等完整嵌套结构可通过 --body 提供。",
|
||||
"en-US":
|
||||
"For local file mounts, use sandbox file upload first; put the available file's id into --body mntConfig[].originFileId with mountPath and optional originFileName. Upload and template must use the same workspace. Other complete nested structures can also be supplied through --body.",
|
||||
"zh-CN":
|
||||
"挂载本地文件前先调用 sandbox file upload;将可用文件的 id 填入 --body 的 mntConfig[].originFileId,并传入 mountPath 和可选的 originFileName。上传和模版须位于同一工作空间。其他完整嵌套结构也可通过 --body 提供。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
@@ -521,6 +523,12 @@ export const sandboxTemplateUpdate = defineCommand({
|
||||
"en-US": "Supplying envConfig or --env replaces the template's complete environment map.",
|
||||
"zh-CN": "传入 envConfig 或 --env 会整体替换模版的环境变量 Map。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Use sandbox file upload for local mount files. In --body mntConfig[], set originFileId to the available file's id and supply mountPath; upload and template must use the same workspace.",
|
||||
"zh-CN":
|
||||
"本地挂载文件先通过 sandbox file upload 上传。在 --body 的 mntConfig[] 中,将 originFileId 设为可用文件的 id 并传入 mountPath;上传和模版须位于同一工作空间。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"By default the command waits for build status ready; --async returns the submitted build immediately.",
|
||||
|
||||
@@ -224,6 +224,7 @@ 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 { sandboxOfficialImages } from "./commands/sandbox/images.ts";
|
||||
export { sandboxFileUpload } from "./commands/sandbox/file.ts";
|
||||
export {
|
||||
sandboxConnect,
|
||||
sandboxCreate,
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createServer, type IncomingHttpHeaders, type Server } from "node:http";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "./helpers.ts";
|
||||
import { SANDBOX_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
const directories: string[] = [];
|
||||
const servers: Server[] = [];
|
||||
const FILE_PATH = "/api/v1/agentstudio/files";
|
||||
const UPLOAD_COMMAND = ["sandbox", "file", "upload"];
|
||||
const RESPONSE = {
|
||||
id: "file_sandbox_test",
|
||||
filename: "config.json",
|
||||
type: "file",
|
||||
status: "available",
|
||||
mime_type: "application/json",
|
||||
size_bytes: 12,
|
||||
requestId: "request-test",
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
servers.splice(0).map(
|
||||
(server) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
}),
|
||||
),
|
||||
);
|
||||
for (const directory of directories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function setup(config: Record<string, unknown> = {}) {
|
||||
const directory = mkdtempSync(join(tmpdir(), "bl-sandbox-file-"));
|
||||
directories.push(directory);
|
||||
writeFileSync(join(directory, "config.json"), JSON.stringify(config));
|
||||
return {
|
||||
directory,
|
||||
env: {
|
||||
BAILIAN_CONFIG_DIR: directory,
|
||||
DASHSCOPE_API_KEY: "",
|
||||
DASHSCOPE_BASE_URL: "",
|
||||
BAILIAN_WORKSPACE_ID: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function gateway(responseBody: unknown = RESPONSE, status = 200) {
|
||||
const received: {
|
||||
method?: string;
|
||||
path?: string;
|
||||
headers: IncomingHttpHeaders;
|
||||
body: Buffer;
|
||||
}[] = [];
|
||||
const server = createServer((request, response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
request.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
request.on("end", () => {
|
||||
received.push({
|
||||
method: request.method,
|
||||
path: request.url,
|
||||
headers: request.headers,
|
||||
body: Buffer.concat(chunks),
|
||||
});
|
||||
response.writeHead(status, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify(responseBody));
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
servers.push(server);
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Expected a local TCP server.");
|
||||
return { origin: `http://127.0.0.1:${address.port}`, received };
|
||||
}
|
||||
|
||||
describe("e2e: Sandbox template file upload", () => {
|
||||
test("help documents upload auth, source, mount mapping, and the runtime boundary", async () => {
|
||||
const result = await runCommandHelp(SANDBOX_ROUTES, [...UPLOAD_COMMAND, "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
for (const text of [
|
||||
"bl sandbox file upload",
|
||||
"--path",
|
||||
"--filename",
|
||||
"--mime-type",
|
||||
"--base-url",
|
||||
"--workspace-id",
|
||||
"source=sandbox_template",
|
||||
FILE_PATH,
|
||||
"mntConfig[].originFileId",
|
||||
"status=checking",
|
||||
"running instance",
|
||||
]) {
|
||||
expect(result.stderr).toContain(text);
|
||||
}
|
||||
expect(result.stderr).not.toContain("--console-site");
|
||||
expect(result.stderr).not.toContain("--yes");
|
||||
});
|
||||
|
||||
test("requires --path and does not expose a source override", async () => {
|
||||
const { env } = setup();
|
||||
for (const args of [[], ["--path", "unused", "--source", "other"]]) {
|
||||
const result = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[...UPLOAD_COMMAND, ...args, "--output", "json"],
|
||||
env,
|
||||
);
|
||||
expect(result.exitCode).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(["flag", "env", "profile", "workspace"])(
|
||||
"%s determines the upload origin; dry-run does not read the local file or need credentials",
|
||||
async (source) => {
|
||||
const selectedOrigin = `https://${source}.example.test:8443`;
|
||||
const { directory, env } = setup(
|
||||
source === "profile"
|
||||
? { active_config: "sandbox", sandbox: { base_url: selectedOrigin } }
|
||||
: {},
|
||||
);
|
||||
const path = join(directory, "does-not-exist.json");
|
||||
const args =
|
||||
source === "flag"
|
||||
? ["--base-url", `${selectedOrigin}/api/v1/agentstudio/sandbox?ignored=1#fragment`]
|
||||
: source === "workspace"
|
||||
? ["--workspace-id", "ws-files"]
|
||||
: [];
|
||||
const result = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[...UPLOAD_COMMAND, "--path", path, ...args, "--dry-run", "--output", "json"],
|
||||
{ ...env, DASHSCOPE_BASE_URL: source === "env" ? selectedOrigin : "" },
|
||||
);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(parseStdoutJson(result.stdout)).toEqual({
|
||||
method: "POST",
|
||||
endpoint:
|
||||
(source === "workspace"
|
||||
? "https://ws-files.cn-beijing.maas.aliyuncs.com"
|
||||
: selectedOrigin) + FILE_PATH,
|
||||
request: {
|
||||
source: "sandbox_template",
|
||||
file: { path, filename: "does-not-exist.json", mimeType: "application/octet-stream" },
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("uploads exact multipart bytes and source with saved Bearer credentials, then reuses the ID in a template mount", async () => {
|
||||
const { origin, received } = await gateway();
|
||||
const { directory, env } = setup({
|
||||
active_config: "sandbox",
|
||||
sandbox: { api_key: "sk-upload-test-only", base_url: origin },
|
||||
});
|
||||
const path = join(directory, "local data.bin");
|
||||
const content = Buffer.from([0, 1, 255, 13, 10, 65]);
|
||||
writeFileSync(path, content);
|
||||
const result = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[
|
||||
...UPLOAD_COMMAND,
|
||||
"--path",
|
||||
path,
|
||||
"--filename",
|
||||
"config.json",
|
||||
"--mime-type",
|
||||
"application/json",
|
||||
"--output",
|
||||
"json",
|
||||
"--verbose",
|
||||
],
|
||||
env,
|
||||
);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(parseStdoutJson(result.stdout)).toEqual(RESPONSE);
|
||||
expect(result.stdout + result.stderr).not.toContain("sk-upload-test-only");
|
||||
expect(received).toHaveLength(1);
|
||||
const request = received[0];
|
||||
expect(request.method).toBe("POST");
|
||||
expect(request.path).toBe(FILE_PATH);
|
||||
expect(request.headers.authorization).toBe("Bearer sk-upload-test-only");
|
||||
expect(request.headers["x-api-key"]).toBeUndefined();
|
||||
expect(request.headers["content-type"]).toMatch(/^multipart\/form-data; boundary=/);
|
||||
const form = await new Request(`${origin}${FILE_PATH}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": request.headers["content-type"]! },
|
||||
body: new Uint8Array(request.body),
|
||||
}).formData();
|
||||
expect([...form.keys()].sort()).toEqual(["file", "source"]);
|
||||
expect(form.get("source")).toBe("sandbox_template");
|
||||
const file = form.get("file");
|
||||
if (!file || typeof file === "string") throw new Error("Expected a multipart file.");
|
||||
expect(file.name).toBe("config.json");
|
||||
expect(file.type).toBe("application/json");
|
||||
expect(Buffer.from(await file.arrayBuffer())).toEqual(content);
|
||||
|
||||
const mount = {
|
||||
originFileId: RESPONSE.id,
|
||||
originFileName: RESPONSE.filename,
|
||||
mountPath: "/home/user/config.json",
|
||||
};
|
||||
const template = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[
|
||||
"sandbox",
|
||||
"template",
|
||||
"create",
|
||||
"--name",
|
||||
"files",
|
||||
"--cpu-count",
|
||||
"1",
|
||||
"--memory-mb",
|
||||
"2048",
|
||||
"--body",
|
||||
JSON.stringify({ mntConfig: [mount] }),
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
env,
|
||||
);
|
||||
expect(template.exitCode, template.stderr).toBe(0);
|
||||
expect(parseStdoutJson(template.stdout)).toMatchObject({ request: { mntConfig: [mount] } });
|
||||
expect(received).toHaveLength(1);
|
||||
});
|
||||
|
||||
test.each(["json", "text", "quiet"])(
|
||||
"%s output returns checking status or the bare ID without polling or creating a template",
|
||||
async (output) => {
|
||||
const pending = { ...RESPONSE, status: "checking" };
|
||||
const { origin, received } = await gateway(pending);
|
||||
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
||||
const path = join(directory, "notes.txt");
|
||||
writeFileSync(path, "Only synthetic test data.");
|
||||
const result = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[
|
||||
...UPLOAD_COMMAND,
|
||||
"--path",
|
||||
path,
|
||||
...(output === "quiet" ? ["--quiet"] : ["--output", output]),
|
||||
],
|
||||
env,
|
||||
);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
if (output === "quiet") expect(result.stdout.trim()).toBe(RESPONSE.id);
|
||||
else {
|
||||
expect(result.stdout).toContain("checking");
|
||||
expect(result.stdout).toContain(RESPONSE.id);
|
||||
}
|
||||
expect(received).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
test("missing files fail locally without an upload request", async () => {
|
||||
const { origin, received } = await gateway();
|
||||
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
||||
const result = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[...UPLOAD_COMMAND, "--path", join(directory, "missing.txt"), "--output", "json"],
|
||||
env,
|
||||
);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stderr).toContain("ENOENT");
|
||||
expect(received).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("server errors are passed through without translating them", async () => {
|
||||
const { origin, received } = await gateway(
|
||||
{ code: "InvalidSource", message: "source rejected by server", request_id: "req-error" },
|
||||
400,
|
||||
);
|
||||
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
||||
const path = join(directory, "notes.txt");
|
||||
writeFileSync(path, "test");
|
||||
const result = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[...UPLOAD_COMMAND, "--path", path, "--output", "json"],
|
||||
env,
|
||||
);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(JSON.parse(result.stderr)).toMatchObject({
|
||||
error: {
|
||||
message: "source rejected by server",
|
||||
http_status: 400,
|
||||
api_code: "InvalidSource",
|
||||
request_id: "req-error",
|
||||
},
|
||||
});
|
||||
expect(received).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("a response without an ID is not reported as a successful upload", async () => {
|
||||
const { origin } = await gateway({ status: "checking" });
|
||||
const { directory, env } = setup({ api_key: "sk-upload-test-only", base_url: origin });
|
||||
const path = join(directory, "notes.txt");
|
||||
writeFileSync(path, "test");
|
||||
const result = await runCommandE2e(
|
||||
SANDBOX_ROUTES,
|
||||
[...UPLOAD_COMMAND, "--path", path, "--quiet", "--output", "json"],
|
||||
env,
|
||||
);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("File ID");
|
||||
});
|
||||
});
|
||||
@@ -185,6 +185,7 @@ export const SKILL_ROUTES: E2eRouteExports = {
|
||||
export const SANDBOX_ROUTES: E2eRouteExports = {
|
||||
"sandbox create": "sandboxCreate",
|
||||
"sandbox official-images": "sandboxOfficialImages",
|
||||
"sandbox file upload": "sandboxFileUpload",
|
||||
"sandbox list": "sandboxList",
|
||||
"sandbox get": "sandboxGet",
|
||||
"sandbox connect": "sandboxConnect",
|
||||
|
||||
@@ -228,6 +228,11 @@ export function ragEndpoint(workspaceId: string, path: string): string {
|
||||
|
||||
// ---- Sandbox control plane (workspace-based host, cn-beijing only) ----
|
||||
|
||||
/** AgentStudio workspace files; template uploads send source=sandbox_template. */
|
||||
export function agentStudioFilesPath(): string {
|
||||
return "/api/v1/agentstudio/files";
|
||||
}
|
||||
|
||||
/** Default Sandbox origin when no shared base URL was configured (cn-beijing only). */
|
||||
export function sandboxBaseUrl(workspaceId: string): string {
|
||||
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com`;
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
profileSchemaPath,
|
||||
ragEndpoint,
|
||||
RAG_PATHS,
|
||||
agentStudioFilesPath,
|
||||
sandboxEndpoint,
|
||||
sandboxBaseUrl,
|
||||
sandboxApiPath,
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
阿里云百炼 Sandbox 沙箱实例与模版生命周期管理入口:用户要创建、查询、连接、暂停、恢复或释放百炼沙箱,
|
||||
或查看内置基础镜像、创建、更新、查询、删除沙箱模版、查看模版构建状态时,使用 `bl sandbox`。
|
||||
或查看内置基础镜像、上传模版挂载文件、创建、更新、查询、删除沙箱模版、查看模版构建状态时,使用 `bl sandbox`。
|
||||
仅覆盖百炼 Sandbox 管控面;不用于宿主执行沙箱设置、E2B 官方云资源或沙箱内命令执行与文件传输。
|
||||
agents.yaml 托管 Agent / Session / Environment 管理交给 bailian-managed-agent。
|
||||
官方安装:`bl skill init`(与共享协议 bailian-protocol 同装)。
|
||||
@@ -19,21 +19,22 @@ Before running `bl`, read the shared [bailian-protocol](../bailian-protocol/SKIL
|
||||
## 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 Base URL through the same CLI chain as Managed Agent: `--base-url` > `DASHSCOPE_BASE_URL` > login/profile `base_url`. Use an origin such as `https://workspace.cn-beijing.maas.aliyuncs.com`; the CLI strips URL paths/query/fragment and appends `/api/v1/agentstudio/sandbox`. The saved API Key is reused. Profile capability fallback follows the shared protocol for both the key and Base URL.
|
||||
- Resolve Base URL through the same CLI chain as Managed Agent: `--base-url` > `DASHSCOPE_BASE_URL` > login/profile `base_url`. Use an origin such as `https://workspace.cn-beijing.maas.aliyuncs.com`; the CLI strips URL paths/query/fragment and appends `/api/v1/agentstudio/sandbox` for lifecycle operations, or `/api/v1/agentstudio/files` for template file uploads. The saved API Key is reused. Profile capability fallback follows the shared protocol for both the key and Base URL.
|
||||
- If no Base URL is configured, resolve the workspace from `--workspace-id`, then `BAILIAN_WORKSPACE_ID`, then configured `workspace_id`, and use `https://{workspace_id}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox`. With a configured Base URL, the workspace flag is optional. The service currently supports `cn-beijing` and requires prior Sandbox SLR authorization.
|
||||
- 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 |
|
||||
| ------------------------------------- | --------------------------------------------------------- |
|
||||
| Discover built-in base image presets | `bl sandbox official-images` (offline, no authentication) |
|
||||
| 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` |
|
||||
| User intent | Command family |
|
||||
| --------------------------------------- | --------------------------------------------------------- |
|
||||
| Discover built-in base image presets | `bl sandbox official-images` (offline, no authentication) |
|
||||
| Upload a local file for template mounts | `bl sandbox file upload` |
|
||||
| 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.
|
||||
|
||||
@@ -48,7 +49,8 @@ For built-in template images, discover the preset ID or Chinese name through `sa
|
||||
- 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`.
|
||||
- `--body` accepts a JSON object inline or through `@path`; explicit flags override body fields. For local template mounts, first use `bl sandbox file upload`: it sends multipart `file` and fixed `source=sandbox_template` directly to `/api/v1/agentstudio/files` with the Bailian API Key. Use the returned `id` as `mntConfig[].originFileId`, with `mountPath` and optional `originFileName`, in template create/update `--body`. The upload and template must use the same workspace. This is not the temporary OSS upload from `bl file upload` or a transfer into a running instance.
|
||||
- File upload returns immediately after the upload response and does not poll security review. `status=checking` is not ready to mount; only use files whose status is `available`. `--quiet` returns the File ID only; inspect the normal/JSON response for status. Upload alone does not authorize creating a template or instance.
|
||||
|
||||
## Common hand-offs
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
| `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 file upload` | API Key | Upload a workspace file for Sandbox template mounts | [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 official-images` | No Auth | List the built-in Sandbox base images (offline) | [sandbox.md](sandbox.md) |
|
||||
@@ -28,9 +29,9 @@ Use this index for the skill-scoped quick index and global flags.
|
||||
|
||||
## By group
|
||||
|
||||
| Group | Commands | Reference |
|
||||
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
||||
| `sandbox` | `connect`, `create`, `delete`, `get`, `list`, `official-images`, `pause`, `resume`, `template build-status`, `template create`, `template delete`, `template get`, `template list`, `template update` | [sandbox.md](sandbox.md) |
|
||||
| Group | Commands | Reference |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
||||
| `sandbox` | `connect`, `create`, `delete`, `file upload`, `get`, `list`, `official-images`, `pause`, `resume`, `template build-status`, `template create`, `template delete`, `template get`, `template list`, `template update` | [sandbox.md](sandbox.md) |
|
||||
|
||||
## Global flags
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ Index: [index.md](index.md)
|
||||
| `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 file upload` | API Key | Upload a workspace file for Sandbox template mounts |
|
||||
| `bl sandbox get` | API Key | Get Sandbox instance details |
|
||||
| `bl sandbox list` | API Key | List Sandbox instances |
|
||||
| `bl sandbox official-images` | No Auth | List the built-in Sandbox base images (offline) |
|
||||
@@ -162,6 +163,47 @@ bl sandbox delete --sandbox-id sbx-xxx --dry-run
|
||||
bl sandbox delete --sandbox-id sbx-xxx --yes
|
||||
```
|
||||
|
||||
### `bl sandbox file upload`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------------------- |
|
||||
| **Name** | `sandbox file upload` |
|
||||
| **Description** | Upload a workspace file for Sandbox template mounts |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl sandbox file upload --path <path> [--filename <name>] [--mime-type <type>]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
|
||||
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
|
||||
| `--path <path>` | string | yes | Local file path |
|
||||
| `--filename <name>` | string | no | Remote filename override |
|
||||
| `--mime-type <type>` | string | no | Multipart file MIME type (default: application/octet-stream) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- POST /api/v1/agentstudio/files with multipart fields file and source=sandbox_template. Uses a Bailian Bearer API Key, not Console authentication or an E2B key; no agents.yaml is needed.
|
||||
- Base URL follows Sandbox: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. Without one, --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id selects the cn-beijing origin. The upload path has no /sandbox prefix.
|
||||
- Returns the upload response immediately; --quiet prints only its id. Upload does not wait for security review: status=checking is not ready to mount. Use an available file's id as mntConfig[].originFileId in template create/update --body, together with mountPath and optional originFileName, in the same workspace. This does not transfer files into a running instance.
|
||||
- --dry-run previews the endpoint, source, and local path without reading or uploading the file. The service detects the MIME type and enforces upload limits.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl sandbox file upload --path ./config.json --output json
|
||||
```
|
||||
|
||||
```bash
|
||||
bl sandbox file upload --path ./config.json --quiet
|
||||
```
|
||||
|
||||
```bash
|
||||
bl sandbox file upload --path ./notes.txt --filename notes.txt --mime-type text/plain --dry-run --output json
|
||||
```
|
||||
|
||||
### `bl sandbox get`
|
||||
|
||||
| Field | Value |
|
||||
@@ -433,7 +475,7 @@ bl sandbox template build-status --template-id tpl-xxx --build-id build-xxx --ou
|
||||
- browser (浏览器): Chromium and a visual desktop for clicking, filling forms, and screenshots. fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/browser:v0.0.44
|
||||
- all-in-one (全能型): Code execution and browser capabilities together. fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/all-in-one:v0.0.44
|
||||
- --image overrides body fromImage/imageName; explicit --from-image/--image-name override the corresponding preset fields. Without --image, image behavior is unchanged.
|
||||
- File mounts and other complete nested structures can be supplied through --body.
|
||||
- For local file mounts, use sandbox file upload first; put the available file's id into --body mntConfig[].originFileId with mountPath and optional originFileName. Upload and template must use the same workspace. Other complete nested structures can also be supplied through --body.
|
||||
- By default the command waits for build status ready; --async returns the submitted build immediately.
|
||||
|
||||
#### Examples
|
||||
@@ -616,6 +658,7 @@ bl sandbox template list --limit 100 --output json
|
||||
- all-in-one (全能型): Code execution and browser capabilities together. fc-e2b-registry.cn-beijing.cr.aliyuncs.com/runtime/all-in-one:v0.0.44
|
||||
- --image overrides body fromImage/imageName; explicit --from-image/--image-name override the corresponding preset fields. Without --image, image behavior is unchanged.
|
||||
- Supplying envConfig or --env replaces the template's complete environment map.
|
||||
- Use sandbox file upload for local mount files. In --body mntConfig[], set originFileId to the available file's id and supply mountPath; upload and template must use the same workspace.
|
||||
- By default the command waits for build status ready; --async returns the submitted build immediately.
|
||||
|
||||
#### Examples
|
||||
|
||||
Reference in New Issue
Block a user