mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
When Bitwarden Secrets Manager supplies a provider key, 'hermes model' and the setup wizard show 'credentials ✓' with no hint of where the key came from — identical to the .env case. Users assume the integration isn't wired up and re-enter the key (or hit Enter and cancel). env_loader now tracks which env vars were injected by an external secret source and exposes get_secret_source() / format_secret_source_suffix() so the provider flows can render 'Anthropic credentials: sk-ant-... ✓ (from Bitwarden)' instead of an unlabeled checkmark. Wired into _prompt_api_key (kimi, z.ai, minimax, opencode, ...), the Anthropic provider flow, the Bedrock flow, and the GitHub Copilot token display. Future secret sources (Vault, 1Password, etc.) drop in by setting their own label in _SECRET_SOURCES; format_secret_source_suffix() has a generic fallback so no call sites need updating.
119 lines
3.8 KiB
Python
119 lines
3.8 KiB
Python
"""Tests for the secret-source tracking in ``hermes_cli.env_loader``.
|
|
|
|
These cover the small public surface that lets `hermes model` / `hermes setup`
|
|
label detected credentials with their origin ("from Bitwarden") so users
|
|
don't see an unexplained "credentials ✓" line when their .env is empty.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from hermes_cli import env_loader # noqa: E402
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_sources():
|
|
"""Each test starts with a clean source map."""
|
|
env_loader._SECRET_SOURCES.clear()
|
|
yield
|
|
env_loader._SECRET_SOURCES.clear()
|
|
|
|
|
|
def test_get_secret_source_returns_none_for_untracked_var():
|
|
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") is None
|
|
|
|
|
|
def test_get_secret_source_returns_label_for_tracked_var():
|
|
env_loader._SECRET_SOURCES["ANTHROPIC_API_KEY"] = "bitwarden"
|
|
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") == "bitwarden"
|
|
|
|
|
|
def test_format_secret_source_suffix_empty_for_untracked():
|
|
# Credentials from .env or the shell shouldn't add noise — the
|
|
# implicit case stays unlabeled.
|
|
assert env_loader.format_secret_source_suffix("ANTHROPIC_API_KEY") == ""
|
|
|
|
|
|
def test_format_secret_source_suffix_bitwarden_uses_proper_name():
|
|
env_loader._SECRET_SOURCES["ANTHROPIC_API_KEY"] = "bitwarden"
|
|
assert (
|
|
env_loader.format_secret_source_suffix("ANTHROPIC_API_KEY")
|
|
== " (from Bitwarden)"
|
|
)
|
|
|
|
|
|
def test_format_secret_source_suffix_generic_label_for_future_sources():
|
|
# Future-proofing: a new secret source (e.g. "vault") should still
|
|
# produce a sensible label without needing to edit every call site.
|
|
env_loader._SECRET_SOURCES["OPENAI_API_KEY"] = "vault"
|
|
assert (
|
|
env_loader.format_secret_source_suffix("OPENAI_API_KEY")
|
|
== " (from vault)"
|
|
)
|
|
|
|
|
|
def test_apply_external_secret_sources_records_bitwarden_origin(tmp_path, monkeypatch):
|
|
"""End-to-end: when ``apply_bitwarden_secrets`` returns applied keys,
|
|
they end up in ``_SECRET_SOURCES`` so the UI can label them."""
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
config_path = tmp_path / "config.yaml"
|
|
config_path.write_text(
|
|
"secrets:\n"
|
|
" bitwarden:\n"
|
|
" enabled: true\n"
|
|
" project_id: test-project\n"
|
|
" access_token_env: BWS_ACCESS_TOKEN\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
# Stub apply_bitwarden_secrets to return a synthetic FetchResult.
|
|
from agent.secret_sources.bitwarden import FetchResult
|
|
|
|
fake_result = FetchResult(
|
|
secrets={"ANTHROPIC_API_KEY": "sk-ant-test"},
|
|
applied=["ANTHROPIC_API_KEY"],
|
|
)
|
|
|
|
def _fake_apply(**_kwargs):
|
|
return fake_result
|
|
|
|
# The import inside _apply_external_secret_sources is lazy, so we
|
|
# patch the *module attribute* it will pull in.
|
|
import agent.secret_sources.bitwarden as bw_module
|
|
|
|
monkeypatch.setattr(bw_module, "apply_bitwarden_secrets", _fake_apply)
|
|
|
|
env_loader._apply_external_secret_sources(tmp_path)
|
|
|
|
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") == "bitwarden"
|
|
assert (
|
|
env_loader.format_secret_source_suffix("ANTHROPIC_API_KEY")
|
|
== " (from Bitwarden)"
|
|
)
|
|
|
|
|
|
def test_apply_external_secret_sources_noop_when_disabled(tmp_path, monkeypatch):
|
|
"""Disabled Bitwarden config must not touch the source map."""
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
config_path = tmp_path / "config.yaml"
|
|
config_path.write_text(
|
|
"secrets:\n"
|
|
" bitwarden:\n"
|
|
" enabled: false\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
env_loader._apply_external_secret_sources(tmp_path)
|
|
|
|
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") is None
|