Files
hermes-agent/tests/gateway/test_multiplex_process_global_sinks.py
Teknium 545e74d0ea fix(gateway): a secondary profile's config.yaml no longer poisons the process env under multiplex
Under gateway.multiplex_profiles every secondary profile's config loads inside
_profile_runtime_scope, yet every apply_yaml_config_fn hook (feishu, matrix,
whatsapp, slack, dingtalk, discord non-gate keys, telegram non-gate keys) and
gateway/config_loader.py::bridge_core_env_settings still wrote os.environ there.
First-writer-wins: the first secondary with a require_mention / allowlist /
allow_bots / reactions block made that policy the DEFAULT profile's (live:
TELEGRAM_REQUIRE_MENTION written from a secondary load), and secondaries read
the default's env for the same keys.

- gateway/platforms/_shared.py::yaml_env_setter: the one env-write shape for
  YAML->env bridges — env wins, skipped under an active secondary scope.
- Every hook now seeds its values into the profile's PlatformConfig.extra and
  uses yaml_env_setter; bridge_core_env_settings seeds telegram/signal
  require_mention into extra and skips the env write under scope.
- Readers that bypassed extra/scope now consult extra first (matrix flags +
  session_scope, slack reactions, telegram reactions/mention_patterns/_extra_bool,
  discord reactions/auto_thread/history_backfill/approval_mentions/allow_mentions,
  feishu allow_bots, dingtalk mention_patterns, signal require_mention).

Salvages the shape of PR #100604 (whatsapp, earliest report #80099), #100435
(discord) and #100448 (telegram) by @nftpoetrist on top of current main.

Fixes #80099

Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
2026-09-11 15:29:15 -07:00

75 lines
3.0 KiB
Python

"""Per-profile isolation for the remaining process-global sinks under gateway.multiplex_profiles:
Yuanbao auto-sethome env write, the config ``terminal.env_passthrough`` allowlist, and the runner-level
Slack ignored-channel fail-safe (which only had the DEFAULT profile's GatewayConfig)."""
from __future__ import annotations
import os
from agent.secret_scope import reset_secret_scope, set_multiplex_active, set_secret_scope
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
def _under_secondary(home, fn):
set_multiplex_active(True)
home_token = set_hermes_home_override(str(home))
secret_token = set_secret_scope({})
try:
return fn()
finally:
reset_secret_scope(secret_token)
reset_hermes_home_override(home_token)
set_multiplex_active(False)
def test_yuanbao_auto_sethome_from_secondary_stays_in_its_config(tmp_path, monkeypatch):
from gateway.platforms.yuanbao import AutoSetHomeMiddleware
monkeypatch.delenv("YUANBAO_HOME_CHANNEL", raising=False)
secondary = tmp_path / "profiles" / "b2"
secondary.mkdir(parents=True)
(secondary / "config.yaml").write_text("{}\n")
class Adapter:
name = "yuanbao-b2"
class Ctx:
chat_id = "dm:tenant-b2"
chat_name = "b2"
_under_secondary(secondary, lambda: AutoSetHomeMiddleware._persist_home(Adapter(), Ctx()))
assert "YUANBAO_HOME_CHANNEL" not in os.environ
assert "dm:tenant-b2" in (secondary / "config.yaml").read_text()
def test_env_passthrough_allowlist_follows_the_active_profile(tmp_path, monkeypatch):
import tools.env_passthrough as ep
default_home = tmp_path / "hermes"
secondary = default_home / "profiles" / "b2"
secondary.mkdir(parents=True)
(default_home / "config.yaml").write_text("terminal:\n env_passthrough: [FOO_DEFAULT]\n")
(secondary / "config.yaml").write_text("terminal:\n env_passthrough: [FOO_B2]\n")
monkeypatch.setenv("HERMES_HOME", str(default_home))
ep._config_passthrough.clear()
assert ep._load_config_passthrough() == {"FOO_DEFAULT"}
assert _under_secondary(secondary, ep._load_config_passthrough) == {"FOO_B2"}
assert ep._load_config_passthrough() == {"FOO_DEFAULT"}
def test_slack_ignored_channels_use_the_routed_adapters_list(monkeypatch):
from gateway.config import GatewayConfig, Platform, PlatformConfig
from gateway.run import _is_slack_ignored_channel
monkeypatch.delenv("SLACK_IGNORED_CHANNELS", raising=False)
default_cfg = GatewayConfig(platforms={Platform.SLACK: PlatformConfig(enabled=True, extra={"ignored_channels": ["C_DEFAULT"]})})
class SecondaryAdapter:
config = PlatformConfig(enabled=True, extra={"ignored_channels": ["C_B2"]})
assert _is_slack_ignored_channel(default_cfg, "C_B2", SecondaryAdapter())
assert not _is_slack_ignored_channel(default_cfg, "C_DEFAULT", SecondaryAdapter())
# No routed adapter (legacy callers): the process-level config still rules.
assert _is_slack_ignored_channel(default_cfg, "C_DEFAULT")