fix(sandbox): support shared --base-url configuration with workspace fallback

This commit is contained in:
chenanran555
2026-09-07 16:37:56 +08:00
parent 332efb05b8
commit c0fca869c2
18 changed files with 604 additions and 195 deletions
@@ -3,7 +3,6 @@ import {
detectOutputFormat,
defineCommand,
ExitCode,
sandboxEndpoint,
SANDBOX_PATHS,
sandboxInstanceActionPath,
sandboxInstancePath,
@@ -20,7 +19,7 @@ import {
readRequestBody,
redactConnectionCredentials,
redactRequestSecrets,
resolveWorkspaceId,
resolveSandboxEndpoint,
SANDBOX_NOTES,
setDefined,
SHOW_CREDENTIALS_FLAG,
@@ -222,14 +221,14 @@ export const sandboxCreate = defineCommand({
flags: CREATE_FLAGS,
exampleArgs: [
"--template-id tpl-xxx --instance-timeout 3600",
"--template-id tpl-xxx --base-url https://workspace.cn-beijing.maas.aliyuncs.com",
"--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 endpoint = resolveSandboxEndpoint(ctx, SANDBOX_PATHS.sandboxes);
const body = await buildSandboxCreateBody(ctx.flags);
if (ctx.settings.dryRun) {
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
@@ -265,8 +264,7 @@ export const sandboxList = defineCommand({
return undefined;
},
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const url = new URL(sandboxEndpoint(workspaceId, SANDBOX_PATHS.sandboxList));
const url = new URL(resolveSandboxEndpoint(ctx, 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);
@@ -314,8 +312,7 @@ export const sandboxGet = defineCommand({
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 endpoint = resolveSandboxEndpoint(ctx, sandboxInstancePath(ctx.flags.sandboxId));
const response = await ctx.client.requestJson<SandboxInfo>({ path: endpoint, method: "GET" });
emitSandboxObject(response, {
format: detectOutputFormat(ctx.settings.output),
@@ -343,9 +340,8 @@ function connectionCommand(action: "connect" | "resume") {
notes: SANDBOX_NOTES,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(
workspaceId,
const endpoint = resolveSandboxEndpoint(
ctx,
sandboxInstanceActionPath(ctx.flags.sandboxId, action),
);
const body = await buildConnectionBody(ctx.flags);
@@ -379,9 +375,8 @@ export const sandboxPause = defineCommand({
notes: SANDBOX_NOTES,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(
workspaceId,
const endpoint = resolveSandboxEndpoint(
ctx,
sandboxInstanceActionPath(ctx.flags.sandboxId, "pause"),
);
if (ctx.settings.dryRun) {
@@ -410,8 +405,7 @@ export const sandboxDelete = defineCommand({
notes: SANDBOX_NOTES,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxInstancePath(ctx.flags.sandboxId));
const endpoint = resolveSandboxEndpoint(ctx, sandboxInstancePath(ctx.flags.sandboxId));
if (ctx.settings.dryRun) {
emitResult({ method: "DELETE", endpoint, request: null }, format);
return;
@@ -1,13 +1,21 @@
import { readFile } from "node:fs/promises";
import { BailianError, ExitCode, type FlagsDef, type LocalizedText } from "bailian-cli-core";
import {
BailianError,
ExitCode,
sandboxApiPath,
sandboxBaseUrl,
type Client,
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)",
"en-US": "Workspace ID for the default Sandbox endpoint; optional with a configured base URL",
"zh-CN": "默认 Sandbox Endpoint 的 Workspace ID;已配置 Base URL 时可省略",
},
},
} satisfies FlagsDef;
@@ -62,8 +70,15 @@ export const SANDBOX_NOTES: LocalizedText[] = [
},
{
"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 解析。",
"Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.",
"zh-CN":
"Base URL 优先级:--base-url > DASHSCOPE_BASE_URL > 登录/Profile 的 base_url。CLI 取其 origin 并追加 /api/v1/agentstudio/sandbox;未配置时使用工作空间的 cn-beijing Endpoint。",
},
{
"en-US":
"Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > config workspace_id.",
"zh-CN":
"未配置 Base URL 时必须提供 Workspace:--workspace-id > BAILIAN_WORKSPACE_ID > 配置项 workspace_id。",
},
{
"en-US":
@@ -80,17 +95,23 @@ export const SANDBOX_NOTES: LocalizedText[] = [
export type JsonObject = Record<string, unknown>;
export function resolveWorkspaceId(ctx: {
interface SandboxEndpointContext {
flags: { workspaceId?: string };
settings: { workspaceId?: string };
identity: { binName: string };
}): string {
client: Pick<Client, "url">;
}
export function resolveSandboxEndpoint(ctx: SandboxEndpointContext, path: string): string {
return ctx.client.url(sandboxApiPath(path), () => sandboxBaseUrl(resolveWorkspaceId(ctx)));
}
export function resolveWorkspaceId(ctx: Omit<SandboxEndpointContext, "client">): string {
const workspaceId = ctx.flags.workspaceId || ctx.settings.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Workspace ID is required.",
"Workspace ID is required when no base URL is configured. / 未配置 Base URL 时必须提供 Workspace ID。",
ExitCode.USAGE,
`Pass --workspace-id, set BAILIAN_WORKSPACE_ID, or configure: ${ctx.identity.binName} config set workspace_id <id>`,
"Pass --workspace-id, set BAILIAN_WORKSPACE_ID, configure workspace_id, or set --base-url. / 请传入 --workspace-id、设置 BAILIAN_WORKSPACE_ID 或 workspace_id 配置,或通过 --base-url 指定地址。",
);
}
return workspaceId;
@@ -3,7 +3,7 @@ import {
detectOutputFormat,
defineCommand,
ExitCode,
sandboxEndpoint,
sandboxApiPath,
SANDBOX_PATHS,
sandboxTemplateBuildStatusPath,
sandboxTemplatePath,
@@ -21,7 +21,7 @@ import {
POLL_INTERVAL_FLAG,
readRequestBody,
redactRequestSecrets,
resolveWorkspaceId,
resolveSandboxEndpoint,
SANDBOX_NOTES,
setDefined,
validateIntegerRange,
@@ -312,7 +312,7 @@ function validatePollInterval(flags: { pollInterval?: number }): string | undefi
async function emitTemplateMutationResult(options: {
response: TemplateInfo;
workspaceId: string;
endpoint: string;
client: Client;
settings: Settings;
async: boolean;
@@ -333,10 +333,13 @@ async function emitTemplateMutationResult(options: {
}
return;
}
const buildEndpoint = sandboxEndpoint(
options.workspaceId,
sandboxTemplateBuildStatusPath(options.response.templateID, options.response.buildID),
);
// Poll the same origin that accepted the build, including custom gateways.
const buildEndpoint = new URL(
sandboxApiPath(
sandboxTemplateBuildStatusPath(options.response.templateID, options.response.buildID),
),
options.endpoint,
).toString();
const build = await waitForTemplateBuild(
options.client,
options.settings,
@@ -372,8 +375,7 @@ export const sandboxTemplateCreate = defineCommand({
validate: validatePollInterval,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, SANDBOX_PATHS.templateCreate);
const endpoint = resolveSandboxEndpoint(ctx, SANDBOX_PATHS.templateCreate);
const body = await buildTemplateCreateBody(ctx.flags);
if (ctx.settings.dryRun) {
emitResult({ method: "POST", endpoint, request: redactRequestSecrets(body) }, format);
@@ -386,7 +388,7 @@ export const sandboxTemplateCreate = defineCommand({
});
await emitTemplateMutationResult({
response,
workspaceId,
endpoint,
client: ctx.client,
settings: ctx.settings,
async: ctx.flags.async,
@@ -419,8 +421,7 @@ export const sandboxTemplateList = defineCommand({
return undefined;
},
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const url = new URL(sandboxEndpoint(workspaceId, SANDBOX_PATHS.templateList));
const url = new URL(resolveSandboxEndpoint(ctx, 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[]>({
@@ -466,8 +467,7 @@ export const sandboxTemplateGet = defineCommand({
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 endpoint = resolveSandboxEndpoint(ctx, 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));
@@ -499,8 +499,7 @@ export const sandboxTemplateUpdate = defineCommand({
validate: validatePollInterval,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxTemplatePath(ctx.flags.templateId));
const endpoint = resolveSandboxEndpoint(ctx, sandboxTemplatePath(ctx.flags.templateId));
const body = await buildTemplateUpdateBody(ctx.flags);
if (ctx.settings.dryRun) {
emitResult({ method: "PUT", endpoint, request: redactRequestSecrets(body) }, format);
@@ -513,7 +512,7 @@ export const sandboxTemplateUpdate = defineCommand({
});
await emitTemplateMutationResult({
response,
workspaceId,
endpoint,
client: ctx.client,
settings: ctx.settings,
async: ctx.flags.async,
@@ -536,9 +535,8 @@ export const sandboxTemplateBuildStatus = defineCommand({
],
notes: SANDBOX_NOTES,
async run(ctx) {
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(
workspaceId,
const endpoint = resolveSandboxEndpoint(
ctx,
sandboxTemplateBuildStatusPath(ctx.flags.templateId, ctx.flags.buildId),
);
const response = await ctx.client.requestJson<TemplateBuildStatus>({
@@ -573,8 +571,7 @@ export const sandboxTemplateDelete = defineCommand({
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const workspaceId = resolveWorkspaceId(ctx);
const endpoint = sandboxEndpoint(workspaceId, sandboxTemplatePath(ctx.flags.templateId));
const endpoint = resolveSandboxEndpoint(ctx, sandboxTemplatePath(ctx.flags.templateId));
if (ctx.settings.dryRun) {
emitResult({ method: "DELETE", endpoint, request: null }, format);
return;
@@ -0,0 +1,233 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer, 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 } from "./helpers.ts";
import { SANDBOX_ROUTES } from "./topic-routes.ts";
const ROUTES = { ...SANDBOX_ROUTES, "auth login": "authLogin" } as const;
const tempDirectories: string[] = [];
const servers: Server[] = [];
const API_PATH = "/api/v1/agentstudio/sandbox";
const PROFILE_CONFIG = {
api_key: "sk-default-test",
base_url: "https://default.example.test",
active_config: "sandbox-test",
"sandbox-test": {
api_key: "sk-profile-test",
base_url: "https://profile.example.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 tempDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
function makeConfigEnv(config: Record<string, unknown> = {}): NodeJS.ProcessEnv {
const directory = mkdtempSync(join(tmpdir(), "bl-sandbox-base-url-"));
tempDirectories.push(directory);
writeFileSync(join(directory, "config.json"), JSON.stringify(config));
return {
BAILIAN_CONFIG_DIR: directory,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
BAILIAN_WORKSPACE_ID: "",
};
}
describe("e2e: Sandbox shared base URL resolution", () => {
test.each([
{
name: "flag overrides env and profile, normalizing paths and preserving the port",
args: ["--base-url", "https://flag.example.test:8443/api/v1/agentstudio/?ignored=1#fragment"],
envBaseUrl: "https://env.example.test",
config: PROFILE_CONFIG,
expectedOrigin: "https://flag.example.test:8443",
},
{
name: "env overrides the active profile",
args: [],
envBaseUrl: "https://env.example.test/compatible-mode/v1/",
config: PROFILE_CONFIG,
expectedOrigin: "https://env.example.test",
},
{
name: "the active profile supplies its configured origin",
args: [],
envBaseUrl: "",
config: PROFILE_CONFIG,
expectedOrigin: "https://profile.example.test",
},
{
name: "an explicit profile selection uses that profile",
args: ["--config", "default"],
envBaseUrl: "",
config: PROFILE_CONFIG,
expectedOrigin: "https://default.example.test",
},
{
name: "an explicit default model origin still overrides workspace inference",
args: ["--base-url", "https://dashscope.aliyuncs.com", "--workspace-id", "unused"],
envBaseUrl: "",
config: {},
expectedOrigin: "https://dashscope.aliyuncs.com",
},
{
name: "without any base URL the workspace endpoint is preserved",
args: ["--workspace-id", "ws-test"],
envBaseUrl: "",
config: {},
expectedOrigin: "https://ws-test.cn-beijing.maas.aliyuncs.com",
},
])("$name", async ({ args, envBaseUrl, config, expectedOrigin }) => {
const result = await runCommandE2e(
ROUTES,
["sandbox", "create", "--template-id", "tpl-test", ...args, "--dry-run", "--output", "json"],
{ ...makeConfigEnv(config), DASHSCOPE_BASE_URL: envBaseUrl },
);
expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson(result.stdout)).toMatchObject({
endpoint: `${expectedOrigin}${API_PATH}/sandboxes`,
});
});
test("an unconfigured URL still requires a workspace, while malformed URLs fail validation", async () => {
const env = makeConfigEnv();
for (const args of [[], ["--base-url", "invalid"], ["--base-url", "file:///tmp/gateway"]]) {
const result = await runCommandE2e(
ROUTES,
[
"sandbox",
"create",
"--template-id",
"tpl-test",
...args,
"--dry-run",
"--output",
"json",
],
env,
);
expect(result.exitCode).toBe(2);
expect(JSON.parse(result.stderr).error.message).toMatch(
/Workspace ID|Invalid model base URL/,
);
}
});
test("auth login persists a base URL that Sandbox uses without a workspace flag", async () => {
const env = makeConfigEnv();
const login = await runCommandE2e(
ROUTES,
[
"auth",
"login",
"--config",
"sandbox-test",
"--api-key",
"sk-login-test",
"--base-url",
"https://login.example.test/api/v1/agentstudio/sandbox/",
],
env,
);
expect(login.exitCode, login.stderr).toBe(0);
const stored = JSON.parse(readFileSync(join(env.BAILIAN_CONFIG_DIR!, "config.json"), "utf8"));
expect(stored.active_config).toBe("sandbox-test");
expect(stored["sandbox-test"].base_url).toBe("https://login.example.test");
const result = await runCommandE2e(
ROUTES,
["sandbox", "pause", "--sandbox-id", "sbx-test", "--dry-run", "--output", "json"],
env,
);
expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson(result.stdout)).toMatchObject({
endpoint: `https://login.example.test${API_PATH}/sandboxes/sbx-test/pause`,
});
});
});
describe("e2e: Sandbox custom gateway transport", () => {
test.each([
{
action: "create",
args: ["--name", "python", "--cpu-count", "1", "--memory-mb", "2048"],
method: "POST",
suffix: "/v3/templates",
},
{
action: "update",
args: ["--template-id", "template/a", "--description", "updated"],
method: "PUT",
suffix: "/templates/template%2Fa",
},
])(
"template $action and build polling use the same gateway and Bearer key",
async ({ action, args, method, suffix }) => {
const received: {
method?: string;
path?: string;
authorization?: string;
e2bKey?: string | string[];
}[] = [];
const server = createServer((request, response) => {
request.resume();
received.push({
method: request.method,
path: request.url,
authorization: request.headers.authorization,
e2bKey: request.headers["x-api-key"],
});
response.setHeader("content-type", "application/json");
response.end(
JSON.stringify({
templateID: "template/a",
buildID: "build a",
status: request.method === "GET" ? "ready" : "building",
}),
);
});
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.");
const origin = `http://127.0.0.1:${address.port}`;
const result = await runCommandE2e(
ROUTES,
["sandbox", "template", action, ...args, "--poll-interval", "1", "--output", "json"],
makeConfigEnv({ api_key: "sk-gateway-test", base_url: origin }),
);
expect(result.exitCode, result.stderr).toBe(0);
expect(received).toEqual([
{
method,
path: `${API_PATH}${suffix}`,
authorization: "Bearer sk-gateway-test",
e2bKey: undefined,
},
{
method: "GET",
path: `${API_PATH}/templates/template%2Fa/builds/build%20a/status`,
authorization: "Bearer sk-gateway-test",
e2bKey: undefined,
},
]);
expect(parseStdoutJson(result.stdout)).toMatchObject({ build: { status: "ready" } });
},
);
});
@@ -1,8 +1,30 @@
import { describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e, runCommandHelp } from "./helpers.ts";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandE2e as runBaseCommandE2e, runCommandHelp } from "./helpers.ts";
import { SANDBOX_ROUTES } from "./topic-routes.ts";
const AUTH_ARGS = ["--api-key", "sk-sandbox-e2e", "--workspace-id", "ws-e2e"];
let configDirectory: string;
beforeEach(() => {
configDirectory = mkdtempSync(join(tmpdir(), "bl-sandbox-e2e-"));
writeFileSync(join(configDirectory, "config.json"), "{}");
});
afterEach(() => {
rmSync(configDirectory, { recursive: true, force: true });
});
function runCommandE2e(routes: typeof SANDBOX_ROUTES, args: string[]) {
return runBaseCommandE2e(routes, args, {
BAILIAN_CONFIG_DIR: configDirectory,
DASHSCOPE_BASE_URL: "",
DASHSCOPE_API_KEY: "",
BAILIAN_WORKSPACE_ID: "",
});
}
describe("e2e: Sandbox command discovery", () => {
test.each([
+85 -51
View File
@@ -1,7 +1,7 @@
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 { BailianError, Client, ExitCode, type AnyCommand, type Settings } from "bailian-cli-core";
import { afterEach, describe, expect, test, vi } from "vite-plus/test";
import {
buildSandboxCreateBody,
@@ -51,10 +51,26 @@ interface RecordedRequest {
timeout?: number;
}
function createUrlResolver(baseUrl?: string): Client["url"] {
const client = new Client({
identity: {
binName: "bl",
version: "test",
npmPackage: "bailian-cli",
clientName: "bailian-cli",
},
settings: SETTINGS,
baseUrl: baseUrl ?? "https://dashscope.aliyuncs.com",
baseUrlIsDefault: baseUrl === undefined,
});
return client.url.bind(client);
}
async function runCommand(
command: AnyCommand,
flags: Record<string, unknown>,
response: unknown,
baseUrl?: string,
): Promise<RecordedRequest> {
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const requestJson = vi.fn(async (_request: RecordedRequest) => response);
@@ -71,7 +87,7 @@ async function runCommand(
},
settings: SETTINGS,
flags,
client: { requestJson, request },
client: { requestJson, request, url: createUrlResolver(baseUrl) },
} as never);
const recorded = requestJson.mock.calls[0]?.[0] ?? request.mock.calls[0]?.[0];
@@ -336,6 +352,20 @@ describe("Sandbox command transport", () => {
"maps $name to its documented method and path",
async ({ command, flags, response, request }) => {
expect(await runCommand(command, flags, response)).toEqual(request);
expect(
await runCommand(
command,
{ ...flags, workspaceId: undefined },
response,
"https://gateway.example.test",
),
).toEqual({
...request,
path: request.path.replace(
"https://ws-test.cn-beijing.maas.aliyuncs.com",
"https://gateway.example.test",
),
});
},
);
@@ -357,6 +387,7 @@ describe("Sandbox command transport", () => {
showCredentials: testCase.showCredentials,
},
client: {
url: createUrlResolver(),
requestJson: async () => ({
sandboxID: "sandbox-test",
envdAccessToken: "envd-secret",
@@ -435,62 +466,65 @@ describe("Sandbox template build polling", () => {
memoryMb: 2048,
async: true,
},
client: { requestJson },
client: { requestJson, url: createUrlResolver() },
} 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",
test.each([undefined, "https://gateway.example.test"])(
"template creation polls the selected origin %s and emits the final envelope",
async (baseUrl) => {
let stdout = "";
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
stdout += String(chunk);
return true;
});
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);
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, url: createUrlResolver(baseUrl) },
} 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",
},
});
});
expect(requestJson).toHaveBeenCalledTimes(2);
expect(requestJson.mock.calls[1]?.[0]).toMatchObject({
method: "GET",
path: `${baseUrl ?? "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 () => {
+1
View File
@@ -4,6 +4,7 @@ export {
resolveOpenApi,
describeAuthState,
resolveModelBaseUrl,
resolveModelBaseUrlState,
} from "./resolver.ts";
export { makeAuthStore, type AuthStore, type AuthPersistPatch } from "./store.ts";
export type {
+14 -3
View File
@@ -8,11 +8,22 @@ import { ExitCode } from "../errors/codes.ts";
// Resolve the credential for a command's declared domain (model = api-key,
// console = access-token), by priority, or throw. Read only from sources.
/** Preserve whether the shared base URL chain selected a configured origin or its default. */
export function resolveModelBaseUrlState(
sources: ResolutionSources,
fallback: string = REGIONS.cn,
): { baseUrl: string; baseUrlIsDefault: boolean } {
const configuredBaseUrl =
sources.flags.baseUrl || sources.env.DASHSCOPE_BASE_URL || sources.file.base_url;
return {
baseUrl: normalizeModelBaseUrl(configuredBaseUrl || fallback),
baseUrlIsDefault: !configuredBaseUrl,
};
}
/** Model-domain baseUrl(flag > env > config file > fallback);无需 key 也可解析。 */
export function resolveModelBaseUrl(s: ResolutionSources, fallback: string = REGIONS.cn): string {
return normalizeModelBaseUrl(
s.flags.baseUrl || s.env.DASHSCOPE_BASE_URL || s.file.base_url || fallback,
);
return resolveModelBaseUrlState(s, fallback).baseUrl;
}
/**
+9 -3
View File
@@ -23,6 +23,8 @@ export interface ClientDeps {
settings: Settings;
/** Model 域 base URL(凭证无关链解析,resolveModelBaseUrl;有 apiCred 时两者一致)。 */
baseUrl: string;
/** True only when the shared URL chain used its default; explicit origins win over service defaults. */
baseUrlIsDefault?: boolean;
apiCred?: ApiKeyCredential;
consoleCred?: ConsoleCredential;
openApiCred?: OpenApiCredential;
@@ -108,9 +110,13 @@ export class Client {
return this.deps.apiCred;
}
/** Full URL for a model-domain {@link path}; build request/display URLs only through this. */
url(path: string): string {
return this.baseUrl + path;
/**
* Full URL for a model-domain path. Services may supply a lazy default origin,
* evaluated only when no flag/env/profile base URL was configured.
*/
url(path: string, defaultBaseUrl?: () => string): string {
const baseUrl = this.deps.baseUrlIsDefault && defaultBaseUrl ? defaultBaseUrl() : this.baseUrl;
return baseUrl + path;
}
private toOpts({ path, ...rest }: ClientRequestOpts): RequestOpts {
+12 -7
View File
@@ -228,14 +228,19 @@ export function ragEndpoint(workspaceId: string, path: string): string {
// ---- 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.
*/
/** 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`;
}
/** Sandbox service prefix, appended to the selected origin just like AgentStudio SDK paths. */
export function sandboxApiPath(path: string): string {
return `/api/v1/agentstudio/sandbox${path}`;
}
/** Build the default workspace-scoped absolute Sandbox control-plane URL. */
export function sandboxEndpoint(workspaceId: string, path: string): string {
return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio/sandbox${path}`;
return sandboxBaseUrl(workspaceId) + sandboxApiPath(path);
}
export const SANDBOX_PATHS = {
+2
View File
@@ -19,6 +19,8 @@ export {
ragEndpoint,
RAG_PATHS,
sandboxEndpoint,
sandboxBaseUrl,
sandboxApiPath,
SANDBOX_PATHS,
sandboxInstancePath,
sandboxInstanceActionPath,
@@ -5,6 +5,7 @@ import {
resolveApiKey,
resolveConsole,
resolveModelBaseUrl,
resolveModelBaseUrlState,
resolveOpenApi,
} from "../src/auth/resolver.ts";
import { getModelProfilePreset } from "../src/config/profile-presets.ts";
@@ -98,6 +99,24 @@ test("baseUrl:非法 flag/env 在 resolver 边界报 usage error", () => {
).toThrow(/Invalid model base URL/);
});
test("baseUrl state distinguishes an implicit default from an explicitly configured default origin", () => {
const defaultOrigin = "https://dashscope.aliyuncs.com";
expect(resolveModelBaseUrlState(src({}))).toEqual({
baseUrl: defaultOrigin,
baseUrlIsDefault: true,
});
for (const sources of [
src({ flags: { baseUrl: defaultOrigin } }),
src({ env: { DASHSCOPE_BASE_URL: defaultOrigin } }),
src({ file: { base_url: defaultOrigin } }),
]) {
expect(resolveModelBaseUrlState(sources)).toEqual({
baseUrl: defaultOrigin,
baseUrlIsDefault: false,
});
}
});
test("命名 config 仍保持 flag > env > selected file", () => {
const env = {
DASHSCOPE_BASE_URL: "https://env.example.com",
+23
View File
@@ -142,6 +142,29 @@ test("Client sends Sandbox REST requests with Bearer auth and no E2B API key", a
}
});
test("Client.url evaluates a service default only when the shared origin was not configured", () => {
const deps = testDeps();
const defaultClient = new Client({
...deps,
baseUrl: "https://dashscope.aliyuncs.com",
baseUrlIsDefault: true,
});
const configuredClient = new Client({
...deps,
baseUrl: "https://dashscope.aliyuncs.com",
baseUrlIsDefault: false,
});
expect(defaultClient.url("/service")).toBe("https://dashscope.aliyuncs.com/service");
expect(defaultClient.url("/service", () => "https://workspace.example.test")).toBe(
"https://workspace.example.test/service",
);
expect(
configuredClient.url("/service", () => {
throw new Error("A configured origin must not require a workspace default.");
}),
).toBe("https://dashscope.aliyuncs.com/service");
});
test("BailianError propagates cause via options-bag and exposes it in toJSON", () => {
const root = Object.assign(new Error("getaddrinfo ENOTFOUND example.invalid"), {
code: "ENOTFOUND",
+2 -2
View File
@@ -29,7 +29,7 @@ import {
buildSources,
buildSettings,
describeAuthState,
resolveModelBaseUrl,
resolveModelBaseUrlState,
makeConfigStore,
makeAuthStore,
flushTelemetry,
@@ -238,7 +238,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
client: new Client({
identity,
settings,
baseUrl: resolveModelBaseUrl(sources),
...resolveModelBaseUrlState(sources),
}),
};
await runMiddleware(ctx);
+3 -3
View File
@@ -19,7 +19,7 @@ import {
resolveApiKey,
resolveConsole,
resolveOpenApi,
resolveModelBaseUrl,
resolveModelBaseUrlState,
selectApiKeyResolutionSources,
trackCommandExecution,
} from "bailian-cli-core";
@@ -128,7 +128,7 @@ export const authStage: Middleware = async (ctx, next) => {
const base = {
identity: ctx.identity,
settings,
baseUrl: resolveModelBaseUrl(sources),
...resolveModelBaseUrlState(sources),
};
if (command.auth === "apiKey") {
const capability = ctx.path.join(".");
@@ -145,7 +145,7 @@ export const authStage: Middleware = async (ctx, next) => {
}
ctx.client = new Client({
...base,
baseUrl: resolveModelBaseUrl(apiSources),
...resolveModelBaseUrlState(apiSources),
apiCred: cred,
});
if (cred) maybeShowStatusBar(settings, cred.token, cred);
@@ -81,6 +81,28 @@ async function runAuth(context: RunContext): Promise<void> {
await authStage(context, async () => {});
}
test.each([undefined, "https://default.example.com", "https://dashscope.aliyuncs.com"])(
"service defaults follow the effective profile base URL after capability fallback: %s",
async (defaultBaseUrl) => {
useTempConfigDir();
await writeConfigFile({ api_key: "sk-default", base_url: defaultBaseUrl });
await writeConfigFile(
{
api_key: "sk-plan",
base_url: "https://plan.example.com",
api_key_capabilities: ["text.chat"],
},
"company-plan",
);
const context = makeContext(["sandbox", "list"]);
await runAuth(context);
expect(context.client.url("/service", () => "https://workspace.example.test")).toBe(
`${defaultBaseUrl ?? "https://workspace.example.test"}/service`,
);
expect(context.client.exportApiCredential()?.token).toBe("sk-default");
},
);
async function captureStderr(operation: () => Promise<void>): Promise<{
output: string;
error?: unknown;
+4 -2
View File
@@ -19,8 +19,8 @@ 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 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.
- 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.
- 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
@@ -36,6 +36,8 @@ Before running `bl`, read the shared [bailian-protocol](../bailian-protocol/SKIL
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.
To save the origin in an isolated Profile, use `bl auth login --config sandbox --api-key <key> --base-url <origin>`. For a one-command override, use `bl sandbox list --base-url <origin>`. Custom gateways also apply to template build-status polling.
## 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.
+91 -74
View File
@@ -38,7 +38,7 @@ Index: [index.md](index.md)
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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) |
@@ -49,7 +49,8 @@ Index: [index.md](index.md)
#### 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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -76,7 +77,7 @@ bl sandbox connect --sandbox-id sbx-xxx --show-credentials --output json
| Flag | Type | Required | Description |
| -------------------------------- | ------- | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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) |
@@ -95,7 +96,8 @@ bl sandbox connect --sandbox-id sbx-xxx --show-credentials --output json
#### 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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -105,6 +107,10 @@ bl sandbox connect --sandbox-id sbx-xxx --show-credentials --output json
bl sandbox create --template-id tpl-xxx --instance-timeout 3600
```
```bash
bl sandbox create --template-id tpl-xxx --base-url https://workspace.cn-beijing.maas.aliyuncs.com
```
```bash
bl sandbox create --body @sandbox.json --dry-run --output json
```
@@ -128,18 +134,19 @@ bl sandbox create --template-id tpl-xxx --show-credentials --output json
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -165,18 +172,19 @@ bl sandbox delete --sandbox-id sbx-xxx --yes
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -201,20 +209,21 @@ bl sandbox get --sandbox-id sbx-xxx --show-credentials --output json
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -243,17 +252,18 @@ bl sandbox list --template-id tpl-xxx --output json
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -280,7 +290,7 @@ bl sandbox pause --sandbox-id sbx-xxx --dry-run --output json
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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) |
@@ -291,7 +301,8 @@ bl sandbox pause --sandbox-id sbx-xxx --dry-run --output json
#### 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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -316,18 +327,19 @@ bl sandbox resume --sandbox-id sbx-xxx --show-credentials --output json
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -354,7 +366,7 @@ bl sandbox template build-status --template-id tpl-xxx --build-id build-xxx --ou
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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 |
@@ -377,7 +389,8 @@ bl sandbox template build-status --template-id tpl-xxx --build-id build-xxx --ou
#### 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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -412,18 +425,19 @@ bl sandbox template create --name browser --cpu-count 4 --memory-mb 8192 --dry-r
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -450,17 +464,18 @@ bl sandbox template delete --template-id tpl-xxx --yes
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -485,18 +500,19 @@ bl sandbox template get --template-id tpl-xxx --output json
#### 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 |
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.
@@ -524,7 +540,7 @@ bl sandbox template list --limit 100 --output json
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) |
| `--workspace-id <id>` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL |
| `--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 |
@@ -546,7 +562,8 @@ bl sandbox template list --limit 100 --output json
#### 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.
- Base URL: --base-url > DASHSCOPE_BASE_URL > login/profile base_url. The CLI uses its origin and appends /api/v1/agentstudio/sandbox; otherwise it uses the workspace-scoped cn-beijing endpoint.
- Without a configured base URL, workspace is required: --workspace-id > BAILIAN_WORKSPACE_ID > 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.