{"repo":"NousResearch/hermes-agent","public_id":"32188","display_label":"Telegram adapter missing is_bot flag for auth filtering","summary":"A bug where the Telegram adapter does not populate the source.is_bot field, causing bot messages to bypass authentication filters and unintentionally trigger the agent loop. Multiple fixes and approaches address setting this flag and applying bot auth policy.","subsystem":"gateway","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":4},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":54052},"resolution":{"state":"resolved","by":"pr-54052"},"freshness":{"state":"verified","last_verified_at":"2026-06-28T07:57:03+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3333333333333333},"fix_edges":[{"issue_number":32188,"pr_number":54052,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":32188,"title":"[Bug]: Telegram adapter never sets `source.is_bot` — bot messages bypass auth filter and trigger agent loop","url":null,"state":"closed"},{"kind":"pr","number":32192,"title":"fix(gateway): pass is_bot flag for Telegram bot message filtering","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54052","number":54052},{"kind":"pr","value":"#32188","number":32188},{"kind":"pr","value":"#33830","number":33830}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":32437,"title":"fix(gateway): populate source.is_bot in Telegram adapter","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54052","number":54052},{"kind":"pr","value":"#32188","number":32188}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":32476,"title":"fix(gateway): set source.is_bot from user.is_bot in Telegram adapter; add TELEGRAM_ALLOW_BOTS (#32188)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54052","number":54052},{"kind":"pr","value":"#32188","number":32188},{"kind":"pr","value":"#33830","number":33830}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":33830,"title":"fix(telegram): apply bot auth policy to Telegram sources","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"fc70d023d","number":0},{"kind":"pr","value":"#54052","number":54052}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":54052,"title":"fix(telegram): populate source.is_bot so bot messages hit the auth filter (#32188)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/authz_mixin.py","plugins/platforms/telegram/adapter.py","tests/gateway/test_config.py","tests/gateway/test_discord_bot_auth_bypass.py","tests/gateway/test_feishu_bot_auth_bypass.py","tests/gateway/test_telegram_bot_auth_bypass.py"],"fix_hunks":[{"file":"gateway/authz_mixin.py","hunks":["@@ -329,6 +329,7 @@ def _is_user_authorized(self, source: SessionSource) -> bool:\n         platform_allow_bots_map = {\n             Platform.DISCORD: \"DISCORD_ALLOW_BOTS\",\n             Platform.FEISHU: \"FEISHU_ALLOW_BOTS\",\n+            Platform.TELEGRAM: \"TELEGRAM_ALLOW_BOTS\",\n         }\n \n         # Plugin platforms: check the registry for auth env var names"]},{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -7253,6 +7253,7 @@ def _build_message_event(\n             thread_id=thread_id_str,\n             chat_topic=chat_topic,\n             message_id=str(message.message_id),\n+            is_bot=bool(getattr(user, \"is_bot\", False)) if user else False,\n         )\n         \n         # Extract reply context if this message is a reply.","@@ -7528,6 +7529,8 @@ def _apply_yaml_config(yaml_cfg: dict, telegram_cfg: dict) -> dict | None:\n         os.environ[\"TELEGRAM_MENTION_PATTERNS\"] = _json.dumps(telegram_cfg[\"mention_patterns\"])\n     if \"exclusive_bot_mentions\" in telegram_cfg and not os.getenv(\"TELEGRAM_EXCLUSIVE_BOT_MENTIONS\"):\n         os.environ[\"TELEGRAM_EXCLUSIVE_BOT_MENTIONS\"] = str(telegram_cfg[\"exclusive_bot_mentions\"]).lower()\n+    if \"allow_bots\" in telegram_cfg and not os.getenv(\"TELEGRAM_ALLOW_BOTS\"):\n+        os.environ[\"TELEGRAM_ALLOW_BOTS\"] = str(telegram_cfg[\"allow_bots\"]).lower()\n     if \"guest_mode\" in telegram_cfg and not os.getenv(\"TELEGRAM_GUEST_MODE\"):\n         os.environ[\"TELEGRAM_GUEST_MODE\"] = str(telegram_cfg[\"guest_mode\"]).lower()\n     if \"observe_unmentioned_group_messages\" in telegram_cfg and not os.getenv(\"TELEGRAM_OBSERVE_UNMENTIONED_GROUP_MESSAGES\"):"]},{"file":"tests/gateway/test_config.py","hunks":["@@ -840,6 +840,38 @@ def test_feishu_allow_bots_env_takes_precedence_over_config_yaml(self, tmp_path,\n \n         assert os.environ.get(\"FEISHU_ALLOW_BOTS\") == \"none\"\n \n+    def test_bridges_telegram_allow_bots_from_config_yaml_to_env(self, tmp_path, monkeypatch):\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        config_path = hermes_home / \"config.yaml\"\n+        config_path.write_text(\n+            \"telegram:\\n  allow_bots: mentions\\n\",\n+            encoding=\"utf-8\",\n+        )\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+        monkeypatch.delenv(\"TELEGRAM_ALLOW_BOTS\", raising=False)\n+\n+        load_gateway_config()\n+\n+        assert os.environ.get(\"TELEGRAM_ALLOW_BOTS\") == \"mentions\"\n+\n+    def test_telegram_allow_bots_env_takes_precedence_over_config_yaml(self, tmp_path, monkeypatch):\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        config_path = hermes_home / \"config.yaml\"\n+        config_path.write_text(\n+            \"telegram:\\n  allow_bots: all\\n\",\n+            encoding=\"utf-8\",\n+        )\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+        monkeypatch.setenv(\"TELEGRAM_ALLOW_BOTS\", \"none\")\n+\n+        load_gateway_config()\n+\n+        assert os.environ.get(\"TELEGRAM_ALLOW_BOTS\") == \"none\"\n+\n     def test_invalid_quick_commands_in_config_yaml_are_ignored(self, tmp_path, monkeypatch):\n         hermes_home = tmp_path / \".hermes\"\n         hermes_home.mkdir()"]},{"file":"tests/gateway/test_discord_bot_auth_bypass.py","hunks":["@@ -31,6 +31,7 @@ def _isolate_discord_env(monkeypatch):\n         \"DISCORD_ALLOWED_USERS\",\n         \"DISCORD_ALLOWED_ROLES\",\n         \"DISCORD_ALLOW_ALL_USERS\",\n+        \"TELEGRAM_ALLOW_BOTS\",\n         \"GATEWAY_ALLOW_ALL_USERS\",\n         \"GATEWAY_ALLOWED_USERS\",\n     ):"]},{"file":"tests/gateway/test_feishu_bot_auth_bypass.py","hunks":["@@ -21,6 +21,7 @@ def _isolate_feishu_env(monkeypatch):\n         \"FEISHU_ALLOW_BOTS\",\n         \"FEISHU_ALLOWED_USERS\",\n         \"FEISHU_ALLOW_ALL_USERS\",\n+        \"TELEGRAM_ALLOW_BOTS\",\n         \"GATEWAY_ALLOW_ALL_USERS\",\n         \"GATEWAY_ALLOWED_USERS\",\n     ):"]},{"file":"tests/gateway/test_telegram_bot_auth_bypass.py","hunks":["@@ -0,0 +1,158 @@\n+\"\"\"Regression guard for Telegram bot-origin authorization (#32188).\"\"\"\n+\n+from types import SimpleNamespace\n+from unittest.mock import MagicMock, patch\n+\n+import pytest\n+\n+from gateway.session import Platform, SessionSource\n+\n+\n[redacted:email](autouse=True)\n+def _isolate_telegram_env(monkeypatch):\n+    for var in (\n+        \"TELEGRAM_ALLOW_BOTS\",\n+        \"TELEGRAM_ALLOWED_USERS\",\n+        \"TELEGRAM_ALLOW_ALL_USERS\",\n+        \"TELEGRAM_GROUP_ALLOWED_USERS\",\n+        \"TELEGRAM_GROUP_ALLOWED_CHATS\",\n+        \"GATEWAY_ALLOW_ALL_USERS\",\n+        \"GATEWAY_ALLOWED_USERS\",\n+    ):\n+        monkeypatch.delenv(var, raising=False)\n+\n+\n+def _make_bare_runner():\n+    from gateway.run import GatewayRunner\n+\n+    runner = object.__new__(GatewayRunner)\n+    runner.pairing_store = SimpleNamespace(is_approved=lambda *_a, **_kw: False)\n+    return runner\n+\n+\n+def _make_telegram_bot_source(bot_id: str = \"999888777\"):\n+    return SessionSource(\n+        platform=Platform.TELEGRAM,\n+        chat_id=\"123\",\n+        chat_type=\"dm\",\n+        user_id=bot_id,\n+        user_name=\"OtherProfileBot\",\n+        is_bot=True,\n+    )\n+\n+\n+def _make_telegram_human_source(user_id: str = \"100200300\"):\n+    return SessionSource(\n+        platform=Platform.TELEGRAM,\n+        chat_id=\"123\",\n+        chat_type=\"dm\",\n+        user_id=user_id,\n+        user_name=\"SomeHuman\",\n+        is_bot=False,\n+    )\n+\n+\n+def test_telegram_bot_authorized_when_allow_bots_mentions(monkeypatch):\n+    runner = _make_bare_runner()\n+    monkeypatch.setenv(\"TELEGRAM_ALLOW_BOTS\", \"mentions\")\n+    monkeypatch.setenv(\"TELEGRAM_ALLOWED_USERS\", \"100200300\")\n+\n+    assert runner._is_user_authorized(_make_telegram_bot_source(\"999888777\")) is True\n+\n+\n+def test_telegram_bot_authorized_when_allow_bots_all(monkeypatch):\n+    runner = _make_bare_runner()\n+    monkeypatch.setenv(\"TELEGRAM_ALLOW_BOTS\", \"all\")\n+    monkeypatch.setenv(\"TELEGRAM_ALLOWED_USERS\", \"100200300\")\n+\n+    assert runner._is_user_authorized(_make_telegram_bot_source()) is True\n+\n+\n+def test_telegram_bot_not_authorized_when_allow_bots_unset(monkeypatch):\n+    runner = _make_bare_runner()\n+    monkeypatch.setenv(\"TELEGRAM_ALLOWED_USERS\", \"100200300\")\n+\n+    assert runner._is_user_authorized(_make_telegram_bot_source(\"999888777\")) is False\n+\n+\n+def test_telegram_bot_not_authorized_when_allow_bots_none(monkeypatch):\n+    runner = _make_bare_runner()\n+    monkeypatch.setenv(\"TELEGRAM_ALLOW_BOTS\", \"none\")\n+    monkeypatch.setenv(\"TELEGRAM_ALLOWED_USERS\", \"100200300\")\n+\n+    assert runner._is_user_authorized(_make_telegram_bot_source(\"999888777\")) is False\n+\n+\n+def test_telegram_human_still_checked_against_allowlist_when_bot_policy_set(monkeypatch):\n+    runner = _make_bare_runner()\n+    monkeypatch.setenv(\"TELEGRAM_ALLOW_BOTS\", \"all\")\n+    monkeypatch.setenv(\"TELEGRAM_ALLOWED_USERS\", \"100200300\")\n+\n+    assert runner._is_user_authorized(_make_telegram_human_source(\"999999999\")) is False\n+    assert runner._is_user_authorized(_make_telegram_human_source(\"100200300\")) is True\n+\n+\n+def _build_telegram_message(*, is_bot: bool):\n+    user = SimpleNamespace(\n+        id=999888777 if is_bot else 100200300,\n+        full_name=\"OtherProfileBot\" if is_bot else \"Alice\",\n+        is_bot=is_bot,\n+    )\n+    chat = SimpleNamespace(\n+        id=123,\n+        type=\"private\",\n+        title=None,\n+        full_name=\"Alice\",\n+        is_forum=False,\n+    )\n+    message = MagicMock()\n+    message.from_user = user\n+    message.chat = chat\n+    message.message_id = 4242\n+    message.message_thread_id = None\n+    message.is_topic_message = False\n+    message.forum_topic_created = None\n+    message.reply_to_message = None\n+    message.quote = None\n+    message.text = \"hello\"\n+    message.caption = None\n+    return message\n+\n+\n+def _capture_build_source_is_bot(is_bot: bool):\n+    from gateway.platforms.base import MessageType\n+    from plugins.platforms.telegram.adapter import TelegramAdapter\n+\n+    adapter = object.__new__(TelegramAdapter)\n+    adapter.platform = Platform.TELEGRAM\n+    adapter.config = SimpleNamespace(extra={})\n+    message = _build_telegram_message(is_bot=is_bot)\n+    captured: dict = {}\n+\n+    def fake_build_source(**kwargs):\n+        captured.update(kwargs)\n+        return SessionSource(\n+            platform=Platform.TELEGRAM,\n+            chat_id=str(kwargs.get(\"chat_id\") or \"\"),\n+            chat_type=kwargs.get(\"chat_type\") or \"dm\",\n+            user_id=kwargs.get(\"user_id\"),\n+            is_bot=kwargs.get(\"is_bot\", False),\n+        )\n+\n+    with patch.object(adapter, \"build_source\", side_effect=fake_build_source):\n+        try:\n+            adapter._build_message_event(message, MessageType.TEXT, update_id=1)\n+        except Exception:\n+            # The method may continue into PTB-specific optional fields after\n+            # source construction; this test only pins the source kwarg.\n+            pass\n+\n+    return captured.get(\"is_bot\")\n+\n+\n+def test_telegram_adapter_propagates_is_bot_true():\n+    assert _capture_build_source_is_bot(True) is True\n+\n+\n+def test_telegram_adapter_propagates_is_bot_false():\n+    assert _capture_build_source_is_bot(False) is False\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}