NousResearch/hermes-agent

Lifting MistralAI ban after clean 2.4.8 release

Cluster 34503 · skills. Data as of

Nothing open on GitHub. Fix merged in #34841 on 29 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #34841. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #34841. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items address restoring or re-enabling MistralAI for STT/TTS after a previous blanket ban, contingent on version 2.4.8 being considered clean. Some approaches use an opt-in gate.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #34841 fix(stt,tts): restore mistralai — 2.4.8 is clean, ban lifted

    merged to main

    files: hermes_cli/security_advisories.py, hermes_cli/tools_config.py, hermes_cli/web_server.py, pyproject.toml, tests/test_project_metadata.py, tests/tools/test_transcription_dotenv_fallback.py, tests/tools/test_transcription_tools.py, tests/tools/test_tts_mistral.py, tools/lazy_deps.py, tools/transcription_tools.py, tools/tts_tool.py, website/docs/user-guide/features/code-execution.md

    Show the diff
    • hermes_cli/security_advisories.py
      @@ -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=(
    • hermes_cli/tools_config.py
      @@ -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",
    • hermes_cli/web_server.py
      @@ -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",
    • pyproject.toml
      @@ -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 = [
    • tests/test_project_metadata.py
      @@ -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:
    • tests/tools/test_transcription_dotenv_fallback.py
      @@ -99,12 +99,6 @@ 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), \
      @@ -112,7 +106,7 @@ def test_explicit_mistral_sees_dotenv(self):
                    patch.object(tt, "_has_local_command", return_value=False), \
                    patch("hermes_cli.config.load_env",
                          return_value={"MISTRAL_API_KEY": "[redacted:secret-assignment]"}):
      -            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
    • tests/tools/test_transcription_tools.py
      @@ -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
      @@ -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)."""
      @@ -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)
      @@ -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."""
    • tests/tools/test_tts_mistral.py
      @@ -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
      @@ -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:
    • tools/lazy_deps.py
      @@ -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",
    • tools/transcription_tools.py
      @@ -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"
       
      @@ -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"
      @@ -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
       
      @@ -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=[redacted:secret-assignment] as client:
    • tools/tts_tool.py
      @@ -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
       
      @@ -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...")
    • uv.lock
      @@ -1243,6 +1243,15 @@ wheels = [
           { url = "https://files.pythonhosted.org/packages/97/a8/c070e1340636acb38d4e6a7e45c46d168a462b48b9b3257e14ca0e5af79b/environs-14.6.0-py3-none-any.whl", hash = "sha256:f8fb3d6c6a55872b0c6db077a28f5a8c7b8984b7c32029613d44cef95cfc0812", size = 17205, upload-time = "2026-02-20T04:02:07.299Z" },
       ]
       
      +[[package]]
      +name = "eval-type-backport"
      +version = "0.3.1"
      +source = { registry = "https://pypi.org/simple" }
      +sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" }
      +wheels = [
      +    { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" },
      +]
      +
       [[package]]
       name = "exa-py"
       version = "2.10.2"
      @@ -1715,6 +1724,9 @@ messaging = [
           { name = "slack-bolt" },
           { name = "slack-sdk" },
       ]
      +mistral = [
      +    { name = "mistralai" },
      +]
       modal = [
           { name = "modal" },
       ]
      @@ -1840,6 +1852,7 @@ requires-dist = [
           { name = "mcp", marker = "extra == 'computer-use'", specifier = "==1.26.0" },
           { name = "mcp", marker = "extra == 'dev'", specifier = "==1.26.0" },
           { name = "mcp", marker = "extra == 'mcp'", specifier = "==1.26.0" },
      +    { name = "mistralai", marker = "extra == 'mistral'", specifier = "==2.4.8" },
           { name = "modal", marker = "extra == 'modal'", specifier = "==1.3.4" },
           { name = "numpy", marker = "extra == 'voice'", specifier = "==2.4.3" },
           { name = "openai", specifier = "==2.24.0" },
      @@ -1876,7 +1889,7 @@ requires-dist = [
           { name = "uvicorn", extras = ["standard"], marker = "extra == 'web'", specifier = "==0.41.0" },
           { name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" },
       ]
      -provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "computer-use", "acp", "bedrock", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
      +provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "pty", "honcho", "mcp", "homeassistant", "sms", "computer-use", "acp", "mistral", "bedrock", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
       
       [[package]]
       name = "hf-xet"
      @@ -2206,6 +2219,15 @@ wheels = [
           { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
       ]
       
      +[[package]]
      +name = "jsonpath-python"
      +version = "1.1.6"
      +source = { registry = "https://pypi.org/simple" }
      +sdist = { url = "https://files.pythonhosted.org/packages/98/18/4ca8742534a5993ff383f7602e325ce2d5d7cc93d72ac5e1cdedbea8a458/jsonpath_python-1.1.6.tar.gz", hash = "sha256:dded9932b4ec41fb8726e09c83afa4e6be618f938c2db287cc2a81723c639671", size = 88178, upload-time = "2026-05-07T01:26:34.482Z" }
      +wheels = [
      +    { url = "https://files.pythonhosted.org/packages/55/8a/1270a6803bd821cbfcdda387eaa13cb41a7b1f7b9bd145979b3bfb9d6cb7/jsonpath_python-1.1.6-py3-none-any.whl", hash = "sha256:a1c50afd8d3fbbaf47a4873bc890dcb3c15da96f5c020327977d844d8731a2d4", size = 14453, upload-time = "2026-05-07T01:26:33.306Z" },
      +]
      +
       [[package]]
       name = "jsonschema"
       version = "4.26.0"
      @@ -2408,6 +2430,25 @@ wheels = [
           { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
       ]
       
      +[[package]]
      +name = "mistralai"
      +version = "2.4.8"
      +source = { registry = "https://pypi.org/simple" }
      +dependencies = [
      +    { name = "eval-type-backport" },
      +    { name = "httpx" },
      +    { name = "jsonpath-python" },
      +    { name = "opentelemetry-api" },
      +    { name = "opentelemetry-semantic-conventions" },
      +    { name = "pydantic" },
      +    { name = "python-dateutil" },
      +    { name = "typing-inspection" },
      +]
      +sdist = { url = "https://files.pythonhosted.org/packages/55/1c/04119828a3da3be8c79efbe59035a621ae22af873c1ee5a4200355025aa6/mistralai-2.4.8.tar.gz", hash = "sha256:4f27b9b7dfd564ae111d3d9992d2a8ad1454aaf3e7675554c686aa3bb89617e2", size = 464443, upload-time = "2026-05-28T10:00:45.72Z" }
      +wheels = [
      +    { url = "https://files.pythonhosted.org/packages/74/2a/d9952a97596ff9570ff7f486084ebfc5637b1bcf62084b97c0f8415713fc/mistralai-2.4.8-py3-none-any.whl", hash = "sha256:edc445c8b5edf332d45db6c708cd1e4d3f62e6eba5d2e8bf3969bdc5117f6472", size = 1110598, upload-time = "2026-05-28T10:00:43.939Z" },
      +]
      +
       [[package]]
       name = "modal"
       version = "1.3.4"
    • website/docs/user-guide/features/code-execution.md
      @@ -219,6 +219,62 @@ terminal:
       
       See the [Security guide](/user-guide/security#environment-variable-passthrough) for full details.
       
      +### `HERMES_*` variables in the child
      +
      +The child process receives only a small, fixed set of operational `HERMES_*`
      +variables by exact name:
      +
      +- `HERMES_HOME`
      +- `HERMES_PROFILE`
      +- `HERMES_CONFIG`
      +- `HERMES_ENV`
      +
      +(plus `HERMES_RPC_DIR` / `HERMES_RPC_SOCKET` / `TZ` / `HOME`, which Hermes
      +injects explicitly so the RPC channel works).
      +
      +:::note Behavior change
      +Earlier versions passed **any** variable whose name began with `HERMES_`
      +through to the child. That broad prefix was removed for security hardening: it
      +could leak `HERMES_*`-named configuration that doesn't match a secret substring
      +(for example `HERMES_BASE_URL`, `HERMES_KANBAN_DB`, or a `HERMES_*_WEBHOOK`
      +endpoint) into arbitrary sandboxed code.
      +
      +If an `execute_code` script — or a repo/plugin module it imports at import time
      +— relied on a `HERMES_*` variable outside the four operational names above, it
      +will now find that variable **unset** in the child. The drop is intentional,
      +not a bug.
      +:::
      +
      +**Workaround — opt the variable back in explicitly.** Both routes pass the
      +variable through `execute_code` *and* `terminal` children, and neither weakens
      +the secret-stripping guarantee (Hermes-managed provider credentials can never
      +be re-allowed this way):
      +
      +1. **Per-machine, in `config.yaml`** — add the exact variable name to the
      +   passthrough allowlist:
      +
      +   ```yaml
      +   terminal:
      +     env_passthrough:
      +       - HERMES_KANBAN_DB
      +       - HERMES_BASE_URL
      +   ```
      +
      +2. **Per-skill, in the skill's frontmatter** — declare it so it is registered
      +   automatically whenever that skill is loaded:
      +
      +   ```yaml
      +   required_environment_variables:
      +     - HERMES_KANBAN_DB
      +   ```
      +
      +**Diagnosing it.** When the child drops one or more non-allowlisted `HERMES_*`
      +variables, Hermes emits a one-line `debug` log naming them and pointing at the
      +`env_passthrough` escape hatch. Run with debug logging (`hermes logs --level
      +DEBUG`, or check `~/.hermes/logs/agent.log`) and look for
      +`execute_code: dropped N non-allowlisted HERMES_* var(s)` if a script behaves
      +as though a `HERMES_*` variable is missing.
      +
       Hermes always writes the script and the auto-generated `hermes_tools.py` RPC stub into a temp staging directory that is cleaned up after execution. In `strict` mode the script also *runs* there; in `project` mode it runs in the session's working directory (the staging directory stays on `PYTHONPATH` so imports still resolve). The child process runs in its own process group so it can be cleanly killed on timeout or interruption.
       
       ## execute_code vs terminal

1 PR closed without merging

From the close comments: superseded by another PR 1

  • #34506 fix(tts): lift mistralai blanket-ban behind opt-in gate (#34503) · superseded by another PR → 3a2c030, #34841
Closed issues · 1
  • Closed#34503fix(stt,tts): lift mistralai blanket-ban — 2.4.8+ is clean
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
skills
Merged PRs
#34841
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
29 May 2026

For your agent

The same facts, typed.