fix(cli): restrict uv-tool-install detection to running interpreter

Copilot review on PR #29703 flagged two issues with the `uv tool list`
fallback in `is_uv_tool_install`:

1. False positive: `uv tool list` returns the *machine*'s installed
   tools, not the active install. A regular pip/venv Hermes on a host
   that also has `uv tool install hermes-agent` available would be
   misclassified as a uv-tool install, and `hermes update` would
   upgrade the wrong copy.

2. Overhead: the subprocess call (up to a 15s timeout) was triggered
   even from `recommended_update_command_for_method`, which just
   computes a display string.

Restrict detection to properties of the running interpreter
(`sys.prefix` and `sys.executable` — both can carry the uv-tool layout
marker depending on entry point). Drop the `uv tool list` fallback and
the `uv_path` parameter entirely. `_cmd_update_pip` now also surfaces a
clear hint when the runtime looks like a uv-tool install but `uv` is
missing from PATH, instead of silently falling back to `python -m pip`.
This commit is contained in:
briandevans
2026-05-30 02:08:11 -07:00
committed by Teknium
parent 1bdb29d938
commit bebd4f8516
3 changed files with 122 additions and 92 deletions
+25 -34
View File
@@ -329,39 +329,31 @@ def stamp_install_method(method: str) -> None:
pass
def is_uv_tool_install(uv_path: Optional[str] = None) -> bool:
"""Return True when Hermes is installed via ``uv tool install hermes-agent``.
def is_uv_tool_install() -> bool:
"""Return True when the *running* Hermes lives in a ``uv tool`` layout.
``uv tool`` installs live outside any virtualenv, so ``uv pip install``
(the previous update path) fails with ``No virtual environment found``.
The fast path inspects ``sys.prefix`` for the standard uv tool layout
(``.../uv/tools/hermes-agent/...``); the authoritative fallback shells
out to ``uv tool list``. Returns False on any error so callers fall
back to the legacy pip path.
``uv tool install hermes-agent`` places the install at
``.../uv/tools/hermes-agent/...`` (default ``~/.local/share/uv/tools``,
or ``$UV_TOOL_DIR/...``). Such installs live outside any virtualenv, so
``uv pip install`` fails with ``No virtual environment found`` and the
update path must use ``uv tool upgrade`` instead.
Detection is intentionally restricted to properties of the running
interpreter (``sys.prefix`` / ``sys.executable``). We deliberately do
NOT consult ``uv tool list``: it would also return True when
``hermes-agent`` happens to be uv-tool-installed on the machine while
the *active* Hermes is a regular pip/venv install, causing
``hermes update`` to upgrade the wrong copy. It would also block on a
subprocess call (~seconds) just to compute a recommendation string.
"""
prefix = os.path.normpath(sys.prefix).replace(os.sep, "/").lower()
if "/uv/tools/hermes-agent/" in prefix + "/":
def _has_uv_tool_marker(path: str) -> bool:
norm = os.path.normpath(path).replace(os.sep, "/").lower()
return "/uv/tools/hermes-agent/" in norm + "/"
if _has_uv_tool_marker(sys.prefix):
return True
if _has_uv_tool_marker(sys.executable or ""):
return True
if uv_path is None:
import shutil
uv_path = shutil.which("uv")
if not uv_path:
return False
try:
result = subprocess.run(
[uv_path, "tool", "list"],
capture_output=True,
text=True,
timeout=15,
)
except (OSError, subprocess.SubprocessError):
return False
if result.returncode != 0:
return False
for line in result.stdout.splitlines():
tokens = line.strip().split()
if tokens and tokens[0] == "hermes-agent":
return True
return False
@@ -374,11 +366,10 @@ def recommended_update_command_for_method(method: str) -> str:
if method == "docker":
return "docker pull nousresearch/hermes-agent:latest"
if method == "pip":
if is_uv_tool_install():
return "uv tool upgrade hermes-agent"
import shutil
uv = shutil.which("uv")
if uv:
if is_uv_tool_install(uv):
return "uv tool upgrade hermes-agent"
if shutil.which("uv"):
return "uv pip install --upgrade hermes-agent"
return "pip install --upgrade hermes-agent"
return "hermes update"
+7 -5
View File
@@ -8977,11 +8977,13 @@ def _cmd_update_pip(args):
print("→ Checking PyPI for updates...")
uv = shutil.which("uv")
if uv:
if is_uv_tool_install(uv):
cmd = [uv, "tool", "upgrade", "hermes-agent"]
else:
cmd = [uv, "pip", "install", "--upgrade", "hermes-agent"]
if is_uv_tool_install():
if not uv:
print("✗ Detected a uv-tool install but `uv` is not on PATH; install uv and retry.")
sys.exit(1)
cmd = [uv, "tool", "upgrade", "hermes-agent"]
elif uv:
cmd = [uv, "pip", "install", "--upgrade", "hermes-agent"]
else:
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "hermes-agent"]