mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(gateway): fall back to sys.executable -m hermes_cli.main when hermes not on PATH
When shutil.which('hermes') returns None, _resolve_hermes_bin() now tries
sys.executable -m hermes_cli.main as a fallback. This handles setups where
Hermes is launched via a venv or module invocation and the hermes symlink is
not on PATH for the gateway process.
Fixes #1049
This commit is contained in:
parent
c207a6b302
commit
f3a38c90fc
2 changed files with 107 additions and 7 deletions
|
|
@ -215,6 +215,33 @@ def _resolve_gateway_model() -> str:
|
|||
return model
|
||||
|
||||
|
||||
def _resolve_hermes_bin() -> Optional[list[str]]:
|
||||
"""Resolve the Hermes update command as argv parts.
|
||||
|
||||
Tries in order:
|
||||
1. ``shutil.which("hermes")`` — standard PATH lookup
|
||||
2. ``sys.executable -m hermes_cli.main`` — fallback when Hermes is running
|
||||
from a venv/module invocation and the ``hermes`` shim is not on PATH
|
||||
|
||||
Returns argv parts ready for quoting/joining, or ``None`` if neither works.
|
||||
"""
|
||||
import shutil
|
||||
|
||||
hermes_bin = shutil.which("hermes")
|
||||
if hermes_bin:
|
||||
return [hermes_bin]
|
||||
|
||||
try:
|
||||
import importlib.util
|
||||
|
||||
if importlib.util.find_spec("hermes_cli") is not None:
|
||||
return [sys.executable, "-m", "hermes_cli.main"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class GatewayRunner:
|
||||
"""
|
||||
Main gateway controller.
|
||||
|
|
@ -3155,9 +3182,14 @@ class GatewayRunner:
|
|||
if not git_dir.exists():
|
||||
return "✗ Not a git repository — cannot update."
|
||||
|
||||
hermes_bin = shutil.which("hermes")
|
||||
if not hermes_bin:
|
||||
return "✗ `hermes` command not found on PATH."
|
||||
hermes_cmd = _resolve_hermes_bin()
|
||||
if not hermes_cmd:
|
||||
return (
|
||||
"✗ Could not locate the `hermes` command. "
|
||||
"Hermes is running, but the update command could not find the "
|
||||
"executable on PATH or via the current Python interpreter. "
|
||||
"Try running `hermes update` manually in your terminal."
|
||||
)
|
||||
|
||||
pending_path = _hermes_home / ".update_pending.json"
|
||||
output_path = _hermes_home / ".update_output.txt"
|
||||
|
|
@ -3173,8 +3205,9 @@ class GatewayRunner:
|
|||
|
||||
# Spawn `hermes update` in a separate cgroup so it survives gateway
|
||||
# restart. systemd-run --user --scope creates a transient scope unit.
|
||||
hermes_cmd_str = " ".join(shlex.quote(part) for part in hermes_cmd)
|
||||
update_cmd = (
|
||||
f"{shlex.quote(hermes_bin)} update > {shlex.quote(str(output_path))} 2>&1; "
|
||||
f"{hermes_cmd_str} update > {shlex.quote(str(output_path))} 2>&1; "
|
||||
f"status=$?; printf '%s' \"$status\" > {shlex.quote(str(exit_code_path))}"
|
||||
)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class TestHandleUpdateCommand:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_hermes_binary(self, tmp_path):
|
||||
"""Returns error when hermes is not on PATH."""
|
||||
"""Returns error when hermes is not on PATH and hermes_cli is not importable."""
|
||||
runner = _make_runner()
|
||||
event = _make_event()
|
||||
|
||||
|
|
@ -102,10 +102,77 @@ class TestHandleUpdateCommand:
|
|||
|
||||
with patch("gateway.run._hermes_home", tmp_path), \
|
||||
patch("gateway.run.__file__", fake_file), \
|
||||
patch("shutil.which", return_value=None):
|
||||
patch("shutil.which", return_value=None), \
|
||||
patch("importlib.util.find_spec", return_value=None):
|
||||
result = await runner._handle_update_command(event)
|
||||
|
||||
assert "not found on PATH" in result
|
||||
assert "Could not locate" in result
|
||||
assert "hermes update" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fallback_to_sys_executable(self, tmp_path):
|
||||
"""Falls back to sys.executable -m hermes_cli.main when hermes not on PATH."""
|
||||
runner = _make_runner()
|
||||
event = _make_event()
|
||||
|
||||
fake_root = tmp_path / "project"
|
||||
fake_root.mkdir()
|
||||
(fake_root / ".git").mkdir()
|
||||
(fake_root / "gateway").mkdir()
|
||||
(fake_root / "gateway" / "run.py").touch()
|
||||
fake_file = str(fake_root / "gateway" / "run.py")
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
||||
mock_popen = MagicMock()
|
||||
fake_spec = MagicMock()
|
||||
|
||||
with patch("gateway.run._hermes_home", hermes_home), \
|
||||
patch("gateway.run.__file__", fake_file), \
|
||||
patch("shutil.which", return_value=None), \
|
||||
patch("importlib.util.find_spec", return_value=fake_spec), \
|
||||
patch("subprocess.Popen", mock_popen):
|
||||
result = await runner._handle_update_command(event)
|
||||
|
||||
assert "Starting Hermes update" in result
|
||||
call_args = mock_popen.call_args[0][0]
|
||||
# The update_cmd uses sys.executable -m hermes_cli.main
|
||||
joined = " ".join(call_args) if isinstance(call_args, list) else call_args
|
||||
assert "hermes_cli.main" in joined or "bash" in call_args[0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_hermes_bin_prefers_which(self, tmp_path):
|
||||
"""_resolve_hermes_bin returns argv parts from shutil.which when available."""
|
||||
from gateway.run import _resolve_hermes_bin
|
||||
|
||||
with patch("shutil.which", return_value="/custom/path/hermes"):
|
||||
result = _resolve_hermes_bin()
|
||||
|
||||
assert result == ["/custom/path/hermes"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_hermes_bin_fallback(self):
|
||||
"""_resolve_hermes_bin falls back to sys.executable argv when which fails."""
|
||||
import sys
|
||||
from gateway.run import _resolve_hermes_bin
|
||||
|
||||
fake_spec = MagicMock()
|
||||
with patch("shutil.which", return_value=None), \
|
||||
patch("importlib.util.find_spec", return_value=fake_spec):
|
||||
result = _resolve_hermes_bin()
|
||||
|
||||
assert result == [sys.executable, "-m", "hermes_cli.main"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_hermes_bin_returns_none_when_both_fail(self):
|
||||
"""_resolve_hermes_bin returns None when both strategies fail."""
|
||||
from gateway.run import _resolve_hermes_bin
|
||||
|
||||
with patch("shutil.which", return_value=None), \
|
||||
patch("importlib.util.find_spec", return_value=None):
|
||||
result = _resolve_hermes_bin()
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_writes_pending_marker(self, tmp_path):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue