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

@ -54,7 +54,8 @@ class TestFindAgentBrowserCache:
def test_cached_after_first_call(self):
import tools.browser_tool as bt
with patch("shutil.which", return_value="/usr/bin/agent-browser"):
with patch("shutil.which", return_value="/usr/bin/agent-browser"), \
patch("tools.browser_tool.agent_browser_runnable", return_value=True):
result1 = bt._find_agent_browser()
result2 = bt._find_agent_browser()
assert result1 == result2 == "/usr/bin/agent-browser"

View file

@ -101,7 +101,8 @@ class TestFindAgentBrowser:
def test_finds_in_current_path(self):
"""Should return result from shutil.which if available on current PATH."""
with patch("shutil.which", return_value="/usr/local/bin/agent-browser"):
with patch("shutil.which", return_value="/usr/local/bin/agent-browser"), \
patch("tools.browser_tool.agent_browser_runnable", return_value=True):
assert _find_agent_browser() == "/usr/local/bin/agent-browser"
def test_finds_in_homebrew_bin(self):
@ -112,6 +113,7 @@ class TestFindAgentBrowser:
return None
with patch("shutil.which", side_effect=mock_which), \
patch("tools.browser_tool.agent_browser_runnable", return_value=True), \
patch("os.path.isdir", return_value=True), \
patch(
"tools.browser_tool._discover_homebrew_node_dirs",