Skip to content

fix(mattermost): scope url/reply_mode/require_mention/free_response_channels/allowed_channels to the active profile under multiplexing - #100647

Closed
nftpoetrist wants to merge 1 commit into
NousResearch:mainfrom
nftpoetrist:fix/mattermost-multiplex-config-scope
Closed

nftpoetrist wants to merge 1 commit into
NousResearch:mainfrom
nftpoetrist:fix/mattermost-multiplex-config-scope

Conversation

@nftpoetrist

Copy link
Copy Markdown
Contributor

Summary

  • 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

Mirrors 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 (fix(dingtalk): scope require_mention/allowlists per profile #100615, item 6 of this series) left in its own analogous extra-first-with-raw-fallback read sites (_dingtalk_require_mention et al. still fall back to bare os.getenv on an extra miss — flagging this as a follow-up worth a look, not fixed here since it's a different file/PR).
  • 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.

Tests

Added TestMultiplexProfileScope (7 tests) to tests/gateway/test_mattermost.py, mirroring the fixture/assertion style established in tests/gateway/test_line_plugin.py's class of the same name, plus 2 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.

Competitor check

This file is a hotspot with several open PRs; searched MATTERMOST_URL, mattermost multiplex, mattermost scope, mattermost require_mention. None address the multiplex-scoping concern fixed here — all are different concerns with two real (but reconcilable) line-level overlaps and two adjacency-only overlaps:

No open or merged PR touches the actual scope-leak fixed here.

Checklist

  • Read current adapter code directly (not from a stale scan summary)
  • Minimal, precedent-matching fix (reuses the established WhatsApp/DingTalk apply_yaml_config_fn pattern)
  • New regression tests, mutation-verified against the pre-fix code
  • Full existing Mattermost test suite passes
  • Fresh competitor PR search immediately before opening this PR

…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 (NousResearch#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.
@alt-glitch alt-glitch added type/bug Something isn't working comp/plugins Plugin system and bundled plugins area/profiles Multi-profile isolation, HERMES_HOME scoping P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Sep 1, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

Mattermost multiplex-scope fix: URL/reply_mode/require_mention/free_response_channels/allowed_channels now read via scope, and _apply_yaml_config seeds PlatformConfig.extra instead of writing process-global env under a secondary profile's scope. Consistent with the sibling adapter fixes.

  • _apply_yaml_config now returns seeded in ALL modes (scoped and unscoped, adapter.py:1260). Verify the caller merges the returned dict into extra only for scoped profiles — if merged unconditionally, an explicitly-set env var (e.g. MATTERMOST_REQUIRE_MENTION) would lose precedence to the YAML value in single-profile deployments, since the read sites now check extra first.
  • require_mention can now arrive as a YAML boolean (adapter.py:894); str(False).lower() not in {"false","0","no"} evaluates correctly, but a YAML integer 0 would stringify as "0" and be treated as disabled — confirm the YAML loader preserves booleans.
  • free_channels_raw handles both list and comma-separated string forms, matching allowed_channels; good.
  • _profile_scoped_config_load swallows all exceptions and fails open to False (adapter.py:1286) — same caveat as the Teams equivalent: an import failure silently returns to unscoped behavior.
  • Tests cover extra-wins, fail-closed, unscoped precedence, the ws-event gating path, and both scoped/unscoped _apply_yaml_config behaviors.

@teknium1

teknium1 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Thanks @nftpoetrist — Merged via #101252 (2e25b47) on current main.

Your commits from this PR were cherry-picked onto the salvage branch with your git authorship preserved, so the credit is yours in git log.

Closing this PR since the work is now on main.

@teknium1 teknium1 closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants