fix(auth): count profile .env keys in the explicit provider gate

os.getenv saw only the launch profile, so a DeepSeek key pasted in
another profile never appeared in Settings → Model until Refresh
models ran against that Bot's own backend.
This commit is contained in:
xxxigm
2026-09-20 22:33:40 +07:00
committed by Austin Pickett
parent 050ea53aba
commit 4ea57fa61b
3 changed files with 47 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
import { deleteEnvVar, getEnvVars, revealEnvVar, setEnvVar } from '@/hermes'
import { useI18n } from '@/i18n'
import { type IconComponent } from '@/lib/icons'
import { queryClient } from '@/lib/query-client'
import { confirm } from '@/store/confirm'
import { notify, notifyError } from '@/store/notifications'
import type { EnvVarInfo } from '@/types/hermes'
@@ -115,6 +116,7 @@ export function useEnvCredentials(profile?: string): UseEnvCredentials {
await setEnvVar(key, value, profile)
patchVar(key, { is_set: true, redacted_value: redactedValue(value) })
clearLocalState(key)
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
notify({ kind: 'success', title: toolsets.savedTitle, message: toolsets.savedMessage(key) })
} catch (err) {
notifyError(err, toolsets.failedSave(key))
@@ -139,6 +141,7 @@ export function useEnvCredentials(profile?: string): UseEnvCredentials {
await setEnvVar(key, trimmed, profile)
patchVar(key, { is_set: true, redacted_value: redactedValue(trimmed) })
clearLocalState(key)
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
notify({ kind: 'success', message: toolsets.savedMessage(key), title: toolsets.savedTitle })
return { ok: true }
@@ -162,6 +165,7 @@ export function useEnvCredentials(profile?: string): UseEnvCredentials {
await deleteEnvVar(key, profile)
patchVar(key, { is_set: false, redacted_value: null })
clearLocalState(key)
void queryClient.invalidateQueries({ queryKey: ['model-options'] })
notify({ kind: 'success', title: toolsets.removedTitle, message: toolsets.removedMessage(key) })
} catch (err) {
notifyError(err, toolsets.failedRemove(key))

View File

@@ -1017,7 +1017,17 @@ _VERTEX_PROVIDER_IDS = ("vertex", "google-vertex", "vertex-ai", "gcp-vertex", "v
def _env_secret(name: str) -> bool:
return has_usable_secret(os.getenv(name, ""))
"""True when *name* resolves to a usable secret in the active profile scope.
Must not read raw ``os.getenv``: under ``hermes serve`` / Desktop multiplex the
process environ is the *launch* profile, so a DeepSeek key pasted into another
profile's ``.env`` would be invisible to ``explicit_only`` Settings → Model
until a Bot-chat Refresh ran against that profile's own backend.
``get_env_value`` is the scope-aware reader (#67027): secret scope, then the
current HERMES_HOME ``.env``.
"""
from hermes_cli.config import get_env_value
return has_usable_secret(get_env_value(name) or "")
def _explicit_env_credentials_present(normalized: str) -> bool:

View File

@@ -190,6 +190,38 @@ def test_stale_env_pool_entry_does_not_count_when_var_unset(tmp_path, monkeypatc
assert is_provider_explicitly_configured("deepseek") is False
def test_profile_dotenv_key_counts_as_explicit_when_process_env_lacks_it(tmp_path, monkeypatch):
"""A DeepSeek key in a named profile's .env must count under that profile's
secret scope even when os.environ (the launch profile) has no DeepSeek var.
Desktop Settings → Model uses explicit_only, which filters through
is_provider_explicitly_configured. os.getenv hid the keyed provider until
Refresh models ran against the bot's own backend.
"""
root = tmp_path / "hermes"
studio = root / "profiles" / "content-studio"
studio.mkdir(parents=True)
(root / "config.yaml").write_text("model: {}\n")
(studio / "config.yaml").write_text("model: {}\n")
(studio / ".env").write_text("DEEPSEEK_API_KEY=sk-studio-deepseek-key\n")
monkeypatch.setenv("HERMES_HOME", str(root))
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
from agent import secret_scope
from tui_gateway import launch_profile_policy as lpp
monkeypatch.setattr(secret_scope, "_MULTIPLEX_ACTIVE", False)
monkeypatch.setattr(lpp, "_snapshot", None)
from hermes_cli.auth import is_provider_explicitly_configured
from hermes_cli.web_server_profiles import _config_profile_scope
assert is_provider_explicitly_configured("deepseek") is False
with _config_profile_scope("content-studio"):
assert is_provider_explicitly_configured("deepseek") is True
with _config_profile_scope(None):
assert is_provider_explicitly_configured("deepseek") is False
# ─── aws_sdk providers (Bedrock) ─────────────────────────────────────────
#
# Bedrock is registered with auth_type="aws_sdk" and an empty