diff --git a/packages/commands/src/commands/managed-agent/_engine/credentials.ts b/packages/commands/src/commands/managed-agent/_engine/credentials.ts index cf0212c..c3ba746 100644 --- a/packages/commands/src/commands/managed-agent/_engine/credentials.ts +++ b/packages/commands/src/commands/managed-agent/_engine/credentials.ts @@ -50,6 +50,7 @@ export interface CredentialHost { */ export const CREDENTIALS_NOTE = [ "Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).", + "The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace.", "Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.", "Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.", ]; @@ -85,13 +86,19 @@ export function prepareProviderEnv(): void { * the block references them and the interpolated value is empty (a literal in * agents.yaml is respected). * - * `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource - * paths onto it verbatim; a value already ending in the suffix is left as-is. - * It is filled even without a credential — `client.baseUrl` is readable - * credential-less (defaults to the CLI's model-domain base URL) — so offline - * commands (which skip the credential assert) still satisfy the SDK's - * "workspace_id or base_url" schema. With no credential the `api_key` is left - * untouched: online commands reject it via {@link assertProviderCredentials}. + * `base_url` is composed from the workspace when one is known — block + * `workspace_id` (agents.yaml literal or interpolated `${BAILIAN_WORKSPACE_ID}`) + * first, then bl's configured `workspace_id` — because agentstudio is served + * only on the workspace-scoped host; the bare model-domain origin 404s it + * (managed-agents API overview: `https://{workspace_id}.cn-beijing.maas. + * aliyuncs.com/api/v1/agentstudio`, region cn-beijing only). Only with no + * workspace at all does the model-domain origin get {@link AGENTSTUDIO_API_PATH} + * suffixed. A value already ending in the suffix is left as-is. base_url is + * filled even without a credential — `client.baseUrl` is readable + * credential-less — so offline commands (which skip the credential assert) + * still satisfy the SDK's "workspace_id or base_url" schema. With no + * credential the `api_key` is left untouched: online commands reject it via + * {@link assertProviderCredentials}. */ export function injectProviderCredentials( providers: Record, @@ -103,16 +110,27 @@ export function injectProviderCredentials( const cred = host.client.exportApiCredential(); if (cred) block.api_key = cred.token; - if ("base_url" in block && !block.base_url) { - // Defensive normalization: the auth chain already normalizes base_url to - // an origin, but never let a trailing slash produce "//api/v1/agentstudio". - const origin = host.client.baseUrl.replace(/\/+$/, ""); - block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH) - ? origin - : `${origin}${AGENTSTUDIO_API_PATH}`; + if ("workspace_id" in block && !block.workspace_id) { + // agents.yaml interpolation already replaced `${BAILIAN_WORKSPACE_ID}` in + // file-based flows; the inline runtime passes an object config that never + // interpolates, so read the env var here too (prepareProviderEnv + // placeholders it to "" when unset). bl's configured workspace_id is the + // last resort. + block.workspace_id = + process.env.BAILIAN_WORKSPACE_ID?.trim() || host.settings.workspaceId || ""; } - if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) { - block.workspace_id = host.settings.workspaceId; + if ("base_url" in block && !block.base_url) { + const workspaceId = typeof block.workspace_id === "string" ? block.workspace_id.trim() : ""; + if (workspaceId) { + block.base_url = `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${AGENTSTUDIO_API_PATH}`; + } else { + // Defensive normalization: the auth chain already normalizes base_url to + // an origin, but never let a trailing slash produce "//api/v1/agentstudio". + const origin = host.client.baseUrl.replace(/\/+$/, ""); + block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH) + ? origin + : `${origin}${AGENTSTUDIO_API_PATH}`; + } } } diff --git a/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts b/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts index a892c76..8515a5e 100644 --- a/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts +++ b/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts @@ -56,15 +56,17 @@ export function inlineStatePath(agentName: string): string { /** * The minimal in-memory project config that materializes into one cloud agent. - * `providers.bailian` carries empty `api_key`/`base_url` placeholders so - * {@link injectProviderCredentials} fills them from bl's auth chain (it only - * writes fields the block already declares). + * `providers.bailian` carries empty `api_key`/`base_url`/`workspace_id` + * placeholders so {@link injectProviderCredentials} fills them from bl's auth + * chain and workspace sources (it only writes fields the block already + * declares). `workspace_id` lets injection compose the workspace-scoped + * agentstudio host instead of the model-domain origin. */ export function buildInlineConfig(opts: InlineAgentOptions): Record { return { version: "1", providers: { - bailian: { api_key: "", base_url: "" }, + bailian: { api_key: "", base_url: "", workspace_id: "" }, }, defaults: { provider: "bailian" }, environments: { diff --git a/packages/commands/tests/credentials-bridge.test.ts b/packages/commands/tests/credentials-bridge.test.ts index 3a1fb98..f2bb81d 100644 --- a/packages/commands/tests/credentials-bridge.test.ts +++ b/packages/commands/tests/credentials-bridge.test.ts @@ -124,7 +124,8 @@ test("inject:已带后缀且尾斜杠的 base_url 去斜杠后原样保留", () expect(providers.bailian.base_url).toBe("https://x.maas.aliyuncs.com/api/v1/agentstudio"); }); -test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则保留", () => { +test("inject:workspace_id 引用且为空时按 env > settings 填充;有字面量则保留", () => { + delete process.env.BAILIAN_WORKSPACE_ID; const empty = { bailian: { api_key: "", workspace_id: "" } }; injectProviderCredentials( empty, @@ -132,6 +133,16 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则 ); expect(empty.bailian.workspace_id).toBe("ws-settings"); + // 内联运行时(对象配置)不做 ${} 插值,env 变量在此补读。 + process.env.BAILIAN_WORKSPACE_ID = "ws-env"; + const fromEnv = { bailian: { api_key: "", workspace_id: "" } }; + injectProviderCredentials( + fromEnv, + makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }), + ); + expect(fromEnv.bailian.workspace_id).toBe("ws-env"); + delete process.env.BAILIAN_WORKSPACE_ID; + const literal = { bailian: { api_key: "", workspace_id: "ws-yaml" } }; injectProviderCredentials( literal, @@ -140,6 +151,37 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则 expect(literal.bailian.workspace_id).toBe("ws-yaml"); }); +test("inject:workspace 已知时 base_url 拼工作空间主机,而非模型域 origin", () => { + // agents.yaml 字面量 workspace_id + 空 base_url。 + const literal = { bailian: { api_key: "", base_url: "", workspace_id: "ws-yaml" } }; + injectProviderCredentials(literal, makeHost({ apiCred: bailianCred() })); + expect(literal.bailian.base_url).toBe( + "https://ws-yaml.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio", + ); + + // 内联块:workspace_id 由 settings 填充后同样走工作空间主机。 + const inline = { bailian: { api_key: "", base_url: "", workspace_id: "" } }; + injectProviderCredentials( + inline, + makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }), + ); + expect(inline.bailian.workspace_id).toBe("ws-settings"); + expect(inline.bailian.base_url).toBe( + "https://ws-settings.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio", + ); + + // 显式 base_url 字面量永远优先于拼装。 + const explicit = { + bailian: { + api_key: "", + base_url: "https://custom.example.com/api/v1/agentstudio", + workspace_id: "ws-yaml", + }, + }; + injectProviderCredentials(explicit, makeHost({ apiCred: bailianCred() })); + expect(explicit.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio"); +}); + test("inject:无凭证时 api_key 保持不变,base_url 仍用 client 默认域名补齐(离线/范围外 schema 可用)", () => { const providers = { bailian: { api_key: "", base_url: "" } }; injectProviderCredentials(providers, makeHost({})); diff --git a/packages/dsh/README.md b/packages/dsh/README.md index c52dae3..ced4aef 100644 --- a/packages/dsh/README.md +++ b/packages/dsh/README.md @@ -28,16 +28,16 @@ - 百炼 API Key。**注意有两类且不可混用**: - | 类型 | 前缀 | 能访问 | 不能访问 | - | --------- | -------- | --------------------------------------- | -------------- | - | TokenPlan | `sk-sp-` | TokenPlan 网关(LLM / vision / 文生图) | 记忆库、知识库 | - | 按量付费 | `sk-ws-` | 记忆库、知识库、DashScope 全量接口 | TokenPlan 网关 | + | 类型 | 前缀 | 能访问 | 不能访问 | + | --------- | -------- | ----------------------------------------------------------- | ------------------------ | + | TokenPlan | `sk-sp-` | TokenPlan 网关(LLM / vision / 文生图) | 记忆库、知识库、远程任务 | + | 按量付费 | `sk-ws-` | 记忆库、知识库、远程任务(agentstudio)、DashScope 全量接口 | TokenPlan 网关 | 两者互相返回 `401 InvalidApiKey`,所以本包用**两个不同的环境变量**,不会互相踩: ```sh export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx # 只给 bailian-tokenplan provider - export DASHSCOPE_API_KEY=sk-ws-xxx # 给 bl、memory、RAG + export DASHSCOPE_API_KEY=sk-ws-xxx # 给 bl、memory、RAG、远程任务 ``` 只有一类 Key 也能用,只是能力范围相应缩小。若只有 TokenPlan Key: @@ -48,7 +48,13 @@ export DASHSCOPE_BASE_URL=https://token-plan.cn-beijing.maas.aliyuncs.com ``` - 这样 LLM / vision / 文生图可用,memory 与 RAG 不可用(保持停用即可)。 + 这样 LLM / vision / 文生图可用(后两者经 `bl` 走 TokenPlan 网关);memory 与 RAG 保持停用即可。**远程任务仍可注册**,但它的凭证解析会看出这是 TokenPlan Key / 网关,调用 `bailian_run_remote_task` 时直接给出带修复指引的报错,而不是以前的 `Bailian API 404`。 + + **按量付费 Key 的解析顺序**(memory / RAG / 远程任务三处一致):行内 `config.apiKey` → `$DASHSCOPE_API_KEY`。 + + **远程任务的端点**另有讲究:managed-agent(agentstudio)API **只**在工作空间前缀主机上提供——`https://{workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio`(普通 dashscope 主机与 TokenPlan 网关都 404),且 Key 只能访问**自己归属的工作空间**(不匹配时 403 `Endpoint.AccessDenied`)。端点解析顺序:行内 `baseUrl` → `$DASHSCOPE_BASE_URL` → 行内 `workspaceId` → `$BAILIAN_WORKSPACE_ID`(后两者自动拼成工作空间主机)。workspace ID 在百炼控制台右上角的工作空间下拉里看。 + + memory / RAG 是显式开启的插件,Key 缺失或误填 `sk-sp-` 会在启动期报错;远程任务默认启用,为避免拖垮 TokenPlan-only 环境,改为调用时报错。 --- @@ -125,10 +131,12 @@ DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见* 把一个任务甩到百炼云端的托管 agent 上跑,不占本地会话。**无需预先写 `agents.yaml` 或 `apply`**:工具首次被调用时,`bl managed-agent run` 会在你的账号里幂等创建一个 agent + cloud environment,之后复用。 - 模型自己按用户意图填 `instructions`(远程 agent 的角色),`task` 是要它做的事。例如你说「在云端帮我审计这个依赖树,它该懂安全」→ 模型调 `bailian_run_remote_task(task="审计依赖树", instructions="你是安全专家")`。 -- **前提**:这条路走的是 managed-agent(agentstudio)服务,需要**按量付费 Key**(`sk-ws-`)+ dashscope 端点,且账号已开通 managed-agent。TokenPlan Key 不适用。若 `DASHSCOPE_API_KEY`/端点没配好,首次调用会返回 `Bailian API 404`。 +- **前提**:这条路走的是 managed-agent(agentstudio)服务,需要**按量付费 Key**(`sk-ws-`)+ **工作空间端点**,且账号已开通 managed-agent。TokenPlan Key 不适用。 +- **凭证解析**:Key 为 `config.apiKey` → `$DASHSCOPE_API_KEY`;端点为 `config.baseUrl` → `$DASHSCOPE_BASE_URL` → `config.workspaceId` → `$BAILIAN_WORKSPACE_ID`(后两者自动拼成 `https://{workspaceId}.cn-beijing.maas.aliyuncs.com`)。凡是解析出来的,都会显式下发给 `bl`,不会落到 `bl` 活动 config profile 的端点上——这正是旧版 `Bailian API 404` 的根因:agentstudio **只**在工作空间前缀主机上提供,TokenPlan 网关与普通 dashscope 主机都 404。 +- **两个高频报错**:`404`=端点不是工作空间主机;`403 Endpoint.AccessDenied`=主机对了但这个 Key 不属于该工作空间。二者都会附带具体修复指引。Key 归属的工作空间在百炼控制台右上角下拉里看。 - 首次会创建云资源(可能计费、启动有延迟);同名 agent 后续复用。默认 agent 名 `dsh-remote-runner`,可在配置里改。 -需要非默认的 agent 名 / 模型时: +需要非默认的 agent 名 / 模型 / 凭证时: ```yaml - id: bailian-tool-managed-agent @@ -136,6 +144,10 @@ DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见* agent: my-runner model: qwen3.8-max timeoutMs: 600000 + # 可选凭证(省略则按上面的解析顺序找): + # apiKey: sk-ws-xxxxxxxx + # workspaceId: llm-xxxxxxxx # 推荐:自动拼成工作空间端点 + # baseUrl: https://llm-xxxxxxxx.cn-beijing.maas.aliyuncs.com # 或用完整端点 ``` --- @@ -157,7 +169,7 @@ DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见* workspaceId: llm-xxxxxxxx # 百炼控制台工作空间 ID agentId: aid-xxxxxxxx # 知识库"检索服务"ID maxResults: 10 - # apiKey 省略则读 $DASHSCOPE_API_KEY + # apiKey 省略则读 $DASHSCOPE_API_KEY(须为按量付费 sk-ws-;误填 sk-sp- 会在启动期报错) ``` 一个实例对一个知识库(`WebSearchRequest` 只带 `query` / `maxResults`,agentId 只能来自配置)。要多个知识库就插多行不同 `id`。 @@ -229,15 +241,18 @@ bl auth status ## 6. 常见问题 -| 现象 | 原因 | -| -------------------------------------- | ---------------------------------------------------------------------- | -| LLM 路由 `401 InvalidApiKey` | `BAILIAN_TOKENPLAN_API_KEY` 没设,或误填了 `sk-ws-` 的按量付费 Key | -| memory / RAG `401 InvalidApiKey` | `DASHSCOPE_API_KEY` 误填了 `sk-sp-` 的 TokenPlan Key | -| `WEB_PROVIDER_AMBIGUOUS` | 有多个搜索 provider,需在 `web` 行 pin `searchProvider` | -| 粘图报 `MODEL_DOES_NOT_SUPPORT_IMAGES` | 当前模型不支持图片输入,换成上表标"是"的,或改用 vision 工具 | -| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` | -| 改了 patch 但没生效 | `config` 是整体替换,检查是否漏写了原有字段;再用 `--dump-config` 确认 | -| `memoryLibraryId does not exist` | 记忆库 ID 属于另一个账号,与当前 Key 不匹配 | +| 现象 | 原因 | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| LLM 路由 `401 InvalidApiKey` | `BAILIAN_TOKENPLAN_API_KEY` 没设,或误填了 `sk-ws-` 的按量付费 Key | +| memory / RAG 启动期报 TokenPlan Key | `DASHSCOPE_API_KEY` / `apiKey` 误填了 `sk-sp-` 的 TokenPlan Key | +| 远程任务 `Bailian API 404` | 端点不是工作空间前缀主机(TokenPlan 网关 / 普通 dashscope 主机都不提供 agentstudio);给该行配 `workspaceId`(或 `baseUrl`),或导出 `BAILIAN_WORKSPACE_ID` / `DASHSCOPE_BASE_URL` | +| 远程任务 `403 Endpoint.AccessDenied` | 主机是工作空间主机,但这个 Key 不属于该工作空间;换成 Key 归属工作空间的 ID(控制台右上角下拉),或用属于该工作空间的 Key | +| 远程任务调用即报 TokenPlan 提示 | `$DASHSCOPE_API_KEY` 是 `sk-sp-`;换按量付费 Key 或在行内配 `apiKey` | +| `WEB_PROVIDER_AMBIGUOUS` | 有多个搜索 provider,需在 `web` 行 pin `searchProvider` | +| 粘图报 `MODEL_DOES_NOT_SUPPORT_IMAGES` | 当前模型不支持图片输入,换成上表标"是"的,或改用 vision 工具 | +| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` | +| 改了 patch 但没生效 | `config` 是整体替换,检查是否漏写了原有字段;再用 `--dump-config` 确认 | +| `memoryLibraryId does not exist` | 记忆库 ID 属于另一个账号,与当前 Key 不匹配 | --- diff --git a/packages/dsh/cordis.patch.yml b/packages/dsh/cordis.patch.yml index eaca60c..0ae0a51 100644 --- a/packages/dsh/cordis.patch.yml +++ b/packages/dsh/cordis.patch.yml @@ -79,18 +79,33 @@ # Enabled by default: the tool creates no resources at load time. It only # provisions a cloud agent when the model actually calls it, and reuses it # after — no deployment-specific ID to configure up front. + # + # Credentials: needs a pay-as-you-go key (sk-ws-), resolved from row config + # `apiKey`, then $DASHSCOPE_API_KEY. The agentstudio API is served only on + # the workspace-scoped host, so the endpoint resolves from row `baseUrl`, + # then $DASHSCOPE_BASE_URL, then row `workspaceId` / $BAILIAN_WORKSPACE_ID + # composed into https://{workspace}.cn-beijing.maas.aliyuncs.com — whatever + # resolves ships to bl explicitly, never leaving the endpoint to bl's + # active-profile base_url (a TokenPlan or bare model-domain origin 404s + # agentstudio). The key must belong to that workspace. A resolved TokenPlan + # key or TokenPlan endpoint rejects at call time with guidance (this row is + # enabled by default and must not break boot for TokenPlan-only setups). - id: bailian-tool-managed-agent name: bailian-cli-dsh/tool-managed-agent # Disabled by default: the knowledge base to query is deployment-specific, # and an enabled provider with no agentId would make `web_search` ambiguous # for everyone. Set workspaceId + agentId and flip `disabled` to use it. + # Key: row config `apiKey`, then $DASHSCOPE_API_KEY (pay-as-you-go sk-ws-; + # a TokenPlan key is rejected at boot). - id: bailian-web-search-rag name: bailian-cli-dsh/web-search-rag disabled: true config: {} # Disabled by default: memory add/search are billed per call. + # Key: row config `apiKey`, then $DASHSCOPE_API_KEY (pay-as-you-go sk-ws-; + # a missing key or a TokenPlan key fails the boot with an actionable message). - id: bailian-memory name: bailian-cli-dsh/memory disabled: true diff --git a/packages/dsh/src/memory/index.ts b/packages/dsh/src/memory/index.ts index 67e9858..5193dc5 100644 --- a/packages/dsh/src/memory/index.ts +++ b/packages/dsh/src/memory/index.ts @@ -26,6 +26,7 @@ import type { ContentBlock, Message } from "@deepseek-ai/dsh-llm"; import { createUserMessage } from "@deepseek-ai/dsh-llm"; import { defineTool } from "@deepseek-ai/dsh-tools"; import z from "@deepseek-ai/schemastery"; +import { isTokenPlanKey, tokenPlanKeyRejection } from "../shared/credentials.ts"; import { dashScopeFetch, resolveApiKey, resolveBaseUrl } from "../shared/http.ts"; /** Cordis plugin name used by loader diagnostics. */ @@ -56,7 +57,12 @@ export interface Config { } export const Config: z = z.object({ - apiKey: z.string().role("secret").description("DashScope key; defaults to $DASHSCOPE_API_KEY."), + apiKey: z + .string() + .role("secret") + .description( + "Pay-as-you-go DashScope key (sk-ws-); defaults to $DASHSCOPE_API_KEY. TokenPlan keys are rejected.", + ), baseUrl: z.string().description("DashScope base URL override."), userId: z.string().description("Memory entity id owning these memories."), memoryLibraryId: z.string().description("Memory library id; defaults to the account default."), @@ -355,9 +361,13 @@ export function apply(ctx: Context, config: Config): void { const apiKey = resolveApiKey(ctx, config.apiKey); if (apiKey === undefined) { throw new Error( - "bailian-memory: no DashScope API key. Set $DASHSCOPE_API_KEY or configure `apiKey`.", + "bailian-memory: no DashScope API key. Set `apiKey` in this row's config or export " + + "$DASHSCOPE_API_KEY (a pay-as-you-go sk-ws- key; the memory API 401s TokenPlan keys).", ); } + if (isTokenPlanKey(apiKey)) { + throw new Error(tokenPlanKeyRejection(name, "the memory API")); + } const client = new MemoryClient( apiKey, resolveBaseUrl(ctx, config.baseUrl), diff --git a/packages/dsh/src/shared/credentials.ts b/packages/dsh/src/shared/credentials.ts new file mode 100644 index 0000000..628fe71 --- /dev/null +++ b/packages/dsh/src/shared/credentials.ts @@ -0,0 +1,93 @@ +/** + * Pure credential classification and pairing shared by the plugins that call + * pay-as-you-go DashScope APIs directly (memory, knowledge base) or through + * `bl managed-agent` (agentstudio). No runtime imports — this module is safe + * to load from tests and its rules are locked by `tests/credentials.test.ts`. + * + * TokenPlan keys (`sk-sp-`) and pay-as-you-go keys (`sk-ws-`) are not + * interchangeable: the TokenPlan gateway 401s a pay-as-you-go key, and the + * service APIs this package calls 401 or 404 a TokenPlan key. The LLM + * provider row keeps its TokenPlan key under a dedicated env name + * (`BAILIAN_TOKENPLAN_API_KEY`); every other plugin needs a pay-as-you-go key + * and rejects a TokenPlan one up front instead of failing at request time. + * + * @module bailian-cli-dsh/shared/credentials + */ + +/** + * Standard DashScope model-domain endpoint. It serves the model APIs plus the + * memory v2 and knowledge indices the plugins call directly — but NOT + * `/api/v1/agentstudio`, which lives on the workspace-scoped host. + */ +export const DASHSCOPE_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com"; + +/** Key prefix that marks a TokenPlan key (which service APIs reject). */ +export const TOKEN_PLAN_KEY_PREFIX = "sk-sp-"; + +/** Whether a key is shaped like a TokenPlan key (which service APIs reject). */ +export function isTokenPlanKey(apiKey: string): boolean { + return apiKey.startsWith(TOKEN_PLAN_KEY_PREFIX); +} + +/** + * Whether a base URL points at the TokenPlan gateway. That gateway serves the + * model-inference routes only — none of the service APIs this package calls, + * including `/api/v1/agentstudio`, so requests to it 404. + */ +export function isTokenPlanEndpoint(baseUrl: string): boolean { + try { + return new URL(baseUrl).hostname.startsWith("token-plan."); + } catch { + // An unparseable URL fails the request later with its own diagnostics; + // this check only classifies well-formed endpoints. + return false; + } +} + +/** + * The standard error wording every plugin uses when it resolves a TokenPlan + * key, so all three surfaces fail with one recognizable, actionable message. + */ +export function tokenPlanKeyRejection(plugin: string, capability: string): string { + return ( + `${plugin}: the resolved API key is a TokenPlan key (${TOKEN_PLAN_KEY_PREFIX}…), which ` + + `${capability} rejects. Use a pay-as-you-go key (sk-ws-): set \`apiKey\` in this row's ` + + "config or $DASHSCOPE_API_KEY. TokenPlan keys belong on $BAILIAN_TOKENPLAN_API_KEY, " + + "which only the `bailian-tokenplan` LLM provider reads." + ); +} + +/** + * Build the `--api-key` / `--base-url` flags handed to `bl managed-agent run`. + * Each resolved half ships independently: + * + * - A resolved key becomes `--api-key`, overriding bl's auth chain so an + * active TokenPlan profile cannot substitute its own key. + * - A resolved endpoint becomes `--base-url`, overriding the ACTIVE PROFILE's + * base_url — the half that fixes the classic `Bailian API 404`, where a + * TokenPlan (or bare model-domain) origin does not serve + * `/api/v1/agentstudio`. + * + * There is deliberately NO fallback endpoint: agentstudio is only served on + * the workspace-scoped host (see {@link workspaceEndpoint}), and an unknown + * workspace is a configuration gap, not a defaultable value. Unresolved halves + * emit nothing and bl's own auth chain decides them. + */ +export function credentialFlags(apiKey: string | undefined, baseUrl: string | undefined): string[] { + const flags: string[] = []; + if (baseUrl !== undefined && baseUrl.length > 0) flags.push("--base-url", baseUrl); + if (apiKey !== undefined && apiKey.length > 0) flags.push("--api-key", apiKey); + return flags; +} + +/** + * Compose the workspace-scoped agentstudio host for a workspace id. The + * managed-agent API is served only from + * `https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio` + * (bl/the SDK append the resource path onto this origin); the plain + * dashscope origin 404s it, and a key only unlocks its own workspace's host + * (a mismatched one 403s `Endpoint.AccessDenied`). + */ +export function workspaceEndpoint(workspaceId: string): string { + return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com`; +} diff --git a/packages/dsh/src/shared/http.ts b/packages/dsh/src/shared/http.ts index 24fa6e1..6ee6815 100644 --- a/packages/dsh/src/shared/http.ts +++ b/packages/dsh/src/shared/http.ts @@ -6,8 +6,9 @@ */ import type { Context } from "@deepseek-ai/cordis"; import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment"; +import { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts"; -export const DASHSCOPE_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com"; +export { DASHSCOPE_DEFAULT_BASE_URL } from "./credentials.ts"; /** A non-2xx DashScope response, carrying the server's own wording. */ export class DashScopeError extends Error { @@ -22,8 +23,10 @@ export class DashScopeError extends Error { } /** - * Resolve the DashScope key from explicit config, then the launch environment - * (process env, project `.env`, harness-home `.env`). + * Resolve the DashScope key: explicit row config first, then the launch + * environment (process env, project `.env`, harness-home `.env`). Callers + * that get `undefined` decide their own failure mode — opt-in plugins reject + * at boot, the managed-agent tool falls through to bl's own auth chain. */ export function resolveApiKey(ctx: Context, explicit?: string): string | undefined { if (explicit !== undefined && explicit.length > 0) return explicit; diff --git a/packages/dsh/src/tool-managed-agent/index.ts b/packages/dsh/src/tool-managed-agent/index.ts index 942ac6a..31bcbc6 100644 --- a/packages/dsh/src/tool-managed-agent/index.ts +++ b/packages/dsh/src/tool-managed-agent/index.ts @@ -14,13 +14,34 @@ * after, so no `agents.yaml` or prior `apply` is required. First use provisions * cloud resources — it may incur cost and take longer to start. * + * Credentials: agentstudio is a pay-as-you-go DashScope API served ONLY on the + * workspace-scoped host `https://{workspace}.cn-beijing.maas.aliyuncs.com` + * (the plain dashscope origin and the TokenPlan gateway both 404 it, and a key + * only unlocks its own workspace's host). `bl` resolves the key as + * `--api-key` > `$DASHSCOPE_API_KEY` > the active config profile, but a + * profile's `base_url` is NOT paired with an env-resolved key — an active + * TokenPlan profile therefore aims agentstudio at the TokenPlan gateway. So + * whenever this plugin resolves a key or an endpoint (row config, then launch + * env), it passes them explicitly; see {@link credentialFlags}. Endpoint + * resolution is `baseUrl`, then `$DASHSCOPE_BASE_URL`, then `workspaceId` + * composed into the workspace host (same for `$BAILIAN_WORKSPACE_ID`). With + * nothing resolvable here both halves are left to bl's own auth chain. + * * @module bailian-cli-dsh/tool-managed-agent */ import type { Context } from "@deepseek-ai/cordis"; +import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment"; import { defineTool } from "@deepseek-ai/dsh-tools"; import type {} from "@deepseek-ai/dsh-tools"; import z from "@deepseek-ai/schemastery"; import { runBlJson } from "../shared/bl.ts"; +import { + credentialFlags, + isTokenPlanEndpoint, + isTokenPlanKey, + tokenPlanKeyRejection, + workspaceEndpoint, +} from "../shared/credentials.ts"; /** Cordis plugin name used by loader diagnostics. */ export const name = "bailian-tool-managed-agent"; @@ -36,6 +57,15 @@ export interface Config { agent?: string; /** Model for the remote agent. */ model?: string; + /** Pay-as-you-go DashScope key; defaults to `$DASHSCOPE_API_KEY`. */ + apiKey?: string; + /** + * Workspace id the key belongs to; composed into the agentstudio host. + * Read from the console's top-right workspace switcher. + */ + workspaceId?: string; + /** Full agentstudio origin; wins over `workspaceId`. */ + baseUrl?: string; /** Cooperative budget; first-run provisioning of a cloud environment is slow. */ timeoutMs?: number; } @@ -43,11 +73,54 @@ export interface Config { export const Config: z = z.object({ agent: z.string().description("Remote agent identity to create/reuse."), model: z.string().description("Model for the remote agent."), + apiKey: z + .string() + .role("secret") + .description( + "Pay-as-you-go DashScope key (sk-ws-); defaults to $DASHSCOPE_API_KEY. TokenPlan keys are rejected.", + ), + workspaceId: z + .string() + .description( + "Workspace the key belongs to (console top-right switcher); defaults to $BAILIAN_WORKSPACE_ID. " + + "Composed into https://{workspaceId}.cn-beijing.maas.aliyuncs.com.", + ), + baseUrl: z + .string() + .description( + "Full agentstudio origin; overrides workspaceId. Defaults to $DASHSCOPE_BASE_URL.", + ), timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."), }); const DEFAULT_TIMEOUT_MS = 600_000; +/** + * Resolve the managed-agent credentials: row config first, then the launch + * environment. Endpoint resolution: `baseUrl` (explicit origin) beats + * `workspaceId` (composed into the workspace-scoped host); env names mirror + * the same split. Agentstudio is only served on the workspace-scoped host, so + * an unresolved endpoint is left unset for bl to resolve (and the failure + * hints below explain the gap when bl cannot either). + */ +function resolveCredentials(ctx: Context, config: Config): { apiKey?: string; baseUrl?: string } { + const launchEnvironment = launchEnvironmentOf(ctx); + const env = (varName: string): string | undefined => { + const value = launchEnvironment.get(varName)?.value; + return value !== undefined && value.length > 0 ? value : undefined; + }; + const apiKey = config.apiKey ?? env("DASHSCOPE_API_KEY"); + const workspaceId = config.workspaceId ?? env("BAILIAN_WORKSPACE_ID"); + const baseUrl = + config.baseUrl ?? + env("DASHSCOPE_BASE_URL") ?? + (workspaceId !== undefined ? workspaceEndpoint(workspaceId) : undefined); + return { + ...(apiKey !== undefined ? { apiKey } : {}), + ...(baseUrl !== undefined ? { baseUrl } : {}), + }; +} + /** The `bl managed-agent run --output json` envelope: a session-event list. */ interface SessionRunResponse { session_id?: string; @@ -65,6 +138,26 @@ function assistantText(response: SessionRunResponse): string { } export function apply(ctx: Context, config: Config): void { + // Resolve credentials at boot so misconfigurations surface as one clear + // message instead of a cryptic 401/404 mid-task. This row is ENABLED BY + // DEFAULT, though, and TokenPlan-only setups legitimately keep + // $DASHSCOPE_API_KEY / $DASHSCOPE_BASE_URL aimed at the TokenPlan gateway + // for the vision/image tools — so a TokenPlan key or endpoint is not a boot + // error here: it becomes a per-call rejection with guidance, and everything + // else keeps working. (Opt-in plugins like bailian-memory reject at boot.) + const credentials = resolveCredentials(ctx, config); + const rejection = + credentials.apiKey !== undefined && isTokenPlanKey(credentials.apiKey) + ? tokenPlanKeyRejection(name, "the managed-agent (agentstudio) API") + : credentials.baseUrl !== undefined && isTokenPlanEndpoint(credentials.baseUrl) + ? `${name}: the resolved endpoint ${credentials.baseUrl} is the TokenPlan gateway, ` + + "which does not serve /api/v1/agentstudio (requests 404). Agentstudio lives on the " + + "workspace-scoped host: set `workspaceId` (the workspace your key belongs to, from " + + "the console's top-right switcher) or `baseUrl` in this row's config, or export " + + "BAILIAN_WORKSPACE_ID / DASHSCOPE_BASE_URL." + : undefined; + const credentialArgv = credentialFlags(credentials.apiKey, credentials.baseUrl); + ctx.tools.register( defineTool({ name: "bailian_run_remote_task", @@ -103,6 +196,7 @@ export function apply(ctx: Context, config: Config): void { }, timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS, async execute(args, exec) { + if (rejection !== undefined) throw new Error(rejection); const argv = [ "managed-agent", "run", @@ -114,11 +208,22 @@ export function apply(ctx: Context, config: Config): void { if (args.instructions !== undefined) argv.push("--instructions", args.instructions); const model = args.model ?? config.model; if (model !== undefined) argv.push("--model", model); + // Atomic credential pair: never let bl pair a key with its active + // profile's base_url (a TokenPlan profile 404s agentstudio). + argv.push(...credentialArgv); - const response = await runBlJson(ctx, argv, { - cwd: exec.agent?.session.header.cwd ?? process.cwd(), - signal: exec.signal, - }); + let response: SessionRunResponse; + try { + response = await runBlJson(ctx, argv, { + cwd: exec.agent?.session.header.cwd ?? process.cwd(), + signal: exec.signal, + }); + } catch (error) { + throw enrichProvisioningError(error, { + fellThroughToBlChain: credentials.apiKey === undefined, + endpointResolved: credentials.baseUrl !== undefined, + }); + } const answer = assistantText(response); if (answer.length === 0) { @@ -129,3 +234,46 @@ export function apply(ctx: Context, config: Config): void { }), ); } + +/** + * Attach an actionable hint to the classic misconfiguration signatures. + * Agentstudio is only served on the workspace-scoped host, and a key only + * unlocks its own workspace, so the three failure modes each get targeted + * guidance: 404 = endpoint is not a workspace host; 403 `Endpoint. + * AccessDenied` = right shape of host but the wrong workspace for this key; + * 401 = TokenPlan key on a pay-as-you-go API. Anything else passes through. + */ +function enrichProvisioningError( + error: unknown, + context: { fellThroughToBlChain: boolean; endpointResolved: boolean }, +): unknown { + if (!(error instanceof Error)) return error; + const message = error.message; + const workspaceHint = + "Agentstudio is served only on the workspace-scoped host " + + "https://{workspaceId}.cn-beijing.maas.aliyuncs.com, and a key only unlocks its own " + + "workspace. Set `workspaceId` (the workspace your key belongs to, from the console's " + + "top-right switcher) or `baseUrl` on the bailian-tool-managed-agent row, or export " + + "BAILIAN_WORKSPACE_ID / DASHSCOPE_BASE_URL."; + + let hint: string | undefined; + if (message.includes("Endpoint.AccessDenied") || message.includes("403")) { + hint = `The host is workspace-scoped but this key belongs to a different workspace. ${workspaceHint}`; + } else if (message.includes("404")) { + hint = context.endpointResolved + ? `The endpoint rejected /api/v1/agentstudio. ${workspaceHint}` + : context.fellThroughToBlChain + ? "No key/endpoint resolved from this row's config or the environment, so bl used its " + + "own auth chain — its active profile endpoint is not the workspace host agentstudio " + + `needs. ${workspaceHint}` + : `The endpoint rejected /api/v1/agentstudio. ${workspaceHint}`; + } else if (message.includes("401")) { + hint = + "The managed-agent API rejected the key. It needs a pay-as-you-go key (sk-ws-); " + + "TokenPlan keys (sk-sp-) only serve the TokenPlan LLM gateway."; + } + + if (hint === undefined) return error; + error.message = `${error.message}\n${hint}`; + return error; +} diff --git a/packages/dsh/src/web-search-rag/index.ts b/packages/dsh/src/web-search-rag/index.ts index 1d31264..d66dc1e 100644 --- a/packages/dsh/src/web-search-rag/index.ts +++ b/packages/dsh/src/web-search-rag/index.ts @@ -23,6 +23,7 @@ import type { import { WebError } from "@deepseek-ai/dsh-web"; import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment"; import z from "@deepseek-ai/schemastery"; +import { isTokenPlanKey, tokenPlanKeyRejection } from "../shared/credentials.ts"; import { dashScopeFetch, resolveApiKey } from "../shared/http.ts"; /** Cordis plugin name used by loader diagnostics. */ @@ -49,7 +50,9 @@ export const Config: z = z.object({ apiKey: z .string() .role("secret") - .description("DashScope API key; defaults to $DASHSCOPE_API_KEY."), + .description( + "Pay-as-you-go DashScope key (sk-ws-); defaults to $DASHSCOPE_API_KEY. TokenPlan keys are rejected.", + ), workspaceId: z.string().description("Bailian workspace id; defaults to $BAILIAN_WORKSPACE_ID."), agentId: z.string().description("Retrieval service (agent) id identifying the knowledge base."), maxResults: z.natural().description("Default source cap when the caller sets none."), @@ -147,12 +150,19 @@ export class BailianKbSearchProvider implements WebSearchProvider { } export function apply(ctx: Context, config: Config): void { + const apiKey = resolveApiKey(ctx, config.apiKey); + // A TokenPlan key would register a provider that looks available and then 401s + // on every search; reject it at boot instead. An absent key stays soft: + // `available()` returns false and dsh falls back to another provider. + if (apiKey !== undefined && isTokenPlanKey(apiKey)) { + throw new Error(tokenPlanKeyRejection(name, "the knowledge-base API")); + } const workspaceId = config.workspaceId ?? launchEnvironmentOf(ctx).get("BAILIAN_WORKSPACE_ID")?.value ?? ""; ctx.web.registerSearchProvider( new BailianKbSearchProvider({ - apiKey: resolveApiKey(ctx, config.apiKey) ?? "", + apiKey: apiKey ?? "", workspaceId, agentId: config.agentId ?? "", maxResults: config.maxResults ?? DEFAULT_MAX_RESULTS, diff --git a/packages/dsh/tests/credentials.test.ts b/packages/dsh/tests/credentials.test.ts new file mode 100644 index 0000000..acd97b7 --- /dev/null +++ b/packages/dsh/tests/credentials.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from "vite-plus/test"; +import { + credentialFlags, + DASHSCOPE_DEFAULT_BASE_URL, + isTokenPlanEndpoint, + isTokenPlanKey, + workspaceEndpoint, +} from "../src/shared/credentials.ts"; + +// 行为锁定:两类 Key(sk-sp- TokenPlan / sk-ws- 按量付费)不可混用,三个直连服务 +// 模块(memory / RAG / managed-agent)都会拦下 TokenPlan Key,而不是等请求时 +// 拿到难懂的 401/404。managed-agent 的凭证两半独立下发:解析出 key 就显式 +// --api-key(不让 bl 用活动 profile 的 key),解析出端点就显式 --base-url +// (不让 bl 用活动 profile 的端点)。agentstudio 只在工作空间前缀主机上提供, +// 因此绝不存在"默认端点"——工作空间未知就是配置缺口,该报错而不是猜。 + +test("isTokenPlanKey classifies by prefix", () => { + expect(isTokenPlanKey("sk-sp-abc123")).toBe(true); + expect(isTokenPlanKey("sk-ws-abc123")).toBe(false); + expect(isTokenPlanKey("")).toBe(false); +}); + +test("isTokenPlanEndpoint classifies the gateway host", () => { + expect(isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com")).toBe(true); + expect( + isTokenPlanEndpoint("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"), + ).toBe(true); + expect(isTokenPlanEndpoint(DASHSCOPE_DEFAULT_BASE_URL)).toBe(false); + expect(isTokenPlanEndpoint(workspaceEndpoint("llm-x"))).toBe(false); + // 不可解析的 URL 交给后续请求自己报错,这里只做形状分类。 + expect(isTokenPlanEndpoint("not a url")).toBe(false); +}); + +test("workspaceEndpoint composes the workspace-scoped agentstudio host", () => { + expect(workspaceEndpoint("llm-kpgesh4vqzf5gzv9")).toBe( + "https://llm-kpgesh4vqzf5gzv9.cn-beijing.maas.aliyuncs.com", + ); + expect(workspaceEndpoint("ws_abc")).toBe("https://ws_abc.cn-beijing.maas.aliyuncs.com"); +}); + +test("credentialFlags: each resolved half ships independently, no defaults", () => { + expect(credentialFlags(undefined, undefined)).toEqual([]); + expect(credentialFlags("", "")).toEqual([]); + // 只有 key:端点留给 bl 解析,绝不塞一个会 404 的默认主机。 + expect(credentialFlags("sk-ws-abc", undefined)).toEqual(["--api-key", "sk-ws-abc"]); + // 只有端点:也下发,key 留给 bl 的 auth chain。 + expect(credentialFlags(undefined, "https://ws.example.com")).toEqual([ + "--base-url", + "https://ws.example.com", + ]); + expect(credentialFlags("sk-ws-abc", "https://ws.example.com")).toEqual([ + "--base-url", + "https://ws.example.com", + "--api-key", + "sk-ws-abc", + ]); +}); diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index 67d5242..1ceabb5 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -53,6 +53,7 @@ Index: [index.md](index.md) #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -87,6 +88,7 @@ bl managed-agent apply --provider bailian --yes #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -153,6 +155,7 @@ bl managed-agent init --provider all #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. - --no-refresh and --dry-run plan offline from local config and state: no remote requests, no state writes, provider keys are not checked. @@ -194,6 +197,7 @@ bl managed-agent plan --no-refresh #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. - Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them. @@ -233,6 +237,7 @@ bl managed-agent run --prompt "Audit this dependency tree" --instructions "You a #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -271,6 +276,7 @@ bl managed-agent session create --agent assistant --title 'debug run' #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -303,6 +309,7 @@ bl managed-agent session delete --session-id sess_abc123 #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -337,6 +344,7 @@ bl managed-agent session events --session-id sess_abc123 --all #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -368,6 +376,7 @@ bl managed-agent session get --session-id sess_abc123 #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -412,6 +421,7 @@ bl managed-agent session list --all #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. - --output json emits one envelope: { session_id, provider, agent, events } — read session_id to chain `session send/get/events/delete`. @@ -449,6 +459,7 @@ bl managed-agent session run --agent assistant --prompt "summarize this repo" #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. @@ -479,6 +490,7 @@ bl managed-agent session send --session-id sess_abc123 --message "continue" #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. - Providers without a skill listing API (e.g. ark) return an empty list. @@ -525,6 +537,7 @@ bl managed-agent skill-list --source custom --provider bailian #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- The agentstudio endpoint is workspace-scoped: the base URL is composed from the workspace id (agents.yaml workspace_id > $BAILIAN_WORKSPACE_ID > bl's configured workspace_id) as https://{workspace}.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio, and the key must belong to that workspace. - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.