fix(bootstrap): discover agent dir via hermes CLI shebang

`discover_agent_dir()` only checked four hard-coded layouts:

  - HERMES_WEBUI_AGENT_DIR
  - $HERMES_HOME/hermes-agent
  - <webui-parent>/hermes-agent
  - ~/.hermes/hermes-agent / ~/hermes-agent

Users who clone hermes-agent somewhere else (e.g. ~/Projects/GitHub/hermes-agent)
hit:

    [bootstrap] ERROR: Python environment cannot import both WebUI dependencies
    and Hermes Agent. Set HERMES_WEBUI_PYTHON to the Hermes Agent venv Python
    or install the WebUI requirements into that environment.

…even though the `hermes` CLI is on PATH and works fine. The CLI is a
console-script with a venv-relative shebang:

    #!/path/to/hermes-agent/venv/bin/python3

After the explicit candidates miss, fall back to introspecting that shebang
and walking up parents until we find `run_agent.py`. That's a reliable
pointer to the install root regardless of where the user cloned the repo.

Tests cover happy path, no `hermes` on PATH, missing/invalid shebang,
shebang pointing outside any agent install (e.g. /usr/bin/python3), and
explicit candidates winning over the shebang fallback.

Verified end-to-end: with hermes-agent at a non-standard path,
`uv run bootstrap.py` now succeeds without any HERMES_WEBUI_AGENT_DIR
override.
This commit is contained in:
Igor Tarasenko
2026-05-07 15:22:11 +02:00
committed by nesquena-hermes
parent 1706bbdcef
commit 9f72472896
2 changed files with 143 additions and 1 deletions

View File

@@ -90,6 +90,41 @@ def ensure_supported_platform() -> None:
)
def _agent_dir_from_hermes_cli() -> Path | None:
"""Resolve the agent install root by inspecting the `hermes` CLI shebang.
The Hermes Agent installer drops a `hermes` console-script in the user's
PATH whose shebang points at the agent's bundled venv:
#!/path/to/hermes-agent/venv/bin/python3
Walking up the parents until we find a directory that contains
`run_agent.py` recovers the install root regardless of where the user
chose to clone the agent (e.g. ~/Projects/GitHub/hermes-agent), which
the hard-coded candidate list in :func:`discover_agent_dir` cannot.
"""
hermes_path = shutil.which("hermes")
if not hermes_path:
return None
try:
with open(hermes_path, "r", encoding="utf-8", errors="replace") as f:
first_line = f.readline().strip()
except OSError:
return None
if not first_line.startswith("#!"):
return None
interp_field = first_line[2:].strip().split(None, 1)
if not interp_field:
return None
interp = Path(interp_field[0])
if not interp.is_absolute():
return None
for parent in interp.parents:
if (parent / "run_agent.py").exists():
return parent.resolve()
return None
def discover_agent_dir() -> Path | None:
home = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))).expanduser()
candidates = [
@@ -105,7 +140,7 @@ def discover_agent_dir() -> Path | None:
candidate = Path(raw).expanduser().resolve()
if candidate.exists() and (candidate / "run_agent.py").exists():
return candidate
return None
return _agent_dir_from_hermes_cli()
def discover_launcher_python(agent_dir: Path | None) -> str: