{"repo":"NousResearch/hermes-agent","public_id":"1973","display_label":"Matrix E2EE dependency installation fixes","summary":"These issues and pull requests address missing or broken Matrix E2EE dependencies during setup and fresh installs, including auto-installation of matrix-nio and related companion packages. Several fixes also adjust plugin connection gating and Discord auto-enable behavior as part of the same setup repair effort.","subsystem":"gateway","counts":{"members":9,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":2,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":31688},"resolution":{"state":"partially_resolved","by":"pr-31688"},"freshness":{"state":"verified","last_verified_at":"2026-05-24T22:44:27+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.20444444444444443},"fix_edges":[{"issue_number":31116,"pr_number":31688,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":31116,"pr_number":31703,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":1973,"title":"[Bug]: Matrix messaging channel not functional","url":null,"state":"closed"},{"kind":"issue","number":31116,"title":"[Bug]: Matrix gateway configuration on a fresh install does not install properly","url":null,"state":"closed"},{"kind":"pr","number":1978,"title":"fix(setup): auto-install matrix-nio during hermes setup","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":1979,"title":"fix(install): include Matrix support in the default full install","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":31232,"title":"fix(setup): install missing Matrix E2EE companion deps","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"54e61f93318d579b145421c447437ec345021eb1","number":0},{"kind":"pr","value":"#31236","number":31236}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":31236,"title":"fix(gateway+setup): repair Matrix E2EE deps and stop Discord auto-enable on fresh installs (#31116)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"54e61f933","number":0},{"kind":"pr","value":"#31703","number":31703}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":31688,"title":"fix(matrix,gateway): Matrix E2EE installs full dep set; plugins respect is_connected","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/config.py","gateway/platforms/matrix.py","hermes_cli/setup.py","tests/gateway/test_matrix.py","tests/gateway/test_platform_registry.py"],"fix_hunks":[{"file":"gateway/config.py","hunks":["@@ -1811,6 +1811,17 @@ def _apply_env_overrides(config: GatewayConfig) -> None:\n     # need to seed ``PlatformConfig.extra`` from env vars (e.g. Google Chat's\n     # project_id / subscription_name) can supply ``env_enablement_fn`` on\n     # their PlatformEntry — called here BEFORE adapter construction.\n+    #\n+    # Enablement gate (#31116): when a plugin registers ``is_connected``\n+    # (the \"has the user actually configured credentials for this?\" check),\n+    # we MUST consult it before flipping ``enabled = True``.  Otherwise\n+    # ``check_fn`` alone — which for adapter plugins typically just\n+    # verifies the SDK is importable / lazy-installs it — silently enables\n+    # platforms the user never opted into, and the gateway then tries to\n+    # connect to Discord / Teams / Google Chat with no token and emits\n+    # noisy retry-forever errors.  ``_platform_status`` was already fixed\n+    # for the same bug class in commit 7849a3d73; this is the runtime\n+    # counterpart.\n     try:\n         from hermes_cli.plugins import discover_plugins\n         discover_plugins()  # idempotent","@@ -1823,6 +1834,29 @@ def _apply_env_overrides(config: GatewayConfig) -> None:\n                 logger.debug(\"check_fn for %s raised: %s\", entry.name, e)\n                 continue\n             platform = Platform(entry.name)\n+            existing_cfg = config.platforms.get(platform)\n+            # Only consult is_connected for platforms that are NOT already\n+            # explicitly configured in YAML / env (existing_cfg with\n+            # enabled=True means the user wrote it themselves or another\n+            # env-var bridge enabled it — keep that decision).\n+            if existing_cfg is None or not existing_cfg.enabled:\n+                if entry.is_connected is not None:\n+                    try:\n+                        probe_cfg = existing_cfg or PlatformConfig()\n+                        configured = bool(entry.is_connected(probe_cfg))\n+                    except Exception as exc:\n+                        logger.debug(\n+                            \"is_connected for %s raised: %s — skipping enablement\",\n+                            entry.name, exc,\n+                        )\n+                        configured = False\n+                    if not configured:\n+                        logger.debug(\n+                            \"Plugin platform '%s' available but not configured \"\n+                            \"(is_connected returned False) — skipping enable\",\n+                            entry.name,\n+                        )\n+                        continue\n             if platform not in config.platforms:\n                 config.platforms[platform] = PlatformConfig()\n             config.platforms[platform].enabled = True"]},{"file":"gateway/platforms/matrix.py","hunks":["@@ -138,7 +138,8 @@ def __init__(self, session_key: str, chat_id: str, message_id: str, resolved: bo\n )\n \n _E2EE_INSTALL_HINT = (\n-    \"Install with: pip install 'mautrix[encryption]'  (requires libolm C library)\"\n+    \"Install with: pip install 'mautrix[encryption]' asyncpg aiosqlite  \"\n+    \"(requires libolm C library)\"\n )\n \n _MATRIX_IMAGE_FILENAME_EXTS = frozenset({","@@ -214,9 +215,22 @@ def _create_matrix_session(proxy_url: str | None):\n \n \n def _check_e2ee_deps() -> bool:\n-    \"\"\"Return True if mautrix E2EE dependencies (python-olm) are available.\"\"\"\n+    \"\"\"Return True if mautrix E2EE dependencies are available.\n+\n+    Verifies python-olm (via mautrix.crypto.OlmMachine), the SQLite crypto\n+    store backend (mautrix.crypto.store.asyncpg.PgCryptoStore — yes, the\n+    PgCryptoStore class also drives the sqlite backend in mautrix 0.21),\n+    and the database drivers actually used at connect time (``asyncpg`` for\n+    the underlying upgrade_table machinery, ``aiosqlite`` for the\n+    ``sqlite:///`` URL we pass to ``Database.create``).  Without all four,\n+    encrypted rooms fail at connect time with a confusing\n+    ``No module named 'asyncpg'`` (#31116).\n+    \"\"\"\n     try:\n         from mautrix.crypto import OlmMachine  # noqa: F401\n+        from mautrix.crypto.store.asyncpg import PgCryptoStore  # noqa: F401\n+        import asyncpg  # noqa: F401\n+        import aiosqlite  # noqa: F401\n \n         return True\n     except (ImportError, AttributeError):","@@ -226,8 +240,13 @@ def _check_e2ee_deps() -> bool:\n def check_matrix_requirements() -> bool:\n     \"\"\"Return True if the Matrix adapter can be used.\n \n-    Lazy-installs mautrix via ``tools.lazy_deps.ensure(\"platform.matrix\")``\n-    on first call if not present. Rebinds all module-level type globals on success.\n+    Lazy-installs the full ``platform.matrix`` feature group via\n+    ``tools.lazy_deps.ensure_and_bind`` whenever any of the declared\n+    packages (mautrix, Markdown, aiosqlite, asyncpg, aiohttp-socks) is\n+    missing — not just mautrix itself.  Previously this short-circuited on\n+    ``import mautrix``, which left the other four packages uninstalled\n+    forever and broke E2EE connect with ``No module named 'asyncpg'``\n+    (#31116).  Rebinds module-level type globals on success.\n     \"\"\"\n     token = [redacted:secret-assignment]\"MATRIX_ACCESS_TOKEN\", \"\")\n     password = [redacted:secret-assignment]\"MATRIX_PASSWORD\", \"\")","@@ -239,9 +258,20 @@ def check_matrix_requirements() -> bool:\n     if not homeserver:\n         logger.warning(\"Matrix: MATRIX_HOMESERVER not set\")\n         return False\n+\n+    # Check whether any package in the platform.matrix feature group is\n+    # missing.  ``feature_missing`` is cheap (per-spec importlib.metadata\n+    # lookups) and correctly handles ``mautrix[encryption]`` by stripping\n+    # the extras marker before checking the bare package.\n     try:\n-        import mautrix  # noqa: F401\n-    except ImportError:\n+        from tools.lazy_deps import feature_missing, ensure_and_bind\n+        missing = feature_missing(\"platform.matrix\")\n+    except Exception as exc:  # pragma: no cover — defensive\n+        logger.debug(\"Matrix: lazy_deps lookup failed: %s\", exc)\n+        missing = ()\n+        ensure_and_bind = None  # type: ignore[assignment]\n+\n+    if missing or ensure_and_bind is None:\n         def _import():\n             from mautrix.types import (\n                 ContentURI, EventID, EventType, PaginationDirection,","@@ -261,10 +291,14 @@ def _import():\n                 \"UserID\": UserID,\n             }\n \n-        from tools.lazy_deps import ensure_and_bind\n+        if ensure_and_bind is None:\n+            return False\n         if not ensure_and_bind(\"platform.matrix\", _import, globals(), prompt=False):\n             logger.warning(\n-                \"Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]'\"\n+                \"Matrix: required packages not installed (%s). \"\n+                \"Run: pip install 'mautrix[encryption]' asyncpg aiosqlite \"\n+                \"Markdown aiohttp-socks\",\n+                \", \".join(missing) if missing else \"platform.matrix\",\n             )\n             return False\n "]},{"file":"hermes_cli/setup.py","hunks":["@@ -2188,28 +2188,58 @@ def _setup_matrix():\n             print_success(\"E2EE enabled\")\n \n         matrix_pkg = \"mautrix[encryption]\" if want_e2ee else \"mautrix\"\n+        # Use the central lazy-deps feature group so we install ALL of\n+        # platform.matrix's dependencies (mautrix, Markdown, aiosqlite,\n+        # asyncpg, aiohttp-socks) — not just mautrix itself.  The previous\n+        # hand-rolled ``pip install mautrix[encryption]`` left asyncpg /\n+        # aiosqlite uninstalled and broke E2EE connect with\n+        # ``No module named 'asyncpg'`` on every fresh install (#31116).\n         try:\n-            __import__(\"mautrix\")\n-        except ImportError:\n-            print_info(f\"Installing {matrix_pkg}...\")\n-            import subprocess\n-            uv_bin = shutil.which(\"uv\")\n-            if uv_bin:\n-                result = subprocess.run(\n-                    [uv_bin, \"pip\", \"install\", \"--python\", sys.executable, matrix_pkg],\n-                    capture_output=True, text=True,\n-                )\n-            else:\n-                result = subprocess.run(\n-                    [sys.executable, \"-m\", \"pip\", \"install\", matrix_pkg],\n-                    capture_output=True, text=True,\n+            from tools.lazy_deps import ensure as _lazy_ensure, feature_missing\n+            _missing_before = feature_missing(\"platform.matrix\")\n+            if _missing_before:\n+                print_info(\n+                    f\"Installing {matrix_pkg} (+ {len(_missing_before)} runtime deps)...\"\n                 )\n-            if result.returncode == 0:\n-                print_success(f\"{matrix_pkg} installed\")\n-            else:\n-                print_warning(f\"Install failed — run manually: pip install '{matrix_pkg}'\")\n-                if result.stderr:\n-                    print_info(f\"  Error: {result.stderr.strip().splitlines()[-1]}\")\n+                try:\n+                    _lazy_ensure(\"platform.matrix\", prompt=False)\n+                    print_success(f\"{matrix_pkg} installed\")\n+                except Exception as exc:\n+                    print_warning(\n+                        f\"Install failed — run manually: pip install \"\n+                        f\"'mautrix[encryption]' asyncpg aiosqlite Markdown \"\n+                        f\"aiohttp-socks\"\n+                    )\n+                    print_info(f\"  Error: {exc}\")\n+        except ImportError:\n+            # tools.lazy_deps unavailable (extreme edge case — partial\n+            # install).  Fall back to the legacy single-package install\n+            # path so the wizard still does *something*.\n+            try:\n+                __import__(\"mautrix\")\n+            except ImportError:\n+                print_info(f\"Installing {matrix_pkg}...\")\n+                import subprocess\n+                uv_bin = shutil.which(\"uv\")\n+                if uv_bin:\n+                    result = subprocess.run(\n+                        [uv_bin, \"pip\", \"install\", \"--python\", sys.executable, matrix_pkg],\n+                        capture_output=True, text=True,\n+                    )\n+                else:\n+                    result = subprocess.run(\n+                        [sys.executable, \"-m\", \"pip\", \"install\", matrix_pkg],\n+                        capture_output=True, text=True,\n+                    )\n+                if result.returncode == 0:\n+                    print_success(f\"{matrix_pkg} installed\")\n+                else:\n+                    print_warning(\n+                        f\"Install failed — run manually: pip install \"\n+                        f\"'{matrix_pkg}' asyncpg aiosqlite Markdown aiohttp-socks\"\n+                    )\n+                    if result.stderr:\n+                        print_info(f\"  Error: {result.stderr.strip().splitlines()[-1]}\")\n \n         print()\n         print_info(\"🔒 Security: Restrict who can use your bot\")"]},{"file":"tests/gateway/test_matrix.py","hunks":["@@ -797,6 +797,79 @@ def test_check_requirements_encryption_true_with_e2ee_deps(self, monkeypatch):\n                 with patch(\"tools.lazy_deps.ensure\", side_effect=ImportError(\"mautrix unavailable\")):\n                     assert matrix_mod.check_matrix_requirements() is False\n \n+    def test_check_e2ee_deps_requires_asyncpg(self, monkeypatch):\n+        \"\"\"E2EE deps check must reject when asyncpg is missing — even if olm is present.\n+\n+        Regression for #31116: ``mautrix[encryption]`` extra installs python-olm\n+        but NOT asyncpg/aiosqlite, which are required by mautrix's crypto store\n+        at connect time.  ``_check_e2ee_deps`` previously only tested\n+        ``OlmMachine`` import and returned True, so the failure manifested as\n+        a confusing ``No module named 'asyncpg'`` deep in\n+        ``MatrixAdapter.connect()``.\n+        \"\"\"\n+        from gateway.platforms.matrix import _check_e2ee_deps\n+        import builtins\n+        real_import = builtins.__import__\n+\n+        def _blocking_import(name, *args, **kwargs):\n+            if name == \"asyncpg\" or name.startswith(\"asyncpg.\"):\n+                raise ImportError(\"blocked for test\")\n+            return real_import(name, *args, **kwargs)\n+\n+        with patch.object(builtins, \"__import__\", _blocking_import):\n+            assert _check_e2ee_deps() is False\n+\n+    def test_check_e2ee_deps_requires_aiosqlite(self):\n+        \"\"\"E2EE deps check must reject when aiosqlite is missing.\n+\n+        Mautrix's ``Database.create(\"sqlite:///...\")`` driver lookup imports\n+        aiosqlite lazily — without it, connect fails at ``crypto_db.start()``.\n+        \"\"\"\n+        from gateway.platforms.matrix import _check_e2ee_deps\n+        import builtins\n+        real_import = builtins.__import__\n+\n+        def _blocking_import(name, *args, **kwargs):\n+            if name == \"aiosqlite\" or name.startswith(\"aiosqlite.\"):\n+                raise ImportError(\"blocked for test\")\n+            return real_import(name, *args, **kwargs)\n+\n+        with patch.object(builtins, \"__import__\", _blocking_import):\n+            assert _check_e2ee_deps() is False\n+\n+    def test_check_requirements_runs_lazy_install_when_partial(self, monkeypatch):\n+        \"\"\"When mautrix is installed but asyncpg/aiosqlite are missing,\n+        check_matrix_requirements must still run the lazy installer.\n+\n+        Regression for #31116: the previous ``try: import mautrix`` gate\n+        short-circuited the install of the OTHER 4 platform.matrix packages,\n+        so a partial install (mautrix only) was treated as fully installed.\n+        \"\"\"\n+        monkeypatch.setenv(\"MATRIX_ACCESS_TOKEN\", \"syt_test\")\n+        monkeypatch.setenv(\"MATRIX_HOMESERVER\", \"https://matrix.example.org\")\n+        monkeypatch.delenv(\"MATRIX_ENCRYPTION\", raising=False)\n+\n+        from gateway.platforms import matrix as matrix_mod\n+\n+        # Simulate \"mautrix installed, asyncpg missing\" → feature_missing\n+        # returns a non-empty tuple → ensure_and_bind MUST be called.\n+        called = {\"ensure_and_bind\": False}\n+\n+        def _fake_ensure_and_bind(feature, importer, target_globals, **kwargs):\n+            called[\"ensure_and_bind\"] = True\n+            assert feature == \"platform.matrix\"\n+            return True  # Pretend install succeeded.\n+\n+        with patch(\"tools.lazy_deps.feature_missing\", return_value=(\"asyncpg==0.31.0\",)), \\\n+             patch(\"tools.lazy_deps.ensure_and_bind\", side_effect=_fake_ensure_and_bind):\n+            matrix_mod.check_matrix_requirements()\n+\n+        assert called[\"ensure_and_bind\"], (\n+            \"check_matrix_requirements must call ensure_and_bind whenever ANY \"\n+            \"platform.matrix dep is missing, not just when mautrix itself is \"\n+            \"missing (#31116)\"\n+        )\n+\n \n # ---------------------------------------------------------------------------\n # Access-token auth / E2EE bootstrap"]},{"file":"tests/gateway/test_platform_registry.py","hunks":["@@ -708,3 +708,190 @@ def test_shared_keys_bridged_for_plugin_platform(self, tmp_path, monkeypatch):\n             assert extra.get(\"allow_from\") == [\"alice\", \"bob\"]\n         finally:\n             _reg.unregister(\"mysharedplat\")\n+\n+\n+class TestPluginEnablementGate:\n+    \"\"\"Plugin platforms must NOT auto-enable on check_fn alone (#31116).\n+\n+    When a plugin registers ``is_connected`` (the \"did the user actually\n+    configure credentials\" probe), ``load_gateway_config`` must consult it\n+    before flipping ``enabled = True``.  Without this gate, ``check_fn``\n+    semantics (\"the SDK is importable\") get conflated with \"the user wants\n+    this platform on\", and the gateway tries to connect to e.g. Discord\n+    with no token — emitting noisy retry-forever errors on every fresh\n+    install that has the plugin loaded.\n+    \"\"\"\n+\n+    def _write_config(self, tmp_path, content: str = \"\"):\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        (hermes_home / \"config.yaml\").write_text(content, encoding=\"utf-8\")\n+        return hermes_home\n+\n+    def test_plugin_with_is_connected_false_is_NOT_enabled(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"check_fn=True + is_connected=False must NOT enable the platform.\n+\n+        Reproduces #31116: Discord plugin loads, its check_fn lazy-installs\n+        discord.py and returns True, but the user has no DISCORD_BOT_TOKEN.\n+        Previously this auto-enabled Discord and the gateway spammed\n+        ``ERROR ... [Discord] No bot token configured`` on every reconnect.\n+        \"\"\"\n+        from gateway.platform_registry import platform_registry as _reg\n+\n+        _reg.register(PlatformEntry(\n+            name=\"myunconfiguredplat\",\n+            label=\"MyUnconfigured\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,             # SDK available\n+            is_connected=lambda cfg: False,    # but user hasn't set credentials\n+            source=\"plugin\",\n+        ))\n+        try:\n+            home = self._write_config(tmp_path)\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config, Platform\n+            cfg = load_gateway_config()\n+\n+            plat = Platform(\"myunconfiguredplat\")\n+            # Either absent entirely, or present but explicitly disabled.\n+            if plat in cfg.platforms:\n+                assert cfg.platforms[plat].enabled is False, (\n+                    \"Plugin with is_connected=False must NOT be auto-enabled\"\n+                )\n+        finally:\n+            _reg.unregister(\"myunconfiguredplat\")\n+\n+    def test_plugin_with_is_connected_true_is_enabled(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"check_fn=True + is_connected=True still enables the platform.\"\"\"\n+        from gateway.platform_registry import platform_registry as _reg\n+\n+        _reg.register(PlatformEntry(\n+            name=\"myconfiguredplat\",\n+            label=\"MyConfigured\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            is_connected=lambda cfg: True,\n+            source=\"plugin\",\n+        ))\n+        try:\n+            home = self._write_config(tmp_path)\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config, Platform\n+            cfg = load_gateway_config()\n+\n+            plat = Platform(\"myconfiguredplat\")\n+            assert plat in cfg.platforms\n+            assert cfg.platforms[plat].enabled is True\n+        finally:\n+            _reg.unregister(\"myconfiguredplat\")\n+\n+    def test_plugin_without_is_connected_falls_back_to_check_fn(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"Legacy plugins that don't register is_connected keep working.\n+\n+        For plugins where ``is_connected is None``, gating on ``check_fn``\n+        alone remains the contract — that's what callers without a\n+        credential probe have always done.\n+        \"\"\"\n+        from gateway.platform_registry import platform_registry as _reg\n+\n+        _reg.register(PlatformEntry(\n+            name=\"mylegacyplat\",\n+            label=\"MyLegacy\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            # is_connected intentionally omitted (None)\n+            source=\"plugin\",\n+        ))\n+        try:\n+            home = self._write_config(tmp_path)\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config, Platform\n+            cfg = load_gateway_config()\n+\n+            plat = Platform(\"mylegacyplat\")\n+            assert plat in cfg.platforms\n+            assert cfg.platforms[plat].enabled is True\n+        finally:\n+            _reg.unregister(\"mylegacyplat\")\n+\n+    def test_is_connected_raises_does_not_enable(self, tmp_path, monkeypatch):\n+        \"\"\"A buggy is_connected must not silently enable the platform.\n+\n+        Treat a raising is_connected as \"configuration unknown\" — refuse to\n+        enable, log, and move on.  Anything else would re-introduce the\n+        #31116 bug for plugins whose probe has a transient failure.\n+        \"\"\"\n+        from gateway.platform_registry import platform_registry as _reg\n+\n+        def _bad_probe(cfg):\n+            raise RuntimeError(\"plugin bug\")\n+\n+        _reg.register(PlatformEntry(\n+            name=\"mybadprobeplat\",\n+            label=\"MyBadProbe\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            is_connected=_bad_probe,\n+            source=\"plugin\",\n+        ))\n+        try:\n+            home = self._write_config(tmp_path)\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config, Platform\n+            cfg = load_gateway_config()\n+\n+            plat = Platform(\"mybadprobeplat\")\n+            if plat in cfg.platforms:\n+                assert cfg.platforms[plat].enabled is False\n+        finally:\n+            _reg.unregister(\"mybadprobeplat\")\n+\n+    def test_yaml_enabled_true_overrides_is_connected_false(\n+        self, tmp_path, monkeypatch\n+    ):\n+        \"\"\"Explicit YAML ``enabled: true`` wins over is_connected=False.\n+\n+        If the user wrote ``platforms.X.enabled: true`` themselves, respect\n+        that — they may be using a credential mechanism the plugin's\n+        is_connected probe doesn't know about.  Don't fight them.\n+        \"\"\"\n+        from gateway.platform_registry import platform_registry as _reg\n+\n+        _reg.register(PlatformEntry(\n+            name=\"myexplicitplat\",\n+            label=\"MyExplicit\",\n+            adapter_factory=lambda cfg: None,\n+            check_fn=lambda: True,\n+            is_connected=lambda cfg: False,\n+            source=\"plugin\",\n+        ))\n+        try:\n+            home = self._write_config(\n+                tmp_path,\n+                \"platforms:\\n\"\n+                \"  myexplicitplat:\\n\"\n+                \"    enabled: true\\n\",\n+            )\n+            monkeypatch.setenv(\"HERMES_HOME\", str(home))\n+\n+            from gateway.config import load_gateway_config, Platform\n+            cfg = load_gateway_config()\n+\n+            plat = Platform(\"myexplicitplat\")\n+            assert plat in cfg.platforms\n+            assert cfg.platforms[plat].enabled is True, (\n+                \"Explicit YAML enabled: true must win over plugin's \"\n+                \"is_connected=False — user has the final say\"\n+            )\n+        finally:\n+            _reg.unregister(\"myexplicitplat\")\n"]}]},{"kind":"pr","number":31699,"title":"fix(gateway+setup): repair Matrix E2EE deps and stop Discord auto-enable (#31116, salvages #31236)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#31688","number":31688},{"kind":"pr","value":"#31116","number":31116},{"kind":"pr","value":"#31703","number":31703}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":31703,"title":"fix(gateway): seed plugin extras before is_connected gate (#31116 follow-up)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/config.py","tests/gateway/test_platform_registry.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}