fix(browser): validate agent-browser is runnable, not just present (#51740)

After `hermes update`, a globally-installed agent-browser's npm postinstall
(fixUnixSymlink) re-points the global symlink (e.g. /opt/homebrew/bin/agent-browser)
at our local node_modules binary. The next update wipes node_modules, leaving a
dangling symlink that `which` still reports but exec fails on with exit 127 —
silently breaking every browser tool (#48521).

Root cause is trust-on-presence: shutil.which/Path.exists accept a name that
resolves but won't run. Add hermes_constants.agent_browser_runnable() (resolves
the path + runs --version) and gate all four resolution sites on it:
_find_agent_browser now skips a dead candidate and falls through to the next
working one (extended PATH -> local .bin -> npx), self-healing the dangling link.
dep_ensure/doctor/nous_subscription validate too; doctor warns on a broken link.

Closes #48521.
This commit is contained in:
Teknium 2026-06-24 00:14:49 -07:00 committed by GitHub
parent a911bcda18
commit 3c75e11571
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 143 additions and 26 deletions

View file

@ -22,12 +22,14 @@ import subprocess
import sys
from pathlib import Path
from hermes_constants import agent_browser_runnable
_IS_WINDOWS = platform.system() == "Windows"
_DEP_CHECKS = {
"node": lambda: shutil.which("node") is not None,
"browser": lambda: (
shutil.which("agent-browser") is not None
agent_browser_runnable(shutil.which("agent-browser"))
or _has_system_browser()
or _has_hermes_agent_browser()
),

View file

@ -13,6 +13,7 @@ from pathlib import Path
from hermes_cli.config import get_project_root, get_hermes_home, get_env_path
from hermes_cli.env_loader import load_hermes_dotenv
from hermes_constants import display_hermes_home
from hermes_constants import agent_browser_runnable
PROJECT_ROOT = get_project_root()
HERMES_HOME = get_hermes_home()
@ -1483,12 +1484,21 @@ def run_doctor(args):
# Check if agent-browser is installed
agent_browser_path = PROJECT_ROOT / "node_modules" / "agent-browser"
agent_browser_ok = False
_which_ab = shutil.which("agent-browser")
if agent_browser_path.exists():
check_ok("agent-browser (Node.js)", "(browser automation)")
agent_browser_ok = True
elif shutil.which("agent-browser"):
elif _which_ab and agent_browser_runnable(_which_ab):
check_ok("agent-browser", "(browser automation)")
agent_browser_ok = True
elif _which_ab:
# Found on PATH but won't run — almost always a dangling global
# symlink left behind by agent-browser's npm postinstall after a
# `hermes update` wiped node_modules (issue #48521).
check_warn(
"agent-browser found but not runnable",
f"(broken symlink at {_which_ab}? run: npm install)",
)
elif _is_termux():
check_info("agent-browser is not installed (expected in the tested Termux path)")
check_info("Install it manually later with: npm install -g agent-browser && agent-browser install")

View file

@ -159,11 +159,17 @@ def _toolset_enabled(config: Dict[str, object], toolset_key: str) -> bool:
def _has_agent_browser() -> bool:
import shutil
agent_browser_bin = shutil.which("agent-browser")
from hermes_constants import agent_browser_runnable
# Validate the resolved binary actually runs — a dangling global symlink
# (issue #48521) is reported by ``which`` but fails at exec. Fall through to
# the local node_modules copy, which the validator also checks.
if agent_browser_runnable(shutil.which("agent-browser")):
return True
local_bin = (
Path(__file__).parent.parent / "node_modules" / ".bin" / "agent-browser"
)
return bool(agent_browser_bin or local_bin.exists())
return agent_browser_runnable(str(local_bin)) if local_bin.exists() else False
def _local_browser_runnable() -> bool: