From f367ebeb5dca58862001b10e7db0bbc28200ffe6 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:41:47 +0530 Subject: [PATCH] refactor(cron): strip external-source residue in strip_launch_profile_env itself _run_job_script popped the launch profile's secret-source names in an inline loop right above strip_launch_profile_env, so only the no_agent child got that protection; the four other callers of the same helper (the external cron worker, scheduler_delivery, kanban dispatch and the byterover plugin) still handed a served profile the launch vault or 1Password names. Fold the names into the helper's residue set, which is already gated on multiplex and on the target not being the launch profile, and keeps administrator-managed keys. --- cron/scheduler_script.py | 15 +-------------- tools/environments/local.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/cron/scheduler_script.py b/cron/scheduler_script.py index 2d8a7e6bfe..cb839a5958 100644 --- a/cron/scheduler_script.py +++ b/cron/scheduler_script.py @@ -356,22 +356,9 @@ 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, is_multiplex_active - from hermes_cli.env_loader import secret_source_names + from agent.secret_scope import current_secret_scope from tools.environments.local import restore_managed_env, 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; 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/tools/environments/local.py b/tools/environments/local.py index dfedeaf782..5ce37a96aa 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -354,13 +354,17 @@ def strip_launch_profile_env(env: dict, target_home: "str | Path | None" = None) if Path(target).resolve() == launch_home.resolve(): return env from hermes_cli.config import TERMINAL_CONFIG_ENV_MAP - from hermes_cli.env_loader import launch_dotenv_keys, managed_dotenv_keys + from hermes_cli.env_loader import launch_dotenv_keys, managed_dotenv_keys, secret_source_names # Current file AND every key any dotenv load put into os.environ this process lifetime: a key # removed or renamed in the launch .env after boot is still in os.environ with the old value, and - # a re-parse of the file alone no longer names it (#107695 review). The administrator-managed .env - # is NOT residue: its values are policy for every profile (``_apply_managed_env`` applies it last, - # with override, so it beats the user's own .env) — leave them in place. + # a re-parse of the file alone no longer names it (#107695 review). External secret sources + # (vault, 1Password, ...) write their names into the same shared os.environ, and a name the + # LAUNCH profile's source supplied is not the target profile's to see; the caller's scope + # overlay puts back exactly the ones the target's own sources supply. The administrator-managed + # .env is NOT residue: its values are policy for every profile (``_apply_managed_env`` applies + # it last, with override, so it beats the user's own .env) — leave them in place. residue = set(load_env_file(launch_home / ".env")) | set(launch_dotenv_keys()) | set(TERMINAL_CONFIG_ENV_MAP.values()) + residue |= set(secret_source_names()) residue -= set(managed_dotenv_keys()) for key in residue: if not _is_global_env(key) or key.startswith("TERMINAL_"):