Credential pool not checked across CLI auth flows

NousResearch/hermes-agent · cluster 5807 · agent

Data as of

7 open items. Fix merged in #16101. Check it covers your case.

The short answer

4open issues
3open PRs
37items grouped
Known?
Yes. 37 related items grouped here, 7 issues already closed.
Fix event
A fix was merged to main in PR #16101. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #16101, #73509, #73883. That is separate from the fix event.
Still open
4 issues and 3 PRs.

A cluster of bugs and fixes around Hermes CLI tools (debug, doctor, model wizard, status, fallback chain) failing to consult the credential_pool or auth.json when checking for API keys, OAuth tokens, and provider configuration. Multiple commands incorrectly reported credentials as missing or providers as unconfigured when valid pooled credentials existed.

Open issues4

  • Issue#20675hermes debug: credential check should include credential_pool OAuth tokens, not just env vars
  • Issue#42269hermes auth add deepseek: credential stored to pool but provider resolution ignores it at runtime
  • Issue#50737bug(auth/profile): OpenRouter pool state can override or leak into selected profile readiness
  • Issue#60106hermes status reports OpenRouter not set when credential exists only in auth pool

Open PRs3

  • PR#42386fix(auth): surface pool base_url and log errors in _resolve_api_key_p…
  • PR#60135fix: hermes status checks credential pool when env var is absent (#60106)
  • PR#60401fix(debug): hermes debug shows OAuth providers as 'not set' when authenticated (Fixes #20675)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #16101 fix(auth): resolve API keys from ~/.hermes/.env and credential_pool

    merged to main

    files: agent/credential_pool.py, hermes_cli/auth.py, tests/tools/test_credential_pool_env_fallback.py

    Show the diff
    • agent/credential_pool.py
      @@ -14,6 +14,7 @@
       from typing import Any, Dict, List, Optional, Set, Tuple
       
       from hermes_constants import OPENROUTER_BASE_URL
      +from hermes_cli.config import get_env_value
       import hermes_cli.auth as auth_mod
       from hermes_cli.auth import (
           CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
      @@ -1273,7 +1274,8 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool
               def _is_source_suppressed(_p, _s):  # type: ignore[misc]
                   return False
           if provider == "openrouter":
      -        token = [redacted:secret-assignment]"OPENROUTER_API_KEY", "").strip()
      +        # Check both os.environ and ~/.hermes/.env file
      +        token = [redacted:secret-assignment]"OPENROUTER_API_KEY") or "").strip()
               if token:
                   source = "env:OPENROUTER_API_KEY"
                   if _is_source_suppressed(provider, source):
      @@ -1299,7 +1301,7 @@ def _is_source_suppressed(_p, _s):  # type: ignore[misc]
       
           env_url = ""
           if pconfig.base_url_env_var:
      -        env_url = os.getenv(pconfig.base_url_env_var, "").strip().rstrip("/")
      +        env_url = (get_env_value(pconfig.base_url_env_var) or "").strip().rstrip("/")
       
           env_vars = list(pconfig.api_key_env_vars)
           if provider == "anthropic":
      @@ -1310,7 +1312,8 @@ def _is_source_suppressed(_p, _s):  # type: ignore[misc]
               ]
       
           for env_var in env_vars:
      -        token = [redacted:secret-assignment] "").strip()
      +        # Check both os.environ and ~/.hermes/.env file
      +        token = [redacted:secret-assignment] or "").strip()
               if not token:
                   continue
               source = f"env:{env_var}"
    • hermes_cli/auth.py
      @@ -467,11 +467,27 @@ def _resolve_api_key_provider_secret(
                   pass
               return "", ""
       
      +    from hermes_cli.config import get_env_value
           for env_var in pconfig.api_key_env_vars:
      -        val = os.getenv(env_var, "").strip()
      +        # Check both os.environ and ~/.hermes/.env file
      +        val = (get_env_value(env_var) or "").strip()
               if has_usable_secret(val):
                   return val, env_var
       
      +    # Fallback: try credential pool (e.g. zai key stored via auth.json)
      +    try:
      +        from agent.credential_pool import load_pool
      +        pool = load_pool(provider_id)
      +        if pool and pool.has_credentials():
      +            entry = pool.peek()
      +            if entry:
      +                key = getattr(entry, "access_token", "") or getattr(entry, "runtime_api_key", "")
      +                key = str(key).strip()
      +                if has_usable_secret(key):
      +                    return key, f"credential_pool:[redacted:secret-assignment]"
      +    except Exception:
      +        pass
      +
           return "", ""
       
       
    • tests/tools/test_credential_pool_env_fallback.py
      @@ -0,0 +1,210 @@
      +"""Tests for credential_pool .env fallback and auth credential_pool lookup.
      +
      +Covers the fix from #15914 / PR #15920:
      +- _seed_from_env reads API keys from ~/.hermes/.env when not in os.environ
      +- _resolve_api_key_provider_secret falls back to credential_pool when env vars are empty
      +- env vars take priority over .env file (handled by get_env_value itself)
      +- env vars take priority over credential pool (fallback only kicks in when env is empty)
      +"""
      +
      +import os
      +from pathlib import Path
      +from unittest.mock import MagicMock, patch
      +
      +import pytest
      +
      +
      +def _make_pconfig(provider_id="deepseek", env_vars=None):
      +    """Create a minimal ProviderConfig for testing.
      +
      +    Default provider_id is 'deepseek' because it's a real api_key provider
      +    in PROVIDER_REGISTRY (needed for _seed_from_env's generic path).
      +    """
      +    from hermes_cli.auth import ProviderConfig
      +    return ProviderConfig(
      +        id=provider_id,
      +        name=provider_id.title(),
      +        auth_type="api_key",
      +        api_key_env_vars=[redacted:secret-assignment] or [f"{provider_id.upper()}_API_KEY"]),
      +    )
      +
      +
      [redacted:email]
      +def isolated_hermes_home(tmp_path, monkeypatch):
      +    """Point HERMES_HOME at a temp dir and clear known API key env vars.
      +
      +    Also invalidates any cached get_env_value state by patching Path.home().
      +    """
      +    home = tmp_path / ".hermes"
      +    home.mkdir()
      +    monkeypatch.setattr(Path, "home", lambda: tmp_path)
      +    monkeypatch.setenv("HERMES_HOME", str(home))
      +
      +    # Clear all known API key env vars so get_env_value falls through to .env
      +    for key in [
      +        "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENROUTER_API_KEY",
      +        "ZAI_API_KEY", "DEEPSEEK_API_KEY", "ANTHROPIC_TOKEN",
      +        "CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_BASE_URL",
      +    ]:
      +        monkeypatch.delenv(key, raising=False)
      +
      +    return home
      +
      +
      +def _write_env_file(home: Path, **kwargs) -> None:
      +    """Write key=value pairs to ~/.hermes/.env."""
      +    lines = [f"{k}={v}" for k, v in kwargs.items()]
      +    (home / ".env").write_text("\n".join(lines) + "\n")
      +
      +
      +class TestCredentialPoolSeedsFromDotEnv:
      +    """_seed_from_env must read keys from ~/.hermes/.env, not just os.environ.
      +
      +    This is the load-bearing behaviour for the fix: when a user adds a key to
      +    .env mid-session or via a non-CLI entry point that doesn't run
      +    load_hermes_dotenv, the credential pool must still discover it.
      +    """
      +
      +    def test_deepseek_key_from_dotenv_only(self, isolated_hermes_home):
      +        """Key in .env but not os.environ → _seed_from_env adds a pool entry."""
      +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
      +        assert "DEEPSEEK_API_KEY" not in os.environ
      +
      +        from agent.credential_pool import _seed_from_env
      +        entries = []
      +        changed, active_sources = _seed_from_env("deepseek", entries)
      +
      +        assert changed is True
      +        assert "env:DEEPSEEK_API_KEY" in active_sources
      +        assert any(
      +            e.access_token == "sk-dotenv-only-12345"
      +            and e.source == "env:DEEPSEEK_API_KEY"
      +            for e in entries
      +        ), f"Expected seeded entry with dotenv key, got: {[(e.source, e.access_token) for e in entries]}"
      +
      +    def test_openrouter_key_from_dotenv_only(self, isolated_hermes_home):
      +        """OpenRouter path has its own branch — verify it also reads .env."""
      +        _write_env_file(isolated_hermes_home, OPENROUTER_API_KEY="[redacted:secret-assignment]")
      +        assert "OPENROUTER_API_KEY" not in os.environ
      +
      +        from agent.credential_pool import _seed_from_env
      +        entries = []
      +        changed, active_sources = _seed_from_env("openrouter", entries)
      +
      +        assert changed is True
      +        assert "env:OPENROUTER_API_KEY" in active_sources
      +        assert any(
      +            e.access_token == "sk-or-dotenv-abc" for e in entries
      +        )
      +
      +    def test_empty_dotenv_no_entries(self, isolated_hermes_home):
      +        """No .env file, no env vars → no entries seeded (and no crash)."""
      +        from agent.credential_pool import _seed_from_env
      +        entries = []
      +        changed, active_sources = _seed_from_env("deepseek", entries)
      +        assert changed is False
      +        assert active_sources == set()
      +        assert entries == []
      +
      +    def test_os_environ_still_wins_over_dotenv(self, isolated_hermes_home, monkeypatch):
      +        """get_env_value checks os.environ first — verify seeding picks that up."""
      +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
      +        monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-env-fresh-xyz")
      +
      +        from agent.credential_pool import _seed_from_env
      +        entries = []
      +        changed, _ = _seed_from_env("deepseek", entries)
      +
      +        assert changed is True
      +        seeded = [e for e in entries if e.source == "env:DEEPSEEK_API_KEY"]
      +        assert len(seeded) == 1
      +        assert seeded[0].access_token == "sk-env-fresh-xyz"
      +
      +
      +class TestAuthResolvesFromDotEnv:
      +    """_resolve_api_key_provider_secret must also read from ~/.hermes/.env."""
      +
      +    def test_key_from_dotenv_only(self, isolated_hermes_home):
      +        """Key in .env but not os.environ → _resolve returns it with the env var source."""
      +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
      +        assert "DEEPSEEK_API_KEY" not in os.environ
      +
      +        from hermes_cli.auth import _resolve_api_key_provider_secret
      +        key, source = _resolve_api_key_provider_secret(
      +            provider_id="deepseek",
      +            pconfig=_make_pconfig(),
      +        )
      +        assert key == "[redacted:openai-key]"
      +        assert source == "DEEPSEEK_API_KEY"
      +
      +
      +class TestAuthCredentialPoolFallback:
      +    """_resolve_api_key_provider_secret falls back to credential pool when env + dotenv are empty."""
      +
      +    def test_credential_pool_fallback_structure(self, isolated_hermes_home):
      +        """Empty env + empty .env → auth falls back to credential pool."""
      +        mock_entry = MagicMock()
      +        mock_entry.access_token = "[redacted:secret-assignment]"
      +        mock_entry.runtime_api_key = ""
      +
      +        mock_pool = MagicMock()
      +        mock_pool.has_credentials.return_value = True
      +        mock_pool.peek.return_value = mock_entry
      +
      +        from hermes_cli.auth import _resolve_api_key_provider_secret
      +        with patch("agent.credential_pool.load_pool", return_value=mock_pool):
      +            key, source = _resolve_api_key_provider_secret(
      +                provider_id="deepseek",
      +                pconfig=_make_pconfig(),
      +            )
      +        assert "test-pool-key-12345" in key
      +        assert "credential_pool" in source
      +
      +    def test_credential_pool_empty_returns_empty(self, isolated_hermes_home):
      +        """Empty env + empty .env + empty pool → empty string."""
      +        mock_pool = MagicMock()
      +        mock_pool.has_credentials.return_value = False
      +
      +        from hermes_cli.auth import _resolve_api_key_provider_secret
      +        with patch("agent.credential_pool.load_pool", return_value=mock_pool):
      +            key, source = _resolve_api_key_provider_secret(
      +                provider_id="deepseek",
      +                pconfig=_make_pconfig(),
      +            )
      +        assert key == ""
      +
      +    def test_env_var_takes_priority_over_pool(self, isolated_hermes_home, monkeypatch):
      +        """os.environ key wins — credential pool is NEVER consulted."""
      +        monkeypatch.setenv("DEEPSEEK_API_KEY", "[redacted:openai-key]")
      +
      +        mock_pool = MagicMock()
      +        mock_pool.has_credentials.return_value = True
      +
      +        from hermes_cli.auth import _resolve_api_key_provider_secret
      +        with patch("agent.credential_pool.load_pool", return_value=mock_pool) as mp:
      +            key, source = _resolve_api_key_provider_secret(
      +                provider_id="deepseek",
      +                pconfig=_make_pconfig(),
      +            )
      +        assert key == "[redacted:openai-key]"
      +        assert source == "DEEPSEEK_API_KEY"
      +        # Pool should not even have been loaded — env var satisfied the request first
      +        mp.assert_not_called()
      +
      +    def test_dotenv_takes_priority_over_pool(self, isolated_hermes_home):
      +        """Key in .env beats credential pool — pool only fires when both env sources are empty."""
      +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
      +        assert "DEEPSEEK_API_KEY" not in os.environ
      +
      +        mock_pool = MagicMock()
      +        mock_pool.has_credentials.return_value = True
      +
      +        from hermes_cli.auth import _resolve_api_key_provider_secret
      +        with patch("agent.credential_pool.load_pool", return_value=mock_pool) as mp:
      +            key, source = _resolve_api_key_provider_secret(
      +                provider_id="deepseek",
      +                pconfig=_make_pconfig(),
      +            )
      +        assert key == "[redacted:openai-key]"
      +        assert source == "DEEPSEEK_API_KEY"
      +        mp.assert_not_called()
  • #27561 fix(doctor): flag missing credentials for active openrouter provider (salvage #26453)

    merged to main

    files: Dockerfile, hermes_cli/doctor.py, scripts/release.py, tests/hermes_cli/test_doctor.py, tests/tools/test_dockerfile_pid1_reaping.py

    Show the diff
    • Dockerfile
      @@ -66,9 +66,11 @@ RUN npm install --prefer-offline --no-audit && \
       # frontend stats the readme path during dep resolution, so we `touch` an
       # empty placeholder — the real README is restored by `COPY . .` below.
       #
      -# `uv sync --frozen --no-install-project --extra all` installs only the
      -# deps reachable through the composite `[all]` extra (handpicked set
      -# intended for the production image).  We do NOT use `--all-extras`:
      +# `uv sync --frozen --no-install-project --extra all --extra messaging`
      +# installs the deps reachable through the composite `[all]` extra
      +# (handpicked set intended for the production image), plus gateway
      +# messaging adapters that should work in the published image without a
      +# first-boot lazy install.  We do NOT use `--all-extras`:
       # that would pull in `[rl]` (atroposlib + tinker + torch + wandb from
       # git), `[yc-bench]` (another git dep), and `[termux-all]` (Android
       # redundancy), none of which belong in the published container.
      @@ -76,7 +78,7 @@ RUN npm install --prefer-offline --no-audit && \
       # The editable link is created after the source copy below.
       COPY pyproject.toml uv.lock ./
       RUN touch ./README.md
      -RUN uv sync --frozen --no-install-project --extra all
      +RUN uv sync --frozen --no-install-project --extra all --extra messaging
       
       # ---------- Source code ----------
       # .dockerignore excludes node_modules, so the installs above survive.
      @@ -94,10 +96,10 @@ RUN cd web && npm run build && \
       # hermes_cli/main.py succeeds (see #18800). /opt/hermes/web is build-time
       # only (HERMES_WEB_DIST points at hermes_cli/web_dist) and is intentionally
       # not chowned here.
      -# The .venv MUST be hermes-writable so lazy_deps.py can install platform
      -# packages (discord.py, telegram, slack, etc.) at first gateway boot.
      -# Without this, `uv pip install` fails with EACCES and all messaging
      -# adapters silently fail to load.  See tools/lazy_deps.py.
      +# The .venv MUST remain hermes-writable so lazy_deps.py can install
      +# remaining optional platform packages and future pin bumps at first use.
      +# Without this, `uv pip install` fails with EACCES and adapters silently
      +# fail to load.  See tools/lazy_deps.py.
       USER root
       RUN chmod -R a+rX /opt/hermes && \
           chown -R hermes:hermes /opt/hermes/.venv /opt/hermes/ui-tui /opt/hermes/node_modules
    • hermes_cli/doctor.py
      @@ -651,31 +651,41 @@ def run_doctor(args):
       
                   # Check credentials for the configured provider.
                   # Limit to API-key providers in PROVIDER_REGISTRY — other provider
      -            # types (OAuth, SDK, openrouter/anthropic/custom/auto) have their
      -            # own env-var checks elsewhere in doctor, and get_auth_status()
      -            # returns a bare {logged_in: False} for anything it doesn't
      -            # explicitly dispatch, which would produce false positives.
      -            if runtime_provider and runtime_provider not in {"auto", "custom", "openrouter"}:
      +            # types (OAuth, SDK, anthropic/custom/auto) have their own env-var
      +            # checks elsewhere in doctor, and get_auth_status() returns a bare
      +            # {logged_in: False} for anything it doesn't explicitly dispatch,
      +            # which would produce false positives.
      +            if runtime_provider and runtime_provider not in ("auto", "custom"):
                       try:
      -                    from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
      -                    pconfig = PROVIDER_REGISTRY.get(runtime_provider)
      -                    if pconfig and getattr(pconfig, "auth_type", "") == "api_key":
      -                        status = get_auth_status(runtime_provider) or {}
      +                    if runtime_provider == "openrouter":
      +                        from hermes_cli.config import get_env_value
      +
                               configured = bool(
      -                            status.get("configured")
      -                            or status.get("logged_in")
      -                            or status.get("api_key")
      +                            str(get_env_value("OPENROUTER_API_KEY") or "").strip()
      +                            or str(get_env_value("OPENAI_API_KEY") or "").strip()
                               )
      -                        if not configured:
      -                            check_fail(
      -                                f"model.provider '{runtime_provider}' is set but no API key is configured",
      -                                "(check ~/.hermes/.env or run 'hermes setup')",
      -                            )
      -                            issues.append(
      -                                f"No credentials found for provider '{runtime_provider}'. "
      -                                f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, "
      -                                f"or switch providers with 'hermes config set model.provider <name>'"
      +                    else:
      +                        from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
      +
      +                        pconfig = PROVIDER_REGISTRY.get(runtime_provider)
      +                        configured = True
      +                        if pconfig and getattr(pconfig, "auth_type", "") == "api_key":
      +                            status = get_auth_status(runtime_provider) or {}
      +                            configured = bool(
      +                                status.get("configured")
      +                                or status.get("logged_in")
      +                                or status.get("api_key")
                                   )
      +                    if not configured:
      +                        check_fail(
      +                            f"model.provider '{runtime_provider}' is set but no API key is configured",
      +                            "(check ~/.hermes/.env or run 'hermes setup')",
      +                        )
      +                        issues.append(
      +                            f"No credentials found for provider '{runtime_provider}'. "
      +                            f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, "
      +                            f"or switch providers with 'hermes config set model.provider <name>'"
      +                        )
                       except Exception:
                           pass
       
    • scripts/release.py
      @@ -1161,6 +1161,7 @@
           "[redacted:email]": "davidcampbelldc",
           "[redacted:email]": "hehehe0803",  # PR #26212 salvage (codex kanban writable root)
           "[redacted:email]": "hehehe0803",
      +    "[redacted:email]": "vaddisrinivas",  # PR #26394 salvage (Docker messaging extra)
       }
       
       
    • tests/hermes_cli/test_doctor.py
      @@ -477,6 +477,48 @@ def test_run_doctor_accepts_bare_custom_provider(monkeypatch, tmp_path):
           assert "model.provider 'custom' is not a recognised provider" not in out
       
       
      +def test_run_doctor_flags_missing_credentials_for_active_openrouter_provider(monkeypatch, tmp_path):
      +    home = tmp_path / ".hermes"
      +    home.mkdir(parents=True, exist_ok=True)
      +    (home / "config.yaml").write_text(
      +        "model:\n"
      +        "  provider: openrouter\n"
      +        "  default: openai/gpt-4.1-mini\n",
      +        encoding="utf-8",
      +    )
      +
      +    monkeypatch.setattr(doctor_mod, "HERMES_HOME", home)
      +    monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", tmp_path / "project")
      +    monkeypatch.setattr(doctor_mod, "_DHH", str(home))
      +    (tmp_path / "project").mkdir(exist_ok=True)
      +
      +    fake_model_tools = types.SimpleNamespace(
      +        check_tool_availability=lambda *a, **kw: ([], []),
      +        TOOLSET_REQUIREMENTS={},
      +    )
      +    monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools)
      +    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
      +    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
      +
      +    try:
      +        from hermes_cli import auth as _auth_mod
      +
      +        monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
      +        monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
      +        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {})
      +        monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {})
      +    except Exception:
      +        pass
      +
      +    buf = io.StringIO()
      +    with contextlib.redirect_stdout(buf):
      +        doctor_mod.run_doctor(Namespace(fix=False))
      +
      +    out = buf.getvalue()
      +    assert "model.provider 'openrouter' is set but no API key is configured" in out
      +    assert "No credentials found for provider 'openrouter'." in out
      +
      +
       @pytest.mark.parametrize(
           ("provider", "default_model"),
           [
    • tests/tools/test_dockerfile_pid1_reaping.py
      @@ -121,6 +121,20 @@ def test_dockerfile_installs_tui_dependencies(dockerfile_text):
           )
       
       
      +def test_dockerfile_preinstalls_gateway_messaging_dependencies(dockerfile_text):
      +    sync_steps = [
      +        step for step in _run_steps(dockerfile_text)
      +        if "uv sync" in step and "--no-install-project" in step
      +    ]
      +
      +    assert sync_steps, "Dockerfile must install Python dependencies with uv sync"
      +    assert any("--extra messaging" in step for step in sync_steps), (
      +        "Published Docker images must preload the [messaging] extra so "
      +        "Telegram/Discord gateway adapters do not depend on first-boot "
      +        "lazy installation (#24698)."
      +    )
      +
      +
       def test_dockerfile_builds_tui_assets(dockerfile_text):
           assert any(
               "ui-tui" in step and "npm" in step and "run build" in step
  • #73509 fix(tools): STT/TTS provider keys resolve through the credential pool via one scoped resolver

    merged to main

    files: tests/tools/test_tool_backend_helpers.py, tests/tools/test_voice_credential_pool_resolution.py, tools/tool_backend_helpers.py, tools/transcription_tools.py, tools/tts_tool.py, tools/xai_http.py

  • #73883 fix(cli): honor pooled credentials in model wizard (#65977)

    merged to main

    files: contributors/emails/[redacted:email], hermes_cli/main.py, hermes_cli/model_setup_flows.py, tests/hermes_cli/test_model_flow_pooled_credentials.py, tests/hermes_cli/test_prompt_api_key.py

19 PRs closed without merging

From the close comments: merged via another change 5 · duplicate 4 · done elsewhere 3 · superseded by another PR 2 · PR volume cleanup 1 · other 4

  • #5808 fix: check credential pool in get_nous_auth_status() fallback · superseded by another PR → #5860
  • #5860 fix(auth): check credential pool in get_nous_auth_status · PR volume cleanup
  • #15920 fix(auth): resolve API keys from ~/.hermes/.env and credential_pool · merged via another change → #16101
  • #15944 fix(auth): add credential_pool fallback when env var is absent · done elsewhere → #16101, #15920, #15914
  • #20698 fix(cli): hermes debug now sees credential_pool OAuth tokens · other
Show 14 more
  • #20761 fix(cli): hermes debug reports OAuth providers as 'not set' when logged in · duplicate → #20698
  • #26448 fix(doctor): flag missing OpenRouter credentials · done elsewhere
  • #26453 Fix doctor summary for active OpenRouter without credentials · merged via another change → 73df329, #27561, #26436
  • #26776 fix(doctor): flag missing OpenRouter credentials · done elsewhere → 73df329, #26448, #26453, #26436
  • #42425 fix(auth): honour pool entry base_url in provider credential resolution (#42269) · other
  • #60214 fix(cli): check credential pool for OpenRouter in hermes status (#60106) · duplicate → #60135, #60106
  • #60392 feat(debug): check credential_pool OAuth tokens in API key status · other
  • #65988 fix(cli): honor pooled credentials in model wizard 🩷 · merged via another change → #73883
  • #66007 fix(cli): model wizard recognises auth.json credential-pool keys (#65977) · duplicate → #65988
  • #66041 fix: hermes model wizard recognises keys from credential pool (#65977) · merged via another change → #73883
  • #68011 fix(tools): resolve TTS/STT provider keys through credential pool (fixes #68003) · superseded by another PR → #68509
  • #68509 fix(tools): resolve TTS/STT provider keys through credential pool · merged via another change → c136400, 8c7ba05, #73509
  • #68518 fix(config): check credential pool in get_env_value (#68003) · other → #68509, #69469, #68003
  • #108333 fix(cli): count credential-pool entries as a configured API key in doctor, status and config show · duplicate → #60135, #60106, #62467
Closed issues · 7
  • Closed#5807fix: hermes doctor reports Nous Portal as 'not logged in' when credentials exist in pool
  • Closed#15914[Bug]: Fallback chain collapses when API key unavailable in os.environ but present in credential_pool
  • Closed#15932bug(auth): API keys in ~/.hermes/.env not resolved by credential_pool._seed_from_env
  • Closed#26428I need to reshare this because I ran into an issue with the Hermes update. [Bug]: hermes doctor does not flag missing credentials for an active openrouter provider
  • Closed#26436[Bug]: hermes doctor does not flag missing credentials for an active openrouter provider
  • Closed#65977[Bug]: `hermes model` wizard says "not configured" for providers that already have a working credential in `auth.json`'s pool
  • Closed#68003[Bug]: STT/TTS tools bypass credential pool — get_env_value() ignores auth store
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.05: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#16101, #27561, #73509, #73883
Closed issues
7
Attempts
19 PRs closed without merging
Fix link checked
29 Jul 2026

For your agent

The same facts, typed.