Skip to content

Commit 50ed680

Browse files
committed
feat: enhance credential handling for managed-agent and memory APIs
- Updated README.md to clarify API key usage and access restrictions for TokenPlan and pay-as-you-go keys. - Introduced shared credential validation logic to prevent TokenPlan keys from being used in incompatible contexts. - Enhanced error messaging for credential resolution failures in managed-agent and memory plugins. - Added tests for credential classification and workspace endpoint composition. - Updated documentation to reflect changes in credential handling and workspace-scoped agentstudio endpoint requirements.
1 parent f919eba commit 50ed680

12 files changed

Lines changed: 476 additions & 50 deletions

File tree

packages/commands/src/commands/managed-agent/_engine/credentials.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface CredentialHost {
5050
*/
5151
export const CREDENTIALS_NOTE = [
5252
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
53+
"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.",
5354
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
5455
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
5556
];
@@ -85,13 +86,19 @@ export function prepareProviderEnv(): void {
8586
* the block references them and the interpolated value is empty (a literal in
8687
* agents.yaml is respected).
8788
*
88-
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
89-
* paths onto it verbatim; a value already ending in the suffix is left as-is.
90-
* It is filled even without a credential — `client.baseUrl` is readable
91-
* credential-less (defaults to the CLI's model-domain base URL) — so offline
92-
* commands (which skip the credential assert) still satisfy the SDK's
93-
* "workspace_id or base_url" schema. With no credential the `api_key` is left
94-
* untouched: online commands reject it via {@link assertProviderCredentials}.
89+
* `base_url` is composed from the workspace when one is known — block
90+
* `workspace_id` (agents.yaml literal or interpolated `${BAILIAN_WORKSPACE_ID}`)
91+
* first, then bl's configured `workspace_id` — because agentstudio is served
92+
* only on the workspace-scoped host; the bare model-domain origin 404s it
93+
* (managed-agents API overview: `https://{workspace_id}.cn-beijing.maas.
94+
* aliyuncs.com/api/v1/agentstudio`, region cn-beijing only). Only with no
95+
* workspace at all does the model-domain origin get {@link AGENTSTUDIO_API_PATH}
96+
* suffixed. A value already ending in the suffix is left as-is. base_url is
97+
* filled even without a credential — `client.baseUrl` is readable
98+
* credential-less — so offline commands (which skip the credential assert)
99+
* still satisfy the SDK's "workspace_id or base_url" schema. With no
100+
* credential the `api_key` is left untouched: online commands reject it via
101+
* {@link assertProviderCredentials}.
95102
*/
96103
export function injectProviderCredentials(
97104
providers: Record<string, unknown>,
@@ -103,16 +110,27 @@ export function injectProviderCredentials(
103110

104111
const cred = host.client.exportApiCredential();
105112
if (cred) block.api_key = cred.token;
106-
if ("base_url" in block && !block.base_url) {
107-
// Defensive normalization: the auth chain already normalizes base_url to
108-
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
109-
const origin = host.client.baseUrl.replace(/\/+$/, "");
110-
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
111-
? origin
112-
: `${origin}${AGENTSTUDIO_API_PATH}`;
113+
if ("workspace_id" in block && !block.workspace_id) {
114+
// agents.yaml interpolation already replaced `${BAILIAN_WORKSPACE_ID}` in
115+
// file-based flows; the inline runtime passes an object config that never
116+
// interpolates, so read the env var here too (prepareProviderEnv
117+
// placeholders it to "" when unset). bl's configured workspace_id is the
118+
// last resort.
119+
block.workspace_id =
120+
process.env.BAILIAN_WORKSPACE_ID?.trim() || host.settings.workspaceId || "";
113121
}
114-
if ("workspace_id" in block && !block.workspace_id && host.settings.workspaceId) {
115-
block.workspace_id = host.settings.workspaceId;
122+
if ("base_url" in block && !block.base_url) {
123+
const workspaceId = typeof block.workspace_id === "string" ? block.workspace_id.trim() : "";
124+
if (workspaceId) {
125+
block.base_url = `https://${workspaceId}.cn-beijing.maas.aliyuncs.com${AGENTSTUDIO_API_PATH}`;
126+
} else {
127+
// Defensive normalization: the auth chain already normalizes base_url to
128+
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
129+
const origin = host.client.baseUrl.replace(/\/+$/, "");
130+
block.base_url = origin.endsWith(AGENTSTUDIO_API_PATH)
131+
? origin
132+
: `${origin}${AGENTSTUDIO_API_PATH}`;
133+
}
116134
}
117135
}
118136

packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,17 @@ export function inlineStatePath(agentName: string): string {
5656

5757
/**
5858
* The minimal in-memory project config that materializes into one cloud agent.
59-
* `providers.bailian` carries empty `api_key`/`base_url` placeholders so
60-
* {@link injectProviderCredentials} fills them from bl's auth chain (it only
61-
* writes fields the block already declares).
59+
* `providers.bailian` carries empty `api_key`/`base_url`/`workspace_id`
60+
* placeholders so {@link injectProviderCredentials} fills them from bl's auth
61+
* chain and workspace sources (it only writes fields the block already
62+
* declares). `workspace_id` lets injection compose the workspace-scoped
63+
* agentstudio host instead of the model-domain origin.
6264
*/
6365
export function buildInlineConfig(opts: InlineAgentOptions): Record<string, unknown> {
6466
return {
6567
version: "1",
6668
providers: {
67-
bailian: { api_key: "", base_url: "" },
69+
bailian: { api_key: "", base_url: "", workspace_id: "" },
6870
},
6971
defaults: { provider: "bailian" },
7072
environments: {

packages/commands/tests/credentials-bridge.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,25 @@ test("inject:已带后缀且尾斜杠的 base_url 去斜杠后原样保留", ()
124124
expect(providers.bailian.base_url).toBe("https://x.maas.aliyuncs.com/api/v1/agentstudio");
125125
});
126126

127-
test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则保留", () => {
127+
test("inject:workspace_id 引用且为空时按 env > settings 填充;有字面量则保留", () => {
128+
delete process.env.BAILIAN_WORKSPACE_ID;
128129
const empty = { bailian: { api_key: "", workspace_id: "" } };
129130
injectProviderCredentials(
130131
empty,
131132
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
132133
);
133134
expect(empty.bailian.workspace_id).toBe("ws-settings");
134135

136+
// 内联运行时(对象配置)不做 ${} 插值,env 变量在此补读。
137+
process.env.BAILIAN_WORKSPACE_ID = "ws-env";
138+
const fromEnv = { bailian: { api_key: "", workspace_id: "" } };
139+
injectProviderCredentials(
140+
fromEnv,
141+
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
142+
);
143+
expect(fromEnv.bailian.workspace_id).toBe("ws-env");
144+
delete process.env.BAILIAN_WORKSPACE_ID;
145+
135146
const literal = { bailian: { api_key: "", workspace_id: "ws-yaml" } };
136147
injectProviderCredentials(
137148
literal,
@@ -140,6 +151,37 @@ test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则
140151
expect(literal.bailian.workspace_id).toBe("ws-yaml");
141152
});
142153

154+
test("inject:workspace 已知时 base_url 拼工作空间主机,而非模型域 origin", () => {
155+
// agents.yaml 字面量 workspace_id + 空 base_url。
156+
const literal = { bailian: { api_key: "", base_url: "", workspace_id: "ws-yaml" } };
157+
injectProviderCredentials(literal, makeHost({ apiCred: bailianCred() }));
158+
expect(literal.bailian.base_url).toBe(
159+
"https://ws-yaml.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
160+
);
161+
162+
// 内联块:workspace_id 由 settings 填充后同样走工作空间主机。
163+
const inline = { bailian: { api_key: "", base_url: "", workspace_id: "" } };
164+
injectProviderCredentials(
165+
inline,
166+
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
167+
);
168+
expect(inline.bailian.workspace_id).toBe("ws-settings");
169+
expect(inline.bailian.base_url).toBe(
170+
"https://ws-settings.cn-beijing.maas.aliyuncs.com/api/v1/agentstudio",
171+
);
172+
173+
// 显式 base_url 字面量永远优先于拼装。
174+
const explicit = {
175+
bailian: {
176+
api_key: "",
177+
base_url: "https://custom.example.com/api/v1/agentstudio",
178+
workspace_id: "ws-yaml",
179+
},
180+
};
181+
injectProviderCredentials(explicit, makeHost({ apiCred: bailianCred() }));
182+
expect(explicit.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio");
183+
});
184+
143185
test("inject:无凭证时 api_key 保持不变,base_url 仍用 client 默认域名补齐(离线/范围外 schema 可用)", () => {
144186
const providers = { bailian: { api_key: "", base_url: "" } };
145187
injectProviderCredentials(providers, makeHost({}));

packages/dsh/README.md

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@
2828

2929
- 百炼 API Key。**注意有两类且不可混用**
3030

31-
| 类型 | 前缀 | 能访问 | 不能访问 |
32-
| --------- | -------- | --------------------------------------- | -------------- |
33-
| TokenPlan | `sk-sp-` | TokenPlan 网关(LLM / vision / 文生图) | 记忆库、知识库 |
34-
| 按量付费 | `sk-ws-` | 记忆库、知识库、DashScope 全量接口 | TokenPlan 网关 |
31+
| 类型 | 前缀 | 能访问 | 不能访问 |
32+
| --------- | -------- | ----------------------------------------------------------- | ------------------------ |
33+
| TokenPlan | `sk-sp-` | TokenPlan 网关(LLM / vision / 文生图) | 记忆库、知识库、远程任务 |
34+
| 按量付费 | `sk-ws-` | 记忆库、知识库、远程任务(agentstudio)、DashScope 全量接口 | TokenPlan 网关 |
3535

3636
两者互相返回 `401 InvalidApiKey`,所以本包用**两个不同的环境变量**,不会互相踩:
3737

3838
```sh
3939
export BAILIAN_TOKENPLAN_API_KEY=sk-sp-xxx # 只给 bailian-tokenplan provider
40-
export DASHSCOPE_API_KEY=sk-ws-xxx # 给 bl、memory、RAG
40+
export DASHSCOPE_API_KEY=sk-ws-xxx # 给 bl、memory、RAG、远程任务
4141
```
4242

4343
只有一类 Key 也能用,只是能力范围相应缩小。若只有 TokenPlan Key:
@@ -48,7 +48,13 @@
4848
export DASHSCOPE_BASE_URL=https://token-plan.cn-beijing.maas.aliyuncs.com
4949
```
5050

51-
这样 LLM / vision / 文生图可用,memory 与 RAG 不可用(保持停用即可)。
51+
这样 LLM / vision / 文生图可用(后两者经 `bl` 走 TokenPlan 网关);memory 与 RAG 保持停用即可。**远程任务仍可注册**,但它的凭证解析会看出这是 TokenPlan Key / 网关,调用 `bailian_run_remote_task` 时直接给出带修复指引的报错,而不是以前的 `Bailian API 404`
52+
53+
**按量付费 Key 的解析顺序**(memory / RAG / 远程任务三处一致):行内 `config.apiKey``$DASHSCOPE_API_KEY`
54+
55+
**远程任务的端点**另有讲究: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 在百炼控制台右上角的工作空间下拉里看。
56+
57+
memory / RAG 是显式开启的插件,Key 缺失或误填 `sk-sp-` 会在启动期报错;远程任务默认启用,为避免拖垮 TokenPlan-only 环境,改为调用时报错。
5258

5359
---
5460

@@ -125,17 +131,23 @@ DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见*
125131
把一个任务甩到百炼云端的托管 agent 上跑,不占本地会话。**无需预先写 `agents.yaml``apply`**:工具首次被调用时,`bl managed-agent run` 会在你的账号里幂等创建一个 agent + cloud environment,之后复用。
126132

127133
- 模型自己按用户意图填 `instructions`(远程 agent 的角色),`task` 是要它做的事。例如你说「在云端帮我审计这个依赖树,它该懂安全」→ 模型调 `bailian_run_remote_task(task="审计依赖树", instructions="你是安全专家")`
128-
- **前提**:这条路走的是 managed-agent(agentstudio)服务,需要**按量付费 Key**`sk-ws-`)+ dashscope 端点,且账号已开通 managed-agent。TokenPlan Key 不适用。若 `DASHSCOPE_API_KEY`/端点没配好,首次调用会返回 `Bailian API 404`
134+
- **前提**:这条路走的是 managed-agent(agentstudio)服务,需要**按量付费 Key**`sk-ws-`)+ **工作空间端点**,且账号已开通 managed-agent。TokenPlan Key 不适用。
135+
- **凭证解析**: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。
136+
- **两个高频报错**`404`=端点不是工作空间主机;`403 Endpoint.AccessDenied`=主机对了但这个 Key 不属于该工作空间。二者都会附带具体修复指引。Key 归属的工作空间在百炼控制台右上角下拉里看。
129137
- 首次会创建云资源(可能计费、启动有延迟);同名 agent 后续复用。默认 agent 名 `dsh-remote-runner`,可在配置里改。
130138

131-
需要非默认的 agent 名 / 模型时
139+
需要非默认的 agent 名 / 模型 / 凭证时
132140

133141
```yaml
134142
- id: bailian-tool-managed-agent
135143
config:
136144
agent: my-runner
137145
model: qwen3.8-max
138146
timeoutMs: 600000
147+
# 可选凭证(省略则按上面的解析顺序找):
148+
# apiKey: sk-ws-xxxxxxxx
149+
# workspaceId: llm-xxxxxxxx # 推荐:自动拼成工作空间端点
150+
# baseUrl: https://llm-xxxxxxxx.cn-beijing.maas.aliyuncs.com # 或用完整端点
139151
```
140152

141153
---
@@ -157,7 +169,7 @@ DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见*
157169
workspaceId: llm-xxxxxxxx # 百炼控制台工作空间 ID
158170
agentId: aid-xxxxxxxx # 知识库"检索服务"ID
159171
maxResults: 10
160-
# apiKey 省略则读 $DASHSCOPE_API_KEY
172+
# apiKey 省略则读 $DASHSCOPE_API_KEY(须为按量付费 sk-ws-;误填 sk-sp- 会在启动期报错)
161173
```
162174

163175
一个实例对一个知识库(`WebSearchRequest` 只带 `query` / `maxResults`,agentId 只能来自配置)。要多个知识库就插多行不同 `id`
@@ -229,15 +241,18 @@ bl auth status
229241

230242
## 6. 常见问题
231243

232-
| 现象 | 原因 |
233-
| -------------------------------------- | ---------------------------------------------------------------------- |
234-
| LLM 路由 `401 InvalidApiKey` | `BAILIAN_TOKENPLAN_API_KEY` 没设,或误填了 `sk-ws-` 的按量付费 Key |
235-
| memory / RAG `401 InvalidApiKey` | `DASHSCOPE_API_KEY` 误填了 `sk-sp-` 的 TokenPlan Key |
236-
| `WEB_PROVIDER_AMBIGUOUS` | 有多个搜索 provider,需在 `web` 行 pin `searchProvider` |
237-
| 粘图报 `MODEL_DOES_NOT_SUPPORT_IMAGES` | 当前模型不支持图片输入,换成上表标"是"的,或改用 vision 工具 |
238-
| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` |
239-
| 改了 patch 但没生效 | `config` 是整体替换,检查是否漏写了原有字段;再用 `--dump-config` 确认 |
240-
| `memoryLibraryId does not exist` | 记忆库 ID 属于另一个账号,与当前 Key 不匹配 |
244+
| 现象 | 原因 |
245+
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
246+
| LLM 路由 `401 InvalidApiKey` | `BAILIAN_TOKENPLAN_API_KEY` 没设,或误填了 `sk-ws-` 的按量付费 Key |
247+
| memory / RAG 启动期报 TokenPlan Key | `DASHSCOPE_API_KEY` / `apiKey` 误填了 `sk-sp-` 的 TokenPlan Key |
248+
| 远程任务 `Bailian API 404` | 端点不是工作空间前缀主机(TokenPlan 网关 / 普通 dashscope 主机都不提供 agentstudio);给该行配 `workspaceId`(或 `baseUrl`),或导出 `BAILIAN_WORKSPACE_ID` / `DASHSCOPE_BASE_URL` |
249+
| 远程任务 `403 Endpoint.AccessDenied` | 主机是工作空间主机,但这个 Key 不属于该工作空间;换成 Key 归属工作空间的 ID(控制台右上角下拉),或用属于该工作空间的 Key |
250+
| 远程任务调用即报 TokenPlan 提示 | `$DASHSCOPE_API_KEY` 是 `sk-sp-`;换按量付费 Key 或在行内配 `apiKey` |
251+
| `WEB_PROVIDER_AMBIGUOUS` | 有多个搜索 provider,需在 `web` 行 pin `searchProvider` |
252+
| 粘图报 `MODEL_DOES_NOT_SUPPORT_IMAGES` | 当前模型不支持图片输入,换成上表标"是"的,或改用 vision 工具 |
253+
| 工具报找不到 `bl` | `bl` 不在 PATH:`npm install -g bailian-cli` |
254+
| 改了 patch 但没生效 | `config` 是整体替换,检查是否漏写了原有字段;再用 `--dump-config` 确认 |
255+
| `memoryLibraryId does not exist` | 记忆库 ID 属于另一个账号,与当前 Key 不匹配 |
241256

242257
---
243258

0 commit comments

Comments
 (0)