Commit graph

606 commits

Author SHA1 Message Date
roytian1217
8b1ff55f53 fix(wecom): strip @mention prefix in group chats for slash command recognition
In WeCom group chats, messages sent as "@BotName /command" arrive with
the @mention prefix intact. This causes is_command() to return False
since the text does not start with "/".

Strip the leading @mention in group messages before creating the
MessageEvent, mirroring the existing behavior in the Telegram adapter.
2026-04-23 02:00:56 -07:00
Nan93
2f48c58b85 fix: normalize iOS unicode dashes in slash command args
iOS auto-corrects -- to — (em dash) and - to – (en dash), causing
commands like /model glm-4.7 —provider zai to fail with
'Model names cannot contain spaces'. Normalize at get_command_args().
2026-04-22 21:30:32 -07:00
fengtianyu88
ec7e92082d fix(qqbot): add backoff upper-bound check for QQCloseError reconnect path
The QQCloseError (non-4008) reconnect path in _listen_loop was
missing the MAX_RECONNECT_ATTEMPTS upper-bound check that exists
in both the Exception handler (line 546) and the 4008 rate-limit
handler (line 486). Without this check, if _reconnect() fails
permanently for any non-4008 close code, backoff_idx grows
indefinitely and the bot retries forever at 60-second intervals
instead of giving up cleanly.

Fix: add the same guard after backoff_idx += 1 in the general
QQCloseError branch, consistent with the existing Exception path.
2026-04-22 21:16:16 -07:00
fuleinist
e371af1df2 Add config option to disable Discord slash commands
Add discord.slash_commands config option (default: true) to allow
users to disable Discord slash command registration when running
alongside other bots that use the same command names.

When set to false in config.yaml:
  discord:
    slash_commands: false

The _register_slash_commands() call is skipped while text-based
parsing of /commands continues to work normally.

Fixes #4881
2026-04-22 20:03:39 -07:00
huangke
6209e85e7d feat: support document/archive extensions in MEDIA: tag extraction
Add epub, pdf, zip, rar, 7z, docx, xlsx, pptx, txt, csv, apk, ipa to
the MEDIA: path regex in extract_media(). These file types were already
routed to send_document() in the delivery loop (base.py:1705), but the
extraction regex only matched media extensions (audio/video/image),
causing document paths to fall through to the generic \S+ branch which
could fail silently in some cases. This explicit list ensures reliable
matching and delivery for all common document formats.
2026-04-22 19:59:11 -07:00
Teknium
9bd1518425 fix(feishu): correct identity model docs and prefer tenant-scoped user_id
Feishu's open_id is app-scoped (same user gets different open_ids per
bot app), not a canonical identity. Functionally correct for single-bot
mode but semantically misleading.

- Add comprehensive Feishu identity model documentation to module docstring
- Prefer user_id (tenant-scoped) over open_id (app-scoped) in
  _resolve_sender_profile when both are available
- Document bot_open_id usage for @mention matching
- Update user_id_alt comment in SessionSource to be platform-generic

Ref: closes analysis from PR #8388 (closed as over-scoped)
2026-04-22 18:06:22 -07:00
Xiping Hu
c0df4a0a7f fix(email): accept **kwargs in send_document to handle metadata param 2026-04-22 17:34:05 -07:00
Teknium
51ca575994 feat(gateway): expose plugin slash commands natively on all platforms + decision-capable command hook
Plugin slash commands now surface as first-class commands in every gateway
enumerator — Discord native slash picker, Telegram BotCommand menu, Slack
/hermes subcommand map — without a separate per-platform plugin API.

The existing 'command:<name>' gateway hook gains a decision protocol via
HookRegistry.emit_collect(): handlers that return a dict with
{'decision': 'deny'|'handled'|'rewrite'|'allow'} can intercept slash
command dispatch before core handling runs, unifying what would otherwise
have been a parallel 'pre_gateway_command' hook surface.

Changes:

- gateway/hooks.py: add HookRegistry.emit_collect() that fires the same
  handler set as emit() but collects non-None return values. Backward
  compatible — fire-and-forget telemetry hooks still work via emit().
- hermes_cli/plugins.py: add optional 'args_hint' param to
  register_command() so plugins can opt into argument-aware native UI
  registration (Discord arg picker, future platforms).
