From d343dc3265b4f0b8c9e9c4fee5a92e7228b0dbaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E9=BA=92?= Date: Wed, 26 Aug 2026 17:10:00 +0800 Subject: [PATCH] fix(auth): only apply profile presets on login --- docs/agents/auth-change.md | 4 +-- docs/agents/config-profile-change.md | 4 +-- packages/core/src/config/loader.ts | 9 +++--- packages/core/tests/config-store.test.ts | 30 ++++--------------- packages/runtime/src/middleware.ts | 4 +-- .../tests/auth-profile-fallback.test.ts | 15 +++------- skills/bailian-protocol/assets/setup.md | 15 +++++----- 7 files changed, 27 insertions(+), 54 deletions(-) diff --git a/docs/agents/auth-change.md b/docs/agents/auth-change.md index c0dc43f..50b524b 100644 --- a/docs/agents/auth-change.md +++ b/docs/agents/auth-change.md @@ -46,8 +46,8 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx - `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment) - `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`: - `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值 - - 配置文件显式声明的 `api_key_capabilities` 优先于内置预设;命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile - - 字段缺失且 Profile 命中内置套餐预设(当前为 `token-plan`)时,runtime 使用当前预设白名单兼容旧配置,但不自动写回磁盘;字段缺失且没有内置预设时不启用降级 + - 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile + - 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录 - fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果 - 显式 `auth login --config ` 在凭证验证并落盘成功后自动激活目标 Profile;未传 `--config` 时继续写当前激活项,失败和 dry-run 不切换 diff --git a/docs/agents/config-profile-change.md b/docs/agents/config-profile-change.md index c9ea5ce..3267f6d 100644 --- a/docs/agents/config-profile-change.md +++ b/docs/agents/config-profile-change.md @@ -21,7 +21,7 @@ `auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。 - 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。 - 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 解析。 -- 自定义 Profile 是否启用 capability fallback 只看 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。内置 `token-plan` 是当前唯一的名称 preset 兼容例外:旧配置缺少该字段时,runtime 使用当前内置白名单,但不自动写回磁盘。 +- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录。 - 对命中内置 preset 的 Profile,API Key 登录验证成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。 - Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get` → `video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。 - Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。 @@ -60,7 +60,7 @@ - 旧配置无 `active_config` -> `default`。 - 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。 - 任意名称 Profile 的叶子路由 capability 命中时使用自身 API Key;未命中或空白名单时使用 `default` API Key;`--api-key` / `--base-url` 和 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一覆盖时跳过 fallback。 -- 旧内置 Plan Profile 缺 capability 字段时使用 preset;显式配置(含 `[]`)覆盖 preset,且不自动迁移写盘。 +- 旧内置 Plan Profile 缺 capability 字段时不启用 fallback;重新登录后使用并持久化当前 preset,显式配置(含 `[]`)按文件值生效。 - 显式命名 `--config` 和 `--config default` 均覆盖激活项且不修改磁盘状态。 - 激活不存在的 Profile 失败且不写盘。 - 悬空 `active_config` 明确失败。 diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index 1c4d4bd..096f991 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -202,14 +202,13 @@ export interface ApiKeyResolutionSourceSelection { } /** - * Select the file-backed API-key source for a command capability. An explicit - * flag or environment API connection override bypasses Profile capability - * fallback entirely. + * Select the file-backed API-key source for a command capability. Only a + * persisted Profile capability list enables fallback. An explicit flag or + * environment API connection override bypasses it entirely. */ export function selectApiKeyResolutionSources( sources: ResolutionSources, capability: string, - presetCapabilities?: readonly string[], ): ApiKeyResolutionSourceSelection { if ( sources.flags.apiKey || @@ -221,7 +220,7 @@ export function selectApiKeyResolutionSources( } if (!sources.configName) return { sources }; - const allowedCapabilities = sources.file.api_key_capabilities ?? presetCapabilities; + const allowedCapabilities = sources.file.api_key_capabilities; if (allowedCapabilities === undefined) return { sources }; if (allowedCapabilities.includes(capability)) return { sources }; diff --git a/packages/core/tests/config-store.test.ts b/packages/core/tests/config-store.test.ts index 875e7fc..a49ae24 100644 --- a/packages/core/tests/config-store.test.ts +++ b/packages/core/tests/config-store.test.ts @@ -18,7 +18,6 @@ import { selectApiKeyResolutionSources, } from "../src/config/loader.ts"; import { getConfigPath } from "../src/config/paths.ts"; -import { getModelProfilePreset } from "../src/config/profile-presets.ts"; /** 在隔离的临时配置目录里执行,结束后恢复环境。 */ async function inTempConfigDir(fn: () => Promise): Promise { @@ -446,42 +445,25 @@ test("显式 API Key 跳过 capability fallback,其他字段仍按既有优先 }); }); -test("旧 token-plan Profile 缺少持久化字段时使用内置 capability 预设", async () => { +test("旧 token-plan Profile 缺少 capability 字段时不启用 fallback", async () => { await inTempConfigDir(async () => { await writeConfigFile({ api_key: "sk-default" }); await writeConfigFile({ api_key: "sk-token-plan" }, "token-plan"); const selectedSources = { ...buildSources({ config: "token-plan" }), env: {} }; - const presetCapabilities = getModelProfilePreset("token-plan")?.apiKeyCapabilities; - const supported = selectApiKeyResolutionSources( - selectedSources, - "video.generate", - presetCapabilities, - ); - expect(supported.sources.file.api_key).toBe("sk-token-plan"); - - const fallback = selectApiKeyResolutionSources( - selectedSources, - "search.web", - presetCapabilities, - ); - expect(fallback.fallbackFrom).toBe("token-plan"); - expect(fallback.sources.file.api_key).toBe("sk-default"); + const selected = selectApiKeyResolutionSources(selectedSources, "search.web"); + expect(selected.fallbackFrom).toBeUndefined(); + expect(selected.sources.file.api_key).toBe("sk-token-plan"); }); }); -test("token-plan 显式空 capability 白名单覆盖内置预设", async () => { +test("token-plan 显式空 capability 白名单启用全量 fallback", async () => { await inTempConfigDir(async () => { await writeConfigFile({ api_key: "sk-default" }); await writeConfigFile({ api_key: "sk-token-plan", api_key_capabilities: [] }, "token-plan"); const selectedSources = { ...buildSources({ config: "token-plan" }), env: {} }; - const presetCapabilities = getModelProfilePreset("token-plan")?.apiKeyCapabilities; - const fallback = selectApiKeyResolutionSources( - selectedSources, - "video.generate", - presetCapabilities, - ); + const fallback = selectApiKeyResolutionSources(selectedSources, "video.generate"); expect(fallback.fallbackFrom).toBe("token-plan"); expect(fallback.sources.file.api_key).toBe("sk-default"); }); diff --git a/packages/runtime/src/middleware.ts b/packages/runtime/src/middleware.ts index 993ba6a..5322479 100644 --- a/packages/runtime/src/middleware.ts +++ b/packages/runtime/src/middleware.ts @@ -15,7 +15,6 @@ import type { import { Client, DEFAULT_LANGUAGE, - getModelProfilePreset, resolveApiKey, resolveConsole, resolveOpenApi, @@ -127,8 +126,7 @@ export const authStage: Middleware = async (ctx, next) => { }; if (command.auth === "apiKey") { const capability = ctx.path.join("."); - const presetCapabilities = getModelProfilePreset(sources.configName)?.apiKeyCapabilities; - const selection = selectApiKeyResolutionSources(sources, capability, presetCapabilities); + const selection = selectApiKeyResolutionSources(sources, capability); const apiSources = selection.sources; if (selection.fallbackFrom && !settings.quiet) { writeApiKeyFallbackNotice(ctx, selection.fallbackFrom); diff --git a/packages/runtime/tests/auth-profile-fallback.test.ts b/packages/runtime/tests/auth-profile-fallback.test.ts index 52a2f4c..7b62fe7 100644 --- a/packages/runtime/tests/auth-profile-fallback.test.ts +++ b/packages/runtime/tests/auth-profile-fallback.test.ts @@ -139,7 +139,7 @@ test("authStage 为支持的 capability 保留所选 Profile API Key", async () expect(context.client.exportApiCredential()?.token).toBe("sk-plan"); }); -test("authStage 为缺少 capability 字段的旧 token-plan 应用内置 preset", async () => { +test("authStage 不为缺少 capability 字段的旧 token-plan 注入内置 preset", async () => { useTempConfigDir(); await writeConfigFile({ api_key: "sk-default", base_url: "https://default.example.com" }); await writeConfigFile( @@ -147,19 +147,12 @@ test("authStage 为缺少 capability 字段的旧 token-plan 应用内置 preset "token-plan", ); - const supportedContext = makeContext(["image", "generate"], { config: "token-plan" }); - await runAuth(supportedContext); - expect(supportedContext.client.exportApiCredential()).toMatchObject({ + const context = makeContext(["search", "web"], { config: "token-plan" }); + await runAuth(context); + expect(context.client.exportApiCredential()).toMatchObject({ token: "sk-token-plan", baseUrl: "https://token-plan.example.com", }); - - const unsupportedContext = makeContext(["search", "web"], { config: "token-plan" }); - await runAuth(unsupportedContext); - expect(unsupportedContext.client.exportApiCredential()).toMatchObject({ - token: "sk-default", - baseUrl: "https://default.example.com", - }); }); test("authStage 在 --api-key 覆盖 Profile API Key 时跳过 capability fallback", async () => { diff --git a/skills/bailian-protocol/assets/setup.md b/skills/bailian-protocol/assets/setup.md index 04a5c65..79d3567 100644 --- a/skills/bailian-protocol/assets/setup.md +++ b/skills/bailian-protocol/assets/setup.md @@ -98,12 +98,11 @@ A named Profile can limit the API Key commands it supports through `api_key_capa - Unless `--quiet` is used, the CLI reports fallback on stderr. With `--output json`, it emits a structured warning. -Custom Profiles opt in only by defining `api_key_capabilities`. A missing field disables fallback, -while `[]` makes every API Key command fall back to `default`. - -The built-in `token-plan` Profile is the compatibility exception: older configs without the field -use the current built-in preset. A successful API Key login appends missing preset capabilities -without removing existing entries. +Profiles opt in only by defining `api_key_capabilities`. A missing field disables fallback, while +`[]` makes every API Key command fall back to `default`. This applies to the built-in `token-plan` +Profile too: the CLI never injects a newer preset at runtime. A successful Token Plan API Key login +appends missing preset capabilities without removing existing entries, so log in again to upgrade +the persisted preset. ```bash bl config set --config company-plan --key api-key-capabilities \ @@ -128,7 +127,9 @@ still follow flag > environment > config. 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`. -The built-in `token-plan` profile defaults to: +The built-in `token-plan` preset contains the following values. The CLI materializes them only after +a successful `auth login --config token-plan`. It does not replace them at runtime; run the login +command again to persist a newer preset: - Base URL: `https://token-plan.cn-beijing.maas.aliyuncs.com` - Text model: `qwen3.8-max`