From 3dedff6a122ea6a26be12b02e20f7ec4250da06e Mon Sep 17 00:00:00 2001 From: John Paul Soliva Date: Sun, 13 Sep 2026 10:01:36 +0900 Subject: [PATCH] fix(cron): only strip launch external-source names when multiplexing The source-name strip added alongside the external-source fix ran unconditionally. Outside multiplexing there is no other profile to leak from -- os.environ IS this profile's environment -- so popping those names relied on the routed scope overlay putting each one back, which in turn relies on the per-home snapshot recorded at boot. Correct today, but it made a single-profile child's credentials depend on bookkeeping that has nothing to do with isolation. Guard it the way strip_launch_profile_env guards itself: no multiplexing, no strip. A single-profile no_agent child now keeps a byte-identical env even if a source's snapshot were ever missing. Pinned by a regression that runs a real child with a source-owned name in os.environ and no multiplex context; making the strip unconditional fails it. (cherry picked from commit afa429b30a1c9c9b4c011f7d2426a9f099911596) --- cron/scheduler_script.py | 16 ++++++++++------ tests/cron/test_cron_no_agent.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/cron/scheduler_script.py b/cron/scheduler_script.py index 36b63f8836..93e83a012e 100644 --- a/cron/scheduler_script.py +++ b/cron/scheduler_script.py @@ -356,18 +356,22 @@ def _run_job_script( # then overlay the installed scope, then sanitize, so routed values pass the same scrub / # passthrough rules as any other. No-op outside multiplex or for the launch profile's own # fires; the parent process is never mutated. - from agent.secret_scope import _is_global_env, current_secret_scope + from agent.secret_scope import _is_global_env, current_secret_scope, is_multiplex_active from hermes_cli.env_loader import secret_source_names from tools.environments.local import strip_launch_profile_env base = strip_launch_profile_env(dict(os.environ)) # strip_launch_profile_env only knows dotenv- and terminal-config-owned names. External # secret sources (vault, 1Password, ...) also write their names into the shared os.environ, # tracked in secret_source_names(), and a name the LAUNCH profile's source supplied is not - # this profile's to see. Drop them all here; the overlay below puts back exactly the ones - # the routed profile's own sources supply (build_profile_secret_scope folds them in). - for name in secret_source_names(): - if not _is_global_env(name): - base.pop(name, None) + # this profile's to see. Drop them; the overlay below puts back exactly the ones the routed + # profile's own sources supply (build_profile_secret_scope folds get_secret_source_values in). + # Guarded like strip_launch_profile_env itself: with no multiplexing there is no other + # profile to leak from -- os.environ IS this profile's environment -- so a single-profile + # child keeps byte-identical env even if a source's per-home snapshot is ever missing. + if is_multiplex_active(): + for name in secret_source_names(): + if not _is_global_env(name): + base.pop(name, None) scope = current_secret_scope() if scope: base.update(scope) diff --git a/tests/cron/test_cron_no_agent.py b/tests/cron/test_cron_no_agent.py index a258973447..39127cdf06 100644 --- a/tests/cron/test_cron_no_agent.py +++ b/tests/cron/test_cron_no_agent.py @@ -467,3 +467,22 @@ def test_a_routed_profile_script_never_receives_a_launch_external_source_value(h assert ok, output assert output.strip() == "|routed-vault-value" assert os.environ["LAUNCH_VAULT_ONLY"] == "launch-vault-value" # parent untouched + + +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"