From dcdbcb8a2b14a5dfd35ca141237d3c7d3715046b Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Tue, 15 Sep 2026 00:56:32 +0530 Subject: [PATCH] fix(env-loader): split source_supplied_names() out of secret_source_names() Widening secret_source_names() to include skipped_existing names silently changed tools/mcp_tool_config.py::_build_safe_env, an untouched consumer that forwards every returned name into MCP stdio child envs. That consumer wants only names a source actually APPLIED (pre-stack semantics), so secret_source_names() goes back to tuple(_SECRET_SOURCES). The routed-child scrub in strip_launch_profile_env is the one site that must also see names a source supplied but lost to a pre-existing process value, so it reads the new source_supplied_names() accessor instead. tools/mcp_tool_config.py is byte-identical to origin/main. --- hermes_cli/env_loader.py | 17 ++++++++++++----- tools/environments/local.py | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/hermes_cli/env_loader.py b/hermes_cli/env_loader.py index 4b05d363f1..60d7f7a740 100644 --- a/hermes_cli/env_loader.py +++ b/hermes_cli/env_loader.py @@ -100,11 +100,18 @@ def _record_supplied_names(report) -> set[str]: def secret_source_names() -> tuple[str, ...]: - """Every env-var name some profile's external secret source supplied (names only — the map is - process-wide, so a value must be resolved through the active profile's secret scope). Includes names - the source supplied but a pre-existing process value won (``skipped_existing``): the launch value - in ``os.environ`` is still not a routed profile's to inherit.""" - return tuple(dict.fromkeys((*_SECRET_SOURCES, *sorted(_SOURCE_SUPPLIED_NAMES)))) + """Every env-var name some profile's external secret source APPLIED (names only — the map is + process-wide, so a value must be resolved through the active profile's secret scope). Consumers that + forward source values into a child (MCP stdio env) want exactly these; see ``source_supplied_names`` + for the wider set the routed-child scrub needs.""" + return tuple(_SECRET_SOURCES) + + +def source_supplied_names() -> tuple[str, ...]: + """Every env-var name an external source supplied for any home — applied, or lost to a pre-existing + process value (``skipped_existing``). The launch value in ``os.environ`` is still not a routed + profile's to inherit, so the strip must see the skipped names too.""" + return tuple(sorted(set(_SECRET_SOURCES) | _SOURCE_SUPPLIED_NAMES)) def launch_dotenv_keys() -> frozenset[str]: diff --git a/tools/environments/local.py b/tools/environments/local.py index 5ce37a96aa..950571f357 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -354,7 +354,7 @@ 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, secret_source_names + from hermes_cli.env_loader import launch_dotenv_keys, managed_dotenv_keys, source_supplied_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). External secret sources @@ -364,7 +364,7 @@ def strip_launch_profile_env(env: dict, target_home: "str | Path | None" = None) # .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(source_supplied_names()) residue -= set(managed_dotenv_keys()) for key in residue: if not _is_global_env(key) or key.startswith("TERMINAL_"):