{provider.tag &&
{provider.tag}
}
+ {(toolset !== 'web' || webCaps.length === 0) && (
+ // Explicit activation — the old row-click-selects UX gave no
+ // signal about which backend was actually in use and made
+ // reading a row's details indistinguishable from choosing it.
+
+ {isBackendActive ? (
+
+
+ {copy.activeBackendHint}
+
+ ) : (
+
+ )}
+
+ )}
{webCaps.length > 0 && (
// Per-capability assignment: writes web.search_backend /
// web.extract_backend without touching the shared
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index 87f4e84914e3..29fa06c439fe 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -910,7 +910,10 @@ export const en: Translations = {
noProviders: 'No providers are available for this toolset right now.',
ready: 'Ready',
needsSignIn: 'Needs sign-in',
- needsSetup: 'Needs setup',
+ needsSetup: 'Setup required',
+ activeBackend: 'Active',
+ activeBackendHint: 'This is your active backend',
+ useBackend: 'Use this backend',
nousIncluded: 'Included with a Nous subscription — sign in to Nous Portal to activate.',
nousAuthNeededTitle: 'Sign in to Nous Portal',
nousAuthNeededMessage: provider => `${provider} is saved but won't activate until you sign in to Nous Portal.`,
diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts
index 88ba5809cca1..22a11179e084 100644
--- a/apps/desktop/src/i18n/ja.ts
+++ b/apps/desktop/src/i18n/ja.ts
@@ -947,6 +947,9 @@ export const ja = defineLocale({
ready: '準備完了',
needsSignIn: 'サインインが必要',
needsSetup: 'セットアップが必要',
+ activeBackend: '使用中',
+ activeBackendHint: 'これが現在アクティブなバックエンドです',
+ useBackend: 'このバックエンドを使う',
nousIncluded: 'Nous サブスクリプションに含まれています。有効にするには Nous Portal にサインインしてください。',
nousAuthNeededTitle: 'Nous Portal にサインイン',
nousAuthNeededMessage: provider =>
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index cbcf9bd1579c..9b986fd90a92 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -786,6 +786,9 @@ export interface Translations {
ready: string
needsSignIn: string
needsSetup: string
+ activeBackend: string
+ activeBackendHint: string
+ useBackend: string
nousIncluded: string
nousAuthNeededTitle: string
nousAuthNeededMessage: (provider: string) => string
diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts
index b7ff39699d8c..9f1c2474e40a 100644
--- a/apps/desktop/src/i18n/zh-hant.ts
+++ b/apps/desktop/src/i18n/zh-hant.ts
@@ -916,6 +916,9 @@ export const zhHant = defineLocale({
ready: '就緒',
needsSignIn: '需要登入',
needsSetup: '需要安裝',
+ activeBackend: '目前後端',
+ activeBackendHint: '這是你目前使用的後端',
+ useBackend: '使用此後端',
nousIncluded: '包含在 Nous 訂閱中;登入 Nous Portal 即可啟用。',
nousAuthNeededTitle: '登入 Nous Portal',
nousAuthNeededMessage: provider => `已儲存 ${provider},但在登入 Nous Portal 之前不會啟用。`,
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index 71b55234df61..7a881a763197 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -1111,6 +1111,9 @@ export const zh: Translations = {
ready: '就绪',
needsSignIn: '需要登录',
needsSetup: '需要安装',
+ activeBackend: '当前后端',
+ activeBackendHint: '这是你当前使用的后端',
+ useBackend: '使用此后端',
nousIncluded: '包含在 Nous 订阅中;登录 Nous Portal 即可激活。',
nousAuthNeededTitle: '登录 Nous Portal',
nousAuthNeededMessage: provider => `已保存 ${provider},但在登录 Nous Portal 之前不会激活。`,
diff --git a/hermes_cli/nous_subscription.py b/hermes_cli/nous_subscription.py
index f2775e3bbe83..bfba9b2dd49f 100644
--- a/hermes_cli/nous_subscription.py
+++ b/hermes_cli/nous_subscription.py
@@ -159,17 +159,35 @@ def _toolset_enabled(config: Dict[str, object], toolset_key: str) -> bool:
def _has_agent_browser() -> bool:
import shutil
- from hermes_constants import agent_browser_runnable
+ from hermes_constants import agent_browser_runnable, with_hermes_node_path
# 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 agent_browser_runnable(str(local_bin)) if local_bin.exists() else False
+
+ # Hermes-managed Node dirs (Windows installer / POSIX $HERMES_HOME/node)
+ # are prepended to PATH at runtime but usually absent from the *probe*
+ # process's PATH — the same rung `_find_agent_browser` searches. Without
+ # it a successful install keeps reporting "needs setup" on Windows.
+ managed_path = with_hermes_node_path().get("PATH", "")
+ if managed_path:
+ managed_hit = shutil.which("agent-browser", path=managed_path)
+ if managed_hit and agent_browser_runnable(managed_hit):
+ return True
+
+ # Local node_modules/.bin: resolve via PATHEXT-aware ``shutil.which`` so
+ # Windows picks the executable ``.cmd`` shim. Probing the extensionless
+ # POSIX shim directly fails exec (WinError 193) even right after a
+ # successful ``npm install`` — the bug that pinned every browser row on
+ # "Setup required" in the desktop GUI.
+ local_bin_dir = Path(__file__).parent.parent / "node_modules" / ".bin"
+ if local_bin_dir.is_dir():
+ local_which = shutil.which("agent-browser", path=str(local_bin_dir))
+ if local_which and agent_browser_runnable(local_which):
+ return True
+ return False
def _local_browser_runnable() -> bool:
diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py
index 1d3f6899fa9a..cdc0243e0bd6 100644
--- a/hermes_cli/tools_config.py
+++ b/hermes_cli/tools_config.py
@@ -540,7 +540,12 @@ TOOL_CATEGORIES = {
"requires_nous_auth": True,
"managed_nous_feature": "browser",
"override_env_vars": ["BROWSER_USE_API_KEY"],
- "post_setup": "agent_browser",
+ # Cloud hook: installs the agent-browser CLI only. Browser Use
+ # hosts its own Chromium, so the local-Chromium install (and
+ # the local-Chromium readiness gate) must not apply here —
+ # with "agent_browser" this row read "needs setup" forever on
+ # machines without a local Chromium build.
+ "post_setup": "browserbase",
},
{
"name": "Camofox",
@@ -2872,8 +2877,19 @@ def _agent_browser_installed() -> bool:
Lightpanda engine, which needs no Chromium). Mirrors the hook so "Run
setup" flips to an installed state only when re-running it would be a
no-op."""
+ import sys
+
from hermes_cli.nous_subscription import _local_browser_runnable
+ # The install hook runs in a spawned ``hermes tools post-setup`` process,
+ # but this probe runs in the long-lived web-server/CLI process, whose
+ # browser_tool module may have cached a stale "Chromium missing" result
+ # from before the install. Drop the cache (when the module is loaded) so
+ # the readiness pill flips to Ready right after a successful setup run.
+ bt = sys.modules.get("tools.browser_tool")
+ if bt is not None:
+ bt._cached_chromium_installed = None
+
return _local_browser_runnable()
diff --git a/plugins/browser/browser_use/provider.py b/plugins/browser/browser_use/provider.py
index 46a220333446..8310862cf7a7 100644
--- a/plugins/browser/browser_use/provider.py
+++ b/plugins/browser/browser_use/provider.py
@@ -313,5 +313,7 @@ class BrowserUseBrowserProvider(BrowserProvider):
"url": "https://browser-use.com",
},
],
- "post_setup": "agent_browser",
+ # Cloud-scoped hook: installs the agent-browser CLI only (no
+ # local Chromium — Browser Use hosts the browser).
+ "post_setup": "browserbase",
}
diff --git a/plugins/browser/browserbase/provider.py b/plugins/browser/browserbase/provider.py
index 41ceb9e83d15..c828ae29b2da 100644
--- a/plugins/browser/browserbase/provider.py
+++ b/plugins/browser/browserbase/provider.py
@@ -293,5 +293,7 @@ class BrowserbaseBrowserProvider(BrowserProvider):
"prompt": "Browserbase project ID",
},
],
- "post_setup": "agent_browser",
+ # Cloud-scoped hook: installs the agent-browser CLI only (no
+ # local Chromium — Browserbase hosts the browser).
+ "post_setup": "browserbase",
}
diff --git a/plugins/browser/firecrawl/provider.py b/plugins/browser/firecrawl/provider.py
index 50f813f60185..892cd6c1dc8f 100644
--- a/plugins/browser/firecrawl/provider.py
+++ b/plugins/browser/firecrawl/provider.py
@@ -167,5 +167,7 @@ class FirecrawlBrowserProvider(BrowserProvider):
"url": "https://firecrawl.dev",
},
],
- "post_setup": "agent_browser",
+ # Cloud-scoped hook: installs the agent-browser CLI only (no
+ # local Chromium — Firecrawl hosts the browser).
+ "post_setup": "browserbase",
}
diff --git a/tests/hermes_cli/test_nous_subscription.py b/tests/hermes_cli/test_nous_subscription.py
index 9faf25965e0e..064c3fa66cbc 100644
--- a/tests/hermes_cli/test_nous_subscription.py
+++ b/tests/hermes_cli/test_nous_subscription.py
@@ -854,3 +854,37 @@ def test_apply_gateway_defaults_sets_stt_use_gateway(monkeypatch):
assert "stt" in changed
assert config["stt"]["provider"] == "openai"
assert config["stt"]["use_gateway"] is True
+
+
+def test_has_agent_browser_resolves_via_hermes_managed_node_path(monkeypatch, tmp_path):
+ """The managed-Node rung: a runnable agent-browser under the Hermes Node
+ dir must count even when it's absent from the probe process's PATH (the
+ Windows installer shape — install succeeded, GUI still said needs setup)."""
+ import shutil as _shutil
+
+ managed_dir = tmp_path / "node"
+ managed_dir.mkdir()
+ managed_bin = managed_dir / "agent-browser"
+ managed_bin.write_text("#!/bin/sh\nexit 0\n")
+ managed_bin.chmod(0o755)
+
+ monkeypatch.setattr(_shutil, "which", lambda cmd, path=None: str(managed_bin) if path else None)
+ monkeypatch.setattr(
+ "hermes_constants.with_hermes_node_path", lambda: {"PATH": str(managed_dir)}
+ )
+ monkeypatch.setattr(
+ "hermes_constants.agent_browser_runnable",
+ lambda p: bool(p) and str(p) == str(managed_bin),
+ )
+
+ assert ns._has_agent_browser() is True
+
+
+def test_has_agent_browser_false_when_nothing_runnable(monkeypatch):
+ import shutil as _shutil
+
+ monkeypatch.setattr(_shutil, "which", lambda cmd, path=None: None)
+ monkeypatch.setattr("hermes_constants.with_hermes_node_path", lambda: {"PATH": ""})
+ monkeypatch.setattr("hermes_constants.agent_browser_runnable", lambda p: False)
+
+ assert ns._has_agent_browser() is False
diff --git a/tests/hermes_cli/test_tools_config.py b/tests/hermes_cli/test_tools_config.py
index 0b3e84251185..5d298000734e 100644
--- a/tests/hermes_cli/test_tools_config.py
+++ b/tests/hermes_cli/test_tools_config.py
@@ -1200,7 +1200,7 @@ def test_computer_use_needs_configuration_when_cua_driver_post_setup_pending():
def test_computer_use_skips_configuration_when_cua_driver_already_installed():
"""Installed post_setup dependencies should keep returning-user toggles no-op."""
- def fake_which(name: str):
+ def fake_which(name: str, path=None):
return "/usr/local/bin/cua-driver" if name == "cua-driver" else None
with patch("shutil.which", side_effect=fake_which):
@@ -1209,7 +1209,7 @@ def test_computer_use_skips_configuration_when_cua_driver_already_installed():
def test_computer_use_respects_custom_cua_driver_command():
"""The setup gate should match runtime's HERMES_CUA_DRIVER_CMD override."""
- def fake_which(name: str):
+ def fake_which(name: str, path=None):
return "/opt/bin/custom-cua" if name == "custom-cua" else None
with patch.dict("os.environ", {"HERMES_CUA_DRIVER_CMD": "custom-cua"}), \
@@ -1219,7 +1219,7 @@ def test_computer_use_respects_custom_cua_driver_command():
def test_computer_use_blank_custom_driver_command_falls_back_to_default():
"""Blank overrides should not make the setup gate look for an empty command."""
- def fake_which(name: str):
+ def fake_which(name: str, path=None):
return "/usr/local/bin/cua-driver" if name == "cua-driver" else None
with patch.dict("os.environ", {"HERMES_CUA_DRIVER_CMD": " "}), \
@@ -1229,7 +1229,7 @@ def test_computer_use_blank_custom_driver_command_falls_back_to_default():
def test_computer_use_post_setup_respects_custom_driver_command_when_installed():
"""post_setup already-installed checks should version-probe the resolved override."""
- def fake_which(name: str):
+ def fake_which(name: str, path=None):
return "/opt/bin/custom-cua" if name == "custom-cua" else None
with patch.dict("os.environ", {"HERMES_CUA_DRIVER_CMD": "custom-cua"}), \
@@ -1250,7 +1250,7 @@ def test_computer_use_post_setup_missing_override_does_not_accept_default_binary
"""A default cua-driver binary must not satisfy a missing runtime override."""
seen = []
- def fake_which(name: str):
+ def fake_which(name: str, path=None):
seen.append(name)
if name == "cua-driver":
return "/usr/local/bin/cua-driver"
diff --git a/tests/plugins/browser/test_browser_provider_plugins.py b/tests/plugins/browser/test_browser_provider_plugins.py
index 986a1d635bfe..c05f92b3b92a 100644
--- a/tests/plugins/browser/test_browser_provider_plugins.py
+++ b/tests/plugins/browser/test_browser_provider_plugins.py
@@ -115,9 +115,11 @@ class TestBundledPluginsRegister:
assert isinstance(schema, dict)
assert "name" in schema
assert "env_vars" in schema
- # Every cloud-browser plugin needs the agent-browser post-setup hook
- # so the picker auto-installs the CLI on selection.
- assert schema.get("post_setup") == "agent_browser"
+ # Every cloud-browser plugin needs the cloud-scoped post-setup hook
+ # ("browserbase" = agent-browser CLI only, no local Chromium install)
+ # so the picker auto-installs the CLI on selection without gating
+ # readiness on a local Chromium build the cloud never uses.
+ assert schema.get("post_setup") == "browserbase"
@pytest.mark.parametrize(
"plugin_name",
@@ -359,13 +361,14 @@ class TestPickerIntegration:
assert names == ["browser-use", "browserbase", "firecrawl"]
def test_picker_rows_carry_post_setup_hook(self) -> None:
- """Every browser plugin row has post_setup='agent_browser' so
- selecting it triggers the agent-browser CLI install."""
+ """Every browser plugin row has the cloud-scoped post_setup hook
+ ('browserbase': agent-browser CLI only) so selecting it installs the
+ CLI without requiring a local Chromium the cloud never uses."""
_ensure_plugins_loaded()
from hermes_cli.tools_config import _plugin_browser_providers
for row in _plugin_browser_providers():
- assert row.get("post_setup") == "agent_browser", (
+ assert row.get("post_setup") == "browserbase", (
f"plugin row {row['browser_provider']!r} missing post_setup hook"
)