From 688bb49df33d9b1ce808db12b4db34daf0b4ccf0 Mon Sep 17 00:00:00 2001 From: dizhaky Date: Tue, 23 Jun 2026 18:11:50 -0400 Subject: [PATCH 1/5] fix(lazy_deps): use primary spec for active_features detection active_features() was using any() over all specs, which caused false activations when a transitive package (aiohttp, cbor2, starlette) shared by multiple features was installed. For example: - aiohttp from Slack -> platform.discord falsely active - cbor2 from Modal -> terminal.vercel falsely active (and vice-versa) - starlette from MCP -> tool.dashboard falsely active Fix: check only specs[0] (the primary/unique package). By convention the first tuple element is always a package exclusive to that feature. This prevents hermes update from installing packages for features the user never enabled. Addresses Codex PR #25 comments. --- tools/lazy_deps.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/lazy_deps.py b/tools/lazy_deps.py index 03fafd4ae0c5..5553300e12ae 100644 --- a/tools/lazy_deps.py +++ b/tools/lazy_deps.py @@ -538,7 +538,12 @@ def active_features() -> list[str]: """ active = [] for feature, specs in LAZY_DEPS.items(): - if any(_is_present(s) for s in specs): + # Check only the first spec (the primary/unique package for this feature). + # Checking any spec would cause false activations when a transitive package + # (e.g. aiohttp, cbor2, starlette) is installed by a different feature — + # see Codex review on PR #25. By convention the first tuple element must be + # a package installed exclusively by this feature. + if _is_present(specs[0]): active.append(feature) return active From 5e9b0b31089e8acac3fce939fa2214b2bc58b68a Mon Sep 17 00:00:00 2001 From: dizhaky Date: Tue, 23 Jun 2026 18:12:15 -0400 Subject: [PATCH 2/5] fix(discord): raise on missing PyNaCl instead of returning False Returning False from join_voice_channel() when PyNaCl is absent bypassed the PyNaCl-specific error guidance in GatewayRunner._handle_voice_channel_join (gateway/run.py:10937). The caller only shows the install hint when the call raises with "pynacl" in the error message; a False return fell through to the generic "Check bot permissions" message instead. Addresses Codex PR #25. --- plugins/platforms/discord/adapter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 410e00e48558..87346300125f 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -1906,13 +1906,13 @@ async def join_voice_channel(self, channel) -> bool: try: import nacl # noqa: F401 — presence check only except ImportError: - logger.warning( - "Discord voice channel join failed: PyNaCl is not installed. " - "The `voice` extra no longer ships PyNaCl (vulnerable pin). " - "Install it manually (`pip install PyNaCl>=1.6.2`) to use " - "Discord voice channels." - ) - return False + # Raise so GatewayRunner._handle_voice_channel_join can surface the + # PyNaCl-specific install guidance (return False falls through to the + # generic "Check bot permissions" message — Codex PR #25). + raise RuntimeError( + "PyNaCl is not installed. The voice extra no longer ships PyNaCl " + "(vulnerable pin). Install manually: pip install PyNaCl>=1.6.2" + ) from None async with self._voice_locks.setdefault(guild_id, asyncio.Lock()): # Already connected in this guild? From aa728c5ed391f44c707b49fc37956ade7d40822e Mon Sep 17 00:00:00 2001 From: dizhaky Date: Tue, 23 Jun 2026 18:31:40 -0400 Subject: [PATCH 3/5] test: update pynacl guard test to expect RuntimeError The adapter now raises RuntimeError (instead of returning False) so that GatewayRunner._handle_voice_channel_join can surface the PyNaCl-specific install guidance. Update the test to match. --- tests/gateway/test_discord_race_polish.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/gateway/test_discord_race_polish.py b/tests/gateway/test_discord_race_polish.py index 95e09c0303ff..dbbb77a0d1d1 100644 --- a/tests/gateway/test_discord_race_polish.py +++ b/tests/gateway/test_discord_race_polish.py @@ -63,7 +63,7 @@ async def slow_connect(self): from plugins.platforms.discord import adapter as discord_mod # Ensure the PyNaCl guard inside join_voice_channel is a no-op for this # test (the test venv may not have nacl). Use patch.dict so the mock - # doesn't leak into other test files in the same pytest session. + # does not leak into other test files in the same pytest session. with patch.dict("sys.modules", {"nacl": MagicMock()}): with patch.object(discord_mod, "VoiceReceiver", MagicMock(return_value=MagicMock(start=lambda: None))): @@ -88,11 +88,11 @@ async def slow_connect(self): # --------------------------------------------------------------------------- @pytest.mark.asyncio -async def test_join_voice_returns_false_when_pynacl_missing(): - """When PyNaCl is not installed, join_voice_channel must fail fast with - a logged warning instead of crashing inside channel.connect() with an - opaque missing-module error. The `voice` extra no longer ships PyNaCl - (vulnerable pin), so this guard is the user-facing safety net.""" +async def test_join_voice_raises_when_pynacl_missing(): + """When PyNaCl is not installed, join_voice_channel must raise RuntimeError + so GatewayRunner._handle_voice_channel_join can surface the PyNaCl-specific + install guidance. Returning False would fall through to the generic + "Check bot permissions" message instead.""" import builtins adapter = _make_adapter() @@ -108,9 +108,9 @@ def _block_nacl(name, *args, **kwargs): channel.guild.id = 123 with patch("builtins.__import__", side_effect=_block_nacl): - result = await adapter.join_voice_channel(channel) + with pytest.raises(RuntimeError, match="PyNaCl is not installed"): + await adapter.join_voice_channel(channel) - assert result is False # channel.connect() must NOT have been called — the guard fires before it. channel.connect.assert_not_called() From 88d771973455e3b99b4c0b239b84279c7068cbe1 Mon Sep 17 00:00:00 2001 From: dizhaky Date: Tue, 23 Jun 2026 19:09:13 -0400 Subject: [PATCH 4/5] fix(nix): update npm lockfile hashes for tui and web --- nix/tui.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/tui.nix b/nix/tui.nix index b8ead660f77c..31d5220274e2 100644 --- a/nix/tui.nix +++ b/nix/tui.nix @@ -4,7 +4,7 @@ let src = ../ui-tui; npmDeps = pkgs.fetchNpmDeps { inherit src; - hash = "sha256-kJdrhcyCtRTecQBMYbv05ZBD0trnKRbpKhej5eGDJpw="; + hash = "sha256-q3Dqx7B9AK/H7ji/XoMkLOxUNH0uTUqoemu+hSiqr5I="; }; npm = hermesNpmLib.mkNpmPassthru { folder = "ui-tui"; attr = "tui"; pname = "hermes-tui"; }; From 6e4bb10f3754c580bfeb3e72bf64e8032d2e488a Mon Sep 17 00:00:00 2001 From: dizhaky Date: Tue, 23 Jun 2026 19:09:23 -0400 Subject: [PATCH 5/5] fix(nix): update npm lockfile hashes for tui and web --- nix/web.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/web.nix b/nix/web.nix index 3bbc2998d5ed..d577f41126e8 100644 --- a/nix/web.nix +++ b/nix/web.nix @@ -4,7 +4,7 @@ let src = ../web; npmDeps = pkgs.fetchNpmDeps { inherit src; - hash = "sha256-ZIcAGppxrXBIIdRgV8V2HHFVkwzFjwUgLLfc5wDDLo8="; + hash = "sha256-peA7M8lvRRVnsM6vl4QbYEr0ElEaGU4zsCENnq8TLBc="; }; npm = hermesNpmLib.mkNpmPassthru { folder = "web"; attr = "web"; pname = "hermes-web"; };