Skip to content

refactor(gateway): migrate Discord adapter to bundled plugin (salvage of #24356) - #30591

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-4c7d30cc
May 22, 2026
Merged

refactor(gateway): migrate Discord adapter to bundled plugin (salvage of #24356)#30591
teknium1 merged 2 commits into
mainfrom
hermes/hermes-4c7d30cc

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Migrates Discord adapter from gateway/platforms/discord.py (5,101 LOC built-in) to plugins/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_env declarations
  • Append register(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-LOC discord_cfg YAML→env bridge (moved into plugin's _apply_yaml_config)
  • gateway/run.py: replace Platform.DISCORD elif with generic gateway_runner post-creation hook
  • tools/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)
  • 78 consumer references rewritten across 28 test files + tools/send_message_tool.py + hermes_cli/commands.py docstring

41 files, +728 / −504.

Salvage notes

Cherry-picked 29bc396f6 onto current main (840 commits ahead of PR branch). Three conflicts resolved:

File Resolution
plugins/platforms/discord/adapter.py Kept lazy view-class init from main (5a33176) + PR's standalone-sender block
tests/tools/test_send_message_tool.py Kept _is_telegram_thread_not_found import from main (df530b4); moved _derive_forum_thread_name to plugin path
tests/gateway/test_discord_attachment_download.py Kept MessageType import from main (448a3f9); moved DiscordAdapter to plugin path

Orphan references the PR didn't see (rewritten by hand):

  • 4 mock.patch("gateway.platforms.discord.*") strings in test_discord_attachment_download.py (added by 448a3f9 after branch) → repointed to plugins.platforms.discord.adapter.*
  • test_discord_lazy_install_views.py (new file on main from 5a33176) → importlib.import_module repointed to plugin path

5 missed-fix commits to discord.py flowed 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_attachment config (407a11b).

Validation

Result
Plugin discovery discord registers from plugins/platforms/discord/ alongside teams / irc / line / google_chat / simplex; all 9 hooks present
Discord-touching tests 818 / 818 pass (27 test_discord_*.py + send_message_tool + voice_command + image/multi-image/stream/text-batching/reload-skills)
Gateway + cron + tools/send_message + setup-gateway sweep 6346 / 6346 pass, 0 failures

Closes #24325. Refs #3823, #25443.
Salvage of #24356 — credit to @kshitijk4poor.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

🚨 CRITICAL Supply Chain Risk Detected

This 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 modified

These files can execute code during package installation or interpreter startup.

Files:

hermes_cli/setup.py

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.

@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-4c7d30cc vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9020 on HEAD, 9017 on base (🆕 +3)

🆕 New issues (65):

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.

@alt-glitch alt-glitch added type/refactor Code restructuring, no behavior change P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins platform/discord Discord bot adapter labels May 22, 2026
…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.
@github-actions

Copy link
Copy Markdown
Contributor

🚨 CRITICAL Supply Chain Risk Detected

This 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 modified

These files can execute code during package installation or interpreter startup.

Files:

hermes_cli/setup.py

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.

@teknium1
teknium1 merged commit 7849a3d into main May 22, 2026
19 of 20 checks passed
@teknium1
teknium1 deleted the hermes/hermes-4c7d30cc branch May 22, 2026 21:21
Gpapas pushed a commit to Gpapas/hermes-agent that referenced this pull request May 23, 2026
Mucky010 pushed a commit to Mucky010/hermes-agent that referenced this pull request May 24, 2026
teknium1 added a commit that referenced this pull request May 25, 2026
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.
teknium1 pushed a commit that referenced this pull request May 25, 2026
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.
sahilm-ti pushed a commit to sahilm-ti/hermes-agent that referenced this pull request May 25, 2026
…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.
sahilm-ti pushed a commit to sahilm-ti/hermes-agent that referenced this pull request May 25, 2026
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.
sahilm-ti pushed a commit to sahilm-ti/hermes-agent that referenced this pull request May 25, 2026
…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.
sahilm-ti pushed a commit to sahilm-ti/hermes-agent that referenced this pull request May 25, 2026
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.
Seven74AI pushed a commit to Seven74AI/hermes-agent that referenced this pull request Jun 13, 2026
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
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.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
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.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
…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.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
…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.
liuchanchen pushed a commit to liuchanchen/hermes-agent that referenced this pull request Jun 23, 2026
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.
xyshanren pushed a commit to xyshanren/hermes-agent-cn that referenced this pull request Jun 25, 2026
…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.
xyshanren pushed a commit to xyshanren/hermes-agent-cn that referenced this pull request Jun 25, 2026
memosr added a commit to memosr/hermes-agent that referenced this pull request Jun 27, 2026
…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.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…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.
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
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.
donbowman pushed a commit to donbowman/hermes-agent that referenced this pull request Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/discord Discord bot adapter type/refactor Code restructuring, no behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(gateway): migrate Discord adapter to bundled plugin (first migration of an existing built-in)

3 participants