diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index ba44d03c10e..f5e464f163e 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -205,15 +205,9 @@ TOOL_CATEGORIES = { ], "tts_provider": "elevenlabs", }, - { - "name": "Mistral (Voxtral TTS)", - "badge": "paid", - "tag": "Multilingual, native Opus", - "env_vars": [ - {"key": "MISTRAL_API_KEY", "prompt": "Mistral API key", "url": "https://console.mistral.ai/"}, - ], - "tts_provider": "mistral", - }, + # Mistral (Voxtral TTS) temporarily hidden — `mistralai` PyPI + # package is currently quarantined (malicious 2.4.6 release on + # 2026-05-12). Restore this entry once PyPI un-quarantines. { "name": "Google Gemini TTS", "badge": "preview", diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 0da49682b22..2a70ee26398 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -273,7 +273,9 @@ _SCHEMA_OVERRIDES: Dict[str, Dict[str, Any]] = { "stt.provider": { "type": "select", "description": "Speech-to-text provider", - "options": ["local", "openai", "mistral"], + # "mistral" temporarily removed — mistralai PyPI package quarantined + # (malicious 2.4.6 release on 2026-05-12). Restore once available. + "options": ["local", "openai"], }, "display.skin": { "type": "select", diff --git a/pyproject.toml b/pyproject.toml index 1eba1aa1657..5d164b6535f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,7 +111,10 @@ termux-all = [ "hermes-agent[dingtalk]", "hermes-agent[feishu]", "hermes-agent[google]", - "hermes-agent[mistral]", + # mistral: omitted from broad termux-all profile — `mistralai` PyPI package + # is currently quarantined (malicious 2.4.6 release). Users who explicitly + # want Voxtral STT/TTS can still `pip install hermes-agent[mistral]` + # directly once PyPI un-quarantines. "hermes-agent[bedrock]", "hermes-agent[homeassistant]", "hermes-agent[sms]", @@ -169,7 +172,11 @@ all = [ "hermes-agent[dingtalk]", "hermes-agent[feishu]", "hermes-agent[google]", - "hermes-agent[mistral]", + # mistral: omitted from [all] — `mistralai` PyPI package is currently + # quarantined (malicious 2.4.6 release on 2026-05-12). Pulling it from + # [all] would break every fresh install / AUR build / Docker build / CI + # run until PyPI un-quarantines. Users who explicitly want Voxtral STT/TTS + # can still `pip install hermes-agent[mistral]` once it's available again. "hermes-agent[bedrock]", "hermes-agent[web]", "hermes-agent[youtube]", diff --git a/tests/tools/test_transcription_dotenv_fallback.py b/tests/tools/test_transcription_dotenv_fallback.py index 39f5ca108e3..73e7a42a59b 100644 --- a/tests/tools/test_transcription_dotenv_fallback.py +++ b/tests/tools/test_transcription_dotenv_fallback.py @@ -69,6 +69,12 @@ class TestProviderSelectionGate: assert tt._get_provider({"enabled": True, "provider": "groq"}) == "groq" def test_explicit_mistral_sees_dotenv(self): + """Mistral STT is intentionally disabled (PyPI quarantine 2026-05-12). + + Even with the dotenv key visible, explicit `provider: mistral` must + return "none" with a warning. Restore the previous behavior once + `mistralai` is un-quarantined on PyPI. + """ from tools import transcription_tools as tt with patch.object(tt, "_HAS_FASTER_WHISPER", False), \ @@ -76,7 +82,7 @@ class TestProviderSelectionGate: patch.object(tt, "_has_local_command", return_value=False), \ patch("hermes_cli.config.load_env", return_value={"MISTRAL_API_KEY": "dotenv-secret"}): - assert tt._get_provider({"enabled": True, "provider": "mistral"}) == "mistral" + assert tt._get_provider({"enabled": True, "provider": "mistral"}) == "none" def test_explicit_xai_sees_dotenv(self): from tools import transcription_tools as tt diff --git a/tests/tools/test_transcription_tools.py b/tests/tools/test_transcription_tools.py index e5b27d9e4d4..ce45cb9f1e6 100644 --- a/tests/tools/test_transcription_tools.py +++ b/tests/tools/test_transcription_tools.py @@ -978,16 +978,23 @@ class TestTranscribeMistral: # ============================================================================ class TestGetProviderMistral: - """Mistral-specific provider selection tests.""" + """Mistral-specific provider selection tests. + + Mistral STT is intentionally disabled in 2026-05-12+ while the + `mistralai` PyPI package is quarantined. These tests document that + explicit `provider: mistral` always returns "none" with a warning, and + that auto-detect skips mistral entirely. + """ def test_mistral_when_key_and_sdk_available(self, monkeypatch): + """Even with key + SDK, explicit mistral returns 'none' (disabled).""" monkeypatch.setenv("MISTRAL_API_KEY", "test-key") with patch("tools.transcription_tools._HAS_MISTRAL", True): from tools.transcription_tools import _get_provider - assert _get_provider({"provider": "mistral"}) == "mistral" + assert _get_provider({"provider": "mistral"}) == "none" def test_mistral_explicit_no_key_returns_none(self, monkeypatch): - """Explicit mistral with no key returns none — no cross-provider fallback.""" + """Explicit mistral with no key returns none.""" monkeypatch.delenv("MISTRAL_API_KEY", raising=False) with patch("tools.transcription_tools._HAS_MISTRAL", True): from tools.transcription_tools import _get_provider @@ -1000,18 +1007,23 @@ class TestGetProviderMistral: from tools.transcription_tools import _get_provider assert _get_provider({"provider": "mistral"}) == "none" - def test_auto_detect_mistral_after_openai(self, monkeypatch): - """Auto-detect: mistral is tried after openai when both are unavailable.""" + def test_auto_detect_skips_mistral(self, monkeypatch): + """Auto-detect intentionally skips mistral (quarantine workaround). + + With no other provider available but MISTRAL_API_KEY set, the result + must be 'none' — mistral is no longer in the auto-detect chain. + """ monkeypatch.delenv("GROQ_API_KEY", raising=False) monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False) monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("XAI_API_KEY", raising=False) monkeypatch.setenv("MISTRAL_API_KEY", "test-key") with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \ patch("tools.transcription_tools._has_local_command", return_value=False), \ patch("tools.transcription_tools._HAS_OPENAI", False), \ patch("tools.transcription_tools._HAS_MISTRAL", True): from tools.transcription_tools import _get_provider - assert _get_provider({}) == "mistral" + assert _get_provider({}) == "none" def test_auto_detect_openai_preferred_over_mistral(self, monkeypatch): """Auto-detect: openai is preferred over mistral (both paid, openai more common).""" @@ -1285,8 +1297,13 @@ class TestGetProviderXAI: from tools.transcription_tools import _get_provider assert _get_provider({}) == "xai" - def test_auto_detect_mistral_preferred_over_xai(self, monkeypatch): - """Auto-detect: mistral is preferred over xai.""" + def test_auto_detect_mistral_skipped_xai_wins(self, monkeypatch): + """Auto-detect skips mistral entirely (quarantine) — xai wins. + + Even with MISTRAL_API_KEY set, mistral is no longer in the + auto-detect chain. xai is the next-best fallback when the + local/groq/openai chain is unavailable. + """ monkeypatch.setenv("MISTRAL_API_KEY", "test-key") monkeypatch.setenv("XAI_API_KEY", "xai-test") monkeypatch.delenv("GROQ_API_KEY", raising=False) @@ -1297,7 +1314,7 @@ class TestGetProviderXAI: patch("tools.transcription_tools._HAS_OPENAI", False), \ patch("tools.transcription_tools._HAS_MISTRAL", True): from tools.transcription_tools import _get_provider - assert _get_provider({}) == "mistral" + assert _get_provider({}) == "xai" def test_auto_detect_no_key_returns_none(self, monkeypatch): """Auto-detect: xai skipped when no key is set.""" diff --git a/tests/tools/test_tts_mistral.py b/tests/tools/test_tts_mistral.py index 6e98946b6c0..818a6c1d117 100644 --- a/tests/tools/test_tts_mistral.py +++ b/tests/tools/test_tts_mistral.py @@ -162,27 +162,34 @@ class TestGenerateMistralTts: class TestTtsDispatcherMistral: - def test_dispatcher_routes_to_mistral( + def test_dispatcher_returns_disabled_error( self, tmp_path, mock_mistral_module, monkeypatch ): + """Mistral TTS is intentionally disabled (PyPI quarantine 2026-05-12). + + The dispatcher must short-circuit with a clear status message before + attempting any SDK import, even when MISTRAL_API_KEY is set and a + mock SDK is wired in. Restore routing once `mistralai` is + un-quarantined on PyPI. + """ import json from tools.tts_tool import text_to_speech_tool monkeypatch.setenv("MISTRAL_API_KEY", "test-key") - mock_mistral_module.audio.speech.complete.return_value = MagicMock( - audio_data=base64.b64encode(b"audio").decode() - ) output_path = str(tmp_path / "out.mp3") with patch("tools.tts_tool._load_tts_config", return_value={"provider": "mistral"}): result = json.loads(text_to_speech_tool("Hello", output_path=output_path)) - assert result["success"] is True - assert result["provider"] == "mistral" - mock_mistral_module.audio.speech.complete.assert_called_once() + assert result["success"] is False + assert "temporarily disabled" in result["error"] + assert "quarantined" in result["error"] + # SDK must not have been called. + mock_mistral_module.audio.speech.complete.assert_not_called() def test_dispatcher_returns_error_when_sdk_not_installed(self, tmp_path, monkeypatch): + """Same disabled message regardless of SDK presence.""" import json from tools.tts_tool import text_to_speech_tool @@ -196,7 +203,7 @@ class TestTtsDispatcherMistral: ) assert result["success"] is False - assert "mistralai" in result["error"] + assert "temporarily disabled" in result["error"] class TestCheckTtsRequirementsMistral: diff --git a/tools/transcription_tools.py b/tools/transcription_tools.py index 663345eb747..5009947895c 100644 --- a/tools/transcription_tools.py +++ b/tools/transcription_tools.py @@ -252,11 +252,16 @@ def _get_provider(stt_config: dict) -> str: return "none" if provider == "mistral": - if _HAS_MISTRAL and get_env_value("MISTRAL_API_KEY"): - return "mistral" + # `mistralai` PyPI package was quarantined on 2026-05-12 after a + # malicious 2.4.6 release. Refuse to use this provider until it's + # available again so we surface a clear message instead of an + # opaque ImportError mid-call. logger.warning( - "STT provider 'mistral' configured but mistralai package " - "not installed or MISTRAL_API_KEY not set" + "STT provider 'mistral' (Voxtral Transcribe) is temporarily " + "disabled — `mistralai` PyPI package is quarantined " + "(malicious 2.4.6 release on 2026-05-12). Falling back to " + "another provider. Set stt.provider in config.yaml to 'local' " + "or 'openai' to silence this warning." ) return "none" @@ -270,7 +275,9 @@ def _get_provider(stt_config: dict) -> str: return provider # Unknown — let it fail downstream - # --- Auto-detect (no explicit provider): local > groq > openai > mistral > xai - + # --- Auto-detect (no explicit provider): local > groq > openai > xai --- + # mistral is intentionally skipped while `mistralai` is quarantined on + # PyPI (malicious 2.4.6 release on 2026-05-12). if _HAS_FASTER_WHISPER: return "local" @@ -282,9 +289,6 @@ def _get_provider(stt_config: dict) -> str: if _HAS_OPENAI and _has_openai_audio_backend(): logger.info("No local STT available, using OpenAI Whisper API") return "openai" - if _HAS_MISTRAL and get_env_value("MISTRAL_API_KEY"): - logger.info("No local STT available, using Mistral Voxtral Transcribe API") - return "mistral" if get_env_value("XAI_API_KEY"): logger.info("No local STT available, using xAI Grok STT API") return "xai" diff --git a/tools/tts_tool.py b/tools/tts_tool.py index 95958fd1833..31e080332b1 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -1662,16 +1662,21 @@ def text_to_speech_tool( _generate_xai_tts(text, file_str, tts_config) elif provider == "mistral": - try: - _import_mistral_client() - except ImportError: - return json.dumps({ - "success": False, - "error": "Mistral provider selected but 'mistralai' package not installed. " - "Run: pip install 'hermes-agent[mistral]'" - }, ensure_ascii=False) - logger.info("Generating speech with Mistral Voxtral TTS...") - _generate_mistral_tts(text, file_str, tts_config) + # `mistralai` PyPI package was quarantined on 2026-05-12 after a + # malicious 2.4.6 release. Surface a clear status message instead + # of attempting an import that would either fail or pull a stale + # cached package. + return json.dumps({ + "success": False, + "error": ( + "Mistral Voxtral TTS is temporarily disabled. The " + "`mistralai` PyPI package was quarantined on 2026-05-12 " + "after a malicious 2.4.6 release. Switch tts.provider in " + "config.yaml to 'edge', 'elevenlabs', 'openai', 'minimax', " + "'gemini', 'xai', 'neutts', or 'kittentts'. Mistral " + "support will return once PyPI un-quarantines the package." + ), + }, ensure_ascii=False) elif provider == "gemini": logger.info("Generating speech with Google Gemini TTS...")