NousResearch/hermes-agent

pip bootstrap and dependency install fixes in setup

Cluster 3002 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #58523 on 7 Jul 2026. Check it covers your case.

The short answer

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

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.

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.

  • #58523 fix(setup): install deps via canonical uv→pip→ensurepip ladder (salvage #3019)

    merged to main

    files: 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

    Show the diff
    • agent/lsp/install.py
      @@ -348,17 +348,15 @@ def _install_pip(pkg: str, bin_name: str) -> Optional[str]:
           pip_target.mkdir(parents=True, exist_ok=True)
           try:
               logger.info("[install] pip install --target %s %s", pip_target, pkg)
      -        proc = subprocess.run(
      -            [sys.executable, "-m", "pip", "install", "--target", str(pip_target), "--quiet", pkg],
      -            check=False,
      -            capture_output=True,
      -            text=True,
      +        from hermes_cli.tools_config import _pip_install
      +
      +        proc = _pip_install(
      +            ["--target", str(pip_target), "--quiet", pkg],
                   timeout=300,
      -            stdin=subprocess.DEVNULL,
               )
               if proc.returncode != 0:
                   logger.warning(
      -                "[install] pip install failed for %s: %s", pkg, proc.stderr.strip()[:500]
      +                "[install] pip install failed for %s: %s", pkg, (proc.stderr or "").strip()[:500]
                   )
                   return None
           except (subprocess.TimeoutExpired, OSError) as e:
    • hermes_cli/dingtalk_auth.py
      @@ -163,17 +163,15 @@ def _ensure_qrcode_installed() -> bool:
       
           import subprocess
       
      -    # Try uv first (Hermes convention), then pip
      -    for cmd in (
      -        [sys.executable, "-m", "uv", "pip", "install", "qrcode"],
      -        [sys.executable, "-m", "pip", "install", "-q", "qrcode"],
      -    ):
      -        try:
      -            subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
      +    from hermes_cli.tools_config import _pip_install
      +
      +    try:
      +        result = _pip_install(["-q", "qrcode"], timeout=120)
      +        if result.returncode == 0:
                   import qrcode  # noqa: F401,F811
                   return True
      -        except (subprocess.CalledProcessError, ImportError, FileNotFoundError):
      -            continue
      +    except (subprocess.SubprocessError, ImportError, OSError):
      +        pass
           return False
       
       
    • hermes_cli/memory_setup.py
      @@ -122,36 +122,19 @@ def _install_dependencies(provider_name: str) -> None:
       
           print(f"\n  Installing dependencies: {', '.join(missing)}")
       
      -    import shutil
      -
      -    uv_path = shutil.which("uv")
      -    if uv_path:
      -        install_cmd = [uv_path, "pip", "install", "--python", sys.executable, "--quiet"] + missing
      -        manual_cmd = f"uv pip install --python {sys.executable} {' '.join(missing)}"
      -    else:
      -        pip_cmd = shutil.which("pip3") or shutil.which("pip")
      -        if not pip_cmd:
      -            print("  ⚠ uv not found — cannot install dependencies")
      -            print("  Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh")
      -            print("  Then re-run: hermes memory setup")
      -            return
      -        print("  ⚠ uv not found. Falling back to standard pip...")
      -        install_cmd = [sys.executable, "-m", "pip", "install", "--quiet"] + missing
      -        manual_cmd = f"{sys.executable} -m pip install {' '.join(missing)}"
      +    from hermes_cli.tools_config import _pip_install
       
      +    manual_cmd = f"uv pip install {' '.join(missing)}"
           try:
      -        subprocess.run(
      -            install_cmd,
      -            check=True, timeout=120,
      -            capture_output=True,
      -        )
      -        print(f"  ✓ Installed {', '.join(missing)}")
      -    except subprocess.CalledProcessError as e:
      -        print(f"  ⚠ Failed to install {', '.join(missing)}")
      -        stderr = (e.stderr or b"").decode()[:200]
      -        if stderr:
      -            print(f"    {stderr}")
      -        print(f"  Run manually: {manual_cmd}")
      +        result = _pip_install(["--quiet"] + missing, timeout=120)
      +        if result.returncode == 0:
      +            print(f"  ✓ Installed {', '.join(missing)}")
      +        else:
      +            print(f"  ⚠ Failed to install {', '.join(missing)}")
      +            stderr = (result.stderr or "")[:200]
      +            if stderr:
      +                print(f"    {stderr}")
      +            print(f"  Run manually: {manual_cmd}")
           except Exception as e:
               print(f"  ⚠ Install failed: {e}")
               print(f"  Run manually: {manual_cmd}")
    • hermes_cli/setup.py
      @@ -828,17 +828,24 @@ def _install_neutts_deps() -> bool:
           print_info("Installing neutts Python package...")
           print_info("This will also download the TTS model (~300MB) on first use.")
           print()
      +
      +    # Route through the canonical uv → pip → ensurepip ladder so pip-less
      +    # venvs (Ubuntu 25.10 `python -m venv`, `uv venv`) work out of the box.
      +    from hermes_cli.tools_config import _pip_install
      +
           try:
      -        subprocess.run(
      -            [sys.executable, "-m", "pip", "install", "-U", "neutts[all]", "--quiet"],
      -            check=True, timeout=300,
      -        )
      -        print_success("neutts installed successfully")
      -        return True
      -    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
      +        result = _pip_install(["-U", "neutts[all]", "--quiet"], timeout=300)
      +    except Exception as e:
               print_error(f"Failed to install neutts: {e}")
      -        print_info("Try manually: python -m pip install -U neutts[all]")
      +        print_info("Try manually: uv pip install -U 'neutts[all]'")
               return False
      +    if result.returncode == 0:
      +        print_success("neutts installed successfully")
      +        return True
      +    err = (result.stderr or "").strip()
      +    print_error(f"Failed to install neutts: {err[:300] if err else 'install failed'}")
      +    print_info("Try manually: uv pip install -U 'neutts[all]'")
      +    return False
       
       
       def _install_kittentts_deps() -> bool:
      @@ -853,17 +860,22 @@ def _install_kittentts_deps() -> bool:
           print()
           print_info("Installing kittentts Python package (~25-80MB model downloaded on first use)...")
           print()
      +
      +    from hermes_cli.tools_config import _pip_install
      +
           try:
      -        subprocess.run(
      -            [sys.executable, "-m", "pip", "install", "-U", wheel_url, "soundfile", "--quiet"],
      -            check=True, timeout=300,
      -        )
      -        print_success("kittentts installed successfully")
      -        return True
      -    except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
      +        result = _pip_install(["-U", wheel_url, "soundfile", "--quiet"], timeout=300)
      +    except Exception as e:
               print_error(f"Failed to install kittentts: {e}")
      -        print_info(f"Try manually: python -m pip install -U '{wheel_url}' soundfile")
      +        print_info(f"Try manually: uv pip install -U '{wheel_url}' soundfile")
               return False
      +    if result.returncode == 0:
      +        print_success("kittentts installed successfully")
      +        return True
      +    err = (result.stderr or "").strip()
      +    print_error(f"Failed to install kittentts: {err[:300] if err else 'install failed'}")
      +    print_info(f"Try manually: uv pip install -U '{wheel_url}' soundfile")
      +    return False
       
       
       def _xai_oauth_logged_in_for_setup() -> bool:
      @@ -1302,32 +1314,13 @@ def setup_terminal_backend(config: dict):
                       __import__("modal")
                   except ImportError:
                       print_info("Installing modal SDK...")
      -                import subprocess
      -
      -                uv_bin = shutil.which("uv")
      -                if uv_bin:
      -                    result = subprocess.run(
      -                        [
      -                            uv_bin,
      -                            "pip",
      -                            "install",
      -                            "--python",
      -                            sys.executable,
      -                            "modal",
      -                        ],
      -                        capture_output=True,
      -                        text=True,
      -                    )
      -                else:
      -                    result = subprocess.run(
      -                        [sys.executable, "-m", "pip", "install", "modal"],
      -                        capture_output=True,
      -                        text=True,
      -                    )
      +                from hermes_cli.tools_config import _pip_install
      +
      +                result = _pip_install(["modal"])
                       if result.returncode == 0:
                           print_success("modal SDK installed")
                       else:
      -                    print_warning("Install failed — run manually: pip install modal")
      +                    print_warning("Install failed — run manually: uv pip install modal")
       
                   # Modal token
                   print()
      @@ -1362,25 +1355,13 @@ def setup_terminal_backend(config: dict):
                   __import__("daytona")
               except ImportError:
                   print_info("Installing daytona SDK...")
      -            import subprocess
      +            from hermes_cli.tools_config import _pip_install
       
      -            uv_bin = shutil.which("uv")
      -            if uv_bin:
      -                result = subprocess.run(
      -                    [uv_bin, "pip", "install", "--python", sys.executable, "daytona"],
      -                    capture_output=True,
      -                    text=True,
      -                )
      -            else:
      -                result = subprocess.run(
      -                    [sys.executable, "-m", "pip", "install", "daytona"],
      -                    capture_output=True,
      -                    text=True,
      -                )
      +            result = _pip_install(["daytona"])
                   if result.returncode == 0:
                       print_success("daytona SDK installed")
                   else:
      -                print_warning("Install failed — run manually: pip install daytona")
      +                print_warning("Install failed — run manually: uv pip install daytona")
                       if result.stderr:
                           print_info(f"  Error: {result.stderr.strip().splitlines()[-1]}")
       
    • plugins/google_meet/cli.py
      @@ -250,10 +250,9 @@ def _confirm(prompt: str) -> bool:
           pip_pkgs = ["playwright", "websockets"]
           print(f"\n[1/3] pip install: {' '.join(pip_pkgs)}")
           try:
      -        res = _sp.run(
      -            [sys.executable, "-m", "pip", "install", "--upgrade", *pip_pkgs],
      -            check=False,
      -        )
      +        from hermes_cli.tools_config import _pip_install
      +
      +        res = _pip_install(["--upgrade", *pip_pkgs], capture_output=False)
               if res.returncode != 0:
                   print("  pip install failed")
                   return 1
    • plugins/memory/honcho/cli.py
      @@ -515,20 +515,16 @@ def _ensure_sdk_installed() -> bool:
               print("  Skipping install. Run: pip install 'honcho-ai>=2.0.1'\n")
               return False
       
      -    import subprocess
           print("  Installing honcho-ai...", flush=True)
      -    result = subprocess.run(
      -        [sys.executable, "-m", "pip", "install", "honcho-ai>=2.0.1"],
      -        capture_output=True,
      -        text=True,
      -        stdin=subprocess.DEVNULL,
      -    )
      +    from hermes_cli.tools_config import _pip_install
      +
      +    result = _pip_install(["honcho-ai>=2.0.1"])
           if result.returncode == 0:
               print("  Installed.\n")
               return True
           else:
      -        print(f"  Install failed:\n{result.stderr.strip()}")
      -        print("  Run manually: pip install 'honcho-ai>=2.0.1'\n")
      +        print(f"  Install failed:\n{(result.stderr or '').strip()}")
      +        print("  Run manually: uv pip install 'honcho-ai>=2.0.1'\n")
               return False
       
       
    • plugins/platforms/google_chat/oauth.py
      @@ -379,13 +379,14 @@ def install_deps() -> bool:
       
           print("Installing Google Chat OAuth dependencies...")
           try:
      -        subprocess.check_call(
      -            [sys.executable, "-m", "pip", "install", "--quiet"] + _REQUIRED_PACKAGES,
      -            stdout=subprocess.DEVNULL,
      -        )
      +        from hermes_cli.tools_config import _pip_install
      +
      +        result = _pip_install(["--quiet"] + _REQUIRED_PACKAGES)
      +        if result.returncode != 0:
      +            raise RuntimeError((result.stderr or "install failed").strip()[:300])
               print("Dependencies installed.")
               return True
      -    except subprocess.CalledProcessError as exc:
      +    except Exception as exc:
               print(f"ERROR: Failed to install dependencies: {exc}")
               print("Or install via the optional extra:")
               print("  pip install 'hermes-agent[google_chat]'")
    • plugins/platforms/matrix/adapter.py
      @@ -4497,23 +4497,14 @@ def interactive_setup() -> None:
                       __import__("mautrix")
                   except ImportError:
                       print_info(f"Installing {matrix_pkg}...")
      -                import subprocess
      -                uv_bin = shutil.which("uv")
      -                if uv_bin:
      -                    result = subprocess.run(
      -                        [uv_bin, "pip", "install", "--python", _sys.executable, matrix_pkg],
      -                        capture_output=True, text=True,
      -                    )
      -                else:
      -                    result = subprocess.run(
      -                        [_sys.executable, "-m", "pip", "install", matrix_pkg],
      -                        capture_output=True, text=True,
      -                    )
      +                from hermes_cli.tools_config import _pip_install
      +
      +                result = _pip_install([matrix_pkg])
                       if result.returncode == 0:
                           print_success(f"{matrix_pkg} installed")
                       else:
                           print_warning(
      -                        f"Install failed — run manually: pip install "
      +                        f"Install failed — run manually: uv pip install "
                               f"'{matrix_pkg}' asyncpg aiosqlite Markdown aiohttp-socks"
                           )
       
    • tests/hermes_cli/test_memory_setup_provider_arg.py
      @@ -51,47 +51,55 @@ def test_unknown_provider_reports_and_returns_early(self, capsys):
       
       
       class TestInstallDependenciesRunner:
      -    """`_install_dependencies` must install via `uv` when present and fall back
      -    to standard `pip` when `uv` is unavailable (e.g. slim containers / CI images
      -    that don't ship uv) instead of dead-ending with "cannot install"."""
      +    """`_install_dependencies` must route through the canonical
      +    ``_pip_install`` ladder (uv → pip → ensurepip): uv when present, standard
      +    pip when uv is unavailable, and an ensurepip bootstrap for pip-less venvs
      +    instead of dead-ending with "cannot install"."""
       
      -    def _run_with_missing_dep(self, tmp_path, which_side_effect):
      +    def _run_with_missing_dep(self, tmp_path, which_side_effect, run_behavior=None):
               """Drive _install_dependencies for a plugin that declares one missing
      -        pip dep, capturing the subprocess.run argv (or None if never called)."""
      +        pip dep, capturing every subprocess.run argv issued by the ladder."""
               import sys
       
               (tmp_path / "plugin.yaml").write_text(
                   "pip_dependencies:\n  - definitely-not-installed-xyz\n", encoding="utf-8"
               )
      -        captured = {}
      +        calls = []
       
               def fake_run(cmd, **kw):
      -            captured["cmd"] = cmd
      -            return SimpleNamespace()
      +            calls.append(cmd)
      +            if run_behavior:
      +                return run_behavior(cmd)
      +            return SimpleNamespace(returncode=0, stdout="", stderr="")
       
               with patch("plugins.memory.find_provider_dir", return_value=tmp_path), \
      -             patch("shutil.which", side_effect=which_side_effect), \
      -             patch("subprocess.run", fake_run):
      +             patch("hermes_cli.tools_config.shutil.which", side_effect=which_side_effect), \
      +             patch("hermes_cli.tools_config.subprocess.run", fake_run):
                   memory_setup._install_dependencies("x")
      -        return captured.get("cmd"), sys.executable
      +        return calls, sys.executable
       
           def test_uses_uv_when_available(self, tmp_path):
      -        cmd, _ = self._run_with_missing_dep(
      +        calls, _ = self._run_with_missing_dep(
                   tmp_path, lambda b: "/usr/bin/uv" if b == "uv" else None
               )
      -        assert cmd is not None
      -        assert cmd[:3] == ["/usr/bin/uv", "pip", "install"]
      -
      -    def test_falls_back_to_pip_when_uv_missing(self, tmp_path, capsys):
      -        """The salvaged behavior (#5954): no uv but pip present -> python -m pip."""
      -        cmd, py = self._run_with_missing_dep(
      -            tmp_path, lambda b: "/usr/bin/pip3" if b == "pip3" else None
      -        )
      -        assert cmd is not None
      -        assert cmd[:4] == [py, "-m", "pip", "install"]
      -        assert "Falling back to standard pip" in capsys.readouterr().out
      -
      -    def test_aborts_when_neither_uv_nor_pip(self, tmp_path, capsys):
      -        cmd, _ = self._run_with_missing_dep(tmp_path, lambda b: None)
      -        assert cmd is None  # no install attempted
      -        assert "cannot install dependencies" in capsys.readouterr().out
      +        assert calls
      +        assert calls[0][:3] == ["/usr/bin/uv", "pip", "install"]
      +
      +    def test_falls_back_to_pip_when_uv_missing(self, tmp_path):
      +        """No uv but pip importable -> python -m pip install."""
      +        calls, py = self._run_with_missing_dep(tmp_path, lambda b: None)
      +        assert calls
      +        # Ladder probes pip first, then installs with it.
      +        assert calls[0][:3] == [py, "-m", "pip"]
      +        assert calls[-1][:4] == [py, "-m", "pip", "install"]
      +
      +    def test_bootstraps_pip_via_ensurepip_when_missing(self, tmp_path):
      +        """Neither uv nor pip -> ensurepip bootstrap, then pip install."""
      +        def behavior(cmd):
      +            if cmd[-1] == "--version":
      +                return SimpleNamespace(returncode=1, stdout="", stderr="")
      +            return SimpleNamespace(returncode=0, stdout="", stderr="")
      +
      +        calls, py = self._run_with_missing_dep(tmp_path, lambda b: None, behavior)
      +        assert any("ensurepip" in c for c in calls)
      +        assert calls[-1][:4] == [py, "-m", "pip", "install"]
    • tests/plugins/test_google_meet_plugin.py
      @@ -778,11 +778,13 @@ def _fake_run(argv, **kwargs):
                patch("shutil.which", return_value="/usr/bin/paplay"):
               rc = _cmd_install(realtime=False, assume_yes=True)
           assert rc == 0
      -    # First invocation: pip install
      -    pip_cmds = [c for c in calls if len(c) > 2 and c[1:4] == ["-m", "pip", "install"]]
      -    assert pip_cmds, f"no pip install run: {calls}"
      -    assert "playwright" in pip_cmds[0]
      -    assert "websockets" in pip_cmds[0]
      +    # First invocation: dependency install via the uv→pip ladder
      +    # (shutil.which is mocked truthy, so the uv tier is taken: `<uv> pip install ...`)
      +    pip_cmds = [
      +        c for c in calls
      +        if "install" in c and "playwright" in c and "websockets" in c
      +    ]
      +    assert pip_cmds, f"no dependency install run: {calls}"
           # Second: playwright install chromium
           pw_cmds = [c for c in calls if len(c) > 2 and c[1:4] == ["-m", "playwright", "install"]]
           assert pw_cmds, f"no playwright install run: {calls}"

2 PRs closed without merging

From the close comments: merged via another change 2

  • #3019 fix(setup): bootstrap pip with ensurepip when not available in venv before neutts install · merged via another change → 569b78c, #58523
  • #8384 fix(install): bootstrap pip when missing from venv on Ubuntu 25.10+ · merged via another change → #3002, #58523, #3019
Closed issues · 1
  • Closed#3002[Bug]: Fails to install NeuTTS during setup
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.50: 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
hermes cli
Merged PRs
#58523
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
7 Jul 2026

For your agent

The same facts, typed.