fix(platforms): IRC/ntfy/Photon/WeCom/Mattermost/SimpleX/Matrix/Teams settings resolve per multiplexed profile (#89168, salvage nftpoetrist series #89169 #80825 #69042) - #101252
Merged
Conversation
Contributor
૮ >ﻌ< ა ci reviewran on 0d7aafe — chore: retrigger CI (zero-job dispatch failure, auto-heal)
|
teknium1
force-pushed
the
salvage/mux-adapter-scoping-a
branch
from
September 2, 2026 11:52
c4dfe83 to
a63ba8a
Compare
teknium1
force-pushed
the
salvage/mux-adapter-scoping-a
branch
4 times, most recently
from
September 2, 2026 13:10
8b50adf to
baaa839
Compare
…ive profile under multiplexing
IRCAdapter.__init__, check_requirements, validate_config, is_connected,
_env_enablement, and _standalone_send all read IRC_SERVER/IRC_PORT/
IRC_NICKNAME/IRC_CHANNEL/IRC_USE_TLS via raw os.getenv -- only
IRC_SERVER_PASSWORD/IRC_NICKSERV_PASSWORD already went through the
module's _get_scoped_secret helper. Under gateway.multiplex_profiles,
env_enablement_fn/check_fn/is_connected all run inside the registry-
enablement loop in load_gateway_config() (gateway/config.py, ~lines
2704-2820), scoped for secondary profiles via _profile_runtime_scope,
and adapter construction runs scoped the same way -- so os.environ there
still holds the DEFAULT profile's env-bridge output.
Notably, __init__'s original `os.getenv("IRC_SERVER") or extra.get(...)`
ordering let a raw env read override even an explicitly configured
config.yaml extra -- a secondary profile that set its own server/channel
via config.yaml extra would still silently connect to the default
profile's IRC server/channel/nick if the default profile bridged its own
config to env (which it always does under multiplex). This is a stronger
variant of the same bug fixed for the sibling LINE/DingTalk/Teams/SMS/
WeCom/ntfy adapters in this series -- there, extra already won because
of the `extra.get(...) or os.getenv(...)` order.
Switch every raw IRC_* read (except IRC_SERVER_PASSWORD/
IRC_NICKSERV_PASSWORD, already scoped) to _get_scoped_secret(), matching
the module's existing helper. Also collapses a double os.getenv("IRC_USE_TLS")
read in __init__ into a single _get_scoped_secret() call (same behavior,
one scope lookup instead of two).
Adds a new TestMultiplexProfileScope class to tests/gateway/test_irc_adapter.py
(6 tests) mirroring the fixture/assertion style established in
tests/gateway/test_line_plugin.py's TestMultiplexProfileScope. Mutation-
verified: stashed the production fix and confirmed 5 of 6 new tests fail
against pre-fix code -- including the "extra wins" test, since IRC's
original env-first ordering meant even an explicit extra config was not
a safe differentiator boundary before the fix (only the DEFAULT-profile-
unscoped-precedence test is a non-differentiating regression guard that
correctly passes either way). Restored the fix; all 23 tests in the file
pass, plus the file's 5 parametrized _get_scoped_secret tests in
test_adapter_startup_secret_scope.py.
…le under multiplexing NtfyAdapter.__init__, _env_enablement, check_requirements, validate_config, is_connected, and _standalone_send all read NTFY_SERVER_URL/NTFY_TOPIC/ NTFY_PUBLISH_TOPIC/NTFY_MARKDOWN/NTFY_HOME_CHANNEL(_NAME) via raw os.getenv -- only NTFY_TOKEN already went through the module's _get_scoped_secret helper. Under gateway.multiplex_profiles, env_enablement_fn/check_fn/ is_connected all run inside the registry-enablement loop in load_gateway_config() (confirmed in gateway/config.py, lines ~2704-2820, inside _profile_runtime_scope for secondary profiles), and adapter construction likewise runs scoped -- so os.environ there still holds the DEFAULT profile's env-bridge output. A secondary profile with its own (or no) ntfy topic configured could silently: - get auto-enabled via _env_enablement()/is_connected() using the default profile's topic, even though it never configured ntfy itself - have its adapter subscribe to / publish on the default profile's topic and server instead of (or in addition to) its own - deliver cron/send_message_tool messages via _standalone_send to the wrong topic Switch every raw NTFY_* read (except the two secret-material fields already scoped: NTFY_TOKEN) to _get_scoped_secret(), matching the established helper already defined in this module and used for NTFY_TOKEN, and the same pattern applied to the sibling LINE/DingTalk/ Teams/SMS/WeCom adapters in this series. Adds a new TestMultiplexProfileScope class to tests/gateway/test_ntfy_plugin.py (7 tests) mirroring the fixture/assertion style established in tests/gateway/test_line_plugin.py's TestMultiplexProfileScope. Mutation- verified: stashed the production fix and confirmed 5 of the 7 new tests fail against pre-fix code (the other 2 are non-differentiating regression guards -- extra-wins-over-env and unscoped-default-profile-precedence -- which correctly pass either way); restored the fix and confirmed all 37 tests in the file, plus the file's 5 parametrized _get_scoped_secret tests in test_adapter_startup_secret_scope.py, pass.
…car config to the active profile under multiplexing
PhotonAdapter.__init__, check_requirements, validate_config,
_env_enablement, _markdown_enabled, _reactions_enabled, and
_standalone_send in adapter.py, plus load_project_credentials and
load_dashboard_project_id in auth.py, all read PHOTON_PROJECT_ID/
PHOTON_NODE_BIN/PHOTON_SIDECAR_PORT/PHOTON_SIDECAR_AUTOSTART/
PHOTON_PROBE_*/PHOTON_REQUIRE_MENTION/PHOTON_MENTION_PATTERNS/
PHOTON_REACTIONS/PHOTON_MARKDOWN/PHOTON_HOME_CHANNEL(_NAME)/
PHOTON_DASHBOARD_PROJECT_ID via raw os.getenv -- only
PHOTON_PROJECT_SECRET and PHOTON_SIDECAR_TOKEN were already scoped via
_get_scoped_secret.
Notably __init__'s project_id read was a stronger variant of the bug
(like the IRC fix in this series, item 11): the original
`os.getenv("PHOTON_PROJECT_ID") or extra.get("project_id") or stored_id`
ordering let a raw env read override even an explicitly configured
config.yaml extra -- a secondary profile that set its own project_id via
extra would still silently authenticate against the default profile's
Spectrum project, because the default profile's project id is always
bridged to os.environ under multiplex and env was checked first.
_reactions_enabled() and the require_mention/mention_patterns reads in
__init__ are exercised on every live inbound message / tapback, not just
at construction, so a secondary profile's reaction/mention-gating
behavior would be driven by the default profile's settings for the
adapter's entire runtime lifetime.
Switch every raw PHOTON_* read (except the two already scoped) to
_get_scoped_secret(), matching the module's existing helper (already
defined identically in both adapter.py and auth.py). Left
_dashboard_host()/_spectrum_host() and the interactive device-login flow
functions in auth.py untouched -- these are CLI-only management-plane
calls (`hermes photon login`/`setup`), not part of the gateway's
per-profile adapter construction/connection lifecycle, so they are not
reachable under a multiplexed secondary profile's scope; noted as a
"Scope note" in the PR body rather than silently expanding scope to
unreachable call sites.
Adds a new tests/plugins/platforms/photon/test_multiplex_profile_scope.py
(9 tests, two classes covering auth.py and adapter.py separately)
mirroring the fixture/assertion style established in
tests/gateway/test_line_plugin.py's TestMultiplexProfileScope, reusing
test_auth.py's tmp_hermes_home isolation pattern so tests don't depend on
the real ~/.hermes/auth.json fallback. Mutation-verified: stashed the
production fix and confirmed 7 of 9 new tests fail against pre-fix code
(the other 2 are non-differentiating regression guards -- unscoped-
default-profile-precedence, one per class -- which correctly pass either
way). Restored the fix; all 140 tests in tests/plugins/platforms/photon/,
the 10 photon-related parametrized tests in
test_adapter_startup_secret_scope.py, and the broader
test_multiplex_adapter_registry.py / test_adapter_connect_classification.py
suites (45 tests) pass.
…hannels/allowed_channels to the active profile under multiplexing MattermostAdapter.__init__, validate_mattermost_config, _standalone_send, and _handle_ws_event's mention-gating block all read MATTERMOST_URL/ MATTERMOST_REPLY_MODE/MATTERMOST_REQUIRE_MENTION/ MATTERMOST_FREE_RESPONSE_CHANNELS/MATTERMOST_ALLOWED_CHANNELS via raw os.getenv -- only MATTERMOST_TOKEN was already scoped via _get_scoped_secret. _apply_yaml_config additionally wrote MATTERMOST_REQUIRE_MENTION/MATTERMOST_FREE_RESPONSE_CHANNELS/ MATTERMOST_ALLOWED_CHANNELS into the process-global os.environ unconditionally (guarded only by `not os.getenv(...)`, first-writer-wins), the same apply_yaml_config_fn bug class already fixed for the Discord/Telegram/WhatsApp/DingTalk adapters in this series. Under gateway.multiplex_profiles, os.environ holds the DEFAULT profile's env-bridge output. A secondary profile with its own (or no) Mattermost config could silently connect to the default profile's server, thread its replies per the default profile's reply_mode, or -- since _handle_ws_event's mention-gating block runs on every LIVE inbound message, not just at construction -- have its require_mention/ free_response_channels/allowed_channels decisions driven by the default profile's settings for the adapter's entire runtime lifetime. Fix, mirroring the WhatsApp/DingTalk apply_yaml_config_fn pattern: - Add _profile_scoped_config_load() (same helper as DingTalk). - Rewrite _apply_yaml_config to skip the env-bridge write under a multiplexed secondary profile's scope, and instead return the YAML values as a dict merged into this profile's own PlatformConfig.extra. - Make require_mention/free_response_channels read extra first (matching the existing allowed_channels precedent), falling back to _get_scoped_secret() instead of raw os.getenv when extra is absent -- fixing a residual gap the DingTalk fix (#100615, this series' item 6) left in its own analogous extra-first-with-raw-fallback read sites (_dingtalk_require_mention et al. still fall back to bare os.getenv). - Switch __init__'s url/reply_mode, validate_mattermost_config's url, and _standalone_send's url to _get_scoped_secret(). - Leave check_mattermost_requirements() (no longer reads any MATTERMOST_* var on current main -- just an aiohttp-importability probe) and _is_connected() (already scope-aware via hermes_cli.gateway.get_env_value, which itself routes through agent.secret_scope.get_secret) untouched. Adds a new TestMultiplexProfileScope class to tests/gateway/test_mattermost.py (7 tests) mirroring the fixture/assertion style established in tests/gateway/test_line_plugin.py's TestMultiplexProfileScope, plus two tests exercising _apply_yaml_config's new seeded-dict return directly. Mutation-verified: stashed the production fix and confirmed 5 of 7 new tests fail against pre-fix code (the other 2 are non-differentiating regression guards -- extra-wins-over-env and unscoped-default-profile- precedence -- which correctly pass either way). Restored the fix; all 30 tests in the file, the plugin-setup test, and the full 75-test tests/gateway/test_adapter_startup_secret_scope.py suite pass.
…iplexing WeComAdapter.__init__ read WECOM_BOT_ID via a raw os.getenv() call, while the immediately adjacent line for WECOM_SECRET already used the module's _get_scoped_secret() helper. Under gateway.multiplex_profiles, a secondary profile's adapter is constructed inside a scoped context where os.environ still holds the DEFAULT profile's env-bridge output -- so a secondary profile's bot would silently connect using the default profile's bot_id while (correctly) using its own secret, or vice versa on a scope miss. Switch the bot_id read to _get_scoped_secret(), matching the sibling _secret/_dm_policy/_group_policy/allow_from reads in the same __init__ that were already migrated in #76664/#93545. _standalone_send's out-of-process fallback branch constructs a fresh WeComAdapter(pconfig) and therefore inherits this fix automatically -- no separate change needed there. Adds two regression tests to the existing TestWeComAdapterAuthzScope class (already covering dm_policy/allow_from scoping per #93522), mirroring its established fixture/assertion style. Mutation-verified: both fail against the pre-fix code (asserting the default profile's bot_id leaks into a secondary profile's scope) and pass with the fix.
…plexing SimplexAdapter.__init__ (auto_accept, group_allowed), the registry gates check_requirements/validate_config/is_connected, _env_enablement and _standalone_send all read SIMPLEX_* via raw os.getenv. Under gateway.multiplex_profiles those paths run inside a secondary profile's scope where os.environ holds the DEFAULT profile's YAML-to-env bridge output -- so a secondary profile that never configured SimpleX was auto-enabled on the default's daemon URL and inherited its group allowlist / auto-accept setting. Route every read through the module-local `_get_scoped_secret` wrapper (get_secret; UnscopedSecretError -> os.getenv for the default profile, which constructs unscoped) -- the same helper the IRC/ntfy/Photon/ Mattermost siblings use. Unlike the extra-only `_scoped_platform_setting` shape proposed in #100241, this honors BOTH the secondary profile's own .env (the scope) and its config.yaml extra, and needs no config.yaml re-read in check_requirements. Rewrite of #100241. Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
Same class as #100627's WECOM_BOT_ID: the one remaining raw os.getenv in WeComAdapter.__init__ let a secondary multiplex profile pick up the default profile's bridged websocket URL. Route it through _get_scoped_secret; folded into the existing scoped-miss test.
…import The multiplex gateway imports plugins/platforms/matrix/adapter.py once, so the module-level _STORE_DIR/_CRYPTO_DB_PATH resolved against the root HERMES_HOME for every profile: all bots' Olm identities landed in one crypto.db and inbound E2EE failed with "no session found" (#89168). connect() runs inside _profile_runtime_scope, so resolve the store dir there via get_hermes_dir (honors the context-local HERMES_HOME) and cache it on the instance -- diagnostics and error-log paths read outside the scope then still report the store actually in use. Mirrors the pairing-store fix (a6397c3). Salvage of #89169 (per-call resolvers collapsed into one cached resolve; dead `_CRYPTO_DB_PATH = None` alias dropped -- no external importers). Also routes the last raw MATRIX_HOMESERVER read in check_matrix_requirements through _startup_env_secret like its token/password neighbours (#69943). Fixes #89168 Co-authored-by: Michael Short <18595461+mjshorty@users.noreply.github.com>
…ofiles _start_one_profile_adapters skipped only Platform.RELAY as shared process-level ingress. WhatsApp is the same shape: the bridge is one authenticated session tied to a single phone number, so a secondary profile has no credential of its own to bring; constructing an adapter for it only produced a connect/retry loop that stalled startup for every profile queued behind it. Treat WhatsApp like Relay -- the active profile owns the connection and route-stamped source.profile fans inbound turns out to secondary profiles. Salvage of #69042 (narrowed by its author to this one behavioral line); test re-expressed on the current secondary-startup fixtures. Co-authored-by: sshawn <28279366+lsshawn@users.noreply.github.com>
teknium1
force-pushed
the
salvage/mux-adapter-scoping-a
branch
from
September 2, 2026 13:54
87753ad to
0d7aafe
Compare
This was referenced Sep 2, 2026
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Under
gateway.multiplex_profiles, secondary profiles were silently reusing the default profile's settings: plugin adapters (IRC, ntfy, Photon, WeCom, Mattermost, SimpleX) read non-secret config via rawos.getenv— which holds the default's YAML→env bridge output — so secondaries connected to the wrong server/topic/project or got auto-enabled off the default's channel; the Matrix E2EE store path was resolved at import so every profile shared one crypto.db; Teams wasn't in the port-binding guard so a secondary tried to bind :3978 twice; and a credential-less WhatsApp secondary spun in a connect/retry loop stalling startup.Changes
*_-setting read goes through the module-local_get_scoped_secret(scope authoritative; env fallback only for the unscoped default profile). Mattermost_apply_yaml_configskips os.environ writes under a profile scope and always seeds extra. SimpleX rewritten from fix(simplex): scope multiplex secondary-profile config, not shared env #100241's extra-only shape so the secondary's own.envis honored. Widened toWECOM_WEBSOCKET_URLand Matrixcheck_matrix_requirementshomeserver.connect()(inside_profile_runtime_scope) and cached on the instance; module constants and dead None alias removed.teamsadded to port-binding platform sets; docs list updated._start_one_profile_adaptersskips WhatsApp on secondaries like Relay.require_mention/free_response_channels/allowed_channelsnow take precedence over an explicit env var (Discord precedent, [Bug]: Discord adapter allow/deny gates are process-global, breaking per-profile isolation under multiplex_profiles #72348).Validation
_env_enablement(own .env ws://bob)$HOME/platforms/matrix/store/crypto.db$HOME/profiles/<p>/platforms/matrix/store/crypto.dbeachCredits
@nftpoetrist (#100640, #100634, #100654, #100627, #100647 cherry-picked with authorship; #100241 rewritten, co-authored), @mjshorty (#89169 / #89168), @JoelMTaylor (#80825, cherry-picked), @lsshawn (#69042, co-authored).
Fixes #89168
Infographic