{"repo":"NousResearch/hermes-agent","public_id":"21203","display_label":"Voice audio detection in WSL, SSH, Docker environments","summary":"Fixes and feature requests focus on making audio device detection honor environment variables like PULSE_SERVER and PIPEWIRE_REMOTE so voice features work in WSL, SSH sessions, and Docker containers.","subsystem":"environments","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":3,"merged_prs":2,"attempt_prs":0},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":22534},"resolution":{"state":"partially_resolved","by":"pr-22534,pr-35719"},"freshness":{"state":"verified","last_verified_at":"2026-05-31T07:11:52+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.30799999999999994},"fix_edges":[{"issue_number":21203,"pr_number":22534,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":35622,"pr_number":35719,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":21203,"title":"[Bug]: no audio device error in hermes chat","url":null,"state":"closed"},{"kind":"issue","number":32009,"title":"[Setup]: Audio in a docker container: Suggested solution with instructions","url":null,"state":"closed"},{"kind":"issue","number":35622,"title":"[Feature]: Allow tts over ssh if pulseadio is reacheable","url":null,"state":"closed"},{"kind":"pr","number":22534,"title":"fix(voice): honor PULSE_SERVER/PIPEWIRE_REMOTE inside Docker (#21203)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_voice_mode.py","tools/voice_mode.py"],"fix_hunks":[{"file":"tests/tools/test_voice_mode.py","hunks":["@@ -216,6 +216,60 @@ def test_termux_api_package_without_android_app_blocks_voice(self, monkeypatch):\n         assert any(\"Termux:API Android app is not installed\" in w for w in result[\"warnings\"])\n \n \n+    def test_docker_with_pulse_server_allows_voice(self, monkeypatch):\n+        \"\"\"Docker with PULSE_SERVER set should NOT block voice mode (#21203).\"\"\"\n+        monkeypatch.delenv(\"SSH_CLIENT\", raising=False)\n+        monkeypatch.delenv(\"SSH_TTY\", raising=False)\n+        monkeypatch.delenv(\"SSH_CONNECTION\", raising=False)\n+        monkeypatch.setenv(\"PULSE_SERVER\", \"unix:/run/user/1000/pulse/native\")\n+        monkeypatch.delenv(\"PIPEWIRE_REMOTE\", raising=False)\n+        monkeypatch.setattr(\"hermes_constants.is_container\", lambda: True)\n+        monkeypatch.setattr(\"tools.voice_mode._import_audio\",\n+                            lambda: (MagicMock(), MagicMock()))\n+\n+        from tools.voice_mode import detect_audio_environment\n+        result = detect_audio_environment()\n+\n+        assert result[\"available\"] is True\n+        assert result[\"warnings\"] == []\n+        assert any(\"container\" in n.lower() for n in result.get(\"notices\", []))\n+\n+    def test_docker_with_pipewire_remote_allows_voice(self, monkeypatch):\n+        \"\"\"Docker with PIPEWIRE_REMOTE set should NOT block voice mode (#21203).\"\"\"\n+        monkeypatch.delenv(\"SSH_CLIENT\", raising=False)\n+        monkeypatch.delenv(\"SSH_TTY\", raising=False)\n+        monkeypatch.delenv(\"SSH_CONNECTION\", raising=False)\n+        monkeypatch.delenv(\"PULSE_SERVER\", raising=False)\n+        monkeypatch.setenv(\"PIPEWIRE_REMOTE\", \"/run/user/1000/pipewire-0\")\n+        monkeypatch.setattr(\"hermes_constants.is_container\", lambda: True)\n+        monkeypatch.setattr(\"tools.voice_mode._import_audio\",\n+                            lambda: (MagicMock(), MagicMock()))\n+\n+        from tools.voice_mode import detect_audio_environment\n+        result = detect_audio_environment()\n+\n+        assert result[\"available\"] is True\n+        assert result[\"warnings\"] == []\n+        assert any(\"container\" in n.lower() for n in result.get(\"notices\", []))\n+\n+    def test_docker_without_audio_forwarding_blocks_voice(self, monkeypatch):\n+        \"\"\"Docker without PULSE_SERVER/PIPEWIRE_REMOTE keeps blocking voice mode.\"\"\"\n+        monkeypatch.delenv(\"SSH_CLIENT\", raising=False)\n+        monkeypatch.delenv(\"SSH_TTY\", raising=False)\n+        monkeypatch.delenv(\"SSH_CONNECTION\", raising=False)\n+        monkeypatch.delenv(\"PULSE_SERVER\", raising=False)\n+        monkeypatch.delenv(\"PIPEWIRE_REMOTE\", raising=False)\n+        monkeypatch.setattr(\"hermes_constants.is_container\", lambda: True)\n+        monkeypatch.setattr(\"tools.voice_mode._import_audio\",\n+                            lambda: (MagicMock(), MagicMock()))\n+\n+        from tools.voice_mode import detect_audio_environment\n+        result = detect_audio_environment()\n+\n+        assert result[\"available\"] is False\n+        assert any(\"container\" in w.lower() for w in result[\"warnings\"])\n+        assert any(\"PULSE_SERVER\" in w or \"PIPEWIRE_REMOTE\" in w for w in result[\"warnings\"])\n+\n     def test_termux_api_microphone_allows_voice_without_sounddevice(self, monkeypatch):\n         monkeypatch.setenv(\"TERMUX_VERSION\", \"0.118.3\")\n         monkeypatch.setenv(\"PREFIX\", \"/data/data/com.termux/files/usr\")"]},{"file":"tools/voice_mode.py","hunks":["@@ -102,10 +102,23 @@ def detect_audio_environment() -> dict:\n     if any(os.environ.get(v) for v in ('SSH_CLIENT', 'SSH_TTY', 'SSH_CONNECTION')):\n         warnings.append(\"Running over SSH -- no audio devices available\")\n \n-    # Docker/Podman container detection\n+    # Docker/Podman container detection — honor host audio forwarding.\n+    # When the user mounts a PulseAudio/PipeWire socket into the container\n+    # and points PULSE_SERVER / PIPEWIRE_REMOTE at it, audio works fine\n+    # (issue #21203).  Only block when no forwarding is configured.\n     from hermes_constants import is_container\n     if is_container():\n-        warnings.append(\"Running inside Docker container -- no audio devices\")\n+        if os.environ.get('PULSE_SERVER') or os.environ.get('PIPEWIRE_REMOTE'):\n+            notices.append(\"Running inside container (Docker/Podman/LXC) with host audio forwarding\")\n+        else:\n+            warnings.append(\n+                \"Running inside container (Docker/Podman/LXC) -- no audio devices.\\n\"\n+                \"  Forward host audio with one of (substitute $XDG_RUNTIME_DIR for your runtime dir,\\n\"\n+                \"  typically /run/user/$UID):\\n\"\n+                \"    PulseAudio:  -v $XDG_RUNTIME_DIR/pulse/native:$XDG_RUNTIME_DIR/pulse/native \\\\\\n\"\n+                \"                 -e PULSE_SERVER=unix:$XDG_RUNTIME_DIR/pulse/native\\n\"\n+                \"    PipeWire:    -e PIPEWIRE_REMOTE=$XDG_RUNTIME_DIR/pipewire-0\"\n+            )\n \n     # WSL detection — PulseAudio bridge makes audio work in WSL.\n     # Only block if PULSE_SERVER is not configured.\n"]}]},{"kind":"pr","number":35719,"title":"fix(voice): allow /voice over SSH when a sound server is reachable","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_voice_mode.py","tools/voice_mode.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}