Skip to content

Commit d343dc3

Browse files
committed
fix(auth): only apply profile presets on login
1 parent 670c155 commit d343dc3

7 files changed

Lines changed: 27 additions & 54 deletions

File tree

docs/agents/auth-change.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
4646
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment)
4747
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
4848
- `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
49-
- 配置文件显式声明的 `api_key_capabilities` 优先于内置预设;命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
50-
- 字段缺失且 Profile 命中内置套餐预设(当前为 `token-plan`)时,runtime 使用当前预设白名单兼容旧配置,但不自动写回磁盘;字段缺失且没有内置预设时不启用降级
49+
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
50+
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录
5151
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
5252
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
5353
`--config` 时继续写当前激活项,失败和 dry-run 不切换

docs/agents/config-profile-change.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
2222
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
2323
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过,file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
24-
- 自定义 Profile 是否启用 capability fallback 只看 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。内置 `token-plan` 是当前唯一的名称 preset 兼容例外:旧配置缺少该字段时,runtime 使用当前内置白名单,但不自动写回磁盘
24+
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录
2525
- 对命中内置 preset 的 Profile,API Key 登录验证成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
2626
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get``video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
2727
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
@@ -60,7 +60,7 @@
6060
- 旧配置无 `active_config` -> `default`
6161
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
6262
- 任意名称 Profile 的叶子路由 capability 命中时使用自身 API Key;未命中或空白名单时使用 `default` API Key;`--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一覆盖时跳过 fallback。
63-
- 旧内置 Plan Profile 缺 capability 字段时使用 preset;显式配置(含 `[]`覆盖 preset,且不自动迁移写盘
63+
- 旧内置 Plan Profile 缺 capability 字段时不启用 fallback;重新登录后使用并持久化当前 preset,显式配置(含 `[]`按文件值生效
6464
- 显式命名 `--config``--config default` 均覆盖激活项且不修改磁盘状态。
6565
- 激活不存在的 Profile 失败且不写盘。
6666
- 悬空 `active_config` 明确失败。

packages/core/src/config/loader.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,13 @@ export interface ApiKeyResolutionSourceSelection {
202202
}
203203

204204
/**
205-
* Select the file-backed API-key source for a command capability. An explicit
206-
* flag or environment API connection override bypasses Profile capability
207-
* fallback entirely.
205+
* Select the file-backed API-key source for a command capability. Only a
206+
* persisted Profile capability list enables fallback. An explicit flag or
207+
* environment API connection override bypasses it entirely.
208208
*/
209209
export function selectApiKeyResolutionSources(
210210
sources: ResolutionSources,
211211
capability: string,
212-
presetCapabilities?: readonly string[],
213212
): ApiKeyResolutionSourceSelection {
214213
if (
215214
sources.flags.apiKey ||
@@ -221,7 +220,7 @@ export function selectApiKeyResolutionSources(
221220
}
222221
if (!sources.configName) return { sources };
223222

224-
const allowedCapabilities = sources.file.api_key_capabilities ?? presetCapabilities;
223+
const allowedCapabilities = sources.file.api_key_capabilities;
225224
if (allowedCapabilities === undefined) return { sources };
226225
if (allowedCapabilities.includes(capability)) return { sources };
227226

packages/core/tests/config-store.test.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
selectApiKeyResolutionSources,
1919
} from "../src/config/loader.ts";
2020
import { getConfigPath } from "../src/config/paths.ts";
21-
import { getModelProfilePreset } from "../src/config/profile-presets.ts";
2221

2322
/** 在隔离的临时配置目录里执行,结束后恢复环境。 */
2423
async function inTempConfigDir(fn: () => Promise<void>): Promise<void> {
@@ -446,42 +445,25 @@ test("显式 API Key 跳过 capability fallback,其他字段仍按既有优先
446445
});
447446
});
448447

449-
test("旧 token-plan Profile 缺少持久化字段时使用内置 capability 预设", async () => {
448+
test("旧 token-plan Profile 缺少 capability 字段时不启用 fallback", async () => {
450449
await inTempConfigDir(async () => {
451450
await writeConfigFile({ api_key: "sk-default" });
452451
await writeConfigFile({ api_key: "sk-token-plan" }, "token-plan");
453452
const selectedSources = { ...buildSources({ config: "token-plan" }), env: {} };
454-
const presetCapabilities = getModelProfilePreset("token-plan")?.apiKeyCapabilities;
455453

456-
const supported = selectApiKeyResolutionSources(
457-
selectedSources,
458-
"video.generate",
459-
presetCapabilities,
460-
);
461-
expect(supported.sources.file.api_key).toBe("sk-token-plan");
462-
463-
const fallback = selectApiKeyResolutionSources(
464-
selectedSources,
465-
"search.web",
466-
presetCapabilities,
467-
);
468-
expect(fallback.fallbackFrom).toBe("token-plan");
469-
expect(fallback.sources.file.api_key).toBe("sk-default");
454+
const selected = selectApiKeyResolutionSources(selectedSources, "search.web");
455+
expect(selected.fallbackFrom).toBeUndefined();
456+
expect(selected.sources.file.api_key).toBe("sk-token-plan");
470457
});
471458
});
472459

473-
test("token-plan 显式空 capability 白名单覆盖内置预设", async () => {
460+
test("token-plan 显式空 capability 白名单启用全量 fallback", async () => {
474461
await inTempConfigDir(async () => {
475462
await writeConfigFile({ api_key: "sk-default" });
476463
await writeConfigFile({ api_key: "sk-token-plan", api_key_capabilities: [] }, "token-plan");
477464
const selectedSources = { ...buildSources({ config: "token-plan" }), env: {} };
478-
const presetCapabilities = getModelProfilePreset("token-plan")?.apiKeyCapabilities;
479465

480-
const fallback = selectApiKeyResolutionSources(
481-
selectedSources,
482-
"video.generate",
483-
presetCapabilities,
484-
);
466+
const fallback = selectApiKeyResolutionSources(selectedSources, "video.generate");
485467
expect(fallback.fallbackFrom).toBe("token-plan");
486468
expect(fallback.sources.file.api_key).toBe("sk-default");
487469
});

packages/runtime/src/middleware.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import type {
1515
import {
1616
Client,
1717
DEFAULT_LANGUAGE,
18-
getModelProfilePreset,
1918
resolveApiKey,
2019
resolveConsole,
2120
resolveOpenApi,
@@ -127,8 +126,7 @@ export const authStage: Middleware = async (ctx, next) => {
127126
};
128127
if (command.auth === "apiKey") {
129128
const capability = ctx.path.join(".");
130-
const presetCapabilities = getModelProfilePreset(sources.configName)?.apiKeyCapabilities;
131-
const selection = selectApiKeyResolutionSources(sources, capability, presetCapabilities);
129+
const selection = selectApiKeyResolutionSources(sources, capability);
132130
const apiSources = selection.sources;
133131
if (selection.fallbackFrom && !settings.quiet) {
134132
writeApiKeyFallbackNotice(ctx, selection.fallbackFrom);

packages/runtime/tests/auth-profile-fallback.test.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,27 +139,20 @@ test("authStage 为支持的 capability 保留所选 Profile API Key", async ()
139139
expect(context.client.exportApiCredential()?.token).toBe("sk-plan");
140140
});
141141

142-
test("authStage 为缺少 capability 字段的旧 token-plan 应用内置 preset", async () => {
142+
test("authStage 不为缺少 capability 字段的旧 token-plan 注入内置 preset", async () => {
143143
useTempConfigDir();
144144
await writeConfigFile({ api_key: "sk-default", base_url: "https://default.example.com" });
145145
await writeConfigFile(
146146
{ api_key: "sk-token-plan", base_url: "https://token-plan.example.com" },
147147
"token-plan",
148148
);
149149

150-
const supportedContext = makeContext(["image", "generate"], { config: "token-plan" });
151-
await runAuth(supportedContext);
152-
expect(supportedContext.client.exportApiCredential()).toMatchObject({
150+
const context = makeContext(["search", "web"], { config: "token-plan" });
151+
await runAuth(context);
152+
expect(context.client.exportApiCredential()).toMatchObject({
153153
token: "sk-token-plan",
154154
baseUrl: "https://token-plan.example.com",
155155
});
156-
157-
const unsupportedContext = makeContext(["search", "web"], { config: "token-plan" });
158-
await runAuth(unsupportedContext);
159-
expect(unsupportedContext.client.exportApiCredential()).toMatchObject({
160-
token: "sk-default",
161-
baseUrl: "https://default.example.com",
162-
});
163156
});
164157

165158
test("authStage 在 --api-key 覆盖 Profile API Key 时跳过 capability fallback", async () => {

skills/bailian-protocol/assets/setup.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,11 @@ A named Profile can limit the API Key commands it supports through `api_key_capa
9898
- Unless `--quiet` is used, the CLI reports fallback on stderr. With `--output json`, it emits a
9999
structured warning.
100100

101-
Custom Profiles opt in only by defining `api_key_capabilities`. A missing field disables fallback,
102-
while `[]` makes every API Key command fall back to `default`.
103-
104-
The built-in `token-plan` Profile is the compatibility exception: older configs without the field
105-
use the current built-in preset. A successful API Key login appends missing preset capabilities
106-
without removing existing entries.
101+
Profiles opt in only by defining `api_key_capabilities`. A missing field disables fallback, while
102+
`[]` makes every API Key command fall back to `default`. This applies to the built-in `token-plan`
103+
Profile too: the CLI never injects a newer preset at runtime. A successful Token Plan API Key login
104+
appends missing preset capabilities without removing existing entries, so log in again to upgrade
105+
the persisted preset.
107106

108107
```bash
109108
bl config set --config company-plan --key api-key-capabilities \
@@ -128,7 +127,9 @@ still follow flag > environment > config.
128127

129128
Activation selects the entire Config for every credential domain, not only model consumption. The only exception is the API Key capability fallback described above. After activating `token-plan`, Token Plan management and Console commands still read their OpenAPI or Console credentials from that Profile. If those credentials remain in `default`, invoke the command with `--config default` or log the corresponding credential domain into `token-plan`.
130129

131-
The built-in `token-plan` profile defaults to:
130+
The built-in `token-plan` preset contains the following values. The CLI materializes them only after
131+
a successful `auth login --config token-plan`. It does not replace them at runtime; run the login
132+
command again to persist a newer preset:
132133

133134
- Base URL: `https://token-plan.cn-beijing.maas.aliyuncs.com`
134135
- Text model: `qwen3.8-max`

0 commit comments

Comments
 (0)