refactor(gateway): migrate Discord adapter to bundled plugin (salvage of #24356) - #30591
Conversation
…ams parity) First migration of an existing built-in platform adapter to the plugin system established by IRC / Teams / LINE / Google Chat. Closes #24325; advances the umbrella refactor in #3823. Matches Teams' shape exactly — adapter under ``plugins/platforms/discord/`` with the standard ``__init__.py`` / ``adapter.py`` / ``plugin.yaml`` shell, ``register(ctx)`` entry point, **no back-compat shim** at the old import path, and full parity for the four hooks Teams uses plus the ``apply_yaml_config_fn`` hook that landed in #25443 (the Discord plugin is the first consumer of that hook): * ``standalone_sender_fn`` — out-of-process cron delivery via REST API * ``setup_fn`` — interactive ``hermes setup gateway`` wizard * ``apply_yaml_config_fn`` — translate ``config.yaml`` ``discord:`` keys into ``DISCORD_*`` env vars (replaces the hardcoded block in ``gateway/config.py``) * ``is_connected`` — declares connection state from ``DISCORD_BOT_TOKEN`` * ``check_fn`` — lazy-installs ``discord.py`` on demand * plus ``allowed_users_env``, ``allow_all_env``, ``cron_deliver_env_var``, ``max_message_length``, ``emoji``, ``required_env``, ``install_hint`` * ``gateway/platforms/discord.py`` (5,101 LOC) → ``plugins/platforms/discord/adapter.py`` (git rename, R090). * New ``plugins/platforms/discord/{__init__.py, plugin.yaml}`` with ``requires_env`` / ``optional_env`` declarations. * Append ``register(ctx)`` block + new hook implementations (``_standalone_send``, ``interactive_setup``, ``_apply_yaml_config``, ``_clean_discord_user_ids``, ``_is_connected``, ``_build_adapter``, plus helpers ``_DISCORD_CHANNEL_TYPE_PROBE_CACHE`` etc.) to the adapter. * Replace the ``Platform.DISCORD elif`` branch in ``GatewayRunner._create_adapter()`` (−9 LOC) with a generic post-creation hook (+6 LOC) in the registry path: any plugin adapter that declares a ``gateway_runner`` attribute now gets it auto-injected. Webhook's built-in branch is unchanged (it doesn't go through the registry path). * Move ``_send_discord`` (190 LOC) and helpers (``_DISCORD_CHANNEL_TYPE_PROBE_CACHE``, ``_remember_channel_is_forum``, ``_probe_is_forum_cached``, ``_derive_forum_thread_name``) from ``tools/send_message_tool.py`` into the plugin as ``_standalone_send``. * Wire via ``standalone_sender_fn=_standalone_send`` (Teams pattern; same gap fixed in #21804 for other plugin platforms). * Replace the Discord ``elif`` in ``tools/send_message_tool.py`` ``_send_to_platform`` with a 10-line registry-hook dispatch. * Drop the ``DiscordAdapter`` import and the ``Platform.DISCORD: DiscordAdapter.MAX_MESSAGE_LENGTH`` ``_MAX_LENGTHS`` entry — the registry's ``max_message_length=2000`` covers it. * Move ``_setup_discord`` and ``_clean_discord_user_ids`` (68 LOC) from ``hermes_cli/setup.py`` into the plugin as ``interactive_setup``. * Wire via ``setup_fn=interactive_setup``. CLI helpers (``prompt``, ``print_info``, etc.) are lazy-imported so the plugin's module-load surface stays minimal. * Remove ``"discord": _s._setup_discord`` from ``hermes_cli/gateway.py::_builtin_setup_fn``. * Remove the entire 32-line ``_PLATFORMS["discord"]`` static dict entry — Discord's setup metadata is now discovered dynamically via ``_all_platforms()`` from the registry entry. * Move the 59-line ``discord_cfg`` YAML→env bridge from ``gateway/config.py::load_gateway_config()`` into the plugin as ``_apply_yaml_config``. Covers ``require_mention``, ``thread_require_mention``, ``free_response_channels``, ``auto_thread``, ``reactions``, ``ignored_channels``, ``allowed_channels``, ``no_thread_channels``, ``allow_mentions.{everyone,roles,users, replied_user}``, and ``reply_to_mode`` (including the YAML 1.1 ``off``-as-False coercion and the ``extra.reply_to_mode`` fallback). * Wire via ``apply_yaml_config_fn=_apply_yaml_config``. * The hook runs BEFORE ``_apply_env_overrides`` and after the generic shared-key loop, exactly as documented in ``website/docs/developer-guide/adding-platform-adapters.md``. * Behavior is preserved exactly — every assignment still uses ``not os.getenv(...)`` guards so env vars take precedence over YAML. All 78 references to the old import path are rewritten — no back-compat shim: * 51 ``from gateway.platforms.discord import X`` → ``from plugins.platforms.discord.adapter import X`` * 5 ``import gateway.platforms.discord as discord_platform`` → ``import plugins.platforms.discord.adapter as discord_platform`` * 1 ``from gateway.platforms import discord as discord_mod`` → ``from plugins.platforms.discord import adapter as discord_mod`` * 21 ``mock.patch("gateway.platforms.discord.X")`` strings → ``mock.patch("plugins.platforms.discord.adapter.X")`` * 1 docstring reference in ``hermes_cli/commands.py`` * 1 import in ``tools/send_message_tool.py`` (now removed entirely) The import-safety test in ``tests/gateway/test_discord_imports.py`` is updated to purge the new canonical module name from ``sys.modules``. **38 files changed, +621 / −473** — net positive due to the YAML hook implementation (89 new LOC in the plugin trading for 59 deleted in core), but every line moved has a clear plugin home now. The git rename is detected at R090 because the adapter gained ~340 LOC of moved-in hook implementations (``_standalone_send`` + ``interactive_setup`` + ``_apply_yaml_config`` + helpers). * All 568 Discord-specific tests pass across 25 ``test_discord_*.py`` files plus voice/send/text-batching/reload-skills/stream-consumer/ integration tests. * All 147 tests in the YAML-touching subset (``test_discord_reply_mode``, ``test_discord_free_response``, ``test_discord_allowed_channels``, ``test_discord_allowed_mentions``, ``test_discord_channel_controls``, ``test_discord_reactions``, ``test_discord_thread_persistence``, ``test_runtime_footer``) pass — this is the strongest signal that the YAML→env hook behaves identically to the legacy block. * Broader gateway/cron/integration sweep (1297 tests) introduces zero new failures vs ``main``. Pre-existing failures in ``tests/gateway/test_tts_media_routing.py`` and ``tests/e2e/test_platform_commands.py`` reproduce identically on the unchanged ``main`` revision. * Plugin discovery sanity check confirms Discord registers alongside the other four platform plugins: Registered platforms: ['discord', 'google_chat', 'irc', 'line', 'teams'] These Discord-shaped tendrils in core were **deliberately not moved** — they are generic platform-registry concerns affecting every platform, not Discord-specific: * ``gateway/config.py:1205`` ``DISCORD_BOT_TOKEN → config.token`` env enablement — same shape Telegram has. The existing ``env_enablement_fn`` registry hook only seeds ``extra``, not ``.token``, so it can't replace this without an adapter refactor to read from ``extra["bot_token"]``. * ``gateway/run.py`` voice-mode hooks (``self.adapters.get(Platform.DISCORD)`` for ``start_voice_mode``/``stop_voice_mode``), role-based auth, ``DISCORD_ALLOW_BOTS`` branch in ``_is_user_authorized``, ``_UPDATE_ALLOWED_PLATFORMS`` frozenset, and the per-platform allowlist maps — generic platform-registry concerns. * ``Platform.DISCORD`` enum literal — stable identifier used as dict keys throughout the codebase; removing it is a separate refactor with no real benefit. * ``tools/discord_tool.py`` and ``tools/environments/local.py`` — first-class agent tools and env-passthrough config, neither is the gateway adapter. Each of these is worth its own scoping issue when the time comes.
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
31 |
unresolved-import |
14 |
unresolved-reference |
5 |
unresolved-global |
5 |
invalid-method-override |
3 |
invalid-argument-type |
3 |
unused-type-ignore-comment |
1 |
invalid-assignment |
1 |
invalid-parameter-default |
1 |
unsupported-operator |
1 |
First entries
plugins/platforms/discord/adapter.py:5628: [unused-type-ignore-comment] unused-type-ignore-comment: Unused blanket `type: ignore` directive
plugins/platforms/discord/adapter.py:264: [unresolved-import] unresolved-import: Cannot resolve imported module `discord.utils`
plugins/platforms/discord/adapter.py:5563: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `ButtonStyle`
plugins/platforms/discord/adapter.py:4171: [unresolved-reference] unresolved-reference: Name `ClarifyChoiceView` used when not defined
plugins/platforms/discord/adapter.py:2506: [invalid-method-override] invalid-method-override: Invalid override of method `send_image_file`: Definition is incompatible with `BasePlatformAdapter.send_image_file`
plugins/platforms/discord/adapter.py:2671: [invalid-method-override] invalid-method-override: Invalid override of method `send_video`: Definition is incompatible with `BasePlatformAdapter.send_video`
plugins/platforms/discord/adapter.py:2790: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `TextChannel`
plugins/platforms/discord/adapter.py:340: [unresolved-import] unresolved-import: Cannot resolve imported module `nacl.secret`
plugins/platforms/discord/adapter.py:5004: [unresolved-global] unresolved-global: Invalid global declaration of `ClarifyChoiceView`: `ClarifyChoiceView` has no declarations or bindings in the global scope
plugins/platforms/discord/adapter.py:4269: [unresolved-reference] unresolved-reference: Name `ModelPickerView` used when not defined
plugins/platforms/discord/adapter.py:5004: [unresolved-global] unresolved-global: Invalid global declaration of `ExecApprovalView`: `ExecApprovalView` has no declarations or bindings in the global scope
plugins/platforms/discord/adapter.py:383: [unresolved-import] unresolved-import: Cannot resolve imported module `davey`
plugins/platforms/discord/adapter.py:4545: [unresolved-attribute] unresolved-attribute: Attribute `user` is not defined on `None` in union `Unknown | None`
plugins/platforms/discord/adapter.py:2729: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `http`
plugins/platforms/discord/adapter.py:105: [unresolved-import] unresolved-import: Module `discord` has no member `Intents`
plugins/platforms/discord/adapter.py:696: [unresolved-attribute] unresolved-attribute: Attribute `Bot` is not defined on `None` in union `Unknown | None`
tests/gateway/test_discord_component_auth.py:24: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `SlashConfirmView`
tests/gateway/test_discord_component_auth.py:22: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `ExecApprovalView`
tests/gateway/test_discord_component_auth.py:23: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `ModelPickerView`
plugins/platforms/discord/adapter.py:4215: [unresolved-reference] unresolved-reference: Name `UpdatePromptView` used when not defined
plugins/platforms/discord/adapter.py:3806: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `Forbidden`
plugins/platforms/discord/adapter.py:2688: [invalid-method-override] invalid-method-override: Invalid override of method `send_document`: Definition is incompatible with `BasePlatformAdapter.send_document`
tests/gateway/test_discord_clarify_buttons.py:30: [unresolved-import] unresolved-import: Module `plugins.platforms.discord.adapter` has no member `ClarifyChoiceView`
plugins/platforms/discord/adapter.py:1848: [unresolved-import] unresolved-import: Cannot resolve imported module `mutagen.oggopus`
plugins/platforms/discord/adapter.py:1980: [unresolved-attribute] unresolved-attribute: Module `discord` has no member `FFmpegPCMAudio`
... and 40 more
✅ Fixed issues (61):
| Rule | Count |
|---|---|
unresolved-attribute |
29 |
unresolved-import |
13 |
unresolved-reference |
5 |
unresolved-global |
5 |
invalid-argument-type |
3 |
invalid-method-override |
3 |
invalid-parameter-default |
1 |
unsupported-operator |
1 |
unused-type-ignore-comment |
1 |
First entries
gateway/platforms/discord.py:3212: [unresolved-attribute] unresolved-attribute: Attribute `Permissions` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:4437: [unresolved-import] unresolved-import: Cannot resolve imported module `aiohttp`
gateway/platforms/discord.py:4099: [unresolved-reference] unresolved-reference: Name `SlashConfirmView` used when not defined
gateway/platforms/discord.py:1677: [unresolved-import] unresolved-import: Cannot resolve imported module `discord`
gateway/platforms/discord.py:145: [unresolved-attribute] unresolved-attribute: Attribute `AllowedMentions` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:4555: [unresolved-attribute] unresolved-attribute: Attribute `MessageType` is not defined on `None` in union `Unknown | None`
tests/gateway/test_discord_model_picker.py:14: [unresolved-import] unresolved-import: Module `gateway.platforms.discord` has no member `ModelPickerView`
gateway/platforms/discord.py:167: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `set[Unknown]`
gateway/platforms/discord.py:5002: [unresolved-global] unresolved-global: Invalid global declaration of `UpdatePromptView`: `UpdatePromptView` has no declarations or bindings in the global scope
gateway/platforms/discord.py:1979: [unresolved-attribute] unresolved-attribute: Attribute `PCMVolumeTransformer` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:2638: [unresolved-attribute] unresolved-attribute: Attribute `File` is not defined on `None` in union `Unknown | None`
tests/gateway/test_discord_component_auth.py:25: [unresolved-import] unresolved-import: Module `gateway.platforms.discord` has no member `UpdatePromptView`
gateway/platforms/discord.py:4467: [unresolved-attribute] unresolved-attribute: Attribute `Thread` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:660: [unresolved-attribute] unresolved-attribute: Attribute `default` is not defined on `<special-form 'typing.Any'>` in union `Unknown | <special-form 'typing.Any'>`
gateway/platforms/discord.py:5002: [unresolved-global] unresolved-global: Invalid global declaration of `SlashConfirmView`: `SlashConfirmView` has no declarations or bindings in the global scope
tests/gateway/test_discord_free_response.py:398: [unresolved-attribute] unresolved-attribute: Attribute `MessageType` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:5506: [unresolved-attribute] unresolved-attribute: Attribute `Embed` is not defined on `None` in union `Unknown | None`
tests/gateway/test_discord_clarify_buttons.py:30: [unresolved-import] unresolved-import: Module `gateway.platforms.discord` has no member `ClarifyChoiceView`
gateway/platforms/discord.py:4205: [invalid-argument-type] invalid-argument-type: Argument to constructor `int.__new__` is incorrect: Expected `str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc`, found `Any | None | str`
gateway/platforms/discord.py:4538: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `None | str` and `ThreadParticipationTracker`
gateway/platforms/discord.py:976: [invalid-argument-type] invalid-argument-type: Argument expression after ** must be a mapping type: Found `Unknown | None | dict[Unknown, Unknown]`
gateway/platforms/discord.py:4869: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_last_chunk_len` on type `MessageEvent`
gateway/platforms/discord.py:401: [unresolved-attribute] unresolved-attribute: Object of type `object` has no attribute `decode`
gateway/platforms/discord.py:5002: [unresolved-global] unresolved-global: Invalid global declaration of `ModelPickerView`: `ModelPickerView` has no declarations or bindings in the global scope
gateway/platforms/discord.py:106: [unresolved-import] unresolved-import: Cannot resolve imported module `discord.ext`
... and 36 more
Unchanged: 4705 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
…ed=False, not silently fall back to check_fn Two bugs surfaced by PR #24356 migrating Discord into the registry: 1. plugins/platforms/discord/adapter.py::_is_connected — read DISCORD_BOT_TOKEN via hermes_cli.gateway.get_env_value (the abstraction tests patch) instead of os.getenv directly. The legacy non-registry path used get_env_value; bypassing it broke test_setup_openclaw_migration which patches gateway_mod.get_env_value to simulate a hermetic env. 2. hermes_cli/gateway.py::_platform_status — when entry.is_connected is defined and returns False, return 'not configured' immediately. Don't fall back to entry.check_fn(), which would let 'SDK is installed' override 'no token configured' and incorrectly report the platform as ready. The fallback to check_fn is the right behaviour only when is_connected is None (not registered). Fixes 5 test failures observed on CI for PR #24356: - tests/hermes_cli/test_setup.py::test_setup_gateway_skips_service_install_when_systemctl_missing - tests/hermes_cli/test_setup.py::test_setup_gateway_in_container_shows_docker_guidance - tests/hermes_cli/test_setup_irc.py::TestIRCGatewaySetupFreshInstall::test_setup_gateway_irc_counts_as_messaging_platform - tests/hermes_cli/test_setup_openclaw_migration.py::TestGetSectionConfigSummary::test_gateway_returns_none_without_tokens - tests/hermes_cli/test_setup_openclaw_migration.py::TestSetupWizardSkipsConfiguredSections::test_sections_skipped_when_migration_imported_settings Same _platform_status bug exists for sibling plugin platforms (teams, google_chat) whose check_fn returns true on SDK install alone; their tests just never exercised the registry path before. The bug only became test-visible when Discord migrated into the registry. Validation: 11,167 tests across tests/gateway/ + tests/cron/ + tests/tools/test_send_message_tool.py + tests/hermes_cli/ pass with zero failures.
🚨 CRITICAL Supply Chain Risk DetectedThis PR contains a pattern that has been used in real supply chain attacks. A maintainer must review the flagged code carefully before merging. 🚨 CRITICAL: Install-hook file added or modifiedThese files can execute code during package installation or interpreter startup. Files: Scanner only fires on high-signal indicators: .pth files, base64+exec/eval combos, subprocess with encoded commands, or install-hook files. Low-signal warnings were removed intentionally — if you're seeing this comment, the finding is worth inspecting. |
The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR #30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (#30591) hit the same false positive yesterday.
Second migration of an existing built-in platform adapter after Discord (PR #30591) — follows the same shape established by IRC / Teams / LINE / Google Chat / SimpleX and the playbook in `references/platform-plugin-migration.md`. Advances the umbrella refactor in #3823. Matches Discord's parity bar — adapter under `plugins/platforms/mattermost/` with the standard `__init__.py` / `adapter.py` / `plugin.yaml` shell, `register(ctx)` entry point, **no back-compat shim** at the old import path, and full parity for all five hooks Discord uses plus the `apply_yaml_config_fn` hook (mattermost is the second consumer of #25443 after Discord): * `standalone_sender_fn` — out-of-process cron delivery via Mattermost REST API. Picks up the thread_id + media_files capabilities the legacy `_send_mattermost` lacked (parity with Discord's `_standalone_send`). * `setup_fn` — interactive `hermes setup gateway` wizard. * `apply_yaml_config_fn` — translates `config.yaml` `mattermost:` keys (`require_mention`, `free_response_channels`, `allowed_channels`) into `MATTERMOST_*` env vars (replaces the hardcoded block in `gateway/config.py`). * `is_connected` — declares connection state from `MATTERMOST_TOKEN` + `MATTERMOST_URL`. * `check_fn` — verifies aiohttp is installed and both required env vars are set. * plus `allowed_users_env`, `allow_all_env`, `cron_deliver_env_var`, `max_message_length` (4000 — Mattermost practical limit), `emoji`, `required_env`, `install_hint`. Files ----- * `gateway/platforms/mattermost.py` (873 LOC) → `plugins/platforms/mattermost/adapter.py` (git rename, R071) + appended `register()` block, hook helpers, and `_standalone_send` with media upload + thread_id support. * New `plugins/platforms/mattermost/{__init__.py, plugin.yaml}` with `requires_env` / `optional_env` declarations covering MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS, MATTERMOST_ALLOW_ALL_USERS, MATTERMOST_HOME_CHANNEL, MATTERMOST_REPLY_MODE, MATTERMOST_REQUIRE_MENTION, MATTERMOST_FREE_RESPONSE_CHANNELS, MATTERMOST_ALLOWED_CHANNELS. * `gateway/config.py`: delete 17-LOC `mattermost_cfg` YAML→env bridge (moved into plugin's `_apply_yaml_config`). * `gateway/run.py::_create_adapter`: delete `Platform.MATTERMOST elif` — replaced by the existing generic plugin-registry-first dispatch. * `tools/send_message_tool.py`: delete `_send_mattermost` (22 LOC) + `Platform.MATTERMOST elif` in `_send_to_platform` — the `else` branch already routes plugin platforms through `_send_via_adapter`, which hits the registry's `standalone_sender_fn`. * `hermes_cli/setup.py`: delete `_setup_mattermost` (44 LOC) — replaced by the plugin's `interactive_setup`. * `hermes_cli/gateway.py`: delete `_PLATFORMS["mattermost"]` dict entry (3 LOC) — plugin's `setup_fn` is dispatched via the plugin path in `_configure_platform`. * Consumer rewrite: 5 test files (test_mattermost.py, test_media_download_retry.py, test_send_multiple_images.py, test_stream_consumer.py, test_ws_auth_retry.py) get `gateway.platforms.mattermost` → `plugins.platforms.mattermost.adapter` with the bulk-rewrite recipe from the platform-plugin-migration playbook. Single `mock.patch` string in test_stream_consumer.py also repointed. * `tests/tools/test_send_message_missing_platforms.py`: thin `(token, extra, chat_id, message)` compat shim around the plugin's `_standalone_send(pconfig, …)` so existing test bodies continue to work without rewriting every signature. Validation ---------- * Plugin discovery: mattermost registers from `plugins/platforms/mattermost/` alongside discord / teams / irc / line / google_chat / simplex. All 9 hooks present (setup_fn, standalone_sender_fn, apply_yaml_config_fn, is_connected, check_fn, allowed_users_env, allow_all_env, cron_deliver_env_var, max_message_length=4000). * Mattermost-touching tests: 62/62 pass (`test_mattermost.py` + `test_send_message_missing_platforms.py`). * Targeted selectors (mattermost or platform_registry or stream_consumer or ws_auth_retry or media_download_retry or send_multiple_images or send_message_tool or platform_connected): 433/433 pass. * Full sweep (`scripts/run_tests.sh tests/gateway/ tests/cron/ tests/tools/test_send_message_tool.py tests/tools/test_send_message_missing_platforms.py tests/integration/`): **6220/6220 pass in 47.8s, 0 failures**. * Lint: ruff clean on all touched files. * Git identity verified: kshitijk4poor. * Rename detection: R071 (similarity dropped from a hypothetical R09x by the ~320-line appended register block — ~36% growth over the 873-LoC base, vs Discord's 5101 LoC base which kept R091). Closes part of #3823.
…h#31744) The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR NousResearch#30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (NousResearch#30591) hit the same false positive yesterday.
Second migration of an existing built-in platform adapter after Discord (PR NousResearch#30591) — follows the same shape established by IRC / Teams / LINE / Google Chat / SimpleX and the playbook in `references/platform-plugin-migration.md`. Advances the umbrella refactor in NousResearch#3823. Matches Discord's parity bar — adapter under `plugins/platforms/mattermost/` with the standard `__init__.py` / `adapter.py` / `plugin.yaml` shell, `register(ctx)` entry point, **no back-compat shim** at the old import path, and full parity for all five hooks Discord uses plus the `apply_yaml_config_fn` hook (mattermost is the second consumer of NousResearch#25443 after Discord): * `standalone_sender_fn` — out-of-process cron delivery via Mattermost REST API. Picks up the thread_id + media_files capabilities the legacy `_send_mattermost` lacked (parity with Discord's `_standalone_send`). * `setup_fn` — interactive `hermes setup gateway` wizard. * `apply_yaml_config_fn` — translates `config.yaml` `mattermost:` keys (`require_mention`, `free_response_channels`, `allowed_channels`) into `MATTERMOST_*` env vars (replaces the hardcoded block in `gateway/config.py`). * `is_connected` — declares connection state from `MATTERMOST_TOKEN` + `MATTERMOST_URL`. * `check_fn` — verifies aiohttp is installed and both required env vars are set. * plus `allowed_users_env`, `allow_all_env`, `cron_deliver_env_var`, `max_message_length` (4000 — Mattermost practical limit), `emoji`, `required_env`, `install_hint`. Files ----- * `gateway/platforms/mattermost.py` (873 LOC) → `plugins/platforms/mattermost/adapter.py` (git rename, R071) + appended `register()` block, hook helpers, and `_standalone_send` with media upload + thread_id support. * New `plugins/platforms/mattermost/{__init__.py, plugin.yaml}` with `requires_env` / `optional_env` declarations covering MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS, MATTERMOST_ALLOW_ALL_USERS, MATTERMOST_HOME_CHANNEL, MATTERMOST_REPLY_MODE, MATTERMOST_REQUIRE_MENTION, MATTERMOST_FREE_RESPONSE_CHANNELS, MATTERMOST_ALLOWED_CHANNELS. * `gateway/config.py`: delete 17-LOC `mattermost_cfg` YAML→env bridge (moved into plugin's `_apply_yaml_config`). * `gateway/run.py::_create_adapter`: delete `Platform.MATTERMOST elif` — replaced by the existing generic plugin-registry-first dispatch. * `tools/send_message_tool.py`: delete `_send_mattermost` (22 LOC) + `Platform.MATTERMOST elif` in `_send_to_platform` — the `else` branch already routes plugin platforms through `_send_via_adapter`, which hits the registry's `standalone_sender_fn`. * `hermes_cli/setup.py`: delete `_setup_mattermost` (44 LOC) — replaced by the plugin's `interactive_setup`. * `hermes_cli/gateway.py`: delete `_PLATFORMS["mattermost"]` dict entry (3 LOC) — plugin's `setup_fn` is dispatched via the plugin path in `_configure_platform`. * Consumer rewrite: 5 test files (test_mattermost.py, test_media_download_retry.py, test_send_multiple_images.py, test_stream_consumer.py, test_ws_auth_retry.py) get `gateway.platforms.mattermost` → `plugins.platforms.mattermost.adapter` with the bulk-rewrite recipe from the platform-plugin-migration playbook. Single `mock.patch` string in test_stream_consumer.py also repointed. * `tests/tools/test_send_message_missing_platforms.py`: thin `(token, extra, chat_id, message)` compat shim around the plugin's `_standalone_send(pconfig, …)` so existing test bodies continue to work without rewriting every signature. Validation ---------- * Plugin discovery: mattermost registers from `plugins/platforms/mattermost/` alongside discord / teams / irc / line / google_chat / simplex. All 9 hooks present (setup_fn, standalone_sender_fn, apply_yaml_config_fn, is_connected, check_fn, allowed_users_env, allow_all_env, cron_deliver_env_var, max_message_length=4000). * Mattermost-touching tests: 62/62 pass (`test_mattermost.py` + `test_send_message_missing_platforms.py`). * Targeted selectors (mattermost or platform_registry or stream_consumer or ws_auth_retry or media_download_retry or send_multiple_images or send_message_tool or platform_connected): 433/433 pass. * Full sweep (`scripts/run_tests.sh tests/gateway/ tests/cron/ tests/tools/test_send_message_tool.py tests/tools/test_send_message_missing_platforms.py tests/integration/`): **6220/6220 pass in 47.8s, 0 failures**. * Lint: ruff clean on all touched files. * Git identity verified: kshitijk4poor. * Rename detection: R071 (similarity dropped from a hypothetical R09x by the ~320-line appended register block — ~36% growth over the 873-LoC base, vs Discord's 5101 LoC base which kept R091). Closes part of NousResearch#3823.
…h#31744) The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR NousResearch#30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (NousResearch#30591) hit the same false positive yesterday.
Second migration of an existing built-in platform adapter after Discord (PR NousResearch#30591) — follows the same shape established by IRC / Teams / LINE / Google Chat / SimpleX and the playbook in `references/platform-plugin-migration.md`. Advances the umbrella refactor in NousResearch#3823. Matches Discord's parity bar — adapter under `plugins/platforms/mattermost/` with the standard `__init__.py` / `adapter.py` / `plugin.yaml` shell, `register(ctx)` entry point, **no back-compat shim** at the old import path, and full parity for all five hooks Discord uses plus the `apply_yaml_config_fn` hook (mattermost is the second consumer of NousResearch#25443 after Discord): * `standalone_sender_fn` — out-of-process cron delivery via Mattermost REST API. Picks up the thread_id + media_files capabilities the legacy `_send_mattermost` lacked (parity with Discord's `_standalone_send`). * `setup_fn` — interactive `hermes setup gateway` wizard. * `apply_yaml_config_fn` — translates `config.yaml` `mattermost:` keys (`require_mention`, `free_response_channels`, `allowed_channels`) into `MATTERMOST_*` env vars (replaces the hardcoded block in `gateway/config.py`). * `is_connected` — declares connection state from `MATTERMOST_TOKEN` + `MATTERMOST_URL`. * `check_fn` — verifies aiohttp is installed and both required env vars are set. * plus `allowed_users_env`, `allow_all_env`, `cron_deliver_env_var`, `max_message_length` (4000 — Mattermost practical limit), `emoji`, `required_env`, `install_hint`. Files ----- * `gateway/platforms/mattermost.py` (873 LOC) → `plugins/platforms/mattermost/adapter.py` (git rename, R071) + appended `register()` block, hook helpers, and `_standalone_send` with media upload + thread_id support. * New `plugins/platforms/mattermost/{__init__.py, plugin.yaml}` with `requires_env` / `optional_env` declarations covering MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS, MATTERMOST_ALLOW_ALL_USERS, MATTERMOST_HOME_CHANNEL, MATTERMOST_REPLY_MODE, MATTERMOST_REQUIRE_MENTION, MATTERMOST_FREE_RESPONSE_CHANNELS, MATTERMOST_ALLOWED_CHANNELS. * `gateway/config.py`: delete 17-LOC `mattermost_cfg` YAML→env bridge (moved into plugin's `_apply_yaml_config`). * `gateway/run.py::_create_adapter`: delete `Platform.MATTERMOST elif` — replaced by the existing generic plugin-registry-first dispatch. * `tools/send_message_tool.py`: delete `_send_mattermost` (22 LOC) + `Platform.MATTERMOST elif` in `_send_to_platform` — the `else` branch already routes plugin platforms through `_send_via_adapter`, which hits the registry's `standalone_sender_fn`. * `hermes_cli/setup.py`: delete `_setup_mattermost` (44 LOC) — replaced by the plugin's `interactive_setup`. * `hermes_cli/gateway.py`: delete `_PLATFORMS["mattermost"]` dict entry (3 LOC) — plugin's `setup_fn` is dispatched via the plugin path in `_configure_platform`. * Consumer rewrite: 5 test files (test_mattermost.py, test_media_download_retry.py, test_send_multiple_images.py, test_stream_consumer.py, test_ws_auth_retry.py) get `gateway.platforms.mattermost` → `plugins.platforms.mattermost.adapter` with the bulk-rewrite recipe from the platform-plugin-migration playbook. Single `mock.patch` string in test_stream_consumer.py also repointed. * `tests/tools/test_send_message_missing_platforms.py`: thin `(token, extra, chat_id, message)` compat shim around the plugin's `_standalone_send(pconfig, …)` so existing test bodies continue to work without rewriting every signature. Validation ---------- * Plugin discovery: mattermost registers from `plugins/platforms/mattermost/` alongside discord / teams / irc / line / google_chat / simplex. All 9 hooks present (setup_fn, standalone_sender_fn, apply_yaml_config_fn, is_connected, check_fn, allowed_users_env, allow_all_env, cron_deliver_env_var, max_message_length=4000). * Mattermost-touching tests: 62/62 pass (`test_mattermost.py` + `test_send_message_missing_platforms.py`). * Targeted selectors (mattermost or platform_registry or stream_consumer or ws_auth_retry or media_download_retry or send_multiple_images or send_message_tool or platform_connected): 433/433 pass. * Full sweep (`scripts/run_tests.sh tests/gateway/ tests/cron/ tests/tools/test_send_message_tool.py tests/tools/test_send_message_missing_platforms.py tests/integration/`): **6220/6220 pass in 47.8s, 0 failures**. * Lint: ruff clean on all touched files. * Git identity verified: kshitijk4poor. * Rename detection: R071 (similarity dropped from a hypothetical R09x by the ~320-line appended register block — ~36% growth over the 873-LoC base, vs Discord's 5101 LoC base which kept R091). Closes part of NousResearch#3823.
The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR #30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (#30591) hit the same false positive yesterday.
Second migration of an existing built-in platform adapter after Discord (PR #30591) — follows the same shape established by IRC / Teams / LINE / Google Chat / SimpleX and the playbook in `references/platform-plugin-migration.md`. Advances the umbrella refactor in #3823. Matches Discord's parity bar — adapter under `plugins/platforms/mattermost/` with the standard `__init__.py` / `adapter.py` / `plugin.yaml` shell, `register(ctx)` entry point, **no back-compat shim** at the old import path, and full parity for all five hooks Discord uses plus the `apply_yaml_config_fn` hook (mattermost is the second consumer of #25443 after Discord): * `standalone_sender_fn` — out-of-process cron delivery via Mattermost REST API. Picks up the thread_id + media_files capabilities the legacy `_send_mattermost` lacked (parity with Discord's `_standalone_send`). * `setup_fn` — interactive `hermes setup gateway` wizard. * `apply_yaml_config_fn` — translates `config.yaml` `mattermost:` keys (`require_mention`, `free_response_channels`, `allowed_channels`) into `MATTERMOST_*` env vars (replaces the hardcoded block in `gateway/config.py`). * `is_connected` — declares connection state from `MATTERMOST_TOKEN` + `MATTERMOST_URL`. * `check_fn` — verifies aiohttp is installed and both required env vars are set. * plus `allowed_users_env`, `allow_all_env`, `cron_deliver_env_var`, `max_message_length` (4000 — Mattermost practical limit), `emoji`, `required_env`, `install_hint`. Files ----- * `gateway/platforms/mattermost.py` (873 LOC) → `plugins/platforms/mattermost/adapter.py` (git rename, R071) + appended `register()` block, hook helpers, and `_standalone_send` with media upload + thread_id support. * New `plugins/platforms/mattermost/{__init__.py, plugin.yaml}` with `requires_env` / `optional_env` declarations covering MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS, MATTERMOST_ALLOW_ALL_USERS, MATTERMOST_HOME_CHANNEL, MATTERMOST_REPLY_MODE, MATTERMOST_REQUIRE_MENTION, MATTERMOST_FREE_RESPONSE_CHANNELS, MATTERMOST_ALLOWED_CHANNELS. * `gateway/config.py`: delete 17-LOC `mattermost_cfg` YAML→env bridge (moved into plugin's `_apply_yaml_config`). * `gateway/run.py::_create_adapter`: delete `Platform.MATTERMOST elif` — replaced by the existing generic plugin-registry-first dispatch. * `tools/send_message_tool.py`: delete `_send_mattermost` (22 LOC) + `Platform.MATTERMOST elif` in `_send_to_platform` — the `else` branch already routes plugin platforms through `_send_via_adapter`, which hits the registry's `standalone_sender_fn`. * `hermes_cli/setup.py`: delete `_setup_mattermost` (44 LOC) — replaced by the plugin's `interactive_setup`. * `hermes_cli/gateway.py`: delete `_PLATFORMS["mattermost"]` dict entry (3 LOC) — plugin's `setup_fn` is dispatched via the plugin path in `_configure_platform`. * Consumer rewrite: 5 test files (test_mattermost.py, test_media_download_retry.py, test_send_multiple_images.py, test_stream_consumer.py, test_ws_auth_retry.py) get `gateway.platforms.mattermost` → `plugins.platforms.mattermost.adapter` with the bulk-rewrite recipe from the platform-plugin-migration playbook. Single `mock.patch` string in test_stream_consumer.py also repointed. * `tests/tools/test_send_message_missing_platforms.py`: thin `(token, extra, chat_id, message)` compat shim around the plugin's `_standalone_send(pconfig, …)` so existing test bodies continue to work without rewriting every signature. Validation ---------- * Plugin discovery: mattermost registers from `plugins/platforms/mattermost/` alongside discord / teams / irc / line / google_chat / simplex. All 9 hooks present (setup_fn, standalone_sender_fn, apply_yaml_config_fn, is_connected, check_fn, allowed_users_env, allow_all_env, cron_deliver_env_var, max_message_length=4000). * Mattermost-touching tests: 62/62 pass (`test_mattermost.py` + `test_send_message_missing_platforms.py`). * Targeted selectors (mattermost or platform_registry or stream_consumer or ws_auth_retry or media_download_retry or send_multiple_images or send_message_tool or platform_connected): 433/433 pass. * Full sweep (`scripts/run_tests.sh tests/gateway/ tests/cron/ tests/tools/test_send_message_tool.py tests/tools/test_send_message_missing_platforms.py tests/integration/`): **6220/6220 pass in 47.8s, 0 failures**. * Lint: ruff clean on all touched files. * Git identity verified: kshitijk4poor. * Rename detection: R071 (similarity dropped from a hypothetical R09x by the ~320-line appended register block — ~36% growth over the 873-LoC base, vs Discord's 5101 LoC base which kept R091). Closes part of #3823.
…h#31744) The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR NousResearch#30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (NousResearch#30591) hit the same false positive yesterday.
Second migration of an existing built-in platform adapter after Discord (PR NousResearch#30591) — follows the same shape established by IRC / Teams / LINE / Google Chat / SimpleX and the playbook in `references/platform-plugin-migration.md`. Advances the umbrella refactor in NousResearch#3823. Matches Discord's parity bar — adapter under `plugins/platforms/mattermost/` with the standard `__init__.py` / `adapter.py` / `plugin.yaml` shell, `register(ctx)` entry point, **no back-compat shim** at the old import path, and full parity for all five hooks Discord uses plus the `apply_yaml_config_fn` hook (mattermost is the second consumer of NousResearch#25443 after Discord): * `standalone_sender_fn` — out-of-process cron delivery via Mattermost REST API. Picks up the thread_id + media_files capabilities the legacy `_send_mattermost` lacked (parity with Discord's `_standalone_send`). * `setup_fn` — interactive `hermes setup gateway` wizard. * `apply_yaml_config_fn` — translates `config.yaml` `mattermost:` keys (`require_mention`, `free_response_channels`, `allowed_channels`) into `MATTERMOST_*` env vars (replaces the hardcoded block in `gateway/config.py`). * `is_connected` — declares connection state from `MATTERMOST_TOKEN` + `MATTERMOST_URL`. * `check_fn` — verifies aiohttp is installed and both required env vars are set. * plus `allowed_users_env`, `allow_all_env`, `cron_deliver_env_var`, `max_message_length` (4000 — Mattermost practical limit), `emoji`, `required_env`, `install_hint`. Files ----- * `gateway/platforms/mattermost.py` (873 LOC) → `plugins/platforms/mattermost/adapter.py` (git rename, R071) + appended `register()` block, hook helpers, and `_standalone_send` with media upload + thread_id support. * New `plugins/platforms/mattermost/{__init__.py, plugin.yaml}` with `requires_env` / `optional_env` declarations covering MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS, MATTERMOST_ALLOW_ALL_USERS, MATTERMOST_HOME_CHANNEL, MATTERMOST_REPLY_MODE, MATTERMOST_REQUIRE_MENTION, MATTERMOST_FREE_RESPONSE_CHANNELS, MATTERMOST_ALLOWED_CHANNELS. * `gateway/config.py`: delete 17-LOC `mattermost_cfg` YAML→env bridge (moved into plugin's `_apply_yaml_config`). * `gateway/run.py::_create_adapter`: delete `Platform.MATTERMOST elif` — replaced by the existing generic plugin-registry-first dispatch. * `tools/send_message_tool.py`: delete `_send_mattermost` (22 LOC) + `Platform.MATTERMOST elif` in `_send_to_platform` — the `else` branch already routes plugin platforms through `_send_via_adapter`, which hits the registry's `standalone_sender_fn`. * `hermes_cli/setup.py`: delete `_setup_mattermost` (44 LOC) — replaced by the plugin's `interactive_setup`. * `hermes_cli/gateway.py`: delete `_PLATFORMS["mattermost"]` dict entry (3 LOC) — plugin's `setup_fn` is dispatched via the plugin path in `_configure_platform`. * Consumer rewrite: 5 test files (test_mattermost.py, test_media_download_retry.py, test_send_multiple_images.py, test_stream_consumer.py, test_ws_auth_retry.py) get `gateway.platforms.mattermost` → `plugins.platforms.mattermost.adapter` with the bulk-rewrite recipe from the platform-plugin-migration playbook. Single `mock.patch` string in test_stream_consumer.py also repointed. * `tests/tools/test_send_message_missing_platforms.py`: thin `(token, extra, chat_id, message)` compat shim around the plugin's `_standalone_send(pconfig, …)` so existing test bodies continue to work without rewriting every signature. Validation ---------- * Plugin discovery: mattermost registers from `plugins/platforms/mattermost/` alongside discord / teams / irc / line / google_chat / simplex. All 9 hooks present (setup_fn, standalone_sender_fn, apply_yaml_config_fn, is_connected, check_fn, allowed_users_env, allow_all_env, cron_deliver_env_var, max_message_length=4000). * Mattermost-touching tests: 62/62 pass (`test_mattermost.py` + `test_send_message_missing_platforms.py`). * Targeted selectors (mattermost or platform_registry or stream_consumer or ws_auth_retry or media_download_retry or send_multiple_images or send_message_tool or platform_connected): 433/433 pass. * Full sweep (`scripts/run_tests.sh tests/gateway/ tests/cron/ tests/tools/test_send_message_tool.py tests/tools/test_send_message_missing_platforms.py tests/integration/`): **6220/6220 pass in 47.8s, 0 failures**. * Lint: ruff clean on all touched files. * Git identity verified: kshitijk4poor. * Rename detection: R071 (similarity dropped from a hypothetical R09x by the ~320-line appended register block — ~36% growth over the 873-LoC base, vs Discord's 5101 LoC base which kept R091). Closes part of NousResearch#3823.
…h#31744) The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR NousResearch#30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (NousResearch#30591) hit the same false positive yesterday.
Second migration of an existing built-in platform adapter after Discord (PR NousResearch#30591) — follows the same shape established by IRC / Teams / LINE / Google Chat / SimpleX and the playbook in `references/platform-plugin-migration.md`. Advances the umbrella refactor in NousResearch#3823. Matches Discord's parity bar — adapter under `plugins/platforms/mattermost/` with the standard `__init__.py` / `adapter.py` / `plugin.yaml` shell, `register(ctx)` entry point, **no back-compat shim** at the old import path, and full parity for all five hooks Discord uses plus the `apply_yaml_config_fn` hook (mattermost is the second consumer of NousResearch#25443 after Discord): * `standalone_sender_fn` — out-of-process cron delivery via Mattermost REST API. Picks up the thread_id + media_files capabilities the legacy `_send_mattermost` lacked (parity with Discord's `_standalone_send`). * `setup_fn` — interactive `hermes setup gateway` wizard. * `apply_yaml_config_fn` — translates `config.yaml` `mattermost:` keys (`require_mention`, `free_response_channels`, `allowed_channels`) into `MATTERMOST_*` env vars (replaces the hardcoded block in `gateway/config.py`). * `is_connected` — declares connection state from `MATTERMOST_TOKEN` + `MATTERMOST_URL`. * `check_fn` — verifies aiohttp is installed and both required env vars are set. * plus `allowed_users_env`, `allow_all_env`, `cron_deliver_env_var`, `max_message_length` (4000 — Mattermost practical limit), `emoji`, `required_env`, `install_hint`. Files ----- * `gateway/platforms/mattermost.py` (873 LOC) → `plugins/platforms/mattermost/adapter.py` (git rename, R071) + appended `register()` block, hook helpers, and `_standalone_send` with media upload + thread_id support. * New `plugins/platforms/mattermost/{__init__.py, plugin.yaml}` with `requires_env` / `optional_env` declarations covering MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS, MATTERMOST_ALLOW_ALL_USERS, MATTERMOST_HOME_CHANNEL, MATTERMOST_REPLY_MODE, MATTERMOST_REQUIRE_MENTION, MATTERMOST_FREE_RESPONSE_CHANNELS, MATTERMOST_ALLOWED_CHANNELS. * `gateway/config.py`: delete 17-LOC `mattermost_cfg` YAML→env bridge (moved into plugin's `_apply_yaml_config`). * `gateway/run.py::_create_adapter`: delete `Platform.MATTERMOST elif` — replaced by the existing generic plugin-registry-first dispatch. * `tools/send_message_tool.py`: delete `_send_mattermost` (22 LOC) + `Platform.MATTERMOST elif` in `_send_to_platform` — the `else` branch already routes plugin platforms through `_send_via_adapter`, which hits the registry's `standalone_sender_fn`. * `hermes_cli/setup.py`: delete `_setup_mattermost` (44 LOC) — replaced by the plugin's `interactive_setup`. * `hermes_cli/gateway.py`: delete `_PLATFORMS["mattermost"]` dict entry (3 LOC) — plugin's `setup_fn` is dispatched via the plugin path in `_configure_platform`. * Consumer rewrite: 5 test files (test_mattermost.py, test_media_download_retry.py, test_send_multiple_images.py, test_stream_consumer.py, test_ws_auth_retry.py) get `gateway.platforms.mattermost` → `plugins.platforms.mattermost.adapter` with the bulk-rewrite recipe from the platform-plugin-migration playbook. Single `mock.patch` string in test_stream_consumer.py also repointed. * `tests/tools/test_send_message_missing_platforms.py`: thin `(token, extra, chat_id, message)` compat shim around the plugin's `_standalone_send(pconfig, …)` so existing test bodies continue to work without rewriting every signature. Validation ---------- * Plugin discovery: mattermost registers from `plugins/platforms/mattermost/` alongside discord / teams / irc / line / google_chat / simplex. All 9 hooks present (setup_fn, standalone_sender_fn, apply_yaml_config_fn, is_connected, check_fn, allowed_users_env, allow_all_env, cron_deliver_env_var, max_message_length=4000). * Mattermost-touching tests: 62/62 pass (`test_mattermost.py` + `test_send_message_missing_platforms.py`). * Targeted selectors (mattermost or platform_registry or stream_consumer or ws_auth_retry or media_download_retry or send_multiple_images or send_message_tool or platform_connected): 433/433 pass. * Full sweep (`scripts/run_tests.sh tests/gateway/ tests/cron/ tests/tools/test_send_message_tool.py tests/tools/test_send_message_missing_platforms.py tests/integration/`): **6220/6220 pass in 47.8s, 0 failures**. * Lint: ruff clean on all touched files. * Git identity verified: kshitijk4poor. * Rename detection: R071 (similarity dropped from a hypothetical R09x by the ~320-line appended register block — ~36% growth over the 873-LoC base, vs Discord's 5101 LoC base which kept R091). Closes part of NousResearch#3823.
…h#31744) The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR NousResearch#30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (NousResearch#30591) hit the same false positive yesterday.
…channel backfill to prevent prompt injection DiscordAdapter._fetch_channel_context reads the last N messages from a channel and prepends them to the model's context when an allowlisted user triggers a response. Two gaps in that path let a non-allowlisted guild member turn the channel into an indirect prompt-injection surface against the authorized user: 1. Backfilled human messages bypassed the allowlist. The same _is_allowed_user() gate applied at message-receipt time wasn't applied to backfill, so any guild member — including users explicitly excluded from DISCORD_ALLOWED_USERS / DISCORD_ALLOWED_ROLES — could plant text the bot would later read into the model's context. 2. Structural [ ] delimiters in display_name and content weren't escaped. Hostile values like display_name='System]' could fake header rows and slip instructions past the channel-context boundary. Fix: * Apply _is_allowed_user(...) to backfilled human messages, mirroring on_message receipt-time behavior. Bots keep their existing include_other_bots gate. _fetch_channel_context is only called from non-DM paths (guarded by 'not _is_dm' at the call site), so is_dm=False is correct. * Backslash-escape [ and ] in both display_name and message content before formatting into the '[name] content' rows. Backwards compatible: when neither DISCORD_ALLOWED_USERS nor DISCORD_ALLOWED_ROLES is configured, _is_allowed_user returns True for everyone — same as existing on_message behavior. The bracket escape is purely additive. Regression tests added in tests/gateway/test_discord_free_response.py: * test_fetch_channel_context_skips_non_allowlisted_users — verifies Mallory's injection attempt is dropped when she's not in the allowlist (the core bug). * test_fetch_channel_context_includes_allowlisted_users — verifies allowlisted humans still appear (positive case). * test_fetch_channel_context_escapes_brackets_in_name_and_content — verifies hostile '[Trusted]' headers can no longer appear bare. All three tests fail without the fix and pass with it; the five existing _fetch_channel_context tests continue to pass. Rebased onto plugins/platforms/discord/adapter.py (Discord adapter migrated to bundled plugin in NousResearch#30591). Logic unchanged from original PR; only the file path moved.
…h#31744) The SETUP_HITS check matched any file ending in setup.py/setup.cfg/ sitecustomize.py/usercustomize.py at any path depth. This produced false positives on every PR touching hermes_cli/setup.py (the CLI setup wizard), which is unrelated to pip/site install hooks. Only the top-level setup.py/setup.cfg execute during 'pip install', and only top-level sitecustomize.py/usercustomize.py are auto-loaded by site.py at interpreter startup. Anchor the regex with '^' so only repo-root matches fire. Symptom: PR NousResearch#30916 (Mattermost plugin migration) flagged purely because it deletes _setup_mattermost() from hermes_cli/setup.py. Discord migration (NousResearch#30591) hit the same false positive yesterday.
Second migration of an existing built-in platform adapter after Discord (PR NousResearch#30591) — follows the same shape established by IRC / Teams / LINE / Google Chat / SimpleX and the playbook in `references/platform-plugin-migration.md`. Advances the umbrella refactor in NousResearch#3823. Matches Discord's parity bar — adapter under `plugins/platforms/mattermost/` with the standard `__init__.py` / `adapter.py` / `plugin.yaml` shell, `register(ctx)` entry point, **no back-compat shim** at the old import path, and full parity for all five hooks Discord uses plus the `apply_yaml_config_fn` hook (mattermost is the second consumer of NousResearch#25443 after Discord): * `standalone_sender_fn` — out-of-process cron delivery via Mattermost REST API. Picks up the thread_id + media_files capabilities the legacy `_send_mattermost` lacked (parity with Discord's `_standalone_send`). * `setup_fn` — interactive `hermes setup gateway` wizard. * `apply_yaml_config_fn` — translates `config.yaml` `mattermost:` keys (`require_mention`, `free_response_channels`, `allowed_channels`) into `MATTERMOST_*` env vars (replaces the hardcoded block in `gateway/config.py`). * `is_connected` — declares connection state from `MATTERMOST_TOKEN` + `MATTERMOST_URL`. * `check_fn` — verifies aiohttp is installed and both required env vars are set. * plus `allowed_users_env`, `allow_all_env`, `cron_deliver_env_var`, `max_message_length` (4000 — Mattermost practical limit), `emoji`, `required_env`, `install_hint`. Files ----- * `gateway/platforms/mattermost.py` (873 LOC) → `plugins/platforms/mattermost/adapter.py` (git rename, R071) + appended `register()` block, hook helpers, and `_standalone_send` with media upload + thread_id support. * New `plugins/platforms/mattermost/{__init__.py, plugin.yaml}` with `requires_env` / `optional_env` declarations covering MATTERMOST_URL, MATTERMOST_TOKEN, MATTERMOST_ALLOWED_USERS, MATTERMOST_ALLOW_ALL_USERS, MATTERMOST_HOME_CHANNEL, MATTERMOST_REPLY_MODE, MATTERMOST_REQUIRE_MENTION, MATTERMOST_FREE_RESPONSE_CHANNELS, MATTERMOST_ALLOWED_CHANNELS. * `gateway/config.py`: delete 17-LOC `mattermost_cfg` YAML→env bridge (moved into plugin's `_apply_yaml_config`). * `gateway/run.py::_create_adapter`: delete `Platform.MATTERMOST elif` — replaced by the existing generic plugin-registry-first dispatch. * `tools/send_message_tool.py`: delete `_send_mattermost` (22 LOC) + `Platform.MATTERMOST elif` in `_send_to_platform` — the `else` branch already routes plugin platforms through `_send_via_adapter`, which hits the registry's `standalone_sender_fn`. * `hermes_cli/setup.py`: delete `_setup_mattermost` (44 LOC) — replaced by the plugin's `interactive_setup`. * `hermes_cli/gateway.py`: delete `_PLATFORMS["mattermost"]` dict entry (3 LOC) — plugin's `setup_fn` is dispatched via the plugin path in `_configure_platform`. * Consumer rewrite: 5 test files (test_mattermost.py, test_media_download_retry.py, test_send_multiple_images.py, test_stream_consumer.py, test_ws_auth_retry.py) get `gateway.platforms.mattermost` → `plugins.platforms.mattermost.adapter` with the bulk-rewrite recipe from the platform-plugin-migration playbook. Single `mock.patch` string in test_stream_consumer.py also repointed. * `tests/tools/test_send_message_missing_platforms.py`: thin `(token, extra, chat_id, message)` compat shim around the plugin's `_standalone_send(pconfig, …)` so existing test bodies continue to work without rewriting every signature. Validation ---------- * Plugin discovery: mattermost registers from `plugins/platforms/mattermost/` alongside discord / teams / irc / line / google_chat / simplex. All 9 hooks present (setup_fn, standalone_sender_fn, apply_yaml_config_fn, is_connected, check_fn, allowed_users_env, allow_all_env, cron_deliver_env_var, max_message_length=4000). * Mattermost-touching tests: 62/62 pass (`test_mattermost.py` + `test_send_message_missing_platforms.py`). * Targeted selectors (mattermost or platform_registry or stream_consumer or ws_auth_retry or media_download_retry or send_multiple_images or send_message_tool or platform_connected): 433/433 pass. * Full sweep (`scripts/run_tests.sh tests/gateway/ tests/cron/ tests/tools/test_send_message_tool.py tests/tools/test_send_message_missing_platforms.py tests/integration/`): **6220/6220 pass in 47.8s, 0 failures**. * Lint: ruff clean on all touched files. * Git identity verified: kshitijk4poor. * Rename detection: R071 (similarity dropped from a hypothetical R09x by the ~320-line appended register block — ~36% growth over the 873-LoC base, vs Discord's 5101 LoC base which kept R091). Closes part of NousResearch#3823.
Migrates Discord adapter from
gateway/platforms/discord.py(5,101 LOC built-in) toplugins/platforms/discord/{__init__.py, adapter.py, plugin.yaml}— first existing built-in promoted into the bundled-plugin shape used by IRC / Teams / LINE / Google Chat / SimpleX. Salvage of #24356 by @kshitijk4poor (840 commits stale; cherry-picked onto current main with conflicts resolved and missed fixes flowed in).Changes
gateway/platforms/discord.py(5,101 LOC) →plugins/platforms/discord/adapter.py(git rename, R091)plugins/platforms/discord/{__init__.py, plugin.yaml}— plugin shell,requires_env/optional_envdeclarationsregister(ctx)block + hook implementations to adapter:check_fn,is_connected,setup_fn,standalone_sender_fn,apply_yaml_config_fn(first consumer of refactor(plugins): add apply_yaml_config_fn registry hook for YAML→env config bridges #25443), plus registry fields (allowed_users_env,allow_all_env,cron_deliver_env_var,max_message_length,emoji,required_env,install_hint)gateway/config.py: delete 59-LOCdiscord_cfgYAML→env bridge (moved into plugin's_apply_yaml_config)gateway/run.py: replacePlatform.DISCORD elifwith genericgateway_runnerpost-creation hooktools/send_message_tool.py: delete_send_discord+ 4 helpers (221 LOC); registry-hook dispatch (10 LOC)hermes_cli/setup.py: delete_setup_discord+_clean_discord_user_ids(68 LOC)hermes_cli/gateway.py: delete_PLATFORMS["discord"]dict (32 LOC)tools/send_message_tool.py+hermes_cli/commands.pydocstring41 files, +728 / −504.
Salvage notes
Cherry-picked
29bc396f6onto current main (840 commits ahead of PR branch). Three conflicts resolved:plugins/platforms/discord/adapter.pytests/tools/test_send_message_tool.py_is_telegram_thread_not_foundimport from main (df530b4); moved_derive_forum_thread_nameto plugin pathtests/gateway/test_discord_attachment_download.pyMessageTypeimport from main (448a3f9); movedDiscordAdapterto plugin pathOrphan references the PR didn't see (rewritten by hand):
mock.patch("gateway.platforms.discord.*")strings intest_discord_attachment_download.py(added by 448a3f9 after branch) → repointed toplugins.platforms.discord.adapter.*test_discord_lazy_install_views.py(new file on main from 5a33176) →importlib.import_modulerepointed to plugin path5 missed-fix commits to
discord.pyflowed in via auto-merge, verified in adapter at expected lines: typing 429 graceful (be0728c), voice note transcription (448a3f9), lazy view classes (5a33176), ruff PLR6201 sweep (5fba236),allow_any_attachmentconfig (407a11b).Validation
plugins/platforms/discord/alongside teams / irc / line / google_chat / simplex; all 9 hooks presenttest_discord_*.py+ send_message_tool + voice_command + image/multi-image/stream/text-batching/reload-skills)Closes #24325. Refs #3823, #25443.
Salvage of #24356 — credit to @kshitijk4poor.