diff --git a/apps/desktop/src/app/settings/env-credentials.tsx b/apps/desktop/src/app/settings/env-credentials.tsx index 8be974dc68..78b0418d60 100644 --- a/apps/desktop/src/app/settings/env-credentials.tsx +++ b/apps/desktop/src/app/settings/env-credentials.tsx @@ -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)) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 507fd2cffd..da6b46d162 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -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: diff --git a/tests/hermes_cli/test_auth_provider_gate.py b/tests/hermes_cli/test_auth_provider_gate.py index a85ddedb0a..d2a185065a 100644 --- a/tests/hermes_cli/test_auth_provider_gate.py +++ b/tests/hermes_cli/test_auth_provider_gate.py @@ -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