From ceef514a50c7229436eb6f898d3202f08498fb8a Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Wed, 9 Sep 2026 17:35:23 +0800 Subject: [PATCH] feat(sandbox): add template file upload with sandbox_template source --- packages/cli/src/commands.ts | 2 + .../commands/src/commands/sandbox/file.ts | 113 +++++++ .../commands/src/commands/sandbox/template.ts | 12 +- packages/commands/src/index.ts | 1 + .../tests/e2e/sandbox-file.e2e.test.ts | 312 ++++++++++++++++++ packages/commands/tests/e2e/topic-routes.ts | 1 + packages/core/src/client/endpoints.ts | 5 + packages/core/src/client/index.ts | 1 + skills/bailian-sandbox/SKILL.md | 26 +- skills/bailian-sandbox/reference/index.md | 7 +- skills/bailian-sandbox/reference/sandbox.md | 45 ++- 11 files changed, 507 insertions(+), 18 deletions(-) create mode 100644 packages/commands/src/commands/sandbox/file.ts create mode 100644 packages/commands/tests/e2e/sandbox-file.e2e.test.ts diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 5551caf..9009976 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -208,6 +208,7 @@ import { managedAgentFileDelete, sandboxCreate, sandboxOfficialImages, + sandboxFileUpload, sandboxList, sandboxGet, sandboxConnect, @@ -439,6 +440,7 @@ export const commands: Record = { "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, diff --git a/packages/commands/src/commands/sandbox/file.ts b/packages/commands/src/commands/sandbox/file.ts new file mode 100644 index 0000000..34f47e2 --- /dev/null +++ b/packages/commands/src/commands/sandbox/file.ts @@ -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 [--filename ] [--mime-type ]", + flags: { + ...WORKSPACE_FLAG, + path: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Local file path", "zh-CN": "本地文件路径" }, + }, + filename: { + type: "string", + valueHint: "", + description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" }, + }, + mimeType: { + type: "string", + valueHint: "", + 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>({ + 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); + }, +}); diff --git a/packages/commands/src/commands/sandbox/template.ts b/packages/commands/src/commands/sandbox/template.ts index 300cdfb..7bfcaf6 100644 --- a/packages/commands/src/commands/sandbox/template.ts +++ b/packages/commands/src/commands/sandbox/template.ts @@ -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.", diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index bf9c1a2..4ecbb49 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -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, diff --git a/packages/commands/tests/e2e/sandbox-file.e2e.test.ts b/packages/commands/tests/e2e/sandbox-file.e2e.test.ts new file mode 100644 index 0000000..c42a915 --- /dev/null +++ b/packages/commands/tests/e2e/sandbox-file.e2e.test.ts @@ -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((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 = {}) { + 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((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"); + }); +}); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 680993c..0b7d4ec 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -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", diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index 0a57d94..99518b9 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -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`; diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 45b9781..64c127f 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -18,6 +18,7 @@ export { profileSchemaPath, ragEndpoint, RAG_PATHS, + agentStudioFilesPath, sandboxEndpoint, sandboxBaseUrl, sandboxApiPath, diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index ca28922..0c2bd93 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -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 diff --git a/skills/bailian-sandbox/reference/index.md b/skills/bailian-sandbox/reference/index.md index 7b7cd66..d3ef521 100644 --- a/skills/bailian-sandbox/reference/index.md +++ b/skills/bailian-sandbox/reference/index.md @@ -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 diff --git a/skills/bailian-sandbox/reference/sandbox.md b/skills/bailian-sandbox/reference/sandbox.md index 6f5503f..e5616a6 100644 --- a/skills/bailian-sandbox/reference/sandbox.md +++ b/skills/bailian-sandbox/reference/sandbox.md @@ -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 [--filename ] [--mime-type ]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--path ` | string | yes | Local file path | +| `--filename ` | string | no | Remote filename override | +| `--mime-type ` | string | no | Multipart file MIME type (default: application/octet-stream) | +| `--api-key ` | string | no | API key | +| `--base-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