- hermes_cli/commands.py: add _iter_plugin_command_entries() helper and
  merge plugin commands into telegram_bot_commands() and
  slack_subcommand_map(). New is_gateway_known_command() recognizes both
  built-in and plugin commands so the gateway hook fires for either.
- gateway/platforms/discord.py: extract _build_auto_slash_command helper
  from the COMMAND_REGISTRY auto-register loop and reuse it for
  plugin-registered commands. Built-in name conflicts are skipped.
- gateway/run.py: before normal slash dispatch, call emit_collect on
  command:<canonical> and honor deny/handled/rewrite/allow decisions.
  Hook now fires for plugin commands too.
- scripts/release.py: AUTHOR_MAP entry for @Magaav.
- Tests: emit_collect semantics, plugin command surfacing per platform,
  decision protocol (deny/handled/rewrite/allow + non-dict tolerance),
  Discord plugin auto-registration + conflict skipping, is_gateway_known_command.

Salvaged from #14131 (@Magaav). Original PR added a parallel
'pre_gateway_command' hook and a platform-keyed plugin command
registry; this re-implementation reuses the existing 'command:<name>'
hook and treats plugin commands as platform-agnostic so the same
capability reaches Telegram and Slack without new API surface.

Co-authored-by: Magaav <73175452+Magaav@users.noreply.github.com>
2026-04-22 16:23:21 -07:00
Roy-oss1
e86acad8f1 feat(feishu): preserve @mention context on inbound messages
Resolve Feishu @_user_N / @_all placeholders into display names plus a
structured [Mentioned: Name (open_id=...), ...] hint so agents can both
reason about who was mentioned and call Feishu OpenAPI tools with stable
open_ids. Strip bot self-mentions only at message edges (leading
unconditionally, trailing only before whitespace/terminal punctuation)
so commands parse cleanly while mid-text references are preserved.
Covers both plain-text and rich-post payloads.

Also fixes a pre-existing hydration bug: Client.request no longer accepts
the 'method' kwarg on lark-oapi 1.5.3, so bot identity silently failed
to hydrate and self-filtering never worked. Migrate to the
BaseRequest.builder() pattern and accept the 'app_name' field the API
actually returns. Tighten identity matching precedence so open_id is
authoritative when present on both sides.
2026-04-22 14:44:07 -07:00
kshitijk4poor
1f216ecbb4 feat(gateway/slack): add SLACK_REACTIONS env toggle for reaction lifecycle
Adds _reactions_enabled() gating to match Discord (DISCORD_REACTIONS) and
Telegram (TELEGRAM_REACTIONS) pattern. Defaults to true to preserve existing
behavior. Gates at three levels:
- _handle_slack_message: skips _reacting_message_ids registration
- on_processing_start: early return
- on_processing_complete: early return

Also adds config.yaml bridge (slack.reactions) and two new tests.
2026-04-22 08:49:24 -07:00
Roopak Nijhara
70a33708e7 fix(gateway/slack): align reaction lifecycle with Discord/Telegram pattern
Slack reactions were placed around handle_message(), which returns
immediately after spawning a background task. This caused the 👀 swap to happen before any real work began.

Fix: implement on_processing_start / on_processing_complete callbacks
(matching Discord/Telegram) so reactions bracket actual _message_handler
work driven by the base class.

Also fixes missing stop_typing() for Slack's assistant thread status
indicator, which left 'is thinking...' stuck in the UI after processing
completed.

- Add _reacting_message_ids set for DM/@mention-only gating
- Add _active_status_threads dict for stop_typing lookup
- Update test_reactions_in_message_flow for new callback pattern
- Add test_reactions_failure_outcome and test_reactions_skipped_for_non_dm_non_mention
2026-04-22 08:49:24 -07:00
WideLee
cf55c738e7 refactor(qqbot): migrate qr onboard flow to sync + consolidate into onboard.py
- Replace async create_bind_task/poll_bind_result with synchronous
  httpx.Client equivalents, eliminating manual event loop management
- Move _render_qr and full qr_register() entry-point into onboard.py,
  mirroring the Feishu onboarding pattern
- Remove _qqbot_render_qr and _qqbot_qr_flow from gateway.py (~90 lines);
  call site becomes a single qr_register() import
- Fix potential segfault: previous code called loop.close() in the EXPIRED
  branch and again in the finally block (double-close crashed under uvloop)
