Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""Conformance kit for :class:`agent.secret_sources.base.SecretSource`.
|
||||
|
||||
Any secret-source backend — bundled or external plugin — can validate
|
||||
itself against the contract by subclassing :class:`SecretSourceConformance`
|
||||
and providing a ``source`` fixture (plus optional per-source config
|
||||
fixtures). Example::
|
||||
|
||||
from tests.secret_sources.conformance import SecretSourceConformance
|
||||
|
||||
class TestMySourceConformance(SecretSourceConformance):
|
||||
@pytest.fixture
|
||||
def source(self):
|
||||
return MySource()
|
||||
|
||||
The checks encode the parts of the contract that break OTHER people
|
||||
when violated: never raising, never prompting (stdin closed), respecting
|
||||
disabled config, valid identity attributes, and orchestrator
|
||||
compatibility.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.secret_sources.base import (
|
||||
SECRET_SOURCE_API_VERSION,
|
||||
FetchResult,
|
||||
SecretSource,
|
||||
)
|
||||
from agent.secret_sources.registry import (
|
||||
_reset_registry_for_tests,
|
||||
apply_all,
|
||||
register_source,
|
||||
)
|
||||
|
||||
|
||||
class SecretSourceConformance:
|
||||
"""Base class of contract checks; subclass and provide ``source``."""
|
||||
|
||||
@pytest.fixture
|
||||
def source(self) -> SecretSource: # pragma: no cover — must override
|
||||
raise NotImplementedError("conformance subclasses must provide a source fixture")
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_cfg(self) -> dict:
|
||||
"""An enabled-but-unconfigured section — the common misconfig case."""
|
||||
return {"enabled": True}
|
||||
|
||||
# -- identity ----------------------------------------------------------
|
||||
|
||||
def test_name_is_lowercase_identifier(self, source):
|
||||
assert source.name, "source.name must be non-empty"
|
||||
assert source.name == source.name.lower()
|
||||
assert source.name.replace("_", "").isalnum()
|
||||
|
||||
def test_label_present(self, source):
|
||||
assert source.label, "source.label must be a human-readable name"
|
||||
|
||||
def test_shape_valid(self, source):
|
||||
assert source.shape in ("mapped", "bulk")
|
||||
|
||||
def test_api_version_current(self, source):
|
||||
assert source.api_version == SECRET_SOURCE_API_VERSION
|
||||
|
||||
# -- contract behavior --------------------------------------------------
|
||||
|
||||
def test_fetch_never_raises_on_malformed_config(self, source, tmp_path):
|
||||
"""Every degenerate config shape must produce a FetchResult, not a raise."""
|
||||
for cfg in ({}, {"enabled": True}, {"enabled": True, "env": "not-a-dict"},
|
||||
{"enabled": True, "cache_ttl_seconds": "bogus"}, None):
|
||||
result = source.fetch(cfg if isinstance(cfg, dict) else {}, tmp_path)
|
||||
assert isinstance(result, FetchResult), (
|
||||
f"fetch() returned {type(result).__name__} for cfg={cfg!r}"
|
||||
)
|
||||
|
||||
def test_fetch_unconfigured_reports_error_not_secrets(self, source, tmp_path,
|
||||
minimal_cfg, monkeypatch):
|
||||
"""enabled=true with nothing else set must fail cleanly with a kind."""
|
||||
result = source.fetch(minimal_cfg, tmp_path)
|
||||
assert isinstance(result, FetchResult)
|
||||
if not result.ok:
|
||||
assert result.error_kind is not None, (
|
||||
"errors must carry a machine-readable ErrorKind"
|
||||
)
|
||||
assert not result.secrets
|
||||
|
||||
def test_disabled_by_default(self, source):
|
||||
assert source.is_enabled({}) is False
|
||||
assert source.is_enabled({"enabled": False}) is False
|
||||
|
||||
def test_timeout_is_positive(self, source, minimal_cfg):
|
||||
assert source.fetch_timeout_seconds(minimal_cfg) > 0
|
||||
# Garbage config must not break the timeout accessor either.
|
||||
assert source.fetch_timeout_seconds({"timeout_seconds": "junk"}) > 0
|
||||
|
||||
def test_protected_vars_are_valid_names(self, source, minimal_cfg):
|
||||
from agent.secret_sources.base import is_valid_env_name
|
||||
|
||||
for var in source.protected_env_vars(minimal_cfg):
|
||||
assert is_valid_env_name(var)
|
||||
|
||||
# -- orchestrator compatibility ------------------------------------------
|
||||
|
||||
def test_registers_and_applies_via_orchestrator(self, source, tmp_path,
|
||||
monkeypatch):
|
||||
"""The source must survive a full apply_all() pass without breaking it."""
|
||||
_reset_registry_for_tests()
|
||||
# Prevent the bundled sources from interfering.
|
||||
monkeypatch.setattr(
|
||||
"agent.secret_sources.registry._ensure_builtin_sources", lambda: None
|
||||
)
|
||||
try:
|
||||
assert register_source(source), "register_source() rejected the source"
|
||||
env: dict = {}
|
||||
report = apply_all(
|
||||
{source.name: {"enabled": True}}, tmp_path, environ=env
|
||||
)
|
||||
names = [sr.name for sr in report.sources]
|
||||
assert source.name in names
|
||||
finally:
|
||||
_reset_registry_for_tests()
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Error remediation for secret sources.
|
||||
|
||||
Covers the ErrorKind classification of Bitwarden's `invalid_client`
|
||||
identity reject, the bws stderr summarizer, the per-source
|
||||
``remediation()`` hook, and the env_loader startup hint printer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.secret_sources import bitwarden as bw
|
||||
from agent.secret_sources import onepassword as op
|
||||
from agent.secret_sources.base import ErrorKind, FetchResult, SecretSource
|
||||
from agent.secret_sources.bitwarden import (
|
||||
BitwardenSource,
|
||||
_classify_bws_error,
|
||||
_summarize_bws_stderr,
|
||||
)
|
||||
from agent.secret_sources.onepassword import OnePasswordSource
|
||||
|
||||
|
||||
_BWS_INVALID_CLIENT_DUMP = """\
|
||||
Error:
|
||||
0: Received error message from server: [400 Bad Request] {"error":"invalid_client"}
|
||||
|
||||
Location:
|
||||
crates/bws/src/main.rs:108
|
||||
|
||||
Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
|
||||
Run with RUST_BACKTRACE=full to include source snippets.
|
||||
"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _summarize_bws_stderr
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_summarize_strips_rust_report_noise():
|
||||
summary = _summarize_bws_stderr(_BWS_INVALID_CLIENT_DUMP)
|
||||
assert "invalid_client" in summary
|
||||
assert "Location:" not in summary
|
||||
assert "main.rs" not in summary
|
||||
assert "Backtrace" not in summary
|
||||
assert "Error:" not in summary
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _classify_bws_error — the invalid_client identity reject is an auth failure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BitwardenSource.fetch — auth failures get a human explanation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_fetch_auth_failure_gets_friendly_error(monkeypatch, tmp_path):
|
||||
src = BitwardenSource()
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.dead")
|
||||
monkeypatch.setattr(bw, "find_bws", lambda install_if_missing=True: tmp_path / "bws")
|
||||
|
||||
def boom(**kwargs):
|
||||
raise RuntimeError(
|
||||
'bws exited 1: Received error message from server: '
|
||||
'[400 Bad Request] {"error":"invalid_client"}'
|
||||
)
|
||||
|
||||
monkeypatch.setattr(bw, "fetch_bitwarden_secrets", boom)
|
||||
result = src.fetch({"enabled": True, "project_id": "p"}, tmp_path)
|
||||
assert result.error_kind == ErrorKind.AUTH_FAILED
|
||||
assert "revoked, expired" in result.error
|
||||
assert "BWS_ACCESS_TOKEN" in result.error
|
||||
assert "invalid_client" in result.error # mechanics preserved
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# remediation() hook
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
|
||||
def test_onepassword_auth_remediation_points_at_token_command():
|
||||
hint = OnePasswordSource().remediation(ErrorKind.AUTH_FAILED, {})
|
||||
assert "hermes secrets onepassword token" in hint
|
||||
assert "OP_SERVICE_ACCOUNT_TOKEN" in hint
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_remediation_never_raises_on_junk_cfg():
|
||||
for cfg in (None, [], "nope", 42):
|
||||
assert isinstance(BitwardenSource().remediation(ErrorKind.AUTH_FAILED, cfg), str)
|
||||
assert isinstance(OnePasswordSource().remediation(ErrorKind.AUTH_FAILED, cfg), str)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# env_loader startup hint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_env_loader_prints_remediation_hint(tmp_path, monkeypatch, capsys):
|
||||
from hermes_cli import env_loader
|
||||
from agent.secret_sources import registry
|
||||
|
||||
registry._reset_registry_for_tests()
|
||||
env_loader.reset_secret_source_cache()
|
||||
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
(home / "config.yaml").write_text(
|
||||
"secrets:\n"
|
||||
" bitwarden:\n"
|
||||
" enabled: true\n"
|
||||
" project_id: proj\n"
|
||||
)
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.dead")
|
||||
monkeypatch.setattr(bw, "find_bws", lambda install_if_missing=True: tmp_path / "bws")
|
||||
|
||||
def boom(**kwargs):
|
||||
raise RuntimeError(
|
||||
'bws exited 1: Received error message from server: '
|
||||
'[400 Bad Request] {"error":"invalid_client"}'
|
||||
)
|
||||
|
||||
monkeypatch.setattr(bw, "fetch_bitwarden_secrets", boom)
|
||||
try:
|
||||
env_loader._apply_external_secret_sources(home)
|
||||
finally:
|
||||
registry._reset_registry_for_tests()
|
||||
env_loader.reset_secret_source_cache()
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "rejected the machine-account access token" in err
|
||||
assert "hermes secrets bitwarden token" in err
|
||||
|
||||
|
||||
def test_remediation_hint_uses_explicit_profile_scope(tmp_path, monkeypatch):
|
||||
from agent.secret_sources import registry
|
||||
from hermes_cli import env_loader
|
||||
|
||||
class ScopedSource(SecretSource):
|
||||
name = "scoped_hint"
|
||||
label = "Scoped hint"
|
||||
shape = "mapped"
|
||||
|
||||
def __init__(self, marker):
|
||||
self.marker = marker
|
||||
|
||||
def fetch(self, cfg, home_path):
|
||||
return FetchResult()
|
||||
|
||||
def remediation(self, kind, cfg):
|
||||
return self.marker
|
||||
|
||||
monkeypatch.setattr(registry, "_ensure_builtin_sources", lambda: None)
|
||||
registry._reset_registry_for_tests()
|
||||
home_a = str((tmp_path / "hint-a").resolve())
|
||||
home_b = str((tmp_path / "hint-b").resolve())
|
||||
source_a = ScopedSource("profile-a")
|
||||
source_b = ScopedSource("profile-b")
|
||||
assert registry.register_source(source_a, scope=home_a)
|
||||
assert registry.register_source(source_b, scope=home_b)
|
||||
try:
|
||||
assert env_loader._remediation_hint(
|
||||
"scoped_hint", ErrorKind.AUTH_FAILED, {}, scope=home_b
|
||||
) == "profile-b"
|
||||
finally:
|
||||
registry._reset_registry_for_tests()
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Orchestrator-level profile secret handling.
|
||||
|
||||
Covers the two halves of the profile-clobber bug cluster:
|
||||
|
||||
- ``secrets.preserve_existing`` (#58073): named env vars keep their existing
|
||||
value even against a source with ``override_existing: true``.
|
||||
- Profile aliasing (#51447): under a named profile, an applied
|
||||
``FOO_<PROFILE>`` var also hydrates the canonical ``FOO`` so adapters and
|
||||
plugins that read fixed env names see the profile's value.
|
||||
|
||||
Both are implemented ONCE in ``apply_all()`` so every backend — bundled or
|
||||
plugin — gets them for free.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.secret_sources import registry
|
||||
from agent.secret_sources.base import ErrorKind, FetchResult, SecretSource
|
||||
|
||||
|
||||
class _FakeBulk(SecretSource):
|
||||
name = "fakebulk"
|
||||
label = "Fake Bulk"
|
||||
shape = "bulk"
|
||||
|
||||
def __init__(self, secrets):
|
||||
self._secrets = secrets
|
||||
|
||||
def override_existing(self, cfg):
|
||||
return bool(cfg.get("override_existing", True))
|
||||
|
||||
def fetch(self, cfg, home_path):
|
||||
res = FetchResult()
|
||||
res.secrets = dict(self._secrets)
|
||||
return res
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_registry():
|
||||
registry._reset_registry_for_tests()
|
||||
registry._BUILTINS_LOADED = True # keep real builtins out
|
||||
yield
|
||||
registry._reset_registry_for_tests()
|
||||
|
||||
|
||||
def _apply(secrets, cfg_extra=None, home=Path("/tmp/x/.hermes"), env=None):
|
||||
registry.register_source(_FakeBulk(secrets), replace=True)
|
||||
cfg = {"fakebulk": {"enabled": True}}
|
||||
cfg.update(cfg_extra or {})
|
||||
env = env if env is not None else {}
|
||||
report = registry.apply_all(cfg, home, environ=env)
|
||||
return report, env
|
||||
|
||||
|
||||
PROFILE_HOME = Path("/home/u/.hermes/profiles/milla")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# preserve_existing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_preserve_existing_beats_override():
|
||||
report, env = _apply(
|
||||
{"FEISHU_APP_SECRET": "shared", "OPENAI_API_KEY": "fresh"},
|
||||
cfg_extra={"preserve_existing": ["FEISHU_APP_SECRET"]},
|
||||
env={"FEISHU_APP_SECRET": "profile-local", "OPENAI_API_KEY": "stale"},
|
||||
)
|
||||
assert env["FEISHU_APP_SECRET"] == "profile-local" # preserved
|
||||
assert env["OPENAI_API_KEY"] == "fresh" # override still works
|
||||
sr = report.sources[0]
|
||||
assert "FEISHU_APP_SECRET" in sr.skipped_existing
|
||||
assert "OPENAI_API_KEY" in sr.applied
|
||||
|
||||
|
||||
def test_preserve_existing_only_guards_set_vars():
|
||||
"""A preserve-listed var with NO existing value still gets applied."""
|
||||
_, env = _apply(
|
||||
{"FEISHU_APP_SECRET": "shared"},
|
||||
cfg_extra={"preserve_existing": ["FEISHU_APP_SECRET"]},
|
||||
env={},
|
||||
)
|
||||
assert env["FEISHU_APP_SECRET"] == "shared"
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# profile aliasing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_profile_suffixed_var_hydrates_canonical():
|
||||
report, env = _apply(
|
||||
{"TELEGRAM_BOT_TOKEN_MILLA": "123:tok"},
|
||||
home=PROFILE_HOME,
|
||||
)
|
||||
assert env["TELEGRAM_BOT_TOKEN_MILLA"] == "123:tok"
|
||||
assert env["TELEGRAM_BOT_TOKEN"] == "123:tok"
|
||||
assert "TELEGRAM_BOT_TOKEN" in report.provenance
|
||||
assert any("applied profile-scoped" in w
|
||||
for w in report.sources[0].result.warnings)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_hyphenated_profile_name_matches_underscore_suffix():
|
||||
_, env = _apply(
|
||||
{"SLACK_APP_TOKEN_MY_BOT": "xapp-1"},
|
||||
home=Path("/home/u/.hermes/profiles/my-bot"),
|
||||
)
|
||||
assert env["SLACK_APP_TOKEN"] == "xapp-1"
|
||||
|
||||
|
||||
def test_source_fetch_reads_injected_environment_without_global_mutation(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
"""Cold-profile bootstrap values reach sources through the local mapping."""
|
||||
from agent.secret_sources.base import get_source_environment
|
||||
|
||||
class _BootstrapSource(SecretSource):
|
||||
name = "bootstrap"
|
||||
shape = "mapped"
|
||||
|
||||
def fetch(self, cfg, home_path):
|
||||
result = FetchResult()
|
||||
result.secrets = {
|
||||
"RESOLVED_API_KEY": get_source_environment()["BOOTSTRAP_TOKEN"]
|
||||
}
|
||||
return result
|
||||
|
||||
registry.register_source(_BootstrapSource())
|
||||
monkeypatch.delenv("BOOTSTRAP_TOKEN", raising=False)
|
||||
env = {"BOOTSTRAP_TOKEN": "profile-token"}
|
||||
_, applied = _apply(
|
||||
{},
|
||||
cfg_extra={"bootstrap": {"enabled": True}},
|
||||
home=tmp_path,
|
||||
env=env,
|
||||
)
|
||||
|
||||
assert applied["RESOLVED_API_KEY"] == "profile-token"
|
||||
assert "BOOTSTRAP_TOKEN" not in __import__("os").environ
|
||||
|
||||
|
||||
def test_empty_injected_environment_does_not_fall_back_to_process(monkeypatch, tmp_path):
|
||||
from agent.secret_sources.base import get_source_environment
|
||||
|
||||
class _CanarySource(SecretSource):
|
||||
name = "canary"
|
||||
shape = "mapped"
|
||||
|
||||
def fetch(self, cfg, home_path):
|
||||
result = FetchResult()
|
||||
assert get_source_environment().get("LEAK_CANARY") is None
|
||||
return result
|
||||
|
||||
registry.register_source(_CanarySource())
|
||||
monkeypatch.setenv("LEAK_CANARY", "global-secret")
|
||||
registry.apply_all(
|
||||
{"canary": {"enabled": True}}, tmp_path, environ={}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
"""Tests for the secret-source contract + orchestrator.
|
||||
|
||||
Covers: registration gating (API version, name/scheme uniqueness, shape),
|
||||
apply_all precedence (mapped beats bulk, first-wins, override_existing,
|
||||
protected vars), conflict surfacing, timeout enforcement, provenance,
|
||||
and Bitwarden's SecretSource adapter — plus the conformance kit run
|
||||
against the bundled Bitwarden source.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from agent.secret_sources.base import ( # noqa: E402
|
||||
SECRET_SOURCE_API_VERSION,
|
||||
ErrorKind,
|
||||
FetchResult,
|
||||
SecretSource,
|
||||
is_valid_env_name,
|
||||
run_secret_cli,
|
||||
scrub_ansi,
|
||||
)
|
||||
from agent.secret_sources import registry as reg # noqa: E402
|
||||
from agent.secret_sources.bitwarden import BitwardenSource # noqa: E402
|
||||
from tests.secret_sources.conformance import SecretSourceConformance # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_registry(monkeypatch):
|
||||
"""Each test starts with an empty registry and no builtin auto-load."""
|
||||
reg._reset_registry_for_tests()
|
||||
monkeypatch.setattr(reg, "_ensure_builtin_sources", lambda: None)
|
||||
yield
|
||||
reg._reset_registry_for_tests()
|
||||
|
||||
|
||||
def _make_source(
|
||||
name="dummy",
|
||||
shape="mapped",
|
||||
secrets=None,
|
||||
error=None,
|
||||
error_kind=None,
|
||||
scheme=None,
|
||||
override=False,
|
||||
protected=(),
|
||||
api_version=SECRET_SOURCE_API_VERSION,
|
||||
fetch_fn=None,
|
||||
):
|
||||
"""Build a minimal conforming source for orchestrator tests."""
|
||||
|
||||
class _Src(SecretSource):
|
||||
def fetch(self, cfg, home_path):
|
||||
if fetch_fn is not None:
|
||||
return fetch_fn(cfg, home_path)
|
||||
res = FetchResult()
|
||||
if error:
|
||||
res.error = error
|
||||
res.error_kind = error_kind or ErrorKind.INTERNAL
|
||||
else:
|
||||
res.secrets = dict(secrets or {})
|
||||
return res
|
||||
|
||||
def override_existing(self, cfg):
|
||||
return override
|
||||
|
||||
def protected_env_vars(self, cfg):
|
||||
return frozenset(protected)
|
||||
|
||||
_Src.name = name
|
||||
_Src.label = name.title()
|
||||
_Src.shape = shape
|
||||
_Src.scheme = scheme
|
||||
_Src.api_version = api_version
|
||||
return _Src()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registration gating
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRegistration:
|
||||
def test_registers_conforming_source(self):
|
||||
assert reg.register_source(_make_source()) is True
|
||||
assert reg.get_source("dummy") is not None
|
||||
|
||||
def test_rejects_non_secretsource_instance(self):
|
||||
assert reg.register_source(object()) is False
|
||||
|
||||
def test_same_name_is_isolated_by_profile(self, tmp_path):
|
||||
from hermes_constants import (
|
||||
reset_hermes_home_override,
|
||||
set_hermes_home_override,
|
||||
)
|
||||
|
||||
home_a = str((tmp_path / "secrets-a").resolve())
|
||||
home_b = str((tmp_path / "secrets-b").resolve())
|
||||
source_a = _make_source(name="profile_secret", secrets={"A": "a"})
|
||||
source_b = _make_source(name="profile_secret", secrets={"B": "b"})
|
||||
assert reg.register_source(source_a, scope=home_a)
|
||||
assert reg.register_source(source_b, scope=home_b)
|
||||
|
||||
token = set_hermes_home_override(home_a)
|
||||
try:
|
||||
assert reg.get_source("profile_secret") is source_a
|
||||
explicit_b_env = {}
|
||||
report = reg.apply_all(
|
||||
{"profile_secret": {"enabled": True}},
|
||||
Path(home_b),
|
||||
environ=explicit_b_env,
|
||||
)
|
||||
assert report.sources[0].result.secrets == {"B": "b"}
|
||||
assert explicit_b_env == {"B": "b"}
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
token = set_hermes_home_override(home_b)
|
||||
try:
|
||||
assert reg.get_source("profile_secret") is source_b
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# apply_all: precedence, conflicts, protection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestApplyAll:
|
||||
def test_disabled_sources_do_not_run(self, tmp_path):
|
||||
called = []
|
||||
|
||||
def _fetch(cfg, home):
|
||||
called.append(True)
|
||||
return FetchResult(secrets={"A": "1"})
|
||||
|
||||
reg.register_source(_make_source(fetch_fn=_fetch))
|
||||
env: dict = {}
|
||||
report = reg.apply_all({"dummy": {"enabled": False}}, tmp_path, environ=env)
|
||||
assert not called
|
||||
assert not report.sources
|
||||
assert env == {}
|
||||
|
||||
def test_applies_secrets_and_records_provenance(self, tmp_path):
|
||||
reg.register_source(_make_source(secrets={"API_KEY": "v1"}))
|
||||
env: dict = {}
|
||||
report = reg.apply_all({"dummy": {"enabled": True}}, tmp_path, environ=env)
|
||||
assert env["API_KEY"] == "v1"
|
||||
assert report.provenance["API_KEY"].source == "dummy"
|
||||
assert report.provenance["API_KEY"].shape == "mapped"
|
||||
assert report.provenance["API_KEY"].overrode_env is False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_failed_source_does_not_block_others(self, tmp_path):
|
||||
reg.register_source(
|
||||
_make_source(name="broken", error="boom", error_kind=ErrorKind.NETWORK)
|
||||
)
|
||||
reg.register_source(_make_source(name="works", secrets={"K": "v"}))
|
||||
env: dict = {}
|
||||
report = reg.apply_all(
|
||||
{"broken": {"enabled": True}, "works": {"enabled": True}},
|
||||
tmp_path, environ=env,
|
||||
)
|
||||
assert env["K"] == "v"
|
||||
broken = [s for s in report.sources if s.name == "broken"][0]
|
||||
assert broken.result.error_kind is ErrorKind.NETWORK
|
||||
|
||||
|
||||
|
||||
|
||||
def test_malformed_secrets_cfg_shapes_are_safe(self, tmp_path):
|
||||
reg.register_source(_make_source(secrets={"K": "v"}))
|
||||
for cfg in (None, [], "junk", {"dummy": "not-a-dict"}, {"sources": "junk"}):
|
||||
report = reg.apply_all(cfg, tmp_path, environ={})
|
||||
assert isinstance(report, reg.ApplyReport)
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHelpers:
|
||||
def test_is_valid_env_name(self):
|
||||
assert is_valid_env_name("GOOD_NAME")
|
||||
assert is_valid_env_name("_LEADING")
|
||||
assert not is_valid_env_name("")
|
||||
assert not is_valid_env_name("1BAD")
|
||||
assert not is_valid_env_name("bad-name")
|
||||
assert not is_valid_env_name("has space")
|
||||
|
||||
|
||||
def test_run_secret_cli_minimal_env(self):
|
||||
proc = run_secret_cli(
|
||||
[sys.executable, "-c",
|
||||
"import os, json; print(json.dumps(sorted(os.environ)))"],
|
||||
)
|
||||
import json
|
||||
|
||||
child_env = json.loads(proc.stdout)
|
||||
# No credential-bearing vars from the parent env leak through.
|
||||
assert not any(k.endswith(("_API_KEY", "_TOKEN", "_SECRET"))
|
||||
for k in child_env)
|
||||
assert "NO_COLOR" in child_env
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bitwarden adapter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBitwardenSource:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_fetch_delegates_to_fetch_bitwarden_secrets(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.token")
|
||||
import agent.secret_sources.bitwarden as bw
|
||||
|
||||
monkeypatch.setattr(bw, "find_bws", lambda **kw: Path("/fake/bws"))
|
||||
captured = {}
|
||||
|
||||
def _fake_fetch(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"MY_KEY": "val"}, ["a warning"]
|
||||
|
||||
monkeypatch.setattr(bw, "fetch_bitwarden_secrets", _fake_fetch)
|
||||
result = BitwardenSource().fetch(
|
||||
{"enabled": True, "project_id": "proj",
|
||||
"server_url": " https://vault.bitwarden.eu "},
|
||||
tmp_path,
|
||||
)
|
||||
assert result.ok
|
||||
assert result.secrets == {"MY_KEY": "val"}
|
||||
assert result.warnings == ["a warning"]
|
||||
assert captured["project_id"] == "proj"
|
||||
assert captured["server_url"] == "https://vault.bitwarden.eu"
|
||||
assert captured["home_path"] == tmp_path
|
||||
|
||||
|
||||
def test_e2e_through_orchestrator(self, tmp_path, monkeypatch):
|
||||
"""Full path: registry → BitwardenSource → env, with fetch mocked."""
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.token")
|
||||
import agent.secret_sources.bitwarden as bw
|
||||
|
||||
monkeypatch.setattr(bw, "find_bws", lambda **kw: Path("/fake/bws"))
|
||||
monkeypatch.setattr(
|
||||
bw, "fetch_bitwarden_secrets",
|
||||
lambda **kw: ({"ANTHROPIC_API_KEY": "sk-ant", "BWS_ACCESS_TOKEN": "steal"}, []),
|
||||
)
|
||||
reg.register_source(BitwardenSource())
|
||||
env = {"BWS_ACCESS_TOKEN": "0.token"}
|
||||
report = reg.apply_all(
|
||||
{"bitwarden": {"enabled": True, "project_id": "proj"}},
|
||||
tmp_path, environ=env,
|
||||
)
|
||||
assert env["ANTHROPIC_API_KEY"] == "sk-ant"
|
||||
# The bootstrap token is protected even though BSM carried it.
|
||||
assert env["BWS_ACCESS_TOKEN"] == "0.token"
|
||||
assert report.provenance["ANTHROPIC_API_KEY"].source == "bitwarden"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conformance kit applied to the bundled source
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBitwardenConformance(SecretSourceConformance):
|
||||
@pytest.fixture
|
||||
def source(self, monkeypatch):
|
||||
# Never hit the network / auto-install path in conformance runs.
|
||||
import agent.secret_sources.bitwarden as bw
|
||||
|
||||
monkeypatch.setattr(bw, "find_bws", lambda **kw: None)
|
||||
monkeypatch.delenv("BWS_ACCESS_TOKEN", raising=False)
|
||||
return BitwardenSource()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1Password adapter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOnePasswordSource:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_mapped_op_beats_bulk_bitwarden_through_orchestrator(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""The headline multi-source scenario: both vaults claim the same var."""
|
||||
import agent.secret_sources.bitwarden as bw
|
||||
import agent.secret_sources.onepassword as op
|
||||
|
||||
monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.token")
|
||||
monkeypatch.setattr(bw, "find_bws", lambda **kw: Path("/fake/bws"))
|
||||
monkeypatch.setattr(
|
||||
bw, "fetch_bitwarden_secrets",
|
||||
lambda **kw: ({"SHARED_KEY": "from-bitwarden",
|
||||
"BW_ONLY": "bw-val"}, []),
|
||||
)
|
||||
monkeypatch.setattr(op, "find_op", lambda *_a, **_kw: Path("/fake/op"))
|
||||
monkeypatch.setattr(
|
||||
op, "fetch_onepassword_secrets",
|
||||
lambda **kw: ({"SHARED_KEY": "from-1password"}, []),
|
||||
)
|
||||
reg.register_source(bw.BitwardenSource())
|
||||
reg.register_source(op.OnePasswordSource())
|
||||
env = {"BWS_ACCESS_TOKEN": "0.token"}
|
||||
report = reg.apply_all(
|
||||
{
|
||||
# bitwarden listed FIRST — mapped 1Password must still win.
|
||||
"sources": ["bitwarden", "onepassword"],
|
||||
"bitwarden": {"enabled": True, "project_id": "proj"},
|
||||
"onepassword": {"enabled": True,
|
||||
"env": {"SHARED_KEY": "op://V/I/F"}},
|
||||
},
|
||||
tmp_path, environ=env,
|
||||
)
|
||||
assert env["SHARED_KEY"] == "from-1password"
|
||||
assert env["BW_ONLY"] == "bw-val"
|
||||
assert report.provenance["SHARED_KEY"].source == "onepassword"
|
||||
assert report.provenance["BW_ONLY"].source == "bitwarden"
|
||||
assert report.conflicts # the shadowed bitwarden claim is surfaced
|
||||
|
||||
|
||||
class TestOnePasswordConformance(SecretSourceConformance):
|
||||
@pytest.fixture
|
||||
def source(self, monkeypatch):
|
||||
import agent.secret_sources.onepassword as op
|
||||
|
||||
monkeypatch.setattr(op, "find_op", lambda *_a, **_kw: None)
|
||||
monkeypatch.delenv("OP_SERVICE_ACCOUNT_TOKEN", raising=False)
|
||||
return op.OnePasswordSource()
|
||||
Reference in New Issue
Block a user