Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions agent/secret_sources/bitwarden.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,13 @@ def apply_bitwarden_secrets(
return result

access_token = os.environ.get(access_token_env, "").strip()

# `enabled: true` with neither a token nor a project configured means
# Bitwarden was never actually set up. Stay silent instead of warning on
# every startup; the errors below are actionable once setup is partial.
if not access_token and not project_id:
return result

if not access_token:
result.error = (
f"secrets.bitwarden.enabled is true but {access_token_env} is "
Expand Down
15 changes: 15 additions & 0 deletions hermes_cli/env_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
# config re-parse, and the ASCII sanitization sweep still ran every time.
_APPLIED_HOMES: set[str] = set()

# Cross-process dedup for the Bitwarden status line. Startup can spawn child
# Python processes that each call load_hermes_dotenv() at import time; they
# inherit os.environ but not the in-process _APPLIED_HOMES guard above.
_BWS_STATUS_PRINTED_ENV = "_HERMES_BWS_STATUS_PRINTED"


def get_secret_source(env_var: str) -> str | None:
"""Return the label of the secret source that supplied ``env_var``, if any.
Expand All @@ -63,6 +68,7 @@ def reset_secret_source_cache() -> None:
that want to refresh after a config change.
"""
_APPLIED_HOMES.clear()
os.environ.pop(_BWS_STATUS_PRINTED_ENV, None)


def format_secret_source_suffix(env_var: str) -> str:
Expand Down Expand Up @@ -305,6 +311,15 @@ def _apply_external_secret_sources(home_path: Path) -> None:
# came from BSM rather than .env.
for name in result.applied:
_SECRET_SOURCES[name] = "bitwarden"

has_status = bool(result.applied or result.error or result.warnings)
if not has_status:
return
if os.environ.get(_BWS_STATUS_PRINTED_ENV):
return
os.environ[_BWS_STATUS_PRINTED_ENV] = "1"

if result.applied:
print(
f" Bitwarden Secrets Manager: applied {len(result.applied)} "
f"secret{'s' if len(result.applied) != 1 else ''} "
Expand Down
10 changes: 10 additions & 0 deletions tests/test_bitwarden_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,16 @@ def test_apply_disabled_returns_empty():
assert not result.error


def test_apply_unconfigured_is_silent(monkeypatch):
monkeypatch.delenv("BWS_ACCESS_TOKEN", raising=False)
result = bw.apply_bitwarden_secrets(
enabled=True, project_id="", auto_install=False
)
assert result.ok
assert not result.applied
assert not result.error


def test_apply_missing_token(monkeypatch):
monkeypatch.delenv("BWS_ACCESS_TOKEN", raising=False)
result = bw.apply_bitwarden_secrets(
Expand Down
76 changes: 76 additions & 0 deletions tests/test_env_loader_secret_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import os
import sys
from pathlib import Path

Expand Down Expand Up @@ -121,6 +122,81 @@ def test_apply_external_secret_sources_noop_when_disabled(tmp_path, monkeypatch)
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") is None


def test_apply_external_secret_sources_dedupes_across_subprocesses(
tmp_path, monkeypatch, capsys
):
"""An inherited parent marker suppresses duplicated child-process noise."""

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",
)

from agent.secret_sources.bitwarden import FetchResult

def _fake_apply(**_kwargs):
return FetchResult(
error=(
"secrets.bitwarden.enabled is true but BWS_ACCESS_TOKEN is "
"not set. Run `hermes secrets bitwarden setup`."
)
)

import agent.secret_sources.bitwarden as bw_module

monkeypatch.setattr(bw_module, "apply_bitwarden_secrets", _fake_apply)
monkeypatch.setenv(env_loader._BWS_STATUS_PRINTED_ENV, "1")

env_loader._apply_external_secret_sources(tmp_path)

captured = capsys.readouterr()
assert "Bitwarden Secrets Manager" not in captured.err


def test_apply_external_secret_sources_prints_warning_once_then_sets_marker(
tmp_path, monkeypatch, capsys
):
"""The first process prints the status line and marks inherited environ."""

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",
)

from agent.secret_sources.bitwarden import FetchResult

err_text = (
"secrets.bitwarden.enabled is true but BWS_ACCESS_TOKEN is "
"not set. Run `hermes secrets bitwarden setup`."
)

def _fake_apply(**_kwargs):
return FetchResult(error=err_text)

import agent.secret_sources.bitwarden as bw_module

monkeypatch.setattr(bw_module, "apply_bitwarden_secrets", _fake_apply)
monkeypatch.delenv(env_loader._BWS_STATUS_PRINTED_ENV, raising=False)

env_loader._apply_external_secret_sources(tmp_path)

captured = capsys.readouterr()
assert err_text in captured.err
assert os.environ.get(env_loader._BWS_STATUS_PRINTED_ENV) == "1"


def test_apply_external_secret_sources_dedupes_within_process(tmp_path, monkeypatch):
"""``load_hermes_dotenv()`` is called at module-import time from several
hot modules (cli.py, hermes_cli/main.py, run_agent.py, ...). The
Expand Down
Loading