2026-04-22 05:50:21 -07:00
Teknium
b43524ecab fix(wecom): visible poll progress + clearer no-bot-info failure + docstring note
Follow-ups on top of salvaged #13923 (@keifergu):
- Print QR poll dot every 3s instead of every 18s so "Fetching
  configuration results..." doesn't look hung.
- On "status=success but no bot_info" from the WeCom query endpoint,
  log the full payload at WARNING and tell the user we're falling
  back to manual entry (was previously a single opaque line).
- Document in the qr_scan_for_bot_info() docstring that the
  work.weixin.qq.com/ai/qc/* endpoints are the admin-console web-UI
  flow, not the public developer API, and may change without notice.

Also add keifergu@tencent.com to scripts/release.py AUTHOR_MAP so
release notes attribute the feature correctly.
2026-04-22 05:15:32 -07:00
keifergu
8bcd77a9c2 feat(wecom): add QR scan flow and interactive setup wizard for bot credentials 2026-04-22 05:15:32 -07:00
Teknium
2aa983e2f2
feat(gateway): recognize .pdf in MEDIA: tag extraction (#13683)
PDFs emitted by tools (report generators, document exporters, etc.) now
deliver as native attachments when wrapped in MEDIA: — same as images,
audio, and video.

Bare .pdf paths are intentionally NOT added to extract_local_files(), so
the agent can still reference PDFs in text without auto-sending them.
2026-04-21 13:48:10 -07:00
Teknium
16accd44bd
fix(telegram): require TELEGRAM_WEBHOOK_SECRET in webhook mode (#13527)
When TELEGRAM_WEBHOOK_URL was set but TELEGRAM_WEBHOOK_SECRET was not,
python-telegram-bot received secret_token=None and the webhook endpoint
accepted any HTTP POST. Anyone who could reach the listener could inject
forged updates — spoofed user IDs, spoofed chat IDs, attacker-controlled
message text — and trigger handlers as if Telegram delivered them.

The fix refuses to start the adapter in webhook mode without the secret.
Polling mode (default, no webhook URL) is unaffected — polling is
authenticated by the bot token directly.

BREAKING CHANGE for webhook-mode deployments that never set
TELEGRAM_WEBHOOK_SECRET. The error message explains remediation:

  export TELEGRAM_WEBHOOK_SECRET="$(openssl rand -hex 32)"

and instructs registering it with Telegram via setWebhook's secret_token
parameter. Release notes must call this out.

Reported in GHSA-3vpc-7q5r-276h by @bupt-Yy-young. Hardening — not CVE
per SECURITY.md §3 "Public Exposure: Deploying the gateway to the
public internet without external authentication or network protection"
covers the historical default, but shipping a fail-open webhook as the
default was the wrong choice and the guard aligns us with the SECURITY.md
threat model.
2026-04-21 06:23:09 -07:00
unlinearity
155b619867 fix(agent): normalize socks:// env proxies for httpx/anthropic
WSL2 / Clash-style setups often export ALL_PROXY=socks://127.0.0.1:PORT. httpx and the Anthropic SDK reject that alias and expect socks5://, so agent startup failed early with "Unknown scheme for proxy URL" before any provider request could proceed.

Add shared normalize_proxy_url()/normalize_proxy_env_vars() helpers in utils.py and route all proxy entry points through them:
  - run_agent._get_proxy_from_env
  - agent.auxiliary_client._validate_proxy_env_urls
  - agent.anthropic_adapter.build_anthropic_client
  - gateway.platforms.base.resolve_proxy_url

Regression coverage:
  - run_agent proxy env resolution
  - auxiliary proxy env normalization
  - gateway proxy URL resolution

Verified with:
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 /home/nonlinear/.hermes/hermes-agent/venv/bin/pytest -o addopts='' -p pytest_asyncio.plugin tests/run_agent/test_create_openai_client_proxy_env.py tests/agent/test_proxy_and_url_validation.py tests/gateway/test_proxy_mode.py

39 passed.
2026-04-21 05:52:46 -07:00
pinion05
b0939d9210 fix: slash commands now respect require_mention in Telegram groups
When require_mention is enabled, slash commands no longer bypass
mention checks. Bare /command without @mention is filtered in groups,
while /command@botname (bot menu) and @botname /command still pass.

Commands still pass unconditionally when require_mention is disabled,
preserving backward compatibility.

Closes #6033
2026-04-21 03:06:56 -07:00
alt-glitch
28b3f49aaa refactor: remove remaining redundant local imports (comprehensive sweep)
Full AST-based scan of all .py files to find every case where a module
or name is imported locally inside a function body but is already
available at module level.  This is the second pass — the first commit
handled the known cases from the lint report; this one catches
everything else.

Files changed (19):

  cli.py                — 16 removals: time as _time/_t/_tmod (×10),
                           re / re as _re (×2), os as _os, sys,
                           partial os from combo import,
                           from model_tools import get_tool_definitions
  gateway/run.py        —  8 removals: MessageEvent as _ME /
                           MessageType as _MT (×3), os as _os2,
                           MessageEvent+MessageType (×2), Platform,
                           BasePlatformAdapter as _BaseAdapter
  run_agent.py          —  6 removals: get_hermes_home as _ghh,
                           partial (contextlib, os as _os),
                           cleanup_vm, cleanup_browser,
                           set_interrupt as _sif (×2),
                           partial get_toolset_for_tool
  hermes_cli/main.py    —  4 removals: get_hermes_home, time as _time,
                           logging as _log, shutil
  hermes_cli/config.py  —  1 removal:  get_hermes_home as _ghome
  hermes_cli/runtime_provider.py
                        —  1 removal:  load_config as _load_bedrock_config
  hermes_cli/setup.py   —  2 removals: importlib.util (×2)
  hermes_cli/nous_subscription.py
                        —  1 removal:  from hermes_cli.config import load_config
  hermes_cli/tools_config.py
                        —  1 removal:  from hermes_cli.config import load_config, save_config
  cron/scheduler.py     —  3 removals: concurrent.futures, json as _json,
                           from hermes_cli.config import load_config
  batch_runner.py       —  1 removal:  list_distributions as get_all_dists
                           (kept print_distribution_info, not at top level)
  tools/send_message_tool.py
                        —  2 removals: import os (×2)
  tools/skills_tool.py  —  1 removal:  logging as _logging
  tools/browser_camofox.py
                        —  1 removal:  from hermes_cli.config import load_config
  tools/image_generation_tool.py
                        —  1 removal:  import fal_client
  environments/tool_context.py
                        —  1 removal:  concurrent.futures
  gateway/platforms/bluebubbles.py
                        —  1 removal:  httpx as _httpx
  gateway/platforms/whatsapp.py
                        —  1 removal:  import asyncio
  tui_gateway/server.py —  2 removals: from datetime import datetime,
                           import time

All alias references (_time, _t, _tmod, _re, _os, _os2, _json, _ghh,
_ghome, _sif, _ME, _MT, _BaseAdapter, _load_bedrock_config, _httpx,
_logging, _log, get_all_dists) updated to use the top-level names.
2026-04-21 00:50:58 -07:00
alt-glitch
1010e5fa3c refactor: remove redundant local imports already available at module level
Sweep ~74 redundant local imports across 21 files where the same module
was already imported at the top level. Also includes type fixes and lint
cleanups on the same branch.
2026-04-21 00:50:58 -07:00
Yukipukii1
3f10c27cc0 fix(gateway/api_server): deduplicate concurrent idempotent requests 2026-04-20 22:13:07 -07:00
Es1la
3821921ef7 fix(whatsapp): kill bridge process tree on Windows disconnect 2026-04-20 20:49:32 -07:00
Dylan Socolobsky
2008e997dc fix(discord): handle properly /slash commands in channels 2026-04-20 14:56:04 -07:00
Dylan Socolobsky
11369a78f9 fix(telegram): handle parentheses in URLs during MarkdownV2 link conversion
The link regex in format_message used [^)]+ for the URL portion, which
  stopped at the first ) character. URLs with nested parentheses (e.g.
  Wikipedia links like Python_(programming_language)) were improperly parsed.

  Use a better regex, which is the same the Slack adapter uses.
2026-04-20 14:56:04 -07:00
Teknium
b65f6ca7fe
fix(telegram): actionable error for DM topics when Topics mode not enabled (#13162)
When createForumTopic fails with 'not a forum' in a private chat,
the error now tells the user exactly what to do: enable Topics in
the DM chat settings from the Telegram app.

Also adds a Prerequisites callout to the docs explaining this
client-side requirement before the config section.
2026-04-20 12:29:22 -07:00
MassiveMassimo
7972ff2a2c feat(whatsapp): add dm_policy and group_policy parity with WeCom/Weixin/QQ adapters
Add dm_policy and group_policy to the WhatsApp adapter, bringing parity
with WeCom/Weixin/QQ. Allows independent control of DM and group access:
disable DMs entirely, allowlist specific senders/groups, or keep open.

- dm_policy: open (default) | allowlist | disabled
- group_policy: open (default) | allowlist | disabled
- Config bridging for YAML → env vars
- 22 tests covering all policy combinations

Backward compatible — defaults preserve existing behavior.

Cherry-picked from PR #11597 by @MassiveMassimo.
Dropped the run.py group auth bypass (would have skipped user auth
for ALL platforms, not just WhatsApp).
2026-04-20 11:56:19 -07:00
JP Lew
9fdfb09aed fix(telegram): cache inbound videos and accept mp4 uploads 2026-04-20 05:10:23 -07:00
sprmn24
ed76185c15 feat(whatsapp): implement send_voice for audio message delivery
WhatsApp already receives incoming voice messages (audio/ogg via the
bridge) but lacked a send_voice implementation, so TTS and audio
responses fell back to the base class send_image path instead of being
delivered as native audio messages.

Route send_voice through the existing _send_media_to_bridge helper
with media_type='audio', matching the pattern used by send_video and
send_document.
2026-04-20 05:00:30 -07:00
Teknium
f683132c1d
feat(api-server): inline image inputs on /v1/chat/completions and /v1/responses (#12969)
OpenAI-compatible clients (Open WebUI, LobeChat, etc.) can now send vision
requests to the API server. Both endpoints accept the canonical OpenAI
multimodal shape:

  Chat Completions: {type: text|image_url, image_url: {url, detail?}}
  Responses:        {type: input_text|input_image, image_url: <str>, detail?}

The server validates and converts both into a single internal shape that the
existing agent pipeline already handles (Anthropic adapter converts,
OpenAI-wire providers pass through). Remote http(s) URLs and data:image/*
URLs are supported.

Uploaded files (file, input_file, file_id) and non-image data: URLs are
rejected with 400 unsupported_content_type.

Changes:

- gateway/platforms/api_server.py
  - _normalize_multimodal_content(): validates + normalizes both Chat and
    Responses content shapes. Returns a plain string for text-only content
    (preserves prompt-cache behavior on existing callers) or a canonical
    [{type:text|image_url,...}] list when images are present.
  - _content_has_visible_payload(): replaces the bare truthy check so a
    user turn with only an image no longer rejects as 'No user message'.
  - _handle_chat_completions and _handle_responses both call the new helper
    for user/assistant content; system messages continue to flatten to text.
  - Codex conversation_history, input[], and inline history paths all share
    the same validator. No duplicated normalizers.

- run_agent.py
  - _summarize_user_message_for_log(): produces a short string summary
    ('[1 image] describe this') from list content for logging, spinner
    previews, and trajectory writes. Fixes AttributeError when list
    user_message hit user_message[:80] + '...' / .replace().
  - _chat_content_to_responses_parts(): module-level helper that converts
    chat-style multimodal content to Responses 'input_text'/'input_image'
    parts. Used in _chat_messages_to_responses_input for Codex routing.
  - _preflight_codex_input_items() now validates and passes through list
    content parts for user/assistant messages instead of stringifying.

- tests/gateway/test_api_server_multimodal.py (new, 38 tests)
  - Unit coverage for _normalize_multimodal_content, including both part
    formats, data URL gating, and all reject paths.
  - Real aiohttp HTTP integration on /v1/chat/completions and /v1/responses
    verifying multimodal payloads reach _run_agent intact.
  - 400 coverage for file / input_file / non-image data URL.

- tests/run_agent/test_run_agent_multimodal_prologue.py (new)
  - Regression coverage for the prologue no-crash contract.
  - _chat_content_to_responses_parts round-trip coverage.

- website/docs/user-guide/features/api-server.md
  - Inline image examples for both endpoints.
  - Updated Limitations: files still unsupported, images now supported.

Validated live against openrouter/anthropic/claude-opus-4.6:
  POST /v1/chat/completions  → 200, vision-accurate description
  POST /v1/responses         → 200, same image, clean output_text
  POST /v1/chat/completions [file] → 400 unsupported_content_type
  POST /v1/responses [input_file]  → 400 unsupported_content_type
  POST /v1/responses [non-image data URL] → 400 unsupported_content_type

Closes #5621, #8253, #4046, #6632.

Co-authored-by: Paul Bergeron <paul@gamma.app>
Co-authored-by: zhangxicen <zhangxicen@example.com>
Co-authored-by: Manuel Schipper <manuelschipper@users.noreply.github.com>
Co-authored-by: pradeep7127 <pradeep7127@users.noreply.github.com>
2026-04-20 04:16:13 -07:00
Roy-oss1
520edd3499 feat(feishu): show processing state via reactions on user messages
Replaces the permanent "OK" receipt reaction with a 3-phase visual
lifecycle:

- Typing animation appears when the agent starts processing.
- Cleared when processing succeeds — the reply message is the signal.
- Replaced with CrossMark when processing fails.
- Cleared when processing is cancelled or interrupted.

When Feishu rejects the reaction-delete call, we keep the Typing in
place and skip adding CrossMark. Showing both at once would leave the
user seeing both "still working" and "done/failed" simultaneously,
which is worse than a stuck Typing.

A FEISHU_REACTIONS env var (default on) disables the whole lifecycle.
User-added reactions with the same emoji still route through to the
agent; only bot-origin reactions are filtered to break the feedback
loop.

Change-Id: I527081da31f0f9d59b451f45de59df4ddab522ba
2026-04-20 02:04:57 -07:00
Ruzzgar
f23123e7b4 fix(gateway): prevent scoped lock and resource leaks on connection failure 2026-04-20 01:44:36 -07:00
Junass1
4c50b4689e fix(gateway): make Telegram DM topic config writes atomic 2026-04-20 00:57:53 -07:00
helix4u
e96758291b fix(signal): normalize direct recipients to UUIDs 2026-04-20 00:35:55 -07:00
Teknium
e330112aa8 refactor(telegram): use entity-only mention detection
Replaces the word-boundary regex scan with pure MessageEntity-based
detection. Telegram's server emits MENTION entities for real @username
mentions and TEXT_MENTION entities for @FirstName mentions; the text-
scanning fallback was both redundant (entities are always present for
real mentions) and broken (matched raw substrings like email addresses,
URLs, code-block contents, and forwarded literal text).

Entity-only detection:
- Closes bug #12545 ("foo@hermes_bot.example" false positive).
- Also fixes edge cases the regex fix would still miss: @handles inside
  URLs and code blocks, where Telegram does not emit mention entities.

Tests rewritten to exercise realistic Telegram payloads (real mentions
carry entities; substring false positives don't).
2026-04-20 00:10:22 -07:00
Tranquil-Flow
1e18e0503f fix(telegram): use word-boundary matching for bot mention detection (#12545) 2026-04-20 00:10:22 -07:00
JackJin
6c0c625952 fix(gateway): accept finalize kwarg in all platform edit_message overrides
stream_consumer._send_or_edit unconditionally passes finalize= to
adapter.edit_message(), but only DingTalk's override accepted the
kwarg. Streaming on Telegram/Discord/Slack/Matrix/Mattermost/Feishu/
WhatsApp raised TypeError the first time a segment break or final
edit fired.

The REQUIRES_EDIT_FINALIZE capability flag only gates the redundant
final edit (and the identical-text short-circuit), not the kwarg
itself — so adapters that opt out of finalize still receive the
keyword argument and must accept it.

Add *, finalize: bool = False to the 7 non-DingTalk signatures; the
body ignores the arg since those platforms treat edits as stateless
(consistent with the base class contract in base.py).

Add a parametrized signature check over every concrete adapter class
so a future override cannot silently drop the kwarg — existing tests
use MagicMock which swallows any kwarg and cannot catch this.

Fixes #12579
2026-04-19 22:46:47 -07:00
Tranquil-Flow
6a228d52f7 fix(webhook): validate HMAC signature before rate limiting (#12544) 2026-04-19 22:45:08 -07:00
Teknium
014248567b fix(feishu): hydrate bot open_id for manual-setup users
Extends _hydrate_bot_identity() to also populate _bot_open_id (not just
_bot_name) by probing /open-apis/bot/v3/info — the same endpoint the
scan-to-create wizard uses. No extra scopes required beyond the tenant
access token.

Closes the manual-setup gap in #12450: users who configured Feishu
without running the wizard, and never set FEISHU_BOT_OPEN_ID, now get
a bot identity that _is_self_sent_bot_message() can actually use to
filter the adapter's own bot-sent events.

Each field is hydrated independently:
  - Env vars (FEISHU_BOT_OPEN_ID / FEISHU_BOT_USER_ID / FEISHU_BOT_NAME)
    still take precedence and skip their respective probe.
  - /bot/v3/info provides open_id + name.
  - Application-info endpoint remains as a best-effort fallback for
    bot_name only (needs admin:app.info:readonly scope).

Tests: 5 new cases covering env-var precedence, probe success, probe
failure fallback, and the end-to-end self-send filter gate after
hydration.
2026-04-19 11:36:04 -07:00
Bingo
2d54e17b82 fix(feishu): allow bot-originated mentions from other bots 2026-04-19 11:36:04 -07:00
Teknium
7e3b356574
refactor(discord): slim down the race-polish fix (#12644)
PR #12558 was heavy for what the fix actually is — essay-length
comments, a dedicated helper method where a setdefault would do, and
a source-inspection test with no real behavior coverage.  The
genuine code change is ~5 lines of new logic (1 field, 2 async with,
an on_ready wait block).

Trimmed:
- Replaced the 12-line _voice_lock_for helper with a setdefault
  one-liner at each call site (join_voice_channel, leave_voice_channel).
- Collapsed the 12-line comment on on_message's _ready_event wait to
  3 lines.  Dropped the warning log on timeout — pass-on-timeout is
  fine; if on_ready hangs that long, the bot is already broken and
  the log wouldn't help.
- Dropped the source-inspection test (greps the module source for
  expected substrings).  It was low-value scaffolding; the
  voice-serialization test covers actual behavior.

Net: -73 lines vs PR #12558.  Same two guarantees preserved, same
test passes (verified by stashing the fix and confirming failure).
2026-04-19 11:08:10 -07:00
Teknium
a521005fe5
fix(discord): close two low-severity adapter races (#12558)
Two small races in gateway/platforms/discord.py, bundled together
since they're adjacent in the adapter and both narrow in impact.

1. on_message vs _resolve_allowed_usernames (startup window)
   DISCORD_ALLOWED_USERS accepts both numeric IDs and raw usernames.
   At connect-time, _resolve_allowed_usernames walks the bot's guilds
   (fetch_members can take multiple seconds) to swap usernames for IDs.
   on_message can fire during that window; _is_allowed_user compares
   the numeric author.id against a set that may still contain raw
   usernames — legitimate users get silently rejected for a few
   seconds after every reconnect.

   Fix: on_message awaits _ready_event (with a 30s timeout) when it
   isn't already set.  on_ready sets the event after the resolve
   completes.  In steady state this is a no-op (event already set);
   only the startup / reconnect window ever blocks.

2. join_voice_channel check-and-connect
   The existing-connection check at _voice_clients.get() and the
   channel.connect() call straddled an await boundary with no lock.
   Two concurrent /voice channel invocations could both see None and
   both call connect(); discord.py raises ClientException
   ("Already connected") on the loser.  Same race class for leave
   running concurrently with _voice_timeout_handler.

   Fix: per-guild asyncio.Lock (_voice_locks dict with lazy alloc via
   _voice_lock_for).  join_voice_channel and leave_voice_channel both
   run their body under the lock.  Sequential within a guild, still
   fully concurrent across guilds.

Both: LOW severity.  The first only affects username-based allowlists
on fast-follow-up messages at startup; the second is a narrow
exception on simultaneous voice commands.  Bundled so the adapter
gets a single coherent polish pass.

Tests (tests/gateway/test_discord_race_polish.py): 2 regression cases.
- test_concurrent_joins_do_not_double_connect: two concurrent
  join_voice_channel calls on the same guild result in exactly one
  channel.connect() invocation.
- test_on_message_blocks_until_ready_event_set: asserts the expected
  wait pattern is present in on_message (source inspection, since
  full discord.py client setup isn't practical here).

Regression-guard validated: against unpatched gateway/platforms/discord.py
both tests fail.  With the fix they pass.  Full Discord suite (118
tests) green.
2026-04-19 05:45:59 -07:00
Teknium
206a449b29
feat(webhook): direct delivery mode for zero-LLM push notifications (#12473)
External services can now push plain-text notifications to a user's chat
via the webhook adapter without invoking the agent. Set deliver_only=true
on a route and the rendered prompt template becomes the literal message
body — dispatched directly to the configured target (Telegram, Discord,
Slack, GitHub PR comment, etc.).

Reuses all existing webhook infrastructure: HMAC-SHA256 signature
validation, per-route rate limiting, idempotency cache, body-size limits,
template rendering with dot-notation, home-channel fallback. No new HTTP
server, no new auth scheme, no new port.

Use cases: Supabase/Firebase webhooks → user notifications, monitoring
alert forwarding, inter-agent pings, background job completion alerts.

Changes:
- gateway/platforms/webhook.py: new _direct_deliver() helper + early
  dispatch branch in _handle_webhook when deliver_only=true. Startup
  validation rejects deliver_only with deliver=log.
- hermes_cli/main.py + hermes_cli/webhook.go: --deliver-only flag on
  subscribe; list/show output marks direct-delivery routes.
- website/docs/user-guide/messaging/webhooks.md: new Direct Delivery
  Mode section with config example, CLI example, response codes.
- skills/devops/webhook-subscriptions/SKILL.md: document --deliver-only
  with use cases (bumped to v1.1.0).
- tests/gateway/test_webhook_deliver_only.py: 14 new tests covering
  agent bypass, template rendering, status codes, HMAC still enforced,
  idempotency still applies, rate limit still applies, startup
  validation, and direct-deliver dispatch.

Validation: 78 webhook tests pass (64 existing + 14 new). E2E verified
with real aiohttp server + real urllib POST — agent not invoked, target
adapter.send() called with rendered template, duplicate delivery_id
suppressed.

Closes the gap identified in PR #12117 (thanks to @H1an1 / Antenna team)
without adding a second HTTP ingress server.
2026-04-19 05:18:19 -07:00
kshitijk4poor
957ca79e8e fix(feishu): drop dead helper and cover repeated fenced blocks 2026-04-19 03:30:36 -07:00
kshitijk4poor
a9debf10ff fix(feishu): harden fenced post row splitting 2026-04-19 03:30:36 -07:00
sgaofen
cc59d133dc fix(feishu): split fenced code blocks in post payload 2026-04-19 03:30:36 -07:00
kshitijk4poor
4b6ff0eb7f fix: tighten gateway interrupt salvage follow-ups
Follow-up on top of the helix4u #12388 cherry-picks:
- make deferred post-delivery callbacks generation-aware end-to-end so
  stale runs cannot clear callbacks registered by a fresher run for the
  same session
- bind callback ownership to the active session event at run start and
  snapshot that generation inside base adapter processing so later event
  mutation cannot retarget cleanup
- pass run_generation through proxy mode and drop stale proxy streams /
  final results the same way local runs are dropped
- centralize stop/new interrupt cleanup into one helper and replace the
  open-coded branches with shared logic
- unify internal control interrupt reason strings via shared constants
- remove the return from base.py's finally block so cleanup no longer
  swallows cancellation/exception flow
- add focused regressions for generation forwarding, proxy stale
  suppression, and newer-callback preservation

This addresses all review findings from the initial #12388 review while
keeping the fix scoped to stale-output/typing-loop interrupt handling.
2026-04-19 03:03:57 -07:00
helix4u
8466268ca5 fix(gateway): keep typing loop overrides backward-compatible 2026-04-19 03:03:57 -07:00
helix4u
150382e8b7 fix(gateway): stop typing loops on session interrupt 2026-04-19 03:03:57 -07:00
kshitijk4poor
ff63e2e005 fix: tighten telegram docker-media salvage follow-ups
Follow-up on top of the helix4u #6392 cherry-pick:
- reuse one helper for actionable Docker-local file-not-found errors
  across document/image/video/audio local-media send paths
- include /outputs/... alongside /output/... in the container-local
  path hint
- soften the gateway startup warning so it does not imply custom
  host-visible mounts are broken; the warning now targets the specific
  risky pattern of emitting container-local MEDIA paths without an
  explicit export mount
- add focused regressions for /outputs/... and non-document media hint
  coverage

This keeps the salvage aligned with the actual MEDIA delivery problem on
current main while reducing false-positive operator messaging.
2026-04-19 01:55:33 -07:00
helix4u
588333908c fix(telegram): warn on docker-only media paths 2026-04-19 01:55:33 -07:00