fix(browser): self-review pass — dead-import, log levels, future-proofing
Addresses findings from two self-review passes pre-merge.
First pass (3-agent parallel review):
1. plugins/browser/browser_use/provider.py: drop the
``_ = managed_nous_tools_enabled`` dead-import-hider in
_get_config_or_none(). The import was actively misleading — the
helper IS used in _get_config() (separate method, separate import),
not here. The "keep static analysis happy" comment was wrong about
what the helper does in this scope.
2. agent/browser_provider.py: drop ``pragma: no cover`` from
is_configured() / provider_name() backward-compat aliases. They ARE
covered by ``TestLegacyAbcAliases`` — the pragma would have masked
future regressions.
3. tools/browser_tool.py: refactor _is_legacy_provider_registry_overridden()
to compare against a module-frozen _DEFAULT_PROVIDER_REGISTRY snapshot
instead of hardcoded set of 3 keys. Future maintainers adding a 4th
built-in provider now just extend _PROVIDER_REGISTRY; the override
detection adapts automatically. Previously the hardcoded
``set(...) != {"browserbase", "browser-use", "firecrawl"}`` would flip
True forever on any 4-key registry, silently routing every install
onto the legacy fixture path.
4. tools/browser_tool.py: when explicit ``browser.cloud_provider`` is set
but the registry has no matching plugin (typo, uninstalled plugin,
discovery failure), emit a WARNING with actionable text instead of
silently falling through to auto-detect. Legacy code surfaced a typed
credentials error via direct class instantiation; this log restores
the signal in the post-migration path.
5. agent/browser_registry.py: trim the triple-redundant _LEGACY_PREFERENCE
documentation. Module docstring + 13-line block-comment + 5-line
inline comment was repeating the same point. Kept the docstring and
trimmed the block-comment to 5 lines.
6. agent/browser_registry.py: upgrade is_available()-raised logging from
DEBUG to WARNING with exc_info=True. A provider's availability check
throwing is unusual enough that users debugging "no cloud provider"
need the traceback in logs.
7. tests/plugins/browser/check_parity_vs_main.py: drop dead top-level
imports (os, shutil, tempfile — only referenced inside the
SUBPROCESS_SCRIPT string literal that runs in a child process).
Second pass (architecture + claim-verification review):
8. tools/browser_tool.py: rewrite the inline comment in _get_cloud_provider
auto-detect branch. Prior text claimed it "routes through the plugin
registry's legacy preference walk so third-party plugins still get a
chance to be selected when they're explicitly configured" — false on
both counts. The branch uses module-level legacy class aliases
(BrowserUseProvider / BrowserbaseProvider) directly; third-party
plugins are intentionally reachable only via explicit
``browser.cloud_provider``. Corrected comment now matches behaviour
and cross-references _LEGACY_PREFERENCE for the firecrawl gate
rationale.
9. tools/browser_tool.py + tests/tools/test_managed_browserbase_and_modal.py:
drop the unused ``get_active_browser_provider as
_registry_get_active_browser_provider`` alias from the
``from agent.browser_registry import ...`` block. It was never
referenced; matching test-stub line in the agent.browser_registry
SimpleNamespace also dropped. ``get_provider`` is still imported (used
by the explicit-config dispatch path at line 535).
10. plugins/browser/firecrawl/provider.py: align emergency_cleanup()
with the early-guard pattern used in browserbase + browser_use
plugins. Previously firecrawl tried the DELETE and relied on
``_headers()`` raising ValueError to trip a "missing credentials"
warning; same final outcome but a different control flow that read
like a bug to a maintainer skimming the three modules. Now: if
is_available() is False, log+return early — identical shape to the
other two providers.
Verification: 54/54 unit tests + 13/13 parity scenarios still pass.
This commit is contained in:
@ -166,10 +166,10 @@ class BrowserProvider(abc.ABC):
|
|||||||
# and :attr:`name`; they may override ``is_configured`` / ``provider_name``
|
# and :attr:`name`; they may override ``is_configured`` / ``provider_name``
|
||||||
# for compatibility with the legacy ABC but it is not required.
|
# for compatibility with the legacy ABC but it is not required.
|
||||||
|
|
||||||
def is_configured(self) -> bool: # pragma: no cover - trivial delegation
|
def is_configured(self) -> bool:
|
||||||
"""Backward-compat alias for :meth:`is_available`."""
|
"""Backward-compat alias for :meth:`is_available`."""
|
||||||
return self.is_available()
|
return self.is_available()
|
||||||
|
|
||||||
def provider_name(self) -> str: # pragma: no cover - trivial delegation
|
def provider_name(self) -> str:
|
||||||
"""Backward-compat alias returning :attr:`display_name`."""
|
"""Backward-compat alias returning :attr:`display_name`."""
|
||||||
return self.display_name
|
return self.display_name
|
||||||
|
|||||||
@ -99,19 +99,11 @@ def get_provider(name: str) -> Optional[BrowserProvider]:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# Legacy preference order — preserves behaviour for users who set no
|
# Legacy auto-detect order — used when no ``browser.cloud_provider`` is set.
|
||||||
# ``browser.cloud_provider`` config key. Matches the historic auto-detect
|
# Matches the pre-migration walk in :func:`tools.browser_tool._get_cloud_provider`.
|
||||||
# order in :func:`tools.browser_tool._get_cloud_provider` (Browser Use first
|
# Firecrawl is intentionally absent so users with ``FIRECRAWL_API_KEY`` set
|
||||||
# because it covers both managed Nous gateway and direct API key; Browserbase
|
# for web-extract don't get silently routed to a paid cloud browser. See
|
||||||
# second as the older direct-credentials fallback). Filtered by
|
# :func:`_resolve` for the full rationale.
|
||||||
# ``is_available()`` at walk time so we don't surface a provider the user
|
|
||||||
# has no credentials for.
|
|
||||||
#
|
|
||||||
# Note: ``firecrawl`` is intentionally absent. Pre-migration, the auto-detect
|
|
||||||
# branch only considered Browser Use → Browserbase; Firecrawl was reachable
|
|
||||||
# only via an explicit ``browser.cloud_provider: firecrawl`` config key.
|
|
||||||
# Preserving that gate prevents users with a ``FIRECRAWL_API_KEY`` set for
|
|
||||||
# web-extract from accidentally getting routed to a (paid) cloud browser.
|
|
||||||
_LEGACY_PREFERENCE = (
|
_LEGACY_PREFERENCE = (
|
||||||
"browser-use",
|
"browser-use",
|
||||||
"browserbase",
|
"browserbase",
|
||||||
@ -159,7 +151,10 @@ def _resolve(configured: Optional[str]) -> Optional[BrowserProvider]:
|
|||||||
try:
|
try:
|
||||||
return bool(p.is_available())
|
return bool(p.is_available())
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
logger.debug("provider %s.is_available() raised %s", p.name, exc)
|
logger.warning(
|
||||||
|
"Browser provider %s.is_available() raised %s — treating as unavailable",
|
||||||
|
p.name, exc, exc_info=True,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 1. Explicit "local" short-circuit.
|
# 1. Explicit "local" short-circuit.
|
||||||
|
|||||||
@ -130,9 +130,10 @@ class BrowserUseBrowserProvider(BrowserProvider):
|
|||||||
# managed_tool_gateway pulls in the Nous auth stack which can be
|
# managed_tool_gateway pulls in the Nous auth stack which can be
|
||||||
# heavy and is not needed for direct-API-key users.
|
# heavy and is not needed for direct-API-key users.
|
||||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||||
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway
|
from tools.tool_backend_helpers import prefers_gateway
|
||||||
|
|
||||||
# 1. Direct API key path (unless user explicitly prefers gateway).
|
# Direct API key wins unless the user has explicitly opted into the
|
||||||
|
# managed Nous gateway via ``tool_gateway.browser: gateway``.
|
||||||
api_key = os.environ.get("BROWSER_USE_API_KEY")
|
api_key = os.environ.get("BROWSER_USE_API_KEY")
|
||||||
if api_key and not prefers_gateway("browser"):
|
if api_key and not prefers_gateway("browser"):
|
||||||
return {
|
return {
|
||||||
@ -141,16 +142,10 @@ class BrowserUseBrowserProvider(BrowserProvider):
|
|||||||
"managed_mode": False,
|
"managed_mode": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. Managed Nous gateway path.
|
|
||||||
managed = resolve_managed_tool_gateway("browser-use")
|
managed = resolve_managed_tool_gateway("browser-use")
|
||||||
if managed is None:
|
if managed is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Hold reference to managed_nous_tools_enabled so static analysis
|
|
||||||
# doesn't flag the import as unused — the helper is consulted by
|
|
||||||
# _get_config() below to compose a more accurate error message.
|
|
||||||
_ = managed_nous_tools_enabled
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"api_key": managed.nous_user_token,
|
"api_key": managed.nous_user_token,
|
||||||
"base_url": managed.gateway_origin.rstrip("/"),
|
"base_url": managed.gateway_origin.rstrip("/"),
|
||||||
|
|||||||
@ -130,17 +130,18 @@ class FirecrawlBrowserProvider(BrowserProvider):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def emergency_cleanup(self, session_id: str) -> None:
|
def emergency_cleanup(self, session_id: str) -> None:
|
||||||
|
if not self.is_available():
|
||||||
|
logger.warning(
|
||||||
|
"Cannot emergency-cleanup Firecrawl session %s — missing credentials",
|
||||||
|
session_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
requests.delete(
|
requests.delete(
|
||||||
f"{self._api_url()}/v2/browser/{session_id}",
|
f"{self._api_url()}/v2/browser/{session_id}",
|
||||||
headers=self._headers(),
|
headers=self._headers(),
|
||||||
timeout=5,
|
timeout=5,
|
||||||
)
|
)
|
||||||
except ValueError:
|
|
||||||
logger.warning(
|
|
||||||
"Cannot emergency-cleanup Firecrawl session %s — missing credentials",
|
|
||||||
session_id,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Emergency cleanup failed for Firecrawl session %s: %s", session_id, e
|
"Emergency cleanup failed for Firecrawl session %s: %s", session_id, e
|
||||||
|
|||||||
@ -19,11 +19,8 @@ Run from the PR worktree:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,6 @@ def _install_fake_tools_package():
|
|||||||
BrowserProvider=_StubBrowserProvider,
|
BrowserProvider=_StubBrowserProvider,
|
||||||
)
|
)
|
||||||
sys.modules["agent.browser_registry"] = types.SimpleNamespace(
|
sys.modules["agent.browser_registry"] = types.SimpleNamespace(
|
||||||
get_active_browser_provider=lambda: None,
|
|
||||||
get_provider=lambda name: None,
|
get_provider=lambda name: None,
|
||||||
list_providers=lambda: [],
|
list_providers=lambda: [],
|
||||||
register_provider=lambda provider: None,
|
register_provider=lambda provider: None,
|
||||||
|
|||||||
@ -90,7 +90,6 @@ except Exception:
|
|||||||
# shims for callers that import them from this module.
|
# shims for callers that import them from this module.
|
||||||
from agent.browser_provider import BrowserProvider as CloudBrowserProvider # noqa: F401 (legacy alias)
|
from agent.browser_provider import BrowserProvider as CloudBrowserProvider # noqa: F401 (legacy alias)
|
||||||
from agent.browser_registry import ( # noqa: F401 (test-patchable surface)
|
from agent.browser_registry import ( # noqa: F401 (test-patchable surface)
|
||||||
get_active_browser_provider as _registry_get_active_browser_provider,
|
|
||||||
get_provider as _registry_get_browser_provider,
|
get_provider as _registry_get_browser_provider,
|
||||||
)
|
)
|
||||||
from plugins.browser.browserbase.provider import ( # noqa: F401 (legacy import surface)
|
from plugins.browser.browserbase.provider import ( # noqa: F401 (legacy import surface)
|
||||||
@ -425,6 +424,10 @@ _PROVIDER_REGISTRY: Dict[str, type] = {
|
|||||||
"browser-use": BrowserUseProvider,
|
"browser-use": BrowserUseProvider,
|
||||||
"firecrawl": FirecrawlProvider,
|
"firecrawl": FirecrawlProvider,
|
||||||
}
|
}
|
||||||
|
# Frozen copy of the import-time _PROVIDER_REGISTRY, used by
|
||||||
|
# ``_is_legacy_provider_registry_overridden`` to detect test-time
|
||||||
|
# monkeypatching. NEVER mutate this dict.
|
||||||
|
_DEFAULT_PROVIDER_REGISTRY: Dict[str, type] = dict(_PROVIDER_REGISTRY)
|
||||||
|
|
||||||
_cached_cloud_provider: Optional[CloudBrowserProvider] = None
|
_cached_cloud_provider: Optional[CloudBrowserProvider] = None
|
||||||
_cloud_provider_resolved = False
|
_cloud_provider_resolved = False
|
||||||
@ -442,25 +445,23 @@ _browser_engine_resolved = False
|
|||||||
def _is_legacy_provider_registry_overridden() -> bool:
|
def _is_legacy_provider_registry_overridden() -> bool:
|
||||||
"""Return True when a test has patched ``_PROVIDER_REGISTRY`` to a custom value.
|
"""Return True when a test has patched ``_PROVIDER_REGISTRY`` to a custom value.
|
||||||
|
|
||||||
Detected by comparing identity with the module-level defaults dict
|
Detected by spotting any registered class that *isn't* the canonical
|
||||||
populated above. Tests that ``monkeypatch.setattr(browser_tool,
|
plugin-backed class for that name. Tests that
|
||||||
"_PROVIDER_REGISTRY", ...)`` swap in a new object; identity differs
|
``monkeypatch.setattr(browser_tool, "_PROVIDER_REGISTRY", ...)`` install
|
||||||
even when the contents happen to match. Used by ``_get_cloud_provider``
|
custom factories (`exploding_factory`, `lambda: fake_provider`, etc.);
|
||||||
to honour test-time overrides (which expect a factory-callable shape)
|
those entries fail the canonical-class identity check below.
|
||||||
instead of routing through the plugin registry.
|
|
||||||
|
Note: a future maintainer adding a 4th built-in provider only needs to
|
||||||
|
extend ``_DEFAULT_PROVIDER_REGISTRY`` below — they do NOT need to update
|
||||||
|
a hardcoded set of keys here. The detection just compares each registered
|
||||||
|
value against the corresponding canonical class.
|
||||||
"""
|
"""
|
||||||
# The module-level _PROVIDER_REGISTRY is built once at import time. A test
|
|
||||||
# that swaps it via monkeypatch creates a new dict; we detect that via
|
|
||||||
# the registered class identities, not by ``is`` on the dict itself
|
|
||||||
# (the patch may install a dict whose values happen to be the same
|
|
||||||
# classes; treat that as "not overridden").
|
|
||||||
try:
|
try:
|
||||||
return (
|
for key, default_cls in _DEFAULT_PROVIDER_REGISTRY.items():
|
||||||
_PROVIDER_REGISTRY.get("browserbase") is not BrowserbaseProvider
|
if _PROVIDER_REGISTRY.get(key) is not default_cls:
|
||||||
or _PROVIDER_REGISTRY.get("browser-use") is not BrowserUseProvider
|
return True
|
||||||
or _PROVIDER_REGISTRY.get("firecrawl") is not FirecrawlProvider
|
# Extra keys not in the default registry → also an override.
|
||||||
or set(_PROVIDER_REGISTRY.keys()) != {"browserbase", "browser-use", "firecrawl"}
|
return len(_PROVIDER_REGISTRY) != len(_DEFAULT_PROVIDER_REGISTRY)
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -532,6 +533,20 @@ def _get_cloud_provider() -> Optional[CloudBrowserProvider]:
|
|||||||
# populated. Idempotent — cheap on subsequent calls.
|
# populated. Idempotent — cheap on subsequent calls.
|
||||||
_ensure_browser_plugins_loaded()
|
_ensure_browser_plugins_loaded()
|
||||||
resolved = _registry_get_browser_provider(provider_key)
|
resolved = _registry_get_browser_provider(provider_key)
|
||||||
|
if resolved is None:
|
||||||
|
# Explicit config name unknown to the registry —
|
||||||
|
# might be a typo, an uninstalled plugin, or a
|
||||||
|
# registry-population failure. Warn the user
|
||||||
|
# (legacy code would have surfaced a typed
|
||||||
|
# credentials error via direct class instantiation;
|
||||||
|
# post-migration we surface this WARNING instead).
|
||||||
|
logger.warning(
|
||||||
|
"browser.cloud_provider=%r is not a registered "
|
||||||
|
"browser plugin; falling back to auto-detect "
|
||||||
|
"(install the corresponding plugin or fix the "
|
||||||
|
"config key spelling).",
|
||||||
|
provider_key,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Failed to instantiate explicit cloud_provider %r; will retry on next call",
|
"Failed to instantiate explicit cloud_provider %r; will retry on next call",
|
||||||
@ -545,12 +560,15 @@ def _get_cloud_provider() -> Optional[CloudBrowserProvider]:
|
|||||||
logger.debug("Could not read cloud_provider from config: %s", e)
|
logger.debug("Could not read cloud_provider from config: %s", e)
|
||||||
|
|
||||||
if resolved is None:
|
if resolved is None:
|
||||||
# Auto-detect path. When tests have patched the per-class names
|
# Auto-detect path: Browser Use first (managed Nous gateway or
|
||||||
# on this module (BrowserUseProvider / BrowserbaseProvider), honour
|
# direct API key), then Browserbase (direct credentials). Uses
|
||||||
# them — the test_browser_cloud_provider_cache test relies on this.
|
# the legacy class names imported at the top of this module so
|
||||||
# Otherwise route through the plugin registry's legacy preference
|
# tests that ``monkeypatch.setattr(browser_tool, "BrowserUseProvider", ...)``
|
||||||
# walk so third-party plugins still get a chance to be selected
|
# keep driving this branch deterministically. Third-party browser
|
||||||
# when they're explicitly configured.
|
# plugins are intentionally NOT reachable from auto-detect — they
|
||||||
|
# participate only via explicit ``browser.cloud_provider: <name>``,
|
||||||
|
# mirroring the firecrawl gate documented on
|
||||||
|
# :data:`agent.browser_registry._LEGACY_PREFERENCE`.
|
||||||
try:
|
try:
|
||||||
fallback_provider = BrowserUseProvider()
|
fallback_provider = BrowserUseProvider()
|
||||||
if fallback_provider.is_configured():
|
if fallback_provider.is_configured():
|
||||||
|
|||||||
Reference in New Issue
Block a user