diff --git a/packages/commands/src/commands/sandbox/instance.ts b/packages/commands/src/commands/sandbox/instance.ts index 98e6142..1893083 100644 --- a/packages/commands/src/commands/sandbox/instance.ts +++ b/packages/commands/src/commands/sandbox/instance.ts @@ -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({ 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; diff --git a/packages/commands/src/commands/sandbox/shared.ts b/packages/commands/src/commands/sandbox/shared.ts index 106c270..f03a65f 100644 --- a/packages/commands/src/commands/sandbox/shared.ts +++ b/packages/commands/src/commands/sandbox/shared.ts @@ -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: "", 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; -export function resolveWorkspaceId(ctx: { +interface SandboxEndpointContext { flags: { workspaceId?: string }; settings: { workspaceId?: string }; - identity: { binName: string }; -}): string { + client: Pick; +} + +export function resolveSandboxEndpoint(ctx: SandboxEndpointContext, path: string): string { + return ctx.client.url(sandboxApiPath(path), () => sandboxBaseUrl(resolveWorkspaceId(ctx))); +} + +export function resolveWorkspaceId(ctx: Omit): 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 `, + "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; diff --git a/packages/commands/src/commands/sandbox/template.ts b/packages/commands/src/commands/sandbox/template.ts index c23b146..b368f42 100644 --- a/packages/commands/src/commands/sandbox/template.ts +++ b/packages/commands/src/commands/sandbox/template.ts @@ -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({ @@ -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({ 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({ @@ -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; diff --git a/packages/commands/tests/e2e/sandbox-base-url.e2e.test.ts b/packages/commands/tests/e2e/sandbox-base-url.e2e.test.ts new file mode 100644 index 0000000..ab0f592 --- /dev/null +++ b/packages/commands/tests/e2e/sandbox-base-url.e2e.test.ts @@ -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((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 = {}): 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((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" } }); + }, + ); +}); diff --git a/packages/commands/tests/e2e/sandbox.e2e.test.ts b/packages/commands/tests/e2e/sandbox.e2e.test.ts index 5929c78..0e3b7e3 100644 --- a/packages/commands/tests/e2e/sandbox.e2e.test.ts +++ b/packages/commands/tests/e2e/sandbox.e2e.test.ts @@ -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([ diff --git a/packages/commands/tests/sandbox.test.ts b/packages/commands/tests/sandbox.test.ts index 7288194..2c7f813 100644 --- a/packages/commands/tests/sandbox.test.ts +++ b/packages/commands/tests/sandbox.test.ts @@ -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, response: unknown, + baseUrl?: string, ): Promise { 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 () => { diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 00acf13..3d46316 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -4,6 +4,7 @@ export { resolveOpenApi, describeAuthState, resolveModelBaseUrl, + resolveModelBaseUrlState, } from "./resolver.ts"; export { makeAuthStore, type AuthStore, type AuthPersistPatch } from "./store.ts"; export type { diff --git a/packages/core/src/auth/resolver.ts b/packages/core/src/auth/resolver.ts index 08c9602..faf2241 100644 --- a/packages/core/src/auth/resolver.ts +++ b/packages/core/src/auth/resolver.ts @@ -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; } /** diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index 785e2c1..067c523 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -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 { diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index a9ed891..0a57d94 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -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 = { diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 50ee01c..45b9781 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -19,6 +19,8 @@ export { ragEndpoint, RAG_PATHS, sandboxEndpoint, + sandboxBaseUrl, + sandboxApiPath, SANDBOX_PATHS, sandboxInstancePath, sandboxInstanceActionPath, diff --git a/packages/core/tests/config-priority.test.ts b/packages/core/tests/config-priority.test.ts index 9d5ce1b..96375fb 100644 --- a/packages/core/tests/config-priority.test.ts +++ b/packages/core/tests/config-priority.test.ts @@ -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", diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index 914fb68..ba3a73a 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -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", diff --git a/packages/runtime/src/create-cli.ts b/packages/runtime/src/create-cli.ts index 1cf98e3..2ed40b4 100644 --- a/packages/runtime/src/create-cli.ts +++ b/packages/runtime/src/create-cli.ts @@ -29,7 +29,7 @@ import { buildSources, buildSettings, describeAuthState, - resolveModelBaseUrl, + resolveModelBaseUrlState, makeConfigStore, makeAuthStore, flushTelemetry, @@ -238,7 +238,7 @@ export function createCli(commands: Record, opts: CliOptions client: new Client({ identity, settings, - baseUrl: resolveModelBaseUrl(sources), + ...resolveModelBaseUrlState(sources), }), }; await runMiddleware(ctx); diff --git a/packages/runtime/src/middleware.ts b/packages/runtime/src/middleware.ts index eed4da4..6609169 100644 --- a/packages/runtime/src/middleware.ts +++ b/packages/runtime/src/middleware.ts @@ -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); diff --git a/packages/runtime/tests/auth-profile-fallback.test.ts b/packages/runtime/tests/auth-profile-fallback.test.ts index 11221f2..d102b3a 100644 --- a/packages/runtime/tests/auth-profile-fallback.test.ts +++ b/packages/runtime/tests/auth-profile-fallback.test.ts @@ -81,6 +81,28 @@ async function runAuth(context: RunContext): Promise { 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): Promise<{ output: string; error?: unknown; diff --git a/skills/bailian-sandbox/SKILL.md b/skills/bailian-sandbox/SKILL.md index 15ec6b4..6360c80 100644 --- a/skills/bailian-sandbox/SKILL.md +++ b/skills/bailian-sandbox/SKILL.md @@ -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 --base-url `. For a one-command override, use `bl sandbox list --base-url `. 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. diff --git a/skills/bailian-sandbox/reference/sandbox.md b/skills/bailian-sandbox/reference/sandbox.md index ebada91..953e500 100644 --- a/skills/bailian-sandbox/reference/sandbox.md +++ b/skills/bailian-sandbox/reference/sandbox.md @@ -38,7 +38,7 @@ Index: [index.md](index.md) | Flag | Type | Required | Description | | ------------------------------ | ------ | -------- | ---------------------------------------------------------------------------------- | -| `--workspace-id ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | | `--sandbox-id ` | string | yes | Sandbox instance ID | | `--body ` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it | | `--instance-timeout ` | 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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | | `--body ` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it | | `--template-id ` | string | no | Ready template ID; may alternatively be supplied as templateID in --body | | `--instance-timeout ` | 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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--sandbox-id ` | string | yes | Sandbox instance ID | -| `--yes` | switch | no | Confirm this high-risk operation | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--sandbox-id ` | string | yes | Sandbox instance ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--sandbox-id ` | string | yes | Sandbox instance ID | -| `--show-credentials` | switch | no | Print envd and traffic access tokens instead of redacting them | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--sandbox-id ` | string | yes | Sandbox instance ID | +| `--show-credentials` | switch | no | Print envd and traffic access tokens instead of redacting them | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--template-id ` | string | no | Filter by template ID | -| `--sandbox-id ` | string | no | Filter by sandbox ID | -| `--state ` | string | no | Filter by state, for example running or paused | -| `--limit ` | number | no | Maximum results (1-50) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--template-id ` | string | no | Filter by template ID | +| `--sandbox-id ` | string | no | Filter by sandbox ID | +| `--state ` | string | no | Filter by state, for example running or paused | +| `--limit ` | number | no | Maximum results (1-50) | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--sandbox-id ` | string | yes | Sandbox instance ID | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--sandbox-id ` | string | yes | Sandbox instance ID | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | | `--sandbox-id ` | string | yes | Sandbox instance ID | | `--body ` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it | | `--instance-timeout ` | 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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--template-id ` | string | yes | Sandbox template ID | -| `--build-id ` | string | yes | Template build ID | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--template-id ` | string | yes | Sandbox template ID | +| `--build-id ` | string | yes | Template build ID | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | | `--body ` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it | | `--name ` | string | no | Template name | | `--cpu-count ` | 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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--template-id ` | string | yes | Sandbox template ID | -| `--yes` | switch | no | Confirm this high-risk operation | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--template-id ` | string | yes | Sandbox template ID | +| `--yes` | switch | no | Confirm this high-risk operation | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--template-id ` | string | yes | Sandbox template ID | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--template-id ` | string | yes | Sandbox template ID | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | -| `--limit ` | number | no | Maximum results (1-100) | -| `--cursor ` | string | no | Server-side pagination cursor | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | ---------------------------------------------------------------------------------- | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | +| `--limit ` | number | no | Maximum results (1-100) | +| `--cursor ` | string | no | Server-side pagination cursor | +| `--api-key ` | string | no | API key | +| `--base-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 ` | string | no | Workspace ID for the Sandbox endpoint (or set BAILIAN_WORKSPACE_ID) | +| `--workspace-id ` | string | no | Workspace ID for the default Sandbox endpoint; optional with a configured base URL | | `--template-id ` | string | yes | Sandbox template ID | | `--body ` | string | no | JSON request body, inline or loaded from an @file path; explicit flags override it | | `--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.