Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion hermes_cli/security_advisories.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ class Advisory:
"them to a hardcoded webhook. If you ran any Python process that "
"imported mistralai 2.4.6 — including hermes when configured "
"with provider=mistral for TTS or STT — assume those credentials "
"are exposed."
"are exposed. PyPI has since removed 2.4.6 and the project ships "
"clean releases again (2.4.7, 2.4.8); this advisory only fires if "
"the compromised 2.4.6 is still installed."
),
url="https://socket.dev/blog/mini-shai-hulud-worm-pypi",
compromised=(
Expand Down
13 changes: 10 additions & 3 deletions hermes_cli/tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,16 @@ def _get_plugin_toolset_keys() -> set:
],
"tts_provider": "elevenlabs",
},
# 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.
# Mistral Voxtral TTS — `mistralai` SDK lazy-installs on first use.
{
"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",
},
{
"name": "Google Gemini TTS",
"badge": "preview",
Expand Down
4 changes: 1 addition & 3 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,7 @@ async def auth_middleware(request: Request, call_next):
"stt.provider": {
"type": "select",
"description": "Speech-to-text provider",
# "mistral" temporarily removed — mistralai PyPI package quarantined
# (malicious 2.4.6 release on 2026-05-12). Restore once available.
"options": ["local", "openai"],
"options": ["local", "openai", "mistral"],
},
"display.skin": {
"type": "select",
Expand Down
25 changes: 9 additions & 16 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,15 @@ sms = ["aiohttp==3.13.3"]
# to it, which is already provided by the `mcp` extra.
computer-use = ["mcp==1.26.0"]
acp = ["agent-client-protocol==0.9.0"]
# mistral: extra REMOVED 2026-05-12 — `mistralai` PyPI project quarantined
# after malicious 2.4.6 release (Mini Shai-Hulud worm). Every version of
# `mistralai` returns 404 on PyPI right now, so any pin we'd write is
# unresolvable, which breaks `uv lock --check` in CI.
#
# To restore once PyPI un-quarantines:
# 1. Verify the new release is clean (read the changelog, check Socket
# advisory page, confirm no malicious code review findings).
# 2. Add back: mistral = ["mistralai==<verified-version>"]
# 3. Re-enable Mistral in:
# - tools/lazy_deps.py (LAZY_DEPS["tts.mistral"], LAZY_DEPS["stt.mistral"])
# - hermes_cli/tools_config.py (un-hide from provider picker)
# - hermes_cli/web_server.py (re-add to dashboard STT options)
# - tools/transcription_tools.py / tools/tts_tool.py (drop disabled stubs)
# 4. Run `uv lock` to regenerate transitives.
# 5. Optionally re-add to [all] only after a few days of clean operation.
# mistral: Voxtral STT + TTS. Pinned to an exact verified-clean version.
# The `mistralai` PyPI project was quarantined 2026-05-12 after the malicious
# 2.4.6 release (Mini Shai-Hulud worm); 2.4.6 was removed from PyPI and the
# project is serving clean releases again (2.4.7 2026-05-25, 2.4.8 2026-05-28).
# Like other opt-in TTS/STT backends, this is lazy-installed via
# tools/lazy_deps.py (stt.mistral / tts.mistral) at first use — deliberately
# NOT re-added to [all] so a future quarantined release can't break fresh
# installs (see [all] policy comment below).
mistral = ["mistralai==2.4.8"]
bedrock = ["boto3==1.42.89"]
azure-identity = ["azure-identity==1.25.3"]
termux = [
Expand Down
1 change: 1 addition & 0 deletions tests/test_project_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def test_lazy_installable_extras_excluded_from_all():
"modal", "daytona",
"messaging", "slack", "matrix", "dingtalk", "feishu",
"honcho", "hindsight",
"mistral", # mistralai — Voxtral STT/TTS, lazy-installed (stt.mistral / tts.mistral)
}
all_extra_specs = optional_dependencies["all"]
for extra in lazy_covered_extras:
Expand Down
8 changes: 1 addition & 7 deletions tests/tools/test_transcription_dotenv_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,20 +99,14 @@ def test_explicit_groq_sees_dotenv(self):
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), \
patch.object(tt, "_HAS_MISTRAL", True), \
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"}) == "none"
assert tt._get_provider({"enabled": True, "provider": "mistral"}) == "mistral"

def test_explicit_xai_sees_dotenv(self):
from tools import transcription_tools as tt
Expand Down
35 changes: 9 additions & 26 deletions tests/tools/test_transcription_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1010,23 +1010,16 @@ def test_permission_error(self, monkeypatch, sample_ogg, mock_mistral_module):
# ============================================================================

class TestGetProviderMistral:
"""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.
"""
"""Mistral-specific provider selection tests."""

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"}) == "none"
assert _get_provider({"provider": "mistral"}) == "mistral"

def test_mistral_explicit_no_key_returns_none(self, monkeypatch):
"""Explicit mistral with no key returns none."""
"""Explicit mistral with no key returns none — no cross-provider fallback."""
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
with patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
Expand All @@ -1039,23 +1032,18 @@ def test_mistral_explicit_no_sdk_returns_none(self, monkeypatch):
from tools.transcription_tools import _get_provider
assert _get_provider({"provider": "mistral"}) == "none"

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.
"""
def test_auto_detect_mistral_after_openai(self, monkeypatch):
"""Auto-detect: mistral is tried after openai when both are unavailable."""
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({}) == "none"
assert _get_provider({}) == "mistral"

def test_auto_detect_openai_preferred_over_mistral(self, monkeypatch):
"""Auto-detect: openai is preferred over mistral (both paid, openai more common)."""
Expand Down Expand Up @@ -1329,13 +1317,8 @@ def test_auto_detect_xai_after_mistral(self, monkeypatch):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "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.
"""
def test_auto_detect_mistral_preferred_over_xai(self, monkeypatch):
"""Auto-detect: mistral is preferred over xai."""
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
monkeypatch.setenv("XAI_API_KEY", "xai-test")
monkeypatch.delenv("GROQ_API_KEY", raising=False)
Expand All @@ -1346,7 +1329,7 @@ def test_auto_detect_mistral_skipped_xai_wins(self, monkeypatch):
patch("tools.transcription_tools._HAS_OPENAI", False), \
patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "xai"
assert _get_provider({}) == "mistral"

def test_auto_detect_no_key_returns_none(self, monkeypatch):
"""Auto-detect: xai skipped when no key is set."""
Expand Down
23 changes: 8 additions & 15 deletions tests/tools/test_tts_mistral.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,34 +162,27 @@ def test_model_from_config_overrides_default(


class TestTtsDispatcherMistral:
def test_dispatcher_returns_disabled_error(
def test_dispatcher_routes_to_mistral(
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 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()
assert result["success"] is True
assert result["provider"] == "mistral"
mock_mistral_module.audio.speech.complete.assert_called_once()

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
Expand All @@ -203,7 +196,7 @@ def test_dispatcher_returns_error_when_sdk_not_installed(self, tmp_path, monkeyp
)

assert result["success"] is False
assert "temporarily disabled" in result["error"]
assert "mistralai" in result["error"]


class TestCheckTtsRequirementsMistral:
Expand Down
11 changes: 6 additions & 5 deletions tools/lazy_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,16 @@
# (see comment at top of [project.dependencies]). When bumping, update
# both this map AND the corresponding extra in pyproject.toml.
#
# NOTE: tts.mistral / stt.mistral entries are intentionally absent —
# the `mistralai` PyPI project is quarantined as of 2026-05-12 (Mini
# Shai-Hulud worm). Re-add when PyPI restores a clean release; see
# comment in pyproject.toml above the (removed) `mistral` extra for
# the full restoration checklist.
# mistralai pin tracks the `mistral` extra in pyproject.toml. PyPI
# quarantined the project 2026-05-12 (malicious 2.4.6, Mini Shai-Hulud);
# 2.4.6 was removed and clean releases resumed (2.4.7, 2.4.8). Voxtral
# STT + TTS share the same SDK.
"tts.mistral": ("mistralai==2.4.8",),
"tts.edge": ("edge-tts==7.2.7",),
"tts.elevenlabs": ("elevenlabs==1.59.0",),

# ─── Speech-to-text providers ──────────────────────────────────────────
"stt.mistral": ("mistralai==2.4.8",),
"stt.faster_whisper": (
"faster-whisper==1.2.1",
"sounddevice==0.5.5",
Expand Down
28 changes: 16 additions & 12 deletions tools/transcription_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,16 +792,11 @@ def _get_provider(stt_config: dict) -> str:
return "none"

if provider == "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.
if _HAS_MISTRAL and get_env_value("MISTRAL_API_KEY"):
return "mistral"
logger.warning(
"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."
"STT provider 'mistral' configured but mistralai package "
"not installed or MISTRAL_API_KEY not set"
)
return "none"

Expand All @@ -817,9 +812,7 @@ def _get_provider(stt_config: dict) -> str:

return provider # Unknown — let it fail downstream

# --- 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).
# --- Auto-detect (no explicit provider): local > groq > openai > mistral > xai ---

if _HAS_FASTER_WHISPER:
return "local"
Expand All @@ -834,6 +827,12 @@ 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"
# Only auto-select Mistral if the SDK is already present — don't trigger a
# lazy-install during passive auto-detection. Explicit `provider: mistral`
# (above) does lazy-install on first transcription call.
if _HAS_MISTRAL and get_env_value("MISTRAL_API_KEY"):
logger.info("No local STT available, using Mistral Voxtral Transcribe API")
return "mistral"
try:
from tools.xai_http import resolve_xai_http_credentials

Expand Down Expand Up @@ -1371,6 +1370,11 @@ def _transcribe_mistral(file_path: str, model_name: str) -> Dict[str, Any]:
return {"success": False, "transcript": "", "error": "MISTRAL_API_KEY not set"}

try:
try:
from tools.lazy_deps import ensure as _lazy_ensure
_lazy_ensure("stt.mistral", prompt=False)
except ImportError:
pass
from mistralai.client import Mistral

with Mistral(api_key=api_key) as client:
Expand Down
40 changes: 24 additions & 16 deletions tools/tts_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,20 @@ def _import_openai_client():
return OpenAIClient

def _import_mistral_client():
"""Lazy import Mistral client. Returns the class or raises ImportError."""
"""Lazy import Mistral client. Returns the class or raises ImportError.

Calls :func:`tools.lazy_deps.ensure` first so the ``mistralai`` SDK gets
installed on demand if the user picked Mistral as their STT/TTS provider
but never ran the post-setup hook (e.g. enabled it by editing config.yaml
directly). Mirrors the ElevenLabs lazy-import path.
"""
try:
from tools.lazy_deps import ensure
ensure("tts.mistral", prompt=False)
except ImportError:
pass
except Exception as e: # FeatureUnavailable or any unexpected error
raise ImportError(str(e))
from mistralai.client import Mistral
return Mistral

Expand Down Expand Up @@ -1974,21 +1987,16 @@ def text_to_speech_tool(
_generate_xai_tts(text, file_str, tts_config)

elif provider == "mistral":
# `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)
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)

elif provider == "gemini":
logger.info("Generating speech with Google Gemini TTS...")
Expand Down
Loading
Loading