Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
"""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 os
|
||||
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 and applied-home guard."""
|
||||
env_loader._SECRET_SOURCES.clear()
|
||||
env_loader._SECRET_SOURCE_VALUES_BY_HOME.clear()
|
||||
env_loader.reset_secret_source_cache()
|
||||
yield
|
||||
env_loader._SECRET_SOURCES.clear()
|
||||
env_loader._SECRET_SOURCE_VALUES_BY_HOME.clear()
|
||||
env_loader.reset_secret_source_cache()
|
||||
|
||||
|
||||
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_get_secret_source_values_returns_home_snapshot_copy(tmp_path):
|
||||
home_a = tmp_path / "profile-a"
|
||||
home_b = tmp_path / "profile-b"
|
||||
home_a.mkdir()
|
||||
home_b.mkdir()
|
||||
|
||||
env_loader._SECRET_SOURCE_VALUES_BY_HOME[str(home_a.resolve())] = {
|
||||
"ANTHROPIC_API_KEY": "sk-profile-a"
|
||||
}
|
||||
|
||||
snapshot = env_loader.get_secret_source_values(home_a)
|
||||
assert snapshot == {
|
||||
"ANTHROPIC_API_KEY": "sk-profile-a"
|
||||
}
|
||||
assert env_loader.get_secret_source_values(home_b) == {}
|
||||
snapshot["ANTHROPIC_API_KEY"] = "mutated"
|
||||
assert env_loader.get_secret_source_values(home_a) == {
|
||||
"ANTHROPIC_API_KEY": "sk-profile-a"
|
||||
}
|
||||
|
||||
|
||||
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_format_secret_source_suffix_onepassword_uses_proper_name():
|
||||
env_loader._SECRET_SOURCES["OPENAI_API_KEY"] = "onepassword"
|
||||
assert (
|
||||
env_loader.format_secret_source_suffix("OPENAI_API_KEY")
|
||||
== " (from 1Password)"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_records_bitwarden_origin(tmp_path, monkeypatch):
|
||||
"""End-to-end: when the Bitwarden source fetches keys, applied vars
|
||||
end up in ``_SECRET_SOURCES`` so the UI can label them."""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.test-token")
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
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 the fetch layer under the SecretSource adapter.
|
||||
import agent.secret_sources.bitwarden as bw_module
|
||||
|
||||
monkeypatch.setattr(bw_module, "find_bws", lambda **_kw: Path("/fake/bws"))
|
||||
monkeypatch.setattr(
|
||||
bw_module,
|
||||
"fetch_bitwarden_secrets",
|
||||
lambda **_kw: ({"ANTHROPIC_API_KEY": "sk-ant-test"}, []),
|
||||
)
|
||||
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
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_cold_profile_bitwarden_uses_profile_bootstrap_without_global_env(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Real Bitwarden adapter reads its token from the profile-local view."""
|
||||
monkeypatch.delenv("BWS_ACCESS_TOKEN", raising=False)
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
(tmp_path / ".env").write_text(
|
||||
"BWS_ACCESS_TOKEN=profile-bootstrap\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden:\n"
|
||||
" enabled: true\n"
|
||||
" project_id: test-project\n"
|
||||
" access_token_env: BWS_ACCESS_TOKEN\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
import agent.secret_sources.bitwarden as bw_module
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
captured = {}
|
||||
monkeypatch.setattr(bw_module, "find_bws", lambda **_kw: Path("/fake/bws"))
|
||||
|
||||
def _fake_fetch(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"ANTHROPIC_API_KEY": "profile-provider-key"}, []
|
||||
|
||||
monkeypatch.setattr(bw_module, "fetch_bitwarden_secrets", _fake_fetch)
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
assert env_loader.hydrate_profile_secret_sources(tmp_path) == {
|
||||
"ANTHROPIC_API_KEY": "profile-provider-key"
|
||||
}
|
||||
assert captured["access_token"] == "profile-bootstrap"
|
||||
assert os.environ.get("BWS_ACCESS_TOKEN") is None
|
||||
assert os.environ.get("ANTHROPIC_API_KEY") is None
|
||||
|
||||
|
||||
def test_single_profile_scoped_load_keeps_override_behavior(tmp_path, monkeypatch):
|
||||
"""Without multiplex, a scoped load keeps its historical override behaviour.
|
||||
|
||||
Ported from #77970 (@DonShelly): the guard must key on the multiplex flag,
|
||||
not on the home override alone -- single-profile ``-p`` runs still load.
|
||||
"""
|
||||
from agent import secret_scope
|
||||
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
||||
|
||||
monkeypatch.delenv("HERMES_TEST_SHARED_ADAPTER_CONFIG", raising=False)
|
||||
other_home = tmp_path / "other"
|
||||
other_home.mkdir()
|
||||
(other_home / ".env").write_text("HERMES_TEST_SHARED_ADAPTER_CONFIG=second\n")
|
||||
|
||||
was_active = secret_scope.is_multiplex_active()
|
||||
secret_scope.set_multiplex_active(False)
|
||||
home_token = set_hermes_home_override(other_home)
|
||||
try:
|
||||
loaded = env_loader.load_hermes_dotenv(hermes_home=other_home)
|
||||
finally:
|
||||
secret_scope.set_multiplex_active(was_active)
|
||||
reset_hermes_home_override(home_token)
|
||||
|
||||
try:
|
||||
assert os.environ.get("HERMES_TEST_SHARED_ADAPTER_CONFIG") == "second"
|
||||
assert (other_home / ".env") in loaded
|
||||
finally:
|
||||
os.environ.pop("HERMES_TEST_SHARED_ADAPTER_CONFIG", None)
|
||||
|
||||
|
||||
def test_multiplex_dotenv_load_hydrates_sources_without_global_env(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""The safe multiplex path must still refresh profile secret sources."""
|
||||
from agent import secret_scope
|
||||
import agent.secret_sources.bitwarden as bw_module
|
||||
from agent.secret_sources import registry as reg_module
|
||||
from hermes_constants import (
|
||||
reset_hermes_home_override,
|
||||
set_hermes_home_override,
|
||||
)
|
||||
|
||||
monkeypatch.delenv("BWS_ACCESS_TOKEN", raising=False)
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
(tmp_path / ".env").write_text(
|
||||
"BWS_ACCESS_TOKEN=profile-bootstrap\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden:\n"
|
||||
" enabled: true\n"
|
||||
" project_id: test-project\n"
|
||||
" access_token_env: BWS_ACCESS_TOKEN\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(bw_module, "find_bws", lambda **_kw: Path("/fake/bws"))
|
||||
monkeypatch.setattr(
|
||||
bw_module,
|
||||
"fetch_bitwarden_secrets",
|
||||
lambda **_kw: ({"ANTHROPIC_API_KEY": "profile-provider-key"}, []),
|
||||
)
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
was_active = secret_scope.is_multiplex_active()
|
||||
home_token = set_hermes_home_override(tmp_path)
|
||||
secret_scope.set_multiplex_active(True)
|
||||
try:
|
||||
assert env_loader.load_hermes_dotenv(hermes_home=tmp_path) == []
|
||||
finally:
|
||||
secret_scope.set_multiplex_active(was_active)
|
||||
reset_hermes_home_override(home_token)
|
||||
|
||||
assert env_loader.get_secret_source_values(tmp_path) == {
|
||||
"ANTHROPIC_API_KEY": "profile-provider-key"
|
||||
}
|
||||
assert os.environ.get("BWS_ACCESS_TOKEN") is None
|
||||
assert os.environ.get("ANTHROPIC_API_KEY") is None
|
||||
|
||||
|
||||
def test_cold_profile_hydration_seeds_op_env_bootstrap(tmp_path, monkeypatch):
|
||||
"""The .op.env bootstrap file must feed cold-profile hydration.
|
||||
|
||||
load_hermes_dotenv() reads <home>/.op.env for OP_SERVICE_ACCOUNT_TOKEN
|
||||
(the documented gitignored 1Password bootstrap); hydration must mirror
|
||||
that or a cold profile using the supported .op.env flow fails 1Password
|
||||
resolution (sweeper review on #74549). .env wins on conflict.
|
||||
"""
|
||||
monkeypatch.delenv("OP_SERVICE_ACCOUNT_TOKEN", raising=False)
|
||||
(tmp_path / ".env").write_text("UNRELATED=x\n", encoding="utf-8")
|
||||
(tmp_path / ".op.env").write_text(
|
||||
"OP_SERVICE_ACCOUNT_TOKEN=ops_from-op-env\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" onepassword:\n"
|
||||
" enabled: true\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
seen_env = {}
|
||||
|
||||
def _capture_apply_all(_cfg, home_path, environ=None):
|
||||
from agent.secret_sources.registry import ApplyReport
|
||||
seen_env.update(environ or {})
|
||||
return ApplyReport(sources=[], provenance={})
|
||||
|
||||
monkeypatch.setattr(reg_module, "apply_all", _capture_apply_all)
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
env_loader.hydrate_profile_secret_sources(tmp_path)
|
||||
|
||||
assert seen_env.get("OP_SERVICE_ACCOUNT_TOKEN") == "ops_from-op-env"
|
||||
# Never leaked into the process env.
|
||||
assert os.environ.get("OP_SERVICE_ACCOUNT_TOKEN") is None
|
||||
|
||||
|
||||
def test_cold_profile_hydration_dotenv_wins_over_op_env(tmp_path, monkeypatch):
|
||||
""".env takes precedence over .op.env for the same key (setdefault)."""
|
||||
monkeypatch.delenv("OP_SERVICE_ACCOUNT_TOKEN", raising=False)
|
||||
(tmp_path / ".env").write_text(
|
||||
"OP_SERVICE_ACCOUNT_TOKEN=ops_from-dotenv\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / ".op.env").write_text(
|
||||
"OP_SERVICE_ACCOUNT_TOKEN=ops_from-op-env\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" onepassword:\n"
|
||||
" enabled: true\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
seen_env = {}
|
||||
|
||||
def _capture_apply_all(_cfg, home_path, environ=None):
|
||||
from agent.secret_sources.registry import ApplyReport
|
||||
seen_env.update(environ or {})
|
||||
return ApplyReport(sources=[], provenance={})
|
||||
|
||||
monkeypatch.setattr(reg_module, "apply_all", _capture_apply_all)
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
env_loader.hydrate_profile_secret_sources(tmp_path)
|
||||
|
||||
assert seen_env.get("OP_SERVICE_ACCOUNT_TOKEN") == "ops_from-dotenv"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
Bitwarden status line previously printed once per call — 3-5x per
|
||||
startup. The applied-home guard must short-circuit subsequent calls
|
||||
so the heavy work (config re-parse, Bitwarden lookup, status print)
|
||||
runs exactly once per HERMES_HOME per process.
|
||||
"""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.test-token")
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
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",
|
||||
)
|
||||
|
||||
call_count = {"n": 0}
|
||||
def _fake_fetch(**_kwargs):
|
||||
call_count["n"] += 1
|
||||
return {"ANTHROPIC_API_KEY": "sk-ant-test"}, []
|
||||
|
||||
import agent.secret_sources.bitwarden as bw_module
|
||||
monkeypatch.setattr(bw_module, "find_bws", lambda **_kw: Path("/fake/bws"))
|
||||
monkeypatch.setattr(bw_module, "fetch_bitwarden_secrets", _fake_fetch)
|
||||
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
# Five calls in a row, simulating module-import-time invocations from
|
||||
# cli.py, hermes_cli/main.py, run_agent.py, trajectory_compressor.py,
|
||||
# gateway/run.py. Only the first should actually call the backend.
|
||||
for _ in range(5):
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
|
||||
assert call_count["n"] == 1, (
|
||||
"Bitwarden backend was called {} time(s); expected exactly 1 — "
|
||||
"the applied-home guard is broken.".format(call_count["n"])
|
||||
)
|
||||
|
||||
# Source tracking still works after dedup.
|
||||
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") == "bitwarden"
|
||||
assert env_loader.get_secret_source_values(tmp_path) == {
|
||||
"ANTHROPIC_API_KEY": "sk-ant-test"
|
||||
}
|
||||
|
||||
# reset_secret_source_cache() forces a fresh pull on the next call.
|
||||
env_loader.reset_secret_source_cache()
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
assert call_count["n"] == 2
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_status_line_suppresses_secret_names(
|
||||
tmp_path, monkeypatch, capsys
|
||||
):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.test-token")
|
||||
monkeypatch.delenv("LEAK_THIS_API_KEY", raising=False)
|
||||
monkeypatch.delenv("LEAK_THIS_TOKEN", raising=False)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden:\n"
|
||||
" enabled: true\n"
|
||||
" project_id: test-project\n"
|
||||
" access_token_env: BWS_ACCESS_TOKEN\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
import agent.secret_sources.bitwarden as bw_module
|
||||
|
||||
monkeypatch.setattr(bw_module, "find_bws", lambda **_kw: Path("/fake/bws"))
|
||||
monkeypatch.setattr(
|
||||
bw_module,
|
||||
"fetch_bitwarden_secrets",
|
||||
lambda **_kw: (
|
||||
{"LEAK_THIS_API_KEY": "sk-test", "LEAK_THIS_TOKEN": "tok-test"},
|
||||
[],
|
||||
),
|
||||
)
|
||||
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "Bitwarden Secrets Manager: applied 2 secrets" in err
|
||||
assert "LEAK_THIS_API_KEY" not in err
|
||||
assert "LEAK_THIS_TOKEN" not in err
|
||||
|
||||
|
||||
def test_external_secret_values_are_isolated_between_homes(tmp_path, monkeypatch):
|
||||
"""A later apply for the same key must not mutate an earlier home snapshot."""
|
||||
from agent.secret_scope import build_profile_secret_scope
|
||||
from agent.secret_sources.base import FetchResult
|
||||
from agent.secret_sources.registry import (
|
||||
AppliedVar,
|
||||
ApplyReport,
|
||||
SourceReport,
|
||||
)
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
home_a = tmp_path / "profile-a"
|
||||
home_b = tmp_path / "profile-b"
|
||||
for home in (home_a, home_b):
|
||||
home.mkdir()
|
||||
(home / "config.yaml").write_text(
|
||||
"secrets:\n test-source:\n enabled: true\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
values = {
|
||||
str(home_a.resolve()): "value-a",
|
||||
str(home_b.resolve()): "value-b",
|
||||
}
|
||||
|
||||
def _fake_apply_all(_cfg, home_path):
|
||||
value = values[str(Path(home_path).resolve())]
|
||||
monkeypatch.setenv("SHARED_API_KEY", value)
|
||||
return ApplyReport(
|
||||
# Real apply_all always appends a SourceReport per enabled
|
||||
# source; the env_loader guard (#40597) early-returns on an
|
||||
# empty sources list, so the fake must match the real shape.
|
||||
sources=[
|
||||
SourceReport(
|
||||
name="test-source",
|
||||
label="Test Source",
|
||||
result=FetchResult(),
|
||||
applied=["SHARED_API_KEY"],
|
||||
)
|
||||
],
|
||||
provenance={
|
||||
"SHARED_API_KEY": AppliedVar(
|
||||
name="SHARED_API_KEY",
|
||||
source="test-source",
|
||||
shape="mapped",
|
||||
overrode_env=True,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(reg_module, "apply_all", _fake_apply_all)
|
||||
|
||||
env_loader._apply_external_secret_sources(home_a)
|
||||
env_loader._apply_external_secret_sources(home_b)
|
||||
|
||||
assert os.environ["SHARED_API_KEY"] == "value-b"
|
||||
assert env_loader.get_secret_source_values(home_a) == {
|
||||
"SHARED_API_KEY": "value-a"
|
||||
}
|
||||
assert env_loader.get_secret_source_values(home_b) == {
|
||||
"SHARED_API_KEY": "value-b"
|
||||
}
|
||||
assert build_profile_secret_scope(home_a) == {
|
||||
"SHARED_API_KEY": "value-a"
|
||||
}
|
||||
assert build_profile_secret_scope(home_b) == {
|
||||
"SHARED_API_KEY": "value-b"
|
||||
}
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_records_onepassword_origin(tmp_path, monkeypatch):
|
||||
"""When the 1Password source resolves refs, applied vars end up in
|
||||
``_SECRET_SOURCES`` labeled ``onepassword``."""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" onepassword:\n"
|
||||
" enabled: true\n"
|
||||
" env:\n"
|
||||
" ANTHROPIC_API_KEY: 'op://Private/Anthropic/credential'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
import agent.secret_sources.onepassword as op_module
|
||||
|
||||
monkeypatch.setattr(op_module, "find_op", lambda *_a, **_kw: Path("/fake/op"))
|
||||
monkeypatch.setattr(
|
||||
op_module,
|
||||
"fetch_onepassword_secrets",
|
||||
lambda **_kw: ({"ANTHROPIC_API_KEY": "sk-ant-test"}, []),
|
||||
)
|
||||
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
|
||||
assert env_loader.get_secret_source("ANTHROPIC_API_KEY") == "onepassword"
|
||||
assert (
|
||||
env_loader.format_secret_source_suffix("ANTHROPIC_API_KEY")
|
||||
== " (from 1Password)"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_survives_non_dict_section(tmp_path, monkeypatch):
|
||||
"""A malformed `secrets:` section must not abort startup (fail-open).
|
||||
|
||||
Both `onepassword: true` (non-dict) and a bad bitwarden section must be
|
||||
coerced to empty config instead of raising AttributeError up through
|
||||
load_hermes_dotenv().
|
||||
"""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden: true\n"
|
||||
" onepassword: true\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
# Must not raise and must not record anything.
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
assert env_loader.get_secret_source("ANYTHING") is None
|
||||
|
||||
|
||||
def test_apply_external_secret_sources_bad_ttl_does_not_crash(tmp_path, monkeypatch):
|
||||
"""A non-numeric cache_ttl_seconds must be coerced, not crash startup."""
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" onepassword:\n"
|
||||
" enabled: true\n"
|
||||
" cache_ttl_seconds: not-a-number\n"
|
||||
" env:\n"
|
||||
" K: 'op://V/I/F'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
captured = {}
|
||||
|
||||
def _fake_fetch(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {}, []
|
||||
|
||||
import agent.secret_sources.onepassword as op_module
|
||||
monkeypatch.setattr(op_module, "find_op", lambda *_a, **_kw: Path("/fake/op"))
|
||||
monkeypatch.setattr(op_module, "fetch_onepassword_secrets", _fake_fetch)
|
||||
|
||||
from agent.secret_sources import registry as reg_module
|
||||
|
||||
reg_module._reset_registry_for_tests()
|
||||
|
||||
env_loader._apply_external_secret_sources(tmp_path)
|
||||
|
||||
# Coerced to the 300s default rather than raising ValueError.
|
||||
assert captured["cache_ttl_seconds"] == 300
|
||||
Reference in New Issue
Block a user