fix(aibox): never fall back to deepseek.key for model profile endpoints (H1)

~/.cert/deepseek.key is a DeepSeek-only credential; when a models profile
specified its own api_key_env (e.g. MOONSHOT_API_KEY) but the env var was
unset, resolve_api_key silently read that file and sent the DeepSeek key
as Bearer to a third-party endpoint (cross-vendor credential leak).

resolve_api_key gains allow_key_file (default True for the legacy flat
api section); create_llm_client passes allow_key_file=profile is None so
any profile path raises a clear RuntimeError instead, pointing users to
set the env var or use "" for local keyless endpoints.

Tests: new TestH1KeyIsolation (4 cases) with a mocked readable
deepseek.key proving the profile path errors out while the flat api
section still reads the file; full suite 276 passed.
This commit is contained in:
qing committed 2026-08-17 23:28:25 +08:00
1 parent 58725c8a12
commit 4480bce684
3 files changed
+68 -3

No files matched your search

+11 -2
View File
@@ -221,13 +221,17 @@ class DeepSeekClient(LLMClient):
def resolve_api_key(api_key: Optional[str], config=None,
env_name: Optional[str] = None,
allow_missing: bool = False) -> str:
allow_missing: bool = False,
allow_key_file: bool = True) -> str:
"""解析 API Key:优先显式传入,其次环境变量,最后 ~/.cert/deepseek.key。
env_name: 档案级环境变量名(来自 models.<name>.api_key_env);
None 时回退 config 的 api.api_key_env。
allow_missing: 本地免密端点(models.<name>.api_key_env 为空串)时,
允许无 Key,返回空串(Bearer 空头,Ollama/vLLM 忽略)。
allow_key_file: 是否允许回退读取 ~/.cert/deepseek.key。该文件是 DeepSeek
专属凭据,模型档案端点(其他厂商)必须传 False——环境变量取不到
时直接报错,绝不把 A 厂商 Key 发给 B 厂商(跨厂商凭据泄漏)。
"""
if api_key:
return api_key
@@ -244,6 +248,10 @@ def resolve_api_key(api_key: Optional[str], config=None,
return env_key
if allow_missing:
return ""
if not allow_key_file:
raise RuntimeError(
f"模型档案端点缺少 API Key:请设置环境变量 {env_name},"
f"或将该档案的 api_key_env 配置为空串以启用本地免密端点")
try:
home = os.environ.get("HOME", os.environ["USERPROFILE"])
key_path = os.path.join(home, ".cert", "deepseek.key")
@@ -314,7 +322,8 @@ def create_llm_client(config, api_key: Optional[str] = None,
timeout = config.get("api.timeout", 30)
resolved_key = resolve_api_key(api_key, config, env_name=env_name,
allow_missing=allow_missing)
allow_missing=allow_missing,
allow_key_file=profile is None)
if profile is not None:
provider = "openai_compatible"