{"repo":"NousResearch/hermes-agent","public_id":"34503","display_label":"Lifting MistralAI ban after clean 2.4.8 release","summary":"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.","subsystem":"skills","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":34841,"merged_at":"2026-05-29T20:24:13+00:00"},"resolution":{"state":"resolved","by":"pr-34841"},"freshness":{"state":"verified","last_verified_at":"2026-05-29T20:24:13+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":34503,"pr_number":34841,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-29T20:24:13+00:00"}],"members":[{"kind":"issue","number":34503,"title":"fix(stt,tts): lift mistralai blanket-ban — 2.4.8+ is clean","url":null,"state":"closed"},{"kind":"pr","number":34506,"title":"fix(tts): lift mistralai blanket-ban behind opt-in gate (#34503)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3a2c03061","number":0},{"kind":"pr","value":"#34841","number":34841}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":34841,"title":"fix(stt,tts): restore mistralai — 2.4.8 is clean, ban lifted","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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"],"fix_hunks":[{"file":"hermes_cli/security_advisories.py","hunks":["@@ -104,7 +104,9 @@ class Advisory:\n             \"them to a hardcoded webhook. If you ran any Python process that \"\n             \"imported mistralai 2.4.6 — including hermes when configured \"\n             \"with provider=mistral for TTS or STT — assume those credentials \"\n-            \"are exposed.\"\n+            \"are exposed. PyPI has since removed 2.4.6 and the project ships \"\n+            \"clean releases again (2.4.7, 2.4.8); this advisory only fires if \"\n+            \"the compromised 2.4.6 is still installed.\"\n         ),\n         url=\"https://socket.dev/blog/mini-shai-hulud-worm-pypi\",\n         compromised=("]},{"file":"hermes_cli/tools_config.py","hunks":["@@ -244,9 +244,16 @@ def _get_plugin_toolset_keys() -> set:\n                 ],\n                 \"tts_provider\": \"elevenlabs\",\n             },\n-            # Mistral (Voxtral TTS) temporarily hidden — `mistralai` PyPI\n-            # package is currently quarantined (malicious 2.4.6 release on\n-            # 2026-05-12). Restore this entry once PyPI un-quarantines.\n+            # Mistral Voxtral TTS — `mistralai` SDK lazy-installs on first use.\n+            {\n+                \"name\": \"Mistral (Voxtral TTS)\",\n+                \"badge\": \"paid\",\n+                \"tag\": \"Multilingual, native Opus\",\n+                \"env_vars\": [\n+                    {\"key\": \"MISTRAL_API_KEY\", \"prompt\": \"Mistral API key\", \"url\": \"https://console.mistral.ai/\"},\n+                ],\n+                \"tts_provider\": \"mistral\",\n+            },\n             {\n                 \"name\": \"Google Gemini TTS\",\n                 \"badge\": \"preview\","]},{"file":"hermes_cli/web_server.py","hunks":["@@ -320,9 +320,7 @@ async def auth_middleware(request: Request, call_next):\n     \"stt.provider\": {\n         \"type\": \"select\",\n         \"description\": \"Speech-to-text provider\",\n-        # \"mistral\" temporarily removed — mistralai PyPI package quarantined\n-        # (malicious 2.4.6 release on 2026-05-12). Restore once available.\n-        \"options\": [\"local\", \"openai\"],\n+        \"options\": [\"local\", \"openai\", \"mistral\"],\n     },\n     \"display.skin\": {\n         \"type\": \"select\","]},{"file":"pyproject.toml","hunks":["@@ -117,22 +117,15 @@ sms = [\"aiohttp==3.13.3\"]\n # to it, which is already provided by the `mcp` extra.\n computer-use = [\"mcp==1.26.0\"]\n acp = [\"agent-client-protocol==0.9.0\"]\n-# mistral: extra REMOVED 2026-05-12 — `mistralai` PyPI project quarantined\n-# after malicious 2.4.6 release (Mini Shai-Hulud worm). Every version of\n-# `mistralai` returns 404 on PyPI right now, so any pin we'd write is\n-# unresolvable, which breaks `uv lock --check` in CI.\n-#\n-# To restore once PyPI un-quarantines:\n-#   1. Verify the new release is clean (read the changelog, check Socket\n-#      advisory page, confirm no malicious code review findings).\n-#   2. Add back: mistral = [\"mistralai==<verified-version>\"]\n-#   3. Re-enable Mistral in:\n-#        - tools/lazy_deps.py (LAZY_DEPS[\"tts.mistral\"], LAZY_DEPS[\"stt.mistral\"])\n-#        - hermes_cli/tools_config.py (un-hide from provider picker)\n-#        - hermes_cli/web_server.py (re-add to dashboard STT options)\n-#        - tools/transcription_tools.py / tools/tts_tool.py (drop disabled stubs)\n-#   4. Run `uv lock` to regenerate transitives.\n-#   5. Optionally re-add to [all] only after a few days of clean operation.\n+# mistral: Voxtral STT + TTS. Pinned to an exact verified-clean version.\n+# The `mistralai` PyPI project was quarantined 2026-05-12 after the malicious\n+# 2.4.6 release (Mini Shai-Hulud worm); 2.4.6 was removed from PyPI and the\n+# project is serving clean releases again (2.4.7 2026-05-25, 2.4.8 2026-05-28).\n+# Like other opt-in TTS/STT backends, this is lazy-installed via\n+# tools/lazy_deps.py (stt.mistral / tts.mistral) at first use — deliberately\n+# NOT re-added to [all] so a future quarantined release can't break fresh\n+# installs (see [all] policy comment below).\n+mistral = [\"mistralai==2.4.8\"]\n bedrock = [\"boto3==1.42.89\"]\n azure-identity = [\"azure-identity==1.25.3\"]\n termux = ["]},{"file":"tests/test_project_metadata.py","hunks":["@@ -73,6 +73,7 @@ def test_lazy_installable_extras_excluded_from_all():\n         \"modal\", \"daytona\",\n         \"messaging\", \"slack\", \"matrix\", \"dingtalk\", \"feishu\",\n         \"honcho\", \"hindsight\",\n+        \"mistral\",  # mistralai — Voxtral STT/TTS, lazy-installed (stt.mistral / tts.mistral)\n     }\n     all_extra_specs = optional_dependencies[\"all\"]\n     for extra in lazy_covered_extras:"]},{"file":"tests/tools/test_transcription_dotenv_fallback.py","hunks":["@@ -99,12 +99,6 @@ def test_explicit_groq_sees_dotenv(self):\n             assert tt._get_provider({\"enabled\": True, \"provider\": \"groq\"}) == \"groq\"\n \n     def test_explicit_mistral_sees_dotenv(self):\n-        \"\"\"Mistral STT is intentionally disabled (PyPI quarantine 2026-05-12).\n-\n-        Even with the dotenv key visible, explicit `provider: mistral` must\n-        return \"none\" with a warning. Restore the previous behavior once\n-        `mistralai` is un-quarantined on PyPI.\n-        \"\"\"\n         from tools import transcription_tools as tt\n \n         with patch.object(tt, \"_HAS_FASTER_WHISPER\", False), \\","@@ -112,7 +106,7 @@ def test_explicit_mistral_sees_dotenv(self):\n              patch.object(tt, \"_has_local_command\", return_value=False), \\\n              patch(\"hermes_cli.config.load_env\",\n                    return_value={\"MISTRAL_API_KEY\": \"[redacted:secret-assignment]\"}):\n-            assert tt._get_provider({\"enabled\": True, \"provider\": \"mistral\"}) == \"none\"\n+            assert tt._get_provider({\"enabled\": True, \"provider\": \"mistral\"}) == \"mistral\"\n \n     def test_explicit_xai_sees_dotenv(self):\n         from tools import transcription_tools as tt"]},{"file":"tests/tools/test_transcription_tools.py","hunks":["@@ -1010,23 +1010,16 @@ def test_permission_error(self, monkeypatch, sample_ogg, mock_mistral_module):\n # ============================================================================\n \n class TestGetProviderMistral:\n-    \"\"\"Mistral-specific provider selection tests.\n-\n-    Mistral STT is intentionally disabled in 2026-05-12+ while the\n-    `mistralai` PyPI package is quarantined. These tests document that\n-    explicit `provider: mistral` always returns \"none\" with a warning, and\n-    that auto-detect skips mistral entirely.\n-    \"\"\"\n+    \"\"\"Mistral-specific provider selection tests.\"\"\"\n \n     def test_mistral_when_key_and_sdk_available(self, monkeypatch):\n-        \"\"\"Even with key + SDK, explicit mistral returns 'none' (disabled).\"\"\"\n         monkeypatch.setenv(\"MISTRAL_API_KEY\", \"test-key\")\n         with patch(\"tools.transcription_tools._HAS_MISTRAL\", True):\n             from tools.transcription_tools import _get_provider\n-            assert _get_provider({\"provider\": \"mistral\"}) == \"none\"\n+            assert _get_provider({\"provider\": \"mistral\"}) == \"mistral\"\n \n     def test_mistral_explicit_no_key_returns_none(self, monkeypatch):\n-        \"\"\"Explicit mistral with no key returns none.\"\"\"\n+        \"\"\"Explicit mistral with no key returns none — no cross-provider fallback.\"\"\"\n         monkeypatch.delenv(\"MISTRAL_API_KEY\", raising=False)\n         with patch(\"tools.transcription_tools._HAS_MISTRAL\", True):\n             from tools.transcription_tools import _get_provider","@@ -1039,23 +1032,18 @@ def test_mistral_explicit_no_sdk_returns_none(self, monkeypatch):\n             from tools.transcription_tools import _get_provider\n             assert _get_provider({\"provider\": \"mistral\"}) == \"none\"\n \n-    def test_auto_detect_skips_mistral(self, monkeypatch):\n-        \"\"\"Auto-detect intentionally skips mistral (quarantine workaround).\n-\n-        With no other provider available but MISTRAL_API_KEY set, the result\n-        must be 'none' — mistral is no longer in the auto-detect chain.\n-        \"\"\"\n+    def test_auto_detect_mistral_after_openai(self, monkeypatch):\n+        \"\"\"Auto-detect: mistral is tried after openai when both are unavailable.\"\"\"\n         monkeypatch.delenv(\"GROQ_API_KEY\", raising=False)\n         monkeypatch.delenv(\"VOICE_TOOLS_OPENAI_KEY\", raising=False)\n         monkeypatch.delenv(\"OPENAI_API_KEY\", raising=False)\n-        monkeypatch.delenv(\"XAI_API_KEY\", raising=False)\n         monkeypatch.setenv(\"MISTRAL_API_KEY\", \"test-key\")\n         with patch(\"tools.transcription_tools._HAS_FASTER_WHISPER\", False), \\\n              patch(\"tools.transcription_tools._has_local_command\", return_value=False), \\\n              patch(\"tools.transcription_tools._HAS_OPENAI\", False), \\\n              patch(\"tools.transcription_tools._HAS_MISTRAL\", True):\n             from tools.transcription_tools import _get_provider\n-            assert _get_provider({}) == \"none\"\n+            assert _get_provider({}) == \"mistral\"\n \n     def test_auto_detect_openai_preferred_over_mistral(self, monkeypatch):\n         \"\"\"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):\n             from tools.transcription_tools import _get_provider\n             assert _get_provider({}) == \"xai\"\n \n-    def test_auto_detect_mistral_skipped_xai_wins(self, monkeypatch):\n-        \"\"\"Auto-detect skips mistral entirely (quarantine) — xai wins.\n-\n-        Even with MISTRAL_API_KEY set, mistral is no longer in the\n-        auto-detect chain. xai is the next-best fallback when the\n-        local/groq/openai chain is unavailable.\n-        \"\"\"\n+    def test_auto_detect_mistral_preferred_over_xai(self, monkeypatch):\n+        \"\"\"Auto-detect: mistral is preferred over xai.\"\"\"\n         monkeypatch.setenv(\"MISTRAL_API_KEY\", \"test-key\")\n         monkeypatch.setenv(\"XAI_API_KEY\", \"xai-test\")\n         monkeypatch.delenv(\"GROQ_API_KEY\", raising=False)","@@ -1346,7 +1329,7 @@ def test_auto_detect_mistral_skipped_xai_wins(self, monkeypatch):\n              patch(\"tools.transcription_tools._HAS_OPENAI\", False), \\\n              patch(\"tools.transcription_tools._HAS_MISTRAL\", True):\n             from tools.transcription_tools import _get_provider\n-            assert _get_provider({}) == \"xai\"\n+            assert _get_provider({}) == \"mistral\"\n \n     def test_auto_detect_no_key_returns_none(self, monkeypatch):\n         \"\"\"Auto-detect: xai skipped when no key is set.\"\"\""]},{"file":"tests/tools/test_tts_mistral.py","hunks":["@@ -162,34 +162,27 @@ def test_model_from_config_overrides_default(\n \n \n class TestTtsDispatcherMistral:\n-    def test_dispatcher_returns_disabled_error(\n+    def test_dispatcher_routes_to_mistral(\n         self, tmp_path, mock_mistral_module, monkeypatch\n     ):\n-        \"\"\"Mistral TTS is intentionally disabled (PyPI quarantine 2026-05-12).\n-\n-        The dispatcher must short-circuit with a clear status message before\n-        attempting any SDK import, even when MISTRAL_API_KEY is set and a\n-        mock SDK is wired in. Restore routing once `mistralai` is\n-        un-quarantined on PyPI.\n-        \"\"\"\n         import json\n \n         from tools.tts_tool import text_to_speech_tool\n \n         monkeypatch.setenv(\"MISTRAL_API_KEY\", \"test-key\")\n+        mock_mistral_module.audio.speech.complete.return_value = MagicMock(\n+            audio_data=base64.b64encode(b\"audio\").decode()\n+        )\n \n         output_path = str(tmp_path / \"out.mp3\")\n         with patch(\"tools.tts_tool._load_tts_config\", return_value={\"provider\": \"mistral\"}):\n             result = json.loads(text_to_speech_tool(\"Hello\", output_path=output_path))\n \n-        assert result[\"success\"] is False\n-        assert \"temporarily disabled\" in result[\"error\"]\n-        assert \"quarantined\" in result[\"error\"]\n-        # SDK must not have been called.\n-        mock_mistral_module.audio.speech.complete.assert_not_called()\n+        assert result[\"success\"] is True\n+        assert result[\"provider\"] == \"mistral\"\n+        mock_mistral_module.audio.speech.complete.assert_called_once()\n \n     def test_dispatcher_returns_error_when_sdk_not_installed(self, tmp_path, monkeypatch):\n-        \"\"\"Same disabled message regardless of SDK presence.\"\"\"\n         import json\n \n         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\n             )\n \n         assert result[\"success\"] is False\n-        assert \"temporarily disabled\" in result[\"error\"]\n+        assert \"mistralai\" in result[\"error\"]\n \n \n class TestCheckTtsRequirementsMistral:"]},{"file":"tools/lazy_deps.py","hunks":["@@ -97,15 +97,16 @@\n     # (see comment at top of [project.dependencies]). When bumping, update\n     # both this map AND the corresponding extra in pyproject.toml.\n     #\n-    # NOTE: tts.mistral / stt.mistral entries are intentionally absent —\n-    # the `mistralai` PyPI project is quarantined as of 2026-05-12 (Mini\n-    # Shai-Hulud worm). Re-add when PyPI restores a clean release; see\n-    # comment in pyproject.toml above the (removed) `mistral` extra for\n-    # the full restoration checklist.\n+    # mistralai pin tracks the `mistral` extra in pyproject.toml. PyPI\n+    # quarantined the project 2026-05-12 (malicious 2.4.6, Mini Shai-Hulud);\n+    # 2.4.6 was removed and clean releases resumed (2.4.7, 2.4.8). Voxtral\n+    # STT + TTS share the same SDK.\n+    \"tts.mistral\": (\"mistralai==2.4.8\",),\n     \"tts.edge\": (\"edge-tts==7.2.7\",),\n     \"tts.elevenlabs\": (\"elevenlabs==1.59.0\",),\n \n     # ─── Speech-to-text providers ──────────────────────────────────────────\n+    \"stt.mistral\": (\"mistralai==2.4.8\",),\n     \"stt.faster_whisper\": (\n         \"faster-whisper==1.2.1\",\n         \"sounddevice==0.5.5\","]},{"file":"tools/transcription_tools.py","hunks":["@@ -792,16 +792,11 @@ def _get_provider(stt_config: dict) -> str:\n             return \"none\"\n \n         if provider == \"mistral\":\n-            # `mistralai` PyPI package was quarantined on 2026-05-12 after a\n-            # malicious 2.4.6 release. Refuse to use this provider until it's\n-            # available again so we surface a clear message instead of an\n-            # opaque ImportError mid-call.\n+            if _HAS_MISTRAL and get_env_value(\"MISTRAL_API_KEY\"):\n+                return \"mistral\"\n             logger.warning(\n-                \"STT provider 'mistral' (Voxtral Transcribe) is temporarily \"\n-                \"disabled — `mistralai` PyPI package is quarantined \"\n-                \"(malicious 2.4.6 release on 2026-05-12). Falling back to \"\n-                \"another provider. Set stt.provider in config.yaml to 'local' \"\n-                \"or 'openai' to silence this warning.\"\n+                \"STT provider 'mistral' configured but mistralai package \"\n+                \"not installed or MISTRAL_API_KEY not set\"\n             )\n             return \"none\"\n ","@@ -817,9 +812,7 @@ def _get_provider(stt_config: dict) -> str:\n \n         return provider  # Unknown — let it fail downstream\n \n-    # --- Auto-detect (no explicit provider): local > groq > openai > xai ---\n-    # mistral is intentionally skipped while `mistralai` is quarantined on\n-    # PyPI (malicious 2.4.6 release on 2026-05-12).\n+    # --- Auto-detect (no explicit provider): local > groq > openai > mistral > xai ---\n \n     if _HAS_FASTER_WHISPER:\n         return \"local\"","@@ -834,6 +827,12 @@ def _get_provider(stt_config: dict) -> str:\n     if _HAS_OPENAI and _has_openai_audio_backend():\n         logger.info(\"No local STT available, using OpenAI Whisper API\")\n         return \"openai\"\n+    # Only auto-select Mistral if the SDK is already present — don't trigger a\n+    # lazy-install during passive auto-detection. Explicit `provider: mistral`\n+    # (above) does lazy-install on first transcription call.\n+    if _HAS_MISTRAL and get_env_value(\"MISTRAL_API_KEY\"):\n+        logger.info(\"No local STT available, using Mistral Voxtral Transcribe API\")\n+        return \"mistral\"\n     try:\n         from tools.xai_http import resolve_xai_http_credentials\n ","@@ -1371,6 +1370,11 @@ def _transcribe_mistral(file_path: str, model_name: str) -> Dict[str, Any]:\n         return {\"success\": False, \"transcript\": \"\", \"error\": \"MISTRAL_API_KEY not set\"}\n \n     try:\n+        try:\n+            from tools.lazy_deps import ensure as _lazy_ensure\n+            _lazy_ensure(\"stt.mistral\", prompt=False)\n+        except ImportError:\n+            pass\n         from mistralai.client import Mistral\n \n         with Mistral(api_key=[redacted:secret-assignment] as client:"]},{"file":"tools/tts_tool.py","hunks":["@@ -121,7 +121,20 @@ def _import_openai_client():\n     return OpenAIClient\n \n def _import_mistral_client():\n-    \"\"\"Lazy import Mistral client. Returns the class or raises ImportError.\"\"\"\n+    \"\"\"Lazy import Mistral client. Returns the class or raises ImportError.\n+\n+    Calls :func:`tools.lazy_deps.ensure` first so the ``mistralai`` SDK gets\n+    installed on demand if the user picked Mistral as their STT/TTS provider\n+    but never ran the post-setup hook (e.g. enabled it by editing config.yaml\n+    directly). Mirrors the ElevenLabs lazy-import path.\n+    \"\"\"\n+    try:\n+        from tools.lazy_deps import ensure\n+        ensure(\"tts.mistral\", prompt=False)\n+    except ImportError:\n+        pass\n+    except Exception as e:  # FeatureUnavailable or any unexpected error\n+        raise ImportError(str(e))\n     from mistralai.client import Mistral\n     return Mistral\n ","@@ -1974,21 +1987,16 @@ def text_to_speech_tool(\n             _generate_xai_tts(text, file_str, tts_config)\n \n         elif provider == \"mistral\":\n-            # `mistralai` PyPI package was quarantined on 2026-05-12 after a\n-            # malicious 2.4.6 release. Surface a clear status message instead\n-            # of attempting an import that would either fail or pull a stale\n-            # cached package.\n-            return json.dumps({\n-                \"success\": False,\n-                \"error\": (\n-                    \"Mistral Voxtral TTS is temporarily disabled. The \"\n-                    \"`mistralai` PyPI package was quarantined on 2026-05-12 \"\n-                    \"after a malicious 2.4.6 release. Switch tts.provider in \"\n-                    \"config.yaml to 'edge', 'elevenlabs', 'openai', 'minimax', \"\n-                    \"'gemini', 'xai', 'neutts', or 'kittentts'. Mistral \"\n-                    \"support will return once PyPI un-quarantines the package.\"\n-                ),\n-            }, ensure_ascii=False)\n+            try:\n+                _import_mistral_client()\n+            except ImportError:\n+                return json.dumps({\n+                    \"success\": False,\n+                    \"error\": \"Mistral provider selected but 'mistralai' package not installed. \"\n+                             \"Run: pip install 'hermes-agent[mistral]'\"\n+                }, ensure_ascii=False)\n+            logger.info(\"Generating speech with Mistral Voxtral TTS...\")\n+            _generate_mistral_tts(text, file_str, tts_config)\n \n         elif provider == \"gemini\":\n             logger.info(\"Generating speech with Google Gemini TTS...\")"]},{"file":"uv.lock","hunks":["@@ -1243,6 +1243,15 @@ wheels = [\n     { 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\" },\n ]\n \n+[[package]]\n+name = \"eval-type-backport\"\n+version = \"0.3.1\"\n+source = { registry = \"https://pypi.org/simple\" }\n+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\" }\n+wheels = [\n+    { 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\" },\n+]\n+\n [[package]]\n name = \"exa-py\"\n version = \"2.10.2\"","@@ -1715,6 +1724,9 @@ messaging = [\n     { name = \"slack-bolt\" },\n     { name = \"slack-sdk\" },\n ]\n+mistral = [\n+    { name = \"mistralai\" },\n+]\n modal = [\n     { name = \"modal\" },\n ]","@@ -1840,6 +1852,7 @@ requires-dist = [\n     { name = \"mcp\", marker = \"extra == 'computer-use'\", specifier = \"==1.26.0\" },\n     { name = \"mcp\", marker = \"extra == 'dev'\", specifier = \"==1.26.0\" },\n     { name = \"mcp\", marker = \"extra == 'mcp'\", specifier = \"==1.26.0\" },\n+    { name = \"mistralai\", marker = \"extra == 'mistral'\", specifier = \"==2.4.8\" },\n     { name = \"modal\", marker = \"extra == 'modal'\", specifier = \"==1.3.4\" },\n     { name = \"numpy\", marker = \"extra == 'voice'\", specifier = \"==2.4.3\" },\n     { name = \"openai\", specifier = \"==2.24.0\" },","@@ -1876,7 +1889,7 @@ requires-dist = [\n     { name = \"uvicorn\", extras = [\"standard\"], marker = \"extra == 'web'\", specifier = \"==0.41.0\" },\n     { name = \"youtube-transcript-api\", marker = \"extra == 'youtube'\", specifier = \"==1.2.4\" },\n ]\n-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\"]\n+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\"]\n \n [[package]]\n name = \"hf-xet\"","@@ -2206,6 +2219,15 @@ wheels = [\n     { 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\" },\n ]\n \n+[[package]]\n+name = \"jsonpath-python\"\n+version = \"1.1.6\"\n+source = { registry = \"https://pypi.org/simple\" }\n+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\" }\n+wheels = [\n+    { 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\" },\n+]\n+\n [[package]]\n name = \"jsonschema\"\n version = \"4.26.0\"","@@ -2408,6 +2430,25 @@ wheels = [\n     { 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\" },\n ]\n \n+[[package]]\n+name = \"mistralai\"\n+version = \"2.4.8\"\n+source = { registry = \"https://pypi.org/simple\" }\n+dependencies = [\n+    { name = \"eval-type-backport\" },\n+    { name = \"httpx\" },\n+    { name = \"jsonpath-python\" },\n+    { name = \"opentelemetry-api\" },\n+    { name = \"opentelemetry-semantic-conventions\" },\n+    { name = \"pydantic\" },\n+    { name = \"python-dateutil\" },\n+    { name = \"typing-inspection\" },\n+]\n+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\" }\n+wheels = [\n+    { 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\" },\n+]\n+\n [[package]]\n name = \"modal\"\n version = \"1.3.4\""]},{"file":"website/docs/user-guide/features/code-execution.md","hunks":["@@ -219,6 +219,62 @@ terminal:\n \n See the [Security guide](/user-guide/security#environment-variable-passthrough) for full details.\n \n+### `HERMES_*` variables in the child\n+\n+The child process receives only a small, fixed set of operational `HERMES_*`\n+variables by exact name:\n+\n+- `HERMES_HOME`\n+- `HERMES_PROFILE`\n+- `HERMES_CONFIG`\n+- `HERMES_ENV`\n+\n+(plus `HERMES_RPC_DIR` / `HERMES_RPC_SOCKET` / `TZ` / `HOME`, which Hermes\n+injects explicitly so the RPC channel works).\n+\n+:::note Behavior change\n+Earlier versions passed **any** variable whose name began with `HERMES_`\n+through to the child. That broad prefix was removed for security hardening: it\n+could leak `HERMES_*`-named configuration that doesn't match a secret substring\n+(for example `HERMES_BASE_URL`, `HERMES_KANBAN_DB`, or a `HERMES_*_WEBHOOK`\n+endpoint) into arbitrary sandboxed code.\n+\n+If an `execute_code` script — or a repo/plugin module it imports at import time\n+— relied on a `HERMES_*` variable outside the four operational names above, it\n+will now find that variable **unset** in the child. The drop is intentional,\n+not a bug.\n+:::\n+\n+**Workaround — opt the variable back in explicitly.** Both routes pass the\n+variable through `execute_code` *and* `terminal` children, and neither weakens\n+the secret-stripping guarantee (Hermes-managed provider credentials can never\n+be re-allowed this way):\n+\n+1. **Per-machine, in `config.yaml`** — add the exact variable name to the\n+   passthrough allowlist:\n+\n+   ```yaml\n+   terminal:\n+     env_passthrough:\n+       - HERMES_KANBAN_DB\n+       - HERMES_BASE_URL\n+   ```\n+\n+2. **Per-skill, in the skill's frontmatter** — declare it so it is registered\n+   automatically whenever that skill is loaded:\n+\n+   ```yaml\n+   required_environment_variables:\n+     - HERMES_KANBAN_DB\n+   ```\n+\n+**Diagnosing it.** When the child drops one or more non-allowlisted `HERMES_*`\n+variables, Hermes emits a one-line `debug` log naming them and pointing at the\n+`env_passthrough` escape hatch. Run with debug logging (`hermes logs --level\n+DEBUG`, or check `~/.hermes/logs/agent.log`) and look for\n+`execute_code: dropped N non-allowlisted HERMES_* var(s)` if a script behaves\n+as though a `HERMES_*` variable is missing.\n+\n 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.\n \n ## execute_code vs terminal\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}