diff --git a/tests/agent/test_env_loader_secret_sources.py b/tests/agent/test_env_loader_secret_sources.py index 31befe867f..a35002ae31 100644 --- a/tests/agent/test_env_loader_secret_sources.py +++ b/tests/agent/test_env_loader_secret_sources.py @@ -543,46 +543,6 @@ def test_apply_external_secret_sources_status_line_suppresses_secret_names( assert "LEAK_THIS_TOKEN" not in err -def test_private_hydration_records_skipped_existing_names_for_the_routed_scrub(tmp_path, monkeypatch): - """The private (routed-profile) hydration path must feed ``secret_source_names()`` like the - process-global path does — including ``skipped_existing`` — or a name first observed through - ``hydrate_profile_secret_sources()`` is invisible to the routed-child scrub and a sibling inherits - the ambient launch value for it (#107695 review on f5f88d5058).""" - from agent.secret_sources import registry as reg_module - from agent.secret_sources.base import FetchResult - from agent.secret_sources.registry import AppliedVar, ApplyReport, SourceReport - - home = tmp_path / "profile-b" - home.mkdir() - (home / "config.yaml").write_text("secrets:\n test-source:\n enabled: true\n", encoding="utf-8") - monkeypatch.setattr(env_loader, "_SOURCE_SUPPLIED_NAMES", set()) - monkeypatch.setattr(env_loader, "_SECRET_SOURCES", {}) - monkeypatch.setattr(env_loader, "_APPLIED_HOMES", set()) - monkeypatch.setattr(env_loader, "_SECRET_SOURCE_VALUES_BY_HOME", {}) - - report = ApplyReport( - sources=[SourceReport(name="test-source", label="Test Source", result=FetchResult(), - applied=["APPLIED_SECRET"], skipped_existing=["CUSTOM_SOURCE_SECRET"])], - provenance={"APPLIED_SECRET": AppliedVar(name="APPLIED_SECRET", source="test-source", - shape="mapped", overrode_env=False)}, - ) - - def _fake_apply_all(_cfg, home_path, environ=None): - if environ is not None: - environ["APPLIED_SECRET"] = "applied-b" - return report - - monkeypatch.setattr(reg_module, "apply_all", _fake_apply_all) - - env_loader.hydrate_profile_secret_sources(home) - - names = set(env_loader.secret_source_names()) - assert {"APPLIED_SECRET", "CUSTOM_SOURCE_SECRET"} <= names - # provenance stays honest: only the APPLIED name carries a source label - assert env_loader.get_secret_source("APPLIED_SECRET") == "test-source" - assert env_loader.get_secret_source("CUSTOM_SOURCE_SECRET") is None - - 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 @@ -818,87 +778,3 @@ def test_home_scoped_reset_preserves_sibling_snapshot(tmp_path, monkeypatch, _fr assert env_loader.get_secret_source_values(home) == {} assert env_loader.get_secret_source_values(sibling) == {"GLM_API_KEY": "vault-b"} assert str(sibling.resolve()) in env_loader._APPLIED_HOMES - - -def test_profile_scope_carries_vault_secrets_only_when_hydrated_first(tmp_path, monkeypatch): - """``build_profile_secret_scope`` only READS the per-home source map, so hydration must run - BEFORE the scope is frozen — the order ``gateway/run.py`` and the external cron worker use. - - This is load-bearing once the process-global dotenv write is suppressed for a scoped home: - a vault-backed secret then has no other route into the run. - """ - from pathlib import Path - - from agent.secret_scope import build_profile_secret_scope - from agent.secret_sources import registry as reg_module - - home = tmp_path / "routed" - home.mkdir() - (home / ".env").write_text("BWS_ACCESS_TOKEN=0.test-token\n", encoding="utf-8") - (home / "config.yaml").write_text( - "secrets:\n bitwarden:\n enabled: true\n project_id: p\n" - " access_token_env: BWS_ACCESS_TOKEN\n", encoding="utf-8") - monkeypatch.setenv("HERMES_HOME", str(home)) - monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.test-token") - - 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: ({"VAULT_ONLY_KEY": "from-vault"}, [])) - reg_module._reset_registry_for_tests() - env_loader._APPLIED_HOMES.discard(str(home.resolve())) - - # Frozen before hydration: the vault value cannot be in the scope. - assert "VAULT_ONLY_KEY" not in build_profile_secret_scope(home) - - # Hydrated first — as run_one_job now does — the scope carries it. - env_loader.hydrate_profile_secret_sources(home) - assert build_profile_secret_scope(home).get("VAULT_ONLY_KEY") == "from-vault" - - -def test_a_plugin_source_discovered_mid_fire_reaches_the_installed_scope(tmp_path, monkeypatch): - """A routed cron fire freezes its scope before its first agent build discovers plugin secret - sources; under multiplex semantics the post-discovery reload is hydrate-only, so without an - in-place refresh THIS fire never saw the plugin credential (#107692 review).""" - import os - - from agent import secret_scope - from agent.secret_sources import registry as reg_module - from agent.secret_sources.base import SECRET_SOURCE_API_VERSION, FetchResult, SecretSource - from hermes_cli.plugins import PluginManager - from hermes_constants import reset_hermes_home_override, set_hermes_home_override - - class _LateVault(SecretSource): - api_version = SECRET_SOURCE_API_VERSION - shape = "bulk" - name = "latevault" - - def is_enabled(self, cfg: dict) -> bool: - return True - - def fetch(self, cfg: dict, home_path: Path) -> FetchResult: - return FetchResult(secrets={"PLUGIN_ONLY_KEY": "from-plugin"}) - - launch, home = tmp_path / "launch", tmp_path / "launch" / "profiles" / "ops" - home.mkdir(parents=True) - (home / ".env").write_text("XAI_API_KEY=routed\n", encoding="utf-8") - (home / "config.yaml").write_text("secrets:\n latevault:\n enabled: true\n", encoding="utf-8") - monkeypatch.setenv("HERMES_HOME", str(launch)) - monkeypatch.delenv("PLUGIN_ONLY_KEY", raising=False) - reg_module._reset_registry_for_tests() - env_loader.reset_secret_source_cache() - - context_token = secret_scope.set_multiplex_context(True) - home_token = set_hermes_home_override(str(home)) - scope_token = secret_scope.set_secret_scope(secret_scope.build_profile_secret_scope(home)) - try: - assert secret_scope.get_secret("PLUGIN_ONLY_KEY") is None # frozen before the plugin existed - assert reg_module.register_source(_LateVault()) # discovery registers the source... - PluginManager(scope_key=str(home))._refresh_secret_sources_after_discovery() # ...and re-pulls - assert secret_scope.get_secret("PLUGIN_ONLY_KEY") == "from-plugin" - finally: - secret_scope.reset_secret_scope(scope_token) - reset_hermes_home_override(home_token) - secret_scope.reset_multiplex_context(context_token) - reg_module._reset_registry_for_tests() - assert "PLUGIN_ONLY_KEY" not in os.environ diff --git a/tests/agent/test_secret_scope.py b/tests/agent/test_secret_scope.py index 857af0c936..5a42f842d8 100644 --- a/tests/agent/test_secret_scope.py +++ b/tests/agent/test_secret_scope.py @@ -378,74 +378,3 @@ class TestSecretScopeAcrossExecutorThreads: finally: pool.shutdown(wait=True) ss.reset_secret_scope(token) - - -class TestMultiplexContext: - """A task can run under multiplex semantics without flipping the process flag: the desktop - backend ticks sibling profiles' cron jobs from a process whose own turns stay single-profile.""" - - def test_context_turns_multiplex_on_for_the_task_only(self): - assert ss.is_multiplex_active() is False - token = ss.set_multiplex_context(True) - try: - assert ss.is_multiplex_active() is True - finally: - ss.reset_multiplex_context(token) - assert ss.is_multiplex_active() is False - - def test_context_propagates_through_copy_context_like_the_pool_dispatch(self): - import contextvars - import threading - - token = ss.set_multiplex_context(True) - try: - ctx = contextvars.copy_context() # what cron's _submit_with_guard hands the worker - finally: - ss.reset_multiplex_context(token) - seen = {} - worker = threading.Thread(target=lambda: seen.update(v=ctx.run(ss.is_multiplex_active))) - worker.start() - worker.join() - assert seen["v"] is True - assert ss.is_multiplex_active() is False # the caller's own context is untouched - - def test_scoped_miss_under_context_never_reads_the_process_env(self, monkeypatch): - monkeypatch.setenv("LAUNCH_ONLY_TOKEN", "launch-token") - token = ss.set_multiplex_context(True) - scope_token = ss.set_secret_scope({"ROUTED_KEY": "routed"}) - try: - assert ss.get_secret("ROUTED_KEY") == "routed" - assert ss.get_secret("LAUNCH_ONLY_TOKEN") is None - finally: - ss.reset_secret_scope(scope_token) - ss.reset_multiplex_context(token) - assert ss.get_secret("LAUNCH_ONLY_TOKEN") == "launch-token" # single-profile semantics resume - - def test_refresh_installed_secret_scope_folds_in_values_learned_after_the_freeze(self, tmp_path): - (tmp_path / ".env").write_text("EARLY_KEY=early\n", encoding="utf-8") - scope_token = ss.set_secret_scope(ss.build_profile_secret_scope(tmp_path)) - try: - (tmp_path / ".env").write_text("EARLY_KEY=early\nLATE_KEY=late\n", encoding="utf-8") - assert ss.get_secret("LATE_KEY") is None - assert ss.refresh_installed_secret_scope(tmp_path) is True - assert ss.get_secret("LATE_KEY") == "late" - finally: - ss.reset_secret_scope(scope_token) - assert ss.refresh_installed_secret_scope(tmp_path) is False # nothing installed - - -def test_refresh_drops_a_name_the_rebuild_no_longer_supplies(tmp_path, monkeypatch): - """refresh_installed_secret_scope must REPLACE the installed mapping, not merge into it: a - rotated/revoked source value would otherwise survive for the rest of the fire (#107695 review).""" - from agent import secret_scope - - (tmp_path / ".env").write_text("KEPT=new\n", encoding="utf-8") - token = secret_scope.set_secret_scope({"REVOKED_PLUGIN_TOKEN": "old", "KEPT": "stale"}) - try: - assert secret_scope.refresh_installed_secret_scope(tmp_path) is True - live = dict(secret_scope.current_secret_scope() or {}) - finally: - secret_scope.reset_secret_scope(token) - - assert live == {"KEPT": "new"}, live - assert "REVOKED_PLUGIN_TOKEN" not in live diff --git a/tests/cron/test_cron_multiplex_desktop_ticker_scope.py b/tests/cron/test_cron_multiplex_desktop_ticker_scope.py index 9cfc915365..cf21d4cbd6 100644 --- a/tests/cron/test_cron_multiplex_desktop_ticker_scope.py +++ b/tests/cron/test_cron_multiplex_desktop_ticker_scope.py @@ -157,8 +157,7 @@ def test_a_routed_profile_fire_runs_under_multiplex_semantics_for_exactly_its_sc The tick only MARKS the fire as routed; multiplex semantics switch on where run_one_job installs the profile's secret scope and off with it — so the routed .env stays out of the shared os.environ, a scope miss never falls back to the launch profile's credentials, and - nothing that runs before the scope (the restart-safe handoff) can be fail-closed (#107692).""" - import contextvars + the parent process env is byte-identical after the tick (#107692).""" import os import cron.scheduler as scheduler @@ -176,13 +175,13 @@ def test_a_routed_profile_fire_runs_under_multiplex_semantics_for_exactly_its_sc monkeypatch.setenv("DISCORD_BOT_TOKEN", "launch-bot") monkeypatch.delenv("ROUTED_ONLY", raising=False) secret_scope.set_multiplex_active(False) # the desktop backend never sets the process flag + environ_before = dict(os.environ) with _profile_cron_scope(routed): - # Before the scope exists — where run_one_job's restart-safe handoff runs — nothing is - # multiplex: the marker is set, the semantics are not. + # Before the scope exists — where run_one_job's restart-safe handoff runs — the marker is + # set, the semantics are not. assert routed_profile_fire() is True assert secret_scope.is_multiplex_active() is False - ctx = contextvars.copy_context() # what _submit_with_guard hands the pool worker tokens = scheduler._install_fire_secret_scope() try: @@ -196,73 +195,4 @@ def test_a_routed_profile_fire_runs_under_multiplex_semantics_for_exactly_its_sc assert secret_scope.is_multiplex_active() is False # off with the scope, not later assert routed_profile_fire() is False - assert os.environ["XAI_API_KEY"] == "launch-key" - assert "ROUTED_ONLY" not in os.environ - - # The marker reaches the worker that performs the write; the worker's own scope install is - # what turns multiplex semantics on there. - seen = {} - - def _worker(): - tokens = scheduler._install_fire_secret_scope() - try: - seen["v"] = secret_scope.is_multiplex_active() - finally: - scheduler._reset_fire_secret_scope(tokens) - - worker = threading.Thread(target=lambda: ctx.run(_worker)) - worker.start() - worker.join() - assert seen["v"] is True - - -def test_the_process_own_profile_fire_keeps_single_profile_semantics(tmp_path, monkeypatch): - """The launch profile's own fire is not routed: its scope install leaves the process in - single-profile semantics, so its .env keeps loading as today and a scope miss still reads - the process env (systemd / ``op run`` injected keys).""" - import cron.scheduler as scheduler - from agent import secret_scope - from cron.scheduler_provider import _profile_cron_scope, routed_profile_fire - - launch = tmp_path / "launch" - (launch / "cron").mkdir(parents=True) - (launch / ".env").write_text("XAI_API_KEY=launch-key\n", encoding="utf-8") - monkeypatch.setenv("HERMES_HOME", str(launch)) - monkeypatch.setenv("SHELL_INJECTED_TOKEN", "from-systemd") - secret_scope.set_multiplex_active(False) - - with _profile_cron_scope(launch): - assert routed_profile_fire() is False - tokens = scheduler._install_fire_secret_scope() - try: - assert secret_scope.is_multiplex_active() is False - assert secret_scope.get_secret("SHELL_INJECTED_TOKEN") == "from-systemd" - finally: - scheduler._reset_fire_secret_scope(tokens) - - -def test_the_restart_safe_handoff_is_not_fail_closed_by_a_routed_tick(tmp_path, monkeypatch): - """run_one_job hands a fire to the external worker BEFORE the body installs the profile scope. - A routed tick must not make that handoff read secrets fail-closed: with a passthrough key - registered, building the worker env there raises UnscopedSecretError if multiplex semantics - are on with no scope (#107399's path). The tick only marks the fire; the handoff keeps its - current semantics (its own scope is #107413 / #106050's seam).""" - from agent import secret_scope - from cron.scheduler_provider import _profile_cron_scope - from tools.env_passthrough import clear_env_passthrough, is_env_passthrough, register_env_passthrough - from tools.environments.local import build_subprocess_env - - launch, routed = tmp_path / "launch", tmp_path / "launch" / "profiles" / "ops" - for home in (launch, routed): - (home / "cron").mkdir(parents=True) - monkeypatch.setenv("HERMES_HOME", str(launch)) - monkeypatch.setenv("SERVICE_TOKEN", "A") - secret_scope.set_multiplex_active(False) - register_env_passthrough(["SERVICE_TOKEN"]) - try: - assert is_env_passthrough("SERVICE_TOKEN") - with _profile_cron_scope(routed): - assert secret_scope.is_multiplex_active() is False - build_subprocess_env(scrub_secrets=True) # the handoff's child env, pre-scope: must not raise - finally: - clear_env_passthrough() + assert dict(os.environ) == environ_before diff --git a/tests/cron/test_cron_no_agent.py b/tests/cron/test_cron_no_agent.py index fd64bd9d1e..0302277b52 100644 --- a/tests/cron/test_cron_no_agent.py +++ b/tests/cron/test_cron_no_agent.py @@ -370,151 +370,12 @@ def test_agent_job_provider_classification_unchanged(error, expected): assert expected in _summarize_cron_failure_for_delivery(job, error) -def test_a_routed_profile_script_receives_its_own_profile_env(hermes_env, monkeypatch): - """A no_agent script fired for a SIBLING profile sees that profile's .env values — via the - installed scope, never by copying them into the parent's os.environ (#107692 review).""" - import os - - from agent import secret_scope - from cron.scheduler_script import _run_job_script - - monkeypatch.setenv("CUSTOM_CRON_VALUE", "launch") - monkeypatch.delenv("ROUTED_ONLY_VALUE", raising=False) - script = hermes_env / "scripts" / "probe_env.sh" - script.write_text('#!/bin/bash\necho "${CUSTOM_CRON_VALUE}|${ROUTED_ONLY_VALUE}"\n') - - context_token = secret_scope.set_multiplex_context(True) - scope_token = secret_scope.set_secret_scope( - {"CUSTOM_CRON_VALUE": "routed", "ROUTED_ONLY_VALUE": "routed-only"}) - try: - ok, output = _run_job_script("probe_env.sh") - finally: - secret_scope.reset_secret_scope(scope_token) - secret_scope.reset_multiplex_context(context_token) - - assert ok, output - assert output.strip() == "routed|routed-only" - assert os.environ["CUSTOM_CRON_VALUE"] == "launch" # the parent process was not mutated - - -def test_a_routed_profile_script_never_receives_a_launch_profile_only_value(hermes_env, monkeypatch): - """Negative control for the overlay above (#107695 review): a name the LAUNCH profile's .env - defines and the routed scope does not must reach the routed child UNSET — not with the launch - value. The secret scrub only knows classified names, so a custom or unclassified secret would - otherwise cross the profile boundary; the launch profile's dotenv residue is dropped first.""" - import os - - from agent import secret_scope - from cron.scheduler_script import _run_job_script - from hermes_constants import get_process_hermes_home, reset_hermes_home_override, set_hermes_home_override - - launch = get_process_hermes_home() - (launch / ".env").write_text("LAUNCH_ONLY_VALUE=launch-only\nCUSTOM_CRON_VALUE=launch\n", encoding="utf-8") - monkeypatch.setenv("LAUNCH_ONLY_VALUE", "launch-only") - monkeypatch.setenv("CUSTOM_CRON_VALUE", "launch") - routed = launch / "profiles" / "ops" - (routed / "scripts").mkdir(parents=True, exist_ok=True) - # Under the routed home override the runner resolves scripts against THAT profile's scripts dir. - script = routed / "scripts" / "probe_launch_only.sh" - script.write_text('#!/bin/bash\necho "${CUSTOM_CRON_VALUE}|${LAUNCH_ONLY_VALUE:-}"\n') - - home_token = set_hermes_home_override(str(routed)) - context_token = secret_scope.set_multiplex_context(True) - scope_token = secret_scope.set_secret_scope({"CUSTOM_CRON_VALUE": "routed"}) - try: - ok, output = _run_job_script("probe_launch_only.sh") - finally: - secret_scope.reset_secret_scope(scope_token) - secret_scope.reset_multiplex_context(context_token) - reset_hermes_home_override(home_token) - - assert ok, output - assert output.strip() == "routed|" - assert os.environ["LAUNCH_ONLY_VALUE"] == "launch-only" # the parent process was not mutated - - -def test_a_routed_profile_script_never_receives_a_launch_external_source_value(hermes_env, monkeypatch): - """External secret sources (vault, 1Password, ...) write their names into the shared - ``os.environ`` too, and ``strip_launch_profile_env`` only knows dotenv- and terminal-owned - names. A name the LAUNCH profile's source supplied must still reach the routed child unset - (#107695 review); a name the ROUTED profile's own source supplies must come through.""" - import os - - from agent import secret_scope - from cron.scheduler_script import _run_job_script - from hermes_cli import env_loader - from hermes_constants import get_process_hermes_home, reset_hermes_home_override, set_hermes_home_override - - launch = get_process_hermes_home() - routed = launch / "profiles" / "ops" - (routed / "scripts").mkdir(parents=True, exist_ok=True) - monkeypatch.setenv("LAUNCH_VAULT_ONLY", "launch-vault-value") - monkeypatch.setitem(env_loader._SECRET_SOURCES, "LAUNCH_VAULT_ONLY", "vault") - monkeypatch.setitem(env_loader._SECRET_SOURCES, "ROUTED_VAULT_ONLY", "vault") - script = routed / "scripts" / "probe_vault.sh" - script.write_text('#!/bin/bash\necho "${LAUNCH_VAULT_ONLY:-}|${ROUTED_VAULT_ONLY:-}"\n') - - home_token = set_hermes_home_override(str(routed)) - context_token = secret_scope.set_multiplex_context(True) - scope_token = secret_scope.set_secret_scope({"ROUTED_VAULT_ONLY": "routed-vault-value"}) - try: - ok, output = _run_job_script("probe_vault.sh") - finally: - secret_scope.reset_secret_scope(scope_token) - secret_scope.reset_multiplex_context(context_token) - reset_hermes_home_override(home_token) - - assert ok, output - assert output.strip() == "|routed-vault-value" - assert os.environ["LAUNCH_VAULT_ONLY"] == "launch-vault-value" # parent untouched - - -def test_a_routed_profile_script_never_receives_a_launch_key_removed_from_dotenv_after_boot(hermes_env, monkeypatch): - """Lifecycle negative control (#107695 review): the launch profile's .env loaded ``STALE_LAUNCH_KEY`` - at boot, the operator then removed the key from the file, and the long-running process still holds - the old value in ``os.environ`` (dotenv never unsets). A re-parse of the CURRENT file no longer names - it, so a strip built from the file alone let the stale value reach a routed child. The strip must - work from every key any dotenv load put into the process env during its lifetime.""" - import os - - from agent import secret_scope - from cron.scheduler_script import _run_job_script - from hermes_cli import env_loader - from hermes_constants import get_process_hermes_home, reset_hermes_home_override, set_hermes_home_override - - launch = get_process_hermes_home() - monkeypatch.setattr(env_loader, "_LOADED_DOTENV_KEYS", set(env_loader._LOADED_DOTENV_KEYS)) - monkeypatch.setenv("STALE_LAUNCH_KEY", "placeholder") # so monkeypatch restores the parent env afterwards - (launch / ".env").write_text("STALE_LAUNCH_KEY=stale-launch-value\n", encoding="utf-8") - env_loader._load_dotenv_with_fallback(launch / ".env", override=True) # the boot-time load - assert os.environ["STALE_LAUNCH_KEY"] == "stale-launch-value" - (launch / ".env").write_text("# key removed after boot\n", encoding="utf-8") - - routed = launch / "profiles" / "ops" - (routed / "scripts").mkdir(parents=True, exist_ok=True) - script = routed / "scripts" / "probe_stale.sh" - script.write_text('#!/bin/bash\necho "${STALE_LAUNCH_KEY:-}"\n') - - home_token = set_hermes_home_override(str(routed)) - context_token = secret_scope.set_multiplex_context(True) - scope_token = secret_scope.set_secret_scope({}) - try: - ok, output = _run_job_script("probe_stale.sh") - finally: - secret_scope.reset_secret_scope(scope_token) - secret_scope.reset_multiplex_context(context_token) - reset_hermes_home_override(home_token) - - assert ok, output - assert output.strip() == "" - assert os.environ["STALE_LAUNCH_KEY"] == "stale-launch-value" # the parent process was not mutated - - -def test_a_routed_profile_script_never_receives_a_launch_source_value_that_lost_to_the_process_env(hermes_env, monkeypatch): - """A launch-profile source SUPPLIED ``CUSTOM_VAULT_SECRET`` but a pre-existing process value won - (``skipped_existing``), so it never entered the provenance map ``secret_source_names()`` used to be - built from — and the launch value reached a routed child with an empty scope (#107695 review). The - ownership set must include every source-supplied name, applied or skipped.""" +def test_a_routed_profile_script_never_receives_a_launch_only_name(hermes_env, monkeypatch): + """A no_agent script fired for a SIBLING profile runs with that profile's scope overlaid and + NONE of the launch profile's residue (#107695 review): a name the launch ``.env`` defines, and a + name a launch external source SUPPLIED — applied, or skipped because a process value already won + (``skipped_existing``, so it never entered the provenance map) — reach the child unset. The + routed profile's own values come through, and the parent ``os.environ`` is never mutated.""" import os from agent import secret_scope @@ -526,45 +387,52 @@ def test_a_routed_profile_script_never_receives_a_launch_source_value_that_lost_ from hermes_constants import get_process_hermes_home, reset_hermes_home_override, set_hermes_home_override launch = get_process_hermes_home() + (launch / ".env").write_text("LAUNCH_ONLY_VALUE=launch-only\nCUSTOM_CRON_VALUE=launch\n", encoding="utf-8") (launch / "config.yaml").write_text("secrets:\n test-source:\n enabled: true\n", encoding="utf-8") - monkeypatch.setenv("CUSTOM_VAULT_SECRET", "launch-value") + for name, value in (("LAUNCH_ONLY_VALUE", "launch-only"), ("CUSTOM_CRON_VALUE", "launch"), + ("LAUNCH_VAULT_ONLY", "launch-vault-value"), ("LAUNCH_SKIPPED_SECRET", "launch-value")): + monkeypatch.setenv(name, value) monkeypatch.setattr(env_loader, "_SOURCE_SUPPLIED_NAMES", set()) - monkeypatch.setattr(env_loader, "_SECRET_SOURCES", {}) + monkeypatch.setattr(env_loader, "_SECRET_SOURCES", {"LAUNCH_VAULT_ONLY": "vault", "ROUTED_VAULT_ONLY": "vault"}) monkeypatch.setattr(env_loader, "_APPLIED_HOMES", set()) monkeypatch.setattr(env_loader, "_SECRET_SOURCE_VALUES_BY_HOME", {}) monkeypatch.setattr(reg_module, "apply_all", lambda _cfg, home_path, **_kw: ApplyReport( sources=[SourceReport(name="test-source", label="Test Source", result=FetchResult(), - applied=[], skipped_existing=["CUSTOM_VAULT_SECRET"])], + applied=[], skipped_existing=["LAUNCH_SKIPPED_SECRET"])], provenance={})) - env_loader._apply_external_secret_sources(launch) # the real registry path, source loses to the env - assert "CUSTOM_VAULT_SECRET" in env_loader.secret_source_names() + env_loader._apply_external_secret_sources(launch) # the real registry path; the source loses to the env + environ_before = dict(os.environ) routed = launch / "profiles" / "ops" (routed / "scripts").mkdir(parents=True, exist_ok=True) - script = routed / "scripts" / "probe_skipped.sh" - script.write_text('#!/bin/bash\necho "${CUSTOM_VAULT_SECRET:-}"\n') + # Under the routed home override the runner resolves scripts against THAT profile's scripts dir. + script = routed / "scripts" / "probe_launch_only.sh" + script.write_text( + '#!/bin/bash\necho "${CUSTOM_CRON_VALUE}|${ROUTED_VAULT_ONLY}|${LAUNCH_ONLY_VALUE:-}' + '|${LAUNCH_VAULT_ONLY:-}|${LAUNCH_SKIPPED_SECRET:-}"\n' + ) home_token = set_hermes_home_override(str(routed)) context_token = secret_scope.set_multiplex_context(True) - scope_token = secret_scope.set_secret_scope({}) + scope_token = secret_scope.set_secret_scope( + {"CUSTOM_CRON_VALUE": "routed", "ROUTED_VAULT_ONLY": "routed-vault-value"}) try: - ok, output = _run_job_script("probe_skipped.sh") + ok, output = _run_job_script("probe_launch_only.sh") finally: secret_scope.reset_secret_scope(scope_token) secret_scope.reset_multiplex_context(context_token) reset_hermes_home_override(home_token) assert ok, output - assert output.strip() == "" - assert os.environ["CUSTOM_VAULT_SECRET"] == "launch-value" # parent untouched + assert output.strip() == "routed|routed-vault-value|||" + assert dict(os.environ) == environ_before def test_a_routed_profile_script_keeps_administrator_managed_values_over_its_own(hermes_env, monkeypatch): """Managed-scope precedence (#107695 review on f5f88d5058): the administrator's managed ``.env`` is - applied LAST with override in the launch process, so it beats the user's own ``.env``. Recording its - keys as launch residue stripped ``ORG_POLICY_FLAG`` before the routed overlay, and the routed - profile's own value replaced policy. Managed keys are not residue, and they are re-applied over the - routed scope so the child sees the same precedence the launch process does.""" + applied LAST with override in the launch process, so it beats the user's own ``.env``. Managed keys + are not launch residue, and they are re-applied over the routed scope so the child sees the same + precedence the launch process does.""" import os from agent import secret_scope @@ -582,8 +450,6 @@ def test_a_routed_profile_script_keeps_administrator_managed_values_over_its_own monkeypatch.setenv("ORG_POLICY_FLAG", "placeholder") env_loader._apply_managed_env() # the boot-time managed load assert os.environ["ORG_POLICY_FLAG"] == "managed-value" - assert "ORG_POLICY_FLAG" in env_loader.managed_dotenv_keys() - assert "ORG_POLICY_FLAG" not in env_loader.launch_dotenv_keys() routed = launch / "profiles" / "ops" (routed / "scripts").mkdir(parents=True, exist_ok=True) @@ -604,55 +470,3 @@ def test_a_routed_profile_script_keeps_administrator_managed_values_over_its_own assert ok, output assert output.strip() == "managed-value" assert os.environ["ORG_POLICY_FLAG"] == "managed-value" # parent untouched - - -def test_strip_launch_profile_env_never_treats_managed_keys_as_residue(hermes_env, monkeypatch): - """The exclusion stands on its own (#107695 review on f5f88d5058): ``kanban_db_dispatch`` and - ``scheduler_delivery`` strip and spawn ``hermes -p `` with NO scope overlay and no managed - re-apply afterwards, so for them the strip itself must leave administrator-managed keys in place. - - The case that matters is a key defined in BOTH the user's launch ``.env`` and the managed ``.env`` — - the precedence conflict managed override exists for. That key IS launch residue by every other rule - (it is in the launch file and was recorded as loaded), and only the managed exclusion keeps the - policy value in the child. A launch-only recorded key is still removed.""" - from agent import secret_scope - from hermes_cli import env_loader - from hermes_constants import get_process_hermes_home, reset_hermes_home_override, set_hermes_home_override - from tools.environments.local import strip_launch_profile_env - - launch = get_process_hermes_home() - routed = launch / "profiles" / "ops" - routed.mkdir(parents=True, exist_ok=True) - # The user's own .env ALSO sets ORG_POLICY_FLAG; the managed .env overrode it at boot. - (launch / ".env").write_text("ORG_POLICY_FLAG=user-value\n", encoding="utf-8") - monkeypatch.setattr(env_loader, "_LOADED_DOTENV_KEYS", {"LAUNCH_ONLY_RECORDED", "ORG_POLICY_FLAG"}) - monkeypatch.setattr(env_loader, "_MANAGED_DOTENV_KEYS", {"ORG_POLICY_FLAG"}) - - home_token = set_hermes_home_override(str(routed)) - context_token = secret_scope.set_multiplex_context(True) - try: - env = strip_launch_profile_env({"ORG_POLICY_FLAG": "managed-value", "LAUNCH_ONLY_RECORDED": "stale"}) - finally: - secret_scope.reset_multiplex_context(context_token) - reset_hermes_home_override(home_token) - - assert env == {"ORG_POLICY_FLAG": "managed-value"} - - -def test_single_profile_child_keeps_its_own_external_source_value(hermes_env, monkeypatch): - """No multiplexing: os.environ IS this profile's environment, so the source-name strip must not - run at all — the child keeps its own vault value even if the per-home snapshot were missing.""" - from agent import secret_scope - from cron.scheduler_script import _run_job_script - from hermes_cli import env_loader - - monkeypatch.setenv("OWN_VAULT_KEY", "own-vault-value") - monkeypatch.setitem(env_loader._SECRET_SOURCES, "OWN_VAULT_KEY", "vault") - script = hermes_env / "scripts" / "probe_own_vault.sh" - script.write_text('#!/bin/bash\necho "${OWN_VAULT_KEY:-}"\n') - - assert secret_scope.is_multiplex_active() is False - ok, output = _run_job_script("probe_own_vault.sh") - - assert ok, output - assert output.strip() == "own-vault-value" diff --git a/tests/cron/test_restart_safe_worker.py b/tests/cron/test_restart_safe_worker.py index 8b9032d1da..b7e55bbf14 100644 --- a/tests/cron/test_restart_safe_worker.py +++ b/tests/cron/test_restart_safe_worker.py @@ -258,49 +258,6 @@ def _stub_external_worker_launch(scheduler, monkeypatch): return spawned, payloads, handoff, get -def test_launch_external_worker_treats_a_routed_fire_as_multiplexed(tmp_path, monkeypatch): - """A fire routed to another profile is multiplexed at the handoff boundary (#107695 review on - f5f88d5058). ``run_one_job`` only enables the context in ``_install_fire_secret_scope``, which runs - AFTER this handoff, so a routed desktop fire on the managed path serialized ``multiplex_active=False`` - and the worker inherited the launch profile's residue. The payload must carry ``True`` and the - worker env must not carry a launch-only value — and the context must not outlive the handoff.""" - import cron.scheduler as scheduler - import hermes_constants - from agent import secret_scope - from hermes_constants import reset_hermes_home_override, set_hermes_home_override - from tools.process_registry import GatewayChildDispatch - - launch = tmp_path / "launch" - routed = tmp_path / "routed" - launch.mkdir() - routed.mkdir() - (launch / ".env").write_text("LAUNCH_ONLY_SECRET=launch-secret\n", encoding="utf-8") - (routed / ".env").write_text("", encoding="utf-8") - monkeypatch.setenv("LAUNCH_ONLY_SECRET", "launch-secret") - monkeypatch.setattr(scheduler, "_get_hermes_home", lambda: routed) - monkeypatch.setattr(hermes_constants, "get_process_hermes_home", lambda: launch) - monkeypatch.setattr("cron.scheduler_provider.routed_profile_fire", lambda: True) - monkeypatch.setattr( - "tools.process_registry.restart_safe_gateway_child_argv", - lambda command, *, unit_suffix, require_restart_safe_scope=False: GatewayChildDispatch( - "scoped", ["scope", "--", *command]), - ) - spawned, payloads, _handoff, _get = _stub_external_worker_launch(scheduler, monkeypatch) - - assert not secret_scope.is_multiplex_active() # the desktop tick itself is NOT a multiplexer - home_token = set_hermes_home_override(str(routed)) - try: - assert scheduler._launch_external_cron_worker( - {"id": "job-r", "execution_id": "exec-1", "prompt": "work"}) is True - finally: - reset_hermes_home_override(home_token) - - assert payloads[0]["multiplex_active"] is True - assert "LAUNCH_ONLY_SECRET" not in spawned[0][1]["env"] - assert not secret_scope.is_multiplex_active() # enabled for the handoff span only - assert os.environ["LAUNCH_ONLY_SECRET"] == "launch-secret" # parent untouched - - def test_launch_external_worker_uses_restart_safe_scope_and_acknowledges( tmp_path, monkeypatch ): diff --git a/tests/hermes_cli/test_secret_source_bootstrap.py b/tests/hermes_cli/test_secret_source_bootstrap.py index efb8e8a127..481b275859 100644 --- a/tests/hermes_cli/test_secret_source_bootstrap.py +++ b/tests/hermes_cli/test_secret_source_bootstrap.py @@ -103,80 +103,6 @@ def test_refresh_secret_sources_repulls_when_plugin_enabled(monkeypatch): assert called == {"reset": 1, "load": 1} -def test_refresh_reconciles_once_when_the_last_plugin_source_is_removed(monkeypatch): - """Removal regression (#107695 review): ``discover_and_load(force=True)`` unloads the old - registration first, so a discovery that finds no enabled plugin source used to return before the - cache reset and the installed-scope refresh — the per-home snapshot and the current scope kept the - removed plugin's names. After a discovery that DID re-apply plugin sources, the next one that finds - none must reconcile exactly once; a home that never had a plugin source stays a no-op.""" - mgr = PluginManager() - called = {"reset": 0, "load": 0, "scope": 0} - - import agent.secret_sources.registry as reg - - sources = [_StubSource()] - monkeypatch.setattr(reg, "list_plugin_sources", lambda: list(sources)) - monkeypatch.setattr("hermes_cli.config.load_config", lambda: {"secrets": {"myvault": {"enabled": True}}}) - monkeypatch.setattr("hermes_cli.env_loader.reset_secret_source_cache", - lambda *a, **kw: called.__setitem__("reset", called["reset"] + 1)) - monkeypatch.setattr("hermes_cli.env_loader.load_hermes_dotenv", - lambda **kw: called.__setitem__("load", called["load"] + 1)) - monkeypatch.setattr("agent.secret_scope.refresh_installed_secret_scope", - lambda *a, **kw: called.__setitem__("scope", called["scope"] + 1) or True) - - mgr._refresh_secret_sources_after_discovery() # plugin source present and enabled - assert called == {"reset": 1, "load": 1, "scope": 1} - - sources.clear() # the plugin is gone (force-reload unloaded it) - mgr._refresh_secret_sources_after_discovery() - assert called == {"reset": 2, "load": 2, "scope": 2} # reconciled once so its names drop out - - mgr._refresh_secret_sources_after_discovery() - assert called == {"reset": 2, "load": 2, "scope": 2} # and not again: nothing left to reconcile - - -def test_refresh_retries_removal_cleanup_after_a_failed_attempt(monkeypatch): - """The reconcile marker must survive a failed cleanup (#107695 review on f5f88d5058): clearing it - before the fallible reset/reload/refresh left the removed plugin's credential active while every - later no-source discovery returned early. It clears only once cleanup succeeds.""" - mgr = PluginManager() - calls = {"load": 0} - fail = {"on": True} - - import agent.secret_sources.registry as reg - - sources = [_StubSource()] - monkeypatch.setattr(reg, "list_plugin_sources", lambda: list(sources)) - monkeypatch.setattr("hermes_cli.config.load_config", lambda: {"secrets": {"myvault": {"enabled": True}}}) - monkeypatch.setattr("hermes_cli.env_loader.reset_secret_source_cache", lambda *a, **kw: None) - monkeypatch.setattr("agent.secret_scope.refresh_installed_secret_scope", lambda *a, **kw: True) - - def _load(**kw): - calls["load"] += 1 - if fail["on"]: - raise RuntimeError("reload blew up") - - monkeypatch.setattr("hermes_cli.env_loader.load_hermes_dotenv", _load) - - fail["on"] = False - mgr._refresh_secret_sources_after_discovery() # enabled: marker set - assert calls["load"] == 1 - - sources.clear() - fail["on"] = True - mgr._refresh_secret_sources_after_discovery() # removal cleanup attempt fails - assert calls["load"] == 2 - assert mgr._plugin_secret_sources_reconciled is True # NOT cleared by a failed attempt - - fail["on"] = False - mgr._refresh_secret_sources_after_discovery() # retried, succeeds - assert calls["load"] == 3 - assert mgr._plugin_secret_sources_reconciled is False - - mgr._refresh_secret_sources_after_discovery() # nothing left to reconcile - assert calls["load"] == 3 - - def test_refresh_respects_custom_is_enabled(monkeypatch): """A source with custom activation (no ``enabled`` key) is re-pulled.""" mgr = PluginManager()