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.
This commit is contained in:
kshitijk4poor
2026-09-14 23:41:47 +05:30
committed by kshitij
parent 5850a50a81
commit f367ebeb5d
2 changed files with 9 additions and 18 deletions

View File

@@ -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)

View File

@@ -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_"):