Commit Graph

16 Commits

Author SHA1 Message Date
Teknium 791417590d refactor(adapters/matrix_group): 9272->6311; matrix dead fetch_history/_send_simple_message/HTMLParser stubs removed, simplex/mattermost/irc one-verb API helpers and send builders 2026-09-02 14:06:37 -07:00
nftpoetrist 56d869d2d9 fix(mattermost): scope url/reply_mode/require_mention/free_response_channels/allowed_channels to the active profile under multiplexing
MattermostAdapter.__init__, validate_mattermost_config, _standalone_send,
and _handle_ws_event's mention-gating block all read MATTERMOST_URL/
MATTERMOST_REPLY_MODE/MATTERMOST_REQUIRE_MENTION/
MATTERMOST_FREE_RESPONSE_CHANNELS/MATTERMOST_ALLOWED_CHANNELS via raw
os.getenv -- only MATTERMOST_TOKEN was already scoped via
_get_scoped_secret. _apply_yaml_config additionally wrote
MATTERMOST_REQUIRE_MENTION/MATTERMOST_FREE_RESPONSE_CHANNELS/
MATTERMOST_ALLOWED_CHANNELS into the process-global os.environ
unconditionally (guarded only by `not os.getenv(...)`, first-writer-wins),
the same apply_yaml_config_fn bug class already fixed for the
Discord/Telegram/WhatsApp/DingTalk adapters in this series.

Under gateway.multiplex_profiles, os.environ holds the DEFAULT profile's
env-bridge output. A secondary profile with its own (or no) Mattermost
config could silently connect to the default profile's server, thread
its replies per the default profile's reply_mode, or -- since
_handle_ws_event's mention-gating block runs on every LIVE inbound
message, not just at construction -- have its require_mention/
free_response_channels/allowed_channels decisions driven by the default
profile's settings for the adapter's entire runtime lifetime.

Fix, mirroring the WhatsApp/DingTalk apply_yaml_config_fn pattern:
- Add _profile_scoped_config_load() (same helper as DingTalk).
- Rewrite _apply_yaml_config to skip the env-bridge write under a
  multiplexed secondary profile's scope, and instead return the YAML
  values as a dict merged into this profile's own PlatformConfig.extra.
- Make require_mention/free_response_channels read extra first (matching
  the existing allowed_channels precedent), falling back to
  _get_scoped_secret() instead of raw os.getenv when extra is absent --
  fixing a residual gap the DingTalk fix (#100615, this series' item 6)
  left in its own analogous extra-first-with-raw-fallback read sites
  (_dingtalk_require_mention et al. still fall back to bare os.getenv).
- Switch __init__'s url/reply_mode, validate_mattermost_config's url, and
  _standalone_send's url to _get_scoped_secret().
- Leave check_mattermost_requirements() (no longer reads any MATTERMOST_*
  var on current main -- just an aiohttp-importability probe) and
  _is_connected() (already scope-aware via hermes_cli.gateway.get_env_value,
  which itself routes through agent.secret_scope.get_secret) untouched.

Adds a new TestMultiplexProfileScope class to tests/gateway/test_mattermost.py
(7 tests) mirroring the fixture/assertion style established in
tests/gateway/test_line_plugin.py's TestMultiplexProfileScope, plus two
tests exercising _apply_yaml_config's new seeded-dict return directly.
Mutation-verified: stashed the production fix and confirmed 5 of 7 new
tests fail against pre-fix code (the other 2 are non-differentiating
regression guards -- extra-wins-over-env and unscoped-default-profile-
precedence -- which correctly pass either way). Restored the fix;
all 30 tests in the file, the plugin-setup test, and the full 75-test
tests/gateway/test_adapter_startup_secret_scope.py suite pass.
2026-09-02 07:01:23 -07:00
Teknium 45b0d8cab5 feat(gateway): one gateway.trust_env key controls aiohttp proxy-env honoring at every adapter site (#48820 bug 3)
Every gateway/plugin platform adapter hard-coded aiohttp.ClientSession(trust_env=True)
(~20 sites), so a gateway launched by a Windows Scheduled Task that inherits a stale
HTTP_PROXY (Clash/V2Ray on 127.0.0.1:7890) looped on 'Cannot connect to host' with no
way to opt out short of NO_PROXY hacks per vendor host.

- gateway/platforms/base.py: gateway_trust_env() reads gateway.trust_env (default true);
  resolve_proxy_url() skips generic HTTP(S)_PROXY/ALL_PROXY + macOS system-proxy
  auto-detect when false (explicit per-platform vars still win).
- All aiohttp ClientSession sites in weixin, qqbot, matrix, line, wecom, slack, sms,
  teams, google_chat now pass trust_env=gateway_trust_env(); mattermost + homeassistant
  bare sessions gain the same kwarg (intent of #70119 / #56229).
- DEFAULT_CONFIG + cli-config.yaml.example + messaging docs.
- tests/gateway/test_gateway_trust_env.py: config flip + no-bare-literal sweep.

Reported-by: @ranlingfeng (#48820), @frontnopipe-cloud (#76309)
Co-authored-by: rcarrata <rcarratalasanchez@gmail.com>
Co-authored-by: Backroads4Me <TEDLANHAM@GMAIL.COM>
2026-09-02 04:13:02 -07:00
teknium1 272f4e4abe feat(plugins): generalize native platform handler registration to every gateway platform
ctx.register_platform_handler(platform, factory) — the generic surface for
plugins to wire native handlers into any platform adapter at connect()
time. Factories receive (native, adapter): the platform's client/app
object (PTB Application, discord.py Bot, slack_bolt AsyncApp, Teams App,
DingTalkStreamClient, aiohttp web.Application) or None for adapters with
no separate native object.

- BasePlatformAdapter._wire_plugin_handlers(native): shared, isolated
  invocation helper — a raising plugin cannot block a platform connect.
- All 27 connectable adapters call it: telegram/slack/teams/line/
  api_server/msgraph_webhook wire before their dispatch tables freeze;
  the rest hook at connect success.
- register_telegram_handler and get_telegram_handler_factories retained
  as thin back-compat aliases over the telegram bucket.
- Source-invariant test guarantees every adapter with connect() keeps
  calling the hook.
2026-08-27 07:51:37 -07:00
Teknium f4749a77a5 fix(mattermost): escalate genuine WS auth failures through the fatal-error hook
Follow-up to the salvaged #80489 substring-fallback removal: the
structured 401/403 branch still exited with a bare return, leaving
_running True — dead listener, healthy-looking is_connected(), gateway
never told (the zombie half of the bug, OOF-156 class). It now sets a
non-retryable mattermost_auth_error with token guidance and notifies
the gateway fatal handler.

Also: pytest.importorskip for aiohttp in the verifier probe file
(module-level import crashed collection in envs without the optional
dep), and probe fixtures updated for the escalation attributes.
2026-08-13 01:51:13 -07:00
Stephen Chin d184d68f37 fix(mattermost): stop misclassifying transient errors as auth failures
The WS reconnect loop had a fallback check that looked for "401", "403",
or "unauthorized" as substrings anywhere in an exception's string form.
A transient error whose message happens to contain those digits (a proxy
body, a stack trace, anything) got treated as a permanent auth failure
and stopped reconnection for good.

I removed the substring fallback and kept only the structured check:
aiohttp.WSServerHandshakeError with status in {401, 403}. That's the only
signal that reliably means the server rejected our credentials.

Added two regression tests: one proving a transient error containing
"401" in its text still retries, and one confirming the existing
_closing early-return path is untouched by the removal.
2026-08-13 01:51:13 -07:00
Teknium d7404f197c fix(secrets): Slack-pattern scoped credential reads — IRC, LINE, Teams, Mattermost
Route IRC_SERVER_PASSWORD/IRC_NICKSERV_PASSWORD, LINE_CHANNEL_ACCESS_TOKEN/
LINE_CHANNEL_SECRET, TEAMS_GRAPH_ACCESS_TOKEN/TEAMS_CLIENT_SECRET and
MATTERMOST_TOKEN reads at __init__/availability/standalone-send time through
a module-level _get_scoped_secret helper (get_secret, UnscopedSecretError ->
os.getenv fallback), mirroring whatsapp_common._get_wsecret / Slack #59739.
Scoped miss returns the default — no cross-profile environ borrow.
2026-08-02 10:00:44 -07:00
David Metcalfe 241bc112e8 fix(platforms): clear home channel when setup prompt left blank
Blank (or whitespace-only) answers to the home-channel prompt in the
interactive setup wizards previously left any previously saved
*_HOME_CHANNEL / *_HOME_ROOM env value in place, so operators could not
clear a stale home channel by re-running setup. Strip the prompt input
and call remove_env_value() on blank answers across the Discord, Slack,
Feishu, Matrix, Mattermost, WeCom and WhatsApp plugin setup wizards,
with per-adapter wizard tests covering set/clear/whitespace flows.

Squash of the three commits from PR #58421 (setup-wizard fix, matrix/
wecom extension, and 6-adapter test coverage) — one commit per
contributor on this salvage branch.

Fixes #12423
Salvaged from #58421
2026-07-23 12:01:24 -07:00
YLChen-007 38863322a4 Harden gateway mass mention handling 2026-07-22 21:14:44 -07:00
Jay Stothard 64746b4bd3 fix(gateway): validate multiplex adapter config by platform 2026-07-16 05:39:58 -07:00
Lohinth 1197d2bc96 fix(mattermost): accept leading-space slash commands 2026-07-05 14:41:57 -07:00
aaronagent d836b2bac4 fix(matrix,mattermost): invite auth check + API path traversal guard
Two platform-security hardenings:

- Matrix: _on_invite now checks the inviter against the existing
  allow-list (_allowed_user_ids / GATEWAY_ALLOW_ALL_USERS) before
  auto-joining. Without this any federated Matrix user could invite
  the bot into arbitrary rooms, exposing its presence and metadata.
  The message and reaction paths already enforce this allow-list; the
  invite path bypassed it.

- Mattermost: _api_get / _api_post / _api_put reject any path
  containing '..'. WebSocket-event values (channel_id, post_id,
  file_id) are interpolated directly into API paths, so a malicious or
  compromised server could craft traversal payloads to make the bot
  issue authenticated requests to arbitrary endpoints with its bearer
  token.

The configurable-E2EE-passphrase change from the original PR is dropped:
the matrix adapter was rewritten onto mautrix and the passphrase-protected
key-export file no longer exists.
2026-06-28 20:47:33 -07:00
teknium1 43b8ba4181 fix(telegram): preserve Bot API update queue on watcher reconnect
After a prolonged outage the in-process network-error ladder escalates to
fatal and GatewayRunner._platform_reconnect_watcher rebuilds a fresh adapter
that reconnects through the bootstrap path. That path called
start_polling(drop_pending_updates=True), discarding every update Telegram
queued during the outage — all messages sent while the bot was down were
silently lost. The in-process ladder and 409-conflict handler already passed
drop_pending_updates=False; only bootstrap did not distinguish a cold first
boot from a reconnect.

Thread an is_reconnect signal from the watcher through
_connect_adapter_with_timeout into adapter.connect(). The base
BasePlatformAdapter.connect() gains a keyword-only is_reconnect=False so every
adapter inherits a tolerant signature (no per-platform breakage when the
runner forwards the kwarg). Telegram translates is_reconnect into
drop_pending_updates=not is_reconnect on both the polling and webhook bootstrap
calls. Cold boot still drops the stale queue; a watcher reconnect preserves it.

Fixes #46621.

Co-authored-by: annguyenNous <annguyen@nousresearch.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
Co-authored-by: Kewe63 <Kewe63@users.noreply.github.com>
2026-06-25 21:29:57 -07:00
teknium e9cd8c5bf3 fix(delivery): drop env-var knob, flag all chunking adapters
Follow-up to ScotterMonk's cron-truncation fix:

- Remove HERMES_DELIVERY_MAX_PLATFORM_OUTPUT env var. Behavioral config
  belongs in config.yaml, not a new HERMES_* env var (.env is secrets
  only). The actual bug is fixed entirely by the adapter-aware skip; the
  configurable cap was unneeded scope. MAX_PLATFORM_OUTPUT is a constant
  again, collapsing the max_output=0 disable branch and the
  audit-vs-truncation threshold divergence.
- Flag the remaining verified-chunking adapters (slack, matrix, feishu,
  mattermost, teams, whatsapp, whatsapp_cloud, weixin, bluebubbles,
  yuanbao) with splits_long_messages=True so the fix covers the whole
  bug class, not just Discord/Telegram. Each verified to chunk in its
  own send() via truncate_message().
- SMS deliberately left False: it chunks for normal replies but a
  multi-segment cron blast is cost-bearing; the 4000-cap + file save is
  the safer default there.
- Update tests: drop the two env-override tests, add a test asserting a
  save failure during truncation (non-chunking) propagates.
2026-06-22 05:41:22 -07:00
Teknium 5a0e0d35b9 fix(mattermost): preserve thread-local delivery hygiene
Salvage the valid thread-routing pieces from #41640:
- route Mattermost progress/status sends through metadata thread IDs
- treat top-level Mattermost channel posts as thread roots for progress
- preserve thread metadata through media/file sends
- allow flat fallback only for final notify-worthy replies on confirmed broken roots

Co-authored-by: Wolfram Ravenwolf <github.com@wolfram.ravenwolf.de>
2026-06-15 15:06:23 -07:00
kshitijk4poor af973e4071 refactor(gateway): migrate Mattermost adapter to bundled plugin
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.
2026-05-24 18:05:33 -07:00