{"repo":"NousResearch/hermes-agent","public_id":"3002","display_label":"pip bootstrap and dependency install fixes in setup","summary":"These issues and PRs address failures to install dependencies (notably NeuTTS) during setup, focusing on reliably locating or bootstrapping pip in virtual environments, particularly on newer Ubuntu systems and PEP 668-constrained environments.","subsystem":"hermes_cli","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":58523,"merged_at":"2026-07-07T11:09:35+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-07T11:09:35+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":3002,"pr_number":58523,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-07T11:09:35+00:00"}],"members":[{"kind":"issue","number":3002,"title":"[Bug]: Fails to install NeuTTS during setup","url":null,"state":"closed"},{"kind":"pr","number":3019,"title":"fix(setup): bootstrap pip with ensurepip when not available in venv before neutts install","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"569b78c1f","number":0},{"kind":"pr","value":"#58523","number":58523}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":8384,"title":"fix(install): bootstrap pip when missing from venv on Ubuntu 25.10+","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#3002","number":3002},{"kind":"pr","value":"#58523","number":58523},{"kind":"pr","value":"#3019","number":3019}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58523,"title":"fix(setup): install deps via canonical uv→pip→ensurepip ladder (salvage #3019)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/lsp/install.py","hermes_cli/dingtalk_auth.py","hermes_cli/memory_setup.py","hermes_cli/setup.py","plugins/google_meet/cli.py","plugins/memory/honcho/cli.py","plugins/platforms/google_chat/oauth.py","plugins/platforms/matrix/adapter.py","tests/hermes_cli/test_memory_setup_provider_arg.py","tests/plugins/test_google_meet_plugin.py"],"fix_hunks":[{"file":"agent/lsp/install.py","hunks":["@@ -348,17 +348,15 @@ def _install_pip(pkg: str, bin_name: str) -> Optional[str]:\n     pip_target.mkdir(parents=True, exist_ok=True)\n     try:\n         logger.info(\"[install] pip install --target %s %s\", pip_target, pkg)\n-        proc = subprocess.run(\n-            [sys.executable, \"-m\", \"pip\", \"install\", \"--target\", str(pip_target), \"--quiet\", pkg],\n-            check=False,\n-            capture_output=True,\n-            text=True,\n+        from hermes_cli.tools_config import _pip_install\n+\n+        proc = _pip_install(\n+            [\"--target\", str(pip_target), \"--quiet\", pkg],\n             timeout=300,\n-            stdin=subprocess.DEVNULL,\n         )\n         if proc.returncode != 0:\n             logger.warning(\n-                \"[install] pip install failed for %s: %s\", pkg, proc.stderr.strip()[:500]\n+                \"[install] pip install failed for %s: %s\", pkg, (proc.stderr or \"\").strip()[:500]\n             )\n             return None\n     except (subprocess.TimeoutExpired, OSError) as e:"]},{"file":"hermes_cli/dingtalk_auth.py","hunks":["@@ -163,17 +163,15 @@ def _ensure_qrcode_installed() -> bool:\n \n     import subprocess\n \n-    # Try uv first (Hermes convention), then pip\n-    for cmd in (\n-        [sys.executable, \"-m\", \"uv\", \"pip\", \"install\", \"qrcode\"],\n-        [sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"qrcode\"],\n-    ):\n-        try:\n-            subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n+    from hermes_cli.tools_config import _pip_install\n+\n+    try:\n+        result = _pip_install([\"-q\", \"qrcode\"], timeout=120)\n+        if result.returncode == 0:\n             import qrcode  # noqa: F401,F811\n             return True\n-        except (subprocess.CalledProcessError, ImportError, FileNotFoundError):\n-            continue\n+    except (subprocess.SubprocessError, ImportError, OSError):\n+        pass\n     return False\n \n "]},{"file":"hermes_cli/memory_setup.py","hunks":["@@ -122,36 +122,19 @@ def _install_dependencies(provider_name: str) -> None:\n \n     print(f\"\\n  Installing dependencies: {', '.join(missing)}\")\n \n-    import shutil\n-\n-    uv_path = shutil.which(\"uv\")\n-    if uv_path:\n-        install_cmd = [uv_path, \"pip\", \"install\", \"--python\", sys.executable, \"--quiet\"] + missing\n-        manual_cmd = f\"uv pip install --python {sys.executable} {' '.join(missing)}\"\n-    else:\n-        pip_cmd = shutil.which(\"pip3\") or shutil.which(\"pip\")\n-        if not pip_cmd:\n-            print(\"  ⚠ uv not found — cannot install dependencies\")\n-            print(\"  Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh\")\n-            print(\"  Then re-run: hermes memory setup\")\n-            return\n-        print(\"  ⚠ uv not found. Falling back to standard pip...\")\n-        install_cmd = [sys.executable, \"-m\", \"pip\", \"install\", \"--quiet\"] + missing\n-        manual_cmd = f\"{sys.executable} -m pip install {' '.join(missing)}\"\n+    from hermes_cli.tools_config import _pip_install\n \n+    manual_cmd = f\"uv pip install {' '.join(missing)}\"\n     try:\n-        subprocess.run(\n-            install_cmd,\n-            check=True, timeout=120,\n-            capture_output=True,\n-        )\n-        print(f\"  ✓ Installed {', '.join(missing)}\")\n-    except subprocess.CalledProcessError as e:\n-        print(f\"  ⚠ Failed to install {', '.join(missing)}\")\n-        stderr = (e.stderr or b\"\").decode()[:200]\n-        if stderr:\n-            print(f\"    {stderr}\")\n-        print(f\"  Run manually: {manual_cmd}\")\n+        result = _pip_install([\"--quiet\"] + missing, timeout=120)\n+        if result.returncode == 0:\n+            print(f\"  ✓ Installed {', '.join(missing)}\")\n+        else:\n+            print(f\"  ⚠ Failed to install {', '.join(missing)}\")\n+            stderr = (result.stderr or \"\")[:200]\n+            if stderr:\n+                print(f\"    {stderr}\")\n+            print(f\"  Run manually: {manual_cmd}\")\n     except Exception as e:\n         print(f\"  ⚠ Install failed: {e}\")\n         print(f\"  Run manually: {manual_cmd}\")"]},{"file":"hermes_cli/setup.py","hunks":["@@ -828,17 +828,24 @@ def _install_neutts_deps() -> bool:\n     print_info(\"Installing neutts Python package...\")\n     print_info(\"This will also download the TTS model (~300MB) on first use.\")\n     print()\n+\n+    # Route through the canonical uv → pip → ensurepip ladder so pip-less\n+    # venvs (Ubuntu 25.10 `python -m venv`, `uv venv`) work out of the box.\n+    from hermes_cli.tools_config import _pip_install\n+\n     try:\n-        subprocess.run(\n-            [sys.executable, \"-m\", \"pip\", \"install\", \"-U\", \"neutts[all]\", \"--quiet\"],\n-            check=True, timeout=300,\n-        )\n-        print_success(\"neutts installed successfully\")\n-        return True\n-    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:\n+        result = _pip_install([\"-U\", \"neutts[all]\", \"--quiet\"], timeout=300)\n+    except Exception as e:\n         print_error(f\"Failed to install neutts: {e}\")\n-        print_info(\"Try manually: python -m pip install -U neutts[all]\")\n+        print_info(\"Try manually: uv pip install -U 'neutts[all]'\")\n         return False\n+    if result.returncode == 0:\n+        print_success(\"neutts installed successfully\")\n+        return True\n+    err = (result.stderr or \"\").strip()\n+    print_error(f\"Failed to install neutts: {err[:300] if err else 'install failed'}\")\n+    print_info(\"Try manually: uv pip install -U 'neutts[all]'\")\n+    return False\n \n \n def _install_kittentts_deps() -> bool:","@@ -853,17 +860,22 @@ def _install_kittentts_deps() -> bool:\n     print()\n     print_info(\"Installing kittentts Python package (~25-80MB model downloaded on first use)...\")\n     print()\n+\n+    from hermes_cli.tools_config import _pip_install\n+\n     try:\n-        subprocess.run(\n-            [sys.executable, \"-m\", \"pip\", \"install\", \"-U\", wheel_url, \"soundfile\", \"--quiet\"],\n-            check=True, timeout=300,\n-        )\n-        print_success(\"kittentts installed successfully\")\n-        return True\n-    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:\n+        result = _pip_install([\"-U\", wheel_url, \"soundfile\", \"--quiet\"], timeout=300)\n+    except Exception as e:\n         print_error(f\"Failed to install kittentts: {e}\")\n-        print_info(f\"Try manually: python -m pip install -U '{wheel_url}' soundfile\")\n+        print_info(f\"Try manually: uv pip install -U '{wheel_url}' soundfile\")\n         return False\n+    if result.returncode == 0:\n+        print_success(\"kittentts installed successfully\")\n+        return True\n+    err = (result.stderr or \"\").strip()\n+    print_error(f\"Failed to install kittentts: {err[:300] if err else 'install failed'}\")\n+    print_info(f\"Try manually: uv pip install -U '{wheel_url}' soundfile\")\n+    return False\n \n \n def _xai_oauth_logged_in_for_setup() -> bool:","@@ -1302,32 +1314,13 @@ def setup_terminal_backend(config: dict):\n                 __import__(\"modal\")\n             except ImportError:\n                 print_info(\"Installing modal SDK...\")\n-                import subprocess\n-\n-                uv_bin = shutil.which(\"uv\")\n-                if uv_bin:\n-                    result = subprocess.run(\n-                        [\n-                            uv_bin,\n-                            \"pip\",\n-                            \"install\",\n-                            \"--python\",\n-                            sys.executable,\n-                            \"modal\",\n-                        ],\n-                        capture_output=True,\n-                        text=True,\n-                    )\n-                else:\n-                    result = subprocess.run(\n-                        [sys.executable, \"-m\", \"pip\", \"install\", \"modal\"],\n-                        capture_output=True,\n-                        text=True,\n-                    )\n+                from hermes_cli.tools_config import _pip_install\n+\n+                result = _pip_install([\"modal\"])\n                 if result.returncode == 0:\n                     print_success(\"modal SDK installed\")\n                 else:\n-                    print_warning(\"Install failed — run manually: pip install modal\")\n+                    print_warning(\"Install failed — run manually: uv pip install modal\")\n \n             # Modal token\n             print()","@@ -1362,25 +1355,13 @@ def setup_terminal_backend(config: dict):\n             __import__(\"daytona\")\n         except ImportError:\n             print_info(\"Installing daytona SDK...\")\n-            import subprocess\n+            from hermes_cli.tools_config import _pip_install\n \n-            uv_bin = shutil.which(\"uv\")\n-            if uv_bin:\n-                result = subprocess.run(\n-                    [uv_bin, \"pip\", \"install\", \"--python\", sys.executable, \"daytona\"],\n-                    capture_output=True,\n-                    text=True,\n-                )\n-            else:\n-                result = subprocess.run(\n-                    [sys.executable, \"-m\", \"pip\", \"install\", \"daytona\"],\n-                    capture_output=True,\n-                    text=True,\n-                )\n+            result = _pip_install([\"daytona\"])\n             if result.returncode == 0:\n                 print_success(\"daytona SDK installed\")\n             else:\n-                print_warning(\"Install failed — run manually: pip install daytona\")\n+                print_warning(\"Install failed — run manually: uv pip install daytona\")\n                 if result.stderr:\n                     print_info(f\"  Error: {result.stderr.strip().splitlines()[-1]}\")\n "]},{"file":"plugins/google_meet/cli.py","hunks":["@@ -250,10 +250,9 @@ def _confirm(prompt: str) -> bool:\n     pip_pkgs = [\"playwright\", \"websockets\"]\n     print(f\"\\n[1/3] pip install: {' '.join(pip_pkgs)}\")\n     try:\n-        res = _sp.run(\n-            [sys.executable, \"-m\", \"pip\", \"install\", \"--upgrade\", *pip_pkgs],\n-            check=False,\n-        )\n+        from hermes_cli.tools_config import _pip_install\n+\n+        res = _pip_install([\"--upgrade\", *pip_pkgs], capture_output=False)\n         if res.returncode != 0:\n             print(\"  pip install failed\")\n             return 1"]},{"file":"plugins/memory/honcho/cli.py","hunks":["@@ -515,20 +515,16 @@ def _ensure_sdk_installed() -> bool:\n         print(\"  Skipping install. Run: pip install 'honcho-ai>=2.0.1'\\n\")\n         return False\n \n-    import subprocess\n     print(\"  Installing honcho-ai...\", flush=True)\n-    result = subprocess.run(\n-        [sys.executable, \"-m\", \"pip\", \"install\", \"honcho-ai>=2.0.1\"],\n-        capture_output=True,\n-        text=True,\n-        stdin=subprocess.DEVNULL,\n-    )\n+    from hermes_cli.tools_config import _pip_install\n+\n+    result = _pip_install([\"honcho-ai>=2.0.1\"])\n     if result.returncode == 0:\n         print(\"  Installed.\\n\")\n         return True\n     else:\n-        print(f\"  Install failed:\\n{result.stderr.strip()}\")\n-        print(\"  Run manually: pip install 'honcho-ai>=2.0.1'\\n\")\n+        print(f\"  Install failed:\\n{(result.stderr or '').strip()}\")\n+        print(\"  Run manually: uv pip install 'honcho-ai>=2.0.1'\\n\")\n         return False\n \n "]},{"file":"plugins/platforms/google_chat/oauth.py","hunks":["@@ -379,13 +379,14 @@ def install_deps() -> bool:\n \n     print(\"Installing Google Chat OAuth dependencies...\")\n     try:\n-        subprocess.check_call(\n-            [sys.executable, \"-m\", \"pip\", \"install\", \"--quiet\"] + _REQUIRED_PACKAGES,\n-            stdout=subprocess.DEVNULL,\n-        )\n+        from hermes_cli.tools_config import _pip_install\n+\n+        result = _pip_install([\"--quiet\"] + _REQUIRED_PACKAGES)\n+        if result.returncode != 0:\n+            raise RuntimeError((result.stderr or \"install failed\").strip()[:300])\n         print(\"Dependencies installed.\")\n         return True\n-    except subprocess.CalledProcessError as exc:\n+    except Exception as exc:\n         print(f\"ERROR: Failed to install dependencies: {exc}\")\n         print(\"Or install via the optional extra:\")\n         print(\"  pip install 'hermes-agent[google_chat]'\")"]},{"file":"plugins/platforms/matrix/adapter.py","hunks":["@@ -4497,23 +4497,14 @@ def interactive_setup() -> None:\n                 __import__(\"mautrix\")\n             except ImportError:\n                 print_info(f\"Installing {matrix_pkg}...\")\n-                import subprocess\n-                uv_bin = shutil.which(\"uv\")\n-                if uv_bin:\n-                    result = subprocess.run(\n-                        [uv_bin, \"pip\", \"install\", \"--python\", _sys.executable, matrix_pkg],\n-                        capture_output=True, text=True,\n-                    )\n-                else:\n-                    result = subprocess.run(\n-                        [_sys.executable, \"-m\", \"pip\", \"install\", matrix_pkg],\n-                        capture_output=True, text=True,\n-                    )\n+                from hermes_cli.tools_config import _pip_install\n+\n+                result = _pip_install([matrix_pkg])\n                 if result.returncode == 0:\n                     print_success(f\"{matrix_pkg} installed\")\n                 else:\n                     print_warning(\n-                        f\"Install failed — run manually: pip install \"\n+                        f\"Install failed — run manually: uv pip install \"\n                         f\"'{matrix_pkg}' asyncpg aiosqlite Markdown aiohttp-socks\"\n                     )\n "]},{"file":"tests/hermes_cli/test_memory_setup_provider_arg.py","hunks":["@@ -51,47 +51,55 @@ def test_unknown_provider_reports_and_returns_early(self, capsys):\n \n \n class TestInstallDependenciesRunner:\n-    \"\"\"`_install_dependencies` must install via `uv` when present and fall back\n-    to standard `pip` when `uv` is unavailable (e.g. slim containers / CI images\n-    that don't ship uv) instead of dead-ending with \"cannot install\".\"\"\"\n+    \"\"\"`_install_dependencies` must route through the canonical\n+    ``_pip_install`` ladder (uv → pip → ensurepip): uv when present, standard\n+    pip when uv is unavailable, and an ensurepip bootstrap for pip-less venvs\n+    instead of dead-ending with \"cannot install\".\"\"\"\n \n-    def _run_with_missing_dep(self, tmp_path, which_side_effect):\n+    def _run_with_missing_dep(self, tmp_path, which_side_effect, run_behavior=None):\n         \"\"\"Drive _install_dependencies for a plugin that declares one missing\n-        pip dep, capturing the subprocess.run argv (or None if never called).\"\"\"\n+        pip dep, capturing every subprocess.run argv issued by the ladder.\"\"\"\n         import sys\n \n         (tmp_path / \"plugin.yaml\").write_text(\n             \"pip_dependencies:\\n  - definitely-not-installed-xyz\\n\", encoding=\"utf-8\"\n         )\n-        captured = {}\n+        calls = []\n \n         def fake_run(cmd, **kw):\n-            captured[\"cmd\"] = cmd\n-            return SimpleNamespace()\n+            calls.append(cmd)\n+            if run_behavior:\n+                return run_behavior(cmd)\n+            return SimpleNamespace(returncode=0, stdout=\"\", stderr=\"\")\n \n         with patch(\"plugins.memory.find_provider_dir\", return_value=tmp_path), \\\n-             patch(\"shutil.which\", side_effect=which_side_effect), \\\n-             patch(\"subprocess.run\", fake_run):\n+             patch(\"hermes_cli.tools_config.shutil.which\", side_effect=which_side_effect), \\\n+             patch(\"hermes_cli.tools_config.subprocess.run\", fake_run):\n             memory_setup._install_dependencies(\"x\")\n-        return captured.get(\"cmd\"), sys.executable\n+        return calls, sys.executable\n \n     def test_uses_uv_when_available(self, tmp_path):\n-        cmd, _ = self._run_with_missing_dep(\n+        calls, _ = self._run_with_missing_dep(\n             tmp_path, lambda b: \"/usr/bin/uv\" if b == \"uv\" else None\n         )\n-        assert cmd is not None\n-        assert cmd[:3] == [\"/usr/bin/uv\", \"pip\", \"install\"]\n-\n-    def test_falls_back_to_pip_when_uv_missing(self, tmp_path, capsys):\n-        \"\"\"The salvaged behavior (#5954): no uv but pip present -> python -m pip.\"\"\"\n-        cmd, py = self._run_with_missing_dep(\n-            tmp_path, lambda b: \"/usr/bin/pip3\" if b == \"pip3\" else None\n-        )\n-        assert cmd is not None\n-        assert cmd[:4] == [py, \"-m\", \"pip\", \"install\"]\n-        assert \"Falling back to standard pip\" in capsys.readouterr().out\n-\n-    def test_aborts_when_neither_uv_nor_pip(self, tmp_path, capsys):\n-        cmd, _ = self._run_with_missing_dep(tmp_path, lambda b: None)\n-        assert cmd is None  # no install attempted\n-        assert \"cannot install dependencies\" in capsys.readouterr().out\n+        assert calls\n+        assert calls[0][:3] == [\"/usr/bin/uv\", \"pip\", \"install\"]\n+\n+    def test_falls_back_to_pip_when_uv_missing(self, tmp_path):\n+        \"\"\"No uv but pip importable -> python -m pip install.\"\"\"\n+        calls, py = self._run_with_missing_dep(tmp_path, lambda b: None)\n+        assert calls\n+        # Ladder probes pip first, then installs with it.\n+        assert calls[0][:3] == [py, \"-m\", \"pip\"]\n+        assert calls[-1][:4] == [py, \"-m\", \"pip\", \"install\"]\n+\n+    def test_bootstraps_pip_via_ensurepip_when_missing(self, tmp_path):\n+        \"\"\"Neither uv nor pip -> ensurepip bootstrap, then pip install.\"\"\"\n+        def behavior(cmd):\n+            if cmd[-1] == \"--version\":\n+                return SimpleNamespace(returncode=1, stdout=\"\", stderr=\"\")\n+            return SimpleNamespace(returncode=0, stdout=\"\", stderr=\"\")\n+\n+        calls, py = self._run_with_missing_dep(tmp_path, lambda b: None, behavior)\n+        assert any(\"ensurepip\" in c for c in calls)\n+        assert calls[-1][:4] == [py, \"-m\", \"pip\", \"install\"]"]},{"file":"tests/plugins/test_google_meet_plugin.py","hunks":["@@ -778,11 +778,13 @@ def _fake_run(argv, **kwargs):\n          patch(\"shutil.which\", return_value=\"/usr/bin/paplay\"):\n         rc = _cmd_install(realtime=False, assume_yes=True)\n     assert rc == 0\n-    # First invocation: pip install\n-    pip_cmds = [c for c in calls if len(c) > 2 and c[1:4] == [\"-m\", \"pip\", \"install\"]]\n-    assert pip_cmds, f\"no pip install run: {calls}\"\n-    assert \"playwright\" in pip_cmds[0]\n-    assert \"websockets\" in pip_cmds[0]\n+    # First invocation: dependency install via the uv→pip ladder\n+    # (shutil.which is mocked truthy, so the uv tier is taken: `<uv> pip install ...`)\n+    pip_cmds = [\n+        c for c in calls\n+        if \"install\" in c and \"playwright\" in c and \"websockets\" in c\n+    ]\n+    assert pip_cmds, f\"no dependency install run: {calls}\"\n     # Second: playwright install chromium\n     pw_cmds = [c for c in calls if len(c) > 2 and c[1:4] == [\"-m\", \"playwright\", \"install\"]]\n     assert pw_cmds, f\"no playwright install run: {calls}\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}