fix(env): seed .op.env bootstrap into cold-profile hydration

The salvaged hydrate_profile_secret_sources (#74549) seeded its
profile-local env from <home>/.env only, but the documented 1Password
bootstrap flow puts OP_SERVICE_ACCOUNT_TOKEN in the gitignored
<home>/.op.env (mirrored from load_hermes_dotenv). A cold profile using
that flow still failed 1Password hydration — the one unaddressed item
from the sweeper review on #74549. Seed .op.env via setdefault so .env
values win; never touches os.environ. Two regression tests.
This commit is contained in:
Teknium 2026-08-01 21:14:07 -07:00
parent 5438e9c629
commit 52308ff455
2 changed files with 81 additions and 0 deletions

View File

@ -208,6 +208,15 @@ def _hydrate_profile_secret_sources(home: Path) -> dict[str, str]:
if _is_global_env(name)
}
local_env.update(load_env_file(home / ".env"))
# Mirror load_hermes_dotenv()'s .op.env bootstrap: the 1Password
# service-account token lives in <home>/.op.env (gitignored), not
# .env. Without seeding it here a cold profile configured for the
# supported .op.env flow fails 1Password hydration (sweeper review
# on #74549). .env values win — never override an existing key.
op_env = home / ".op.env"
if op_env.exists():
for _name, _value in load_env_file(op_env).items():
local_env.setdefault(_name, _value)
local_env["HERMES_HOME"] = str(home)
report = apply_all(cfg, home, environ=local_env)
except Exception: # noqa: BLE001 — preserve fail-open startup behavior

View File

@ -174,6 +174,78 @@ def test_cold_profile_bitwarden_uses_profile_bootstrap_without_global_env(
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."""