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.
This commit is contained in:
@@ -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]:
|
||||
|
||||
@@ -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_"):
|
||||
|
||||
Reference in New Issue
Block